在旋转的CANVAS上绘制-第2部分 [英] Draw on rotated CANVAS - Part 2

查看:69
本文介绍了在旋转的CANVAS上绘制-第2部分的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

作为对此问题和答案的后续操作 ...我还有另一个问题要解决解决:

As a follow up to this question and answer...I have another issue to solve:

当我在画布上绘制然后应用诸如旋转之类的变换时,我想保留绘制的内容并继续绘制。

When I draw on a canvas and then apply some transformations like rotation, I would like to keep what was drawn and continue the drawing.

要对此进行测试,请使用鼠标绘制一些内容,然后单击旋转。

To test this, use the mouse to draw something and then click "rotate".

这是我正在尝试的方法,但是

This is what I'm trying, but the canvas gets erased.

JS

//main variables
canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
canvas.ctx = canvas.getContext("2d");
ctx = canvas.ctx;

canvas_aux = document.createElement("canvas");
canvas_aux.width = 500;
canvas_aux.height = 300;
canvas_aux.ctx = canvas.getContext("2d");
ctx_aux = canvas_aux.ctx;


function rotate()
{
    ctx_aux.drawImage(canvas, 0, 0); //new line: save current drawing

    timer += timerStep;

    var cw = canvas.width / 2;
    var ch = canvas.height / 2;

    ctx.setTransform(1, 0, 0, 1, 0, 0);  // reset the transform so we can clear
    ctx.clearRect(0, 0, canvas.width, canvas.height);  // clear the canvas

    createMatrix(cw, ch -50, scale, timer);

    var m = matrix;
    ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);

    //draw();
    ctx.drawImage(canvas_aux, 0, 0); //new line: repaint current drawing

    if(timer <= rotation )
    {
        requestAnimationFrame(rotate);
    }
}

DEMO(链接的问题/答案中原始版本的更新版本) )

DEMO (updated version of original in linked question/answer)

https://jsfiddle.net/mgf8uz7s / 1 /

推荐答案

记录所有路径,使用画布缓冲区保持界面流畅


您有几个选项,具体取决于需求。

Record all paths, use canvas buffer to keep interface smooth

You have several options which will depend on what the requirements are.


  1. 屏幕外缓冲区用于保存渲染的行。渲染到屏幕外缓冲区,然后将缓冲区绘制到显示画布。这是最快的方法,但是您使用的是像素,因此,如果缩放,则会出现像素瑕疵,这将限制绘图区域的大小(仍然很大,但不是伪无限大),并严格限制可以提供的撤消次数到内存限制

  1. Offscreen buffer/s to hold the rendered lines. Render to the offscreen buffer then draw the buffer to the display canvas. This is the quickest method but you are working with pixels, thus if you zoom you will get pixel artifacts and it will limit the size of the drawing area (still large but not pseudo infinite) and severely restrict the number of undos your can provide due to memory limits

绘制缓冲区时,基本上记录鼠标的移动和点击,然后在每次更新时重新渲染所有可见的路径显示器。这将使您可以缩放和旋转而没有像素伪像,为您提供尽可能大的绘制区域(在64位双精度的限制之内),并且奖励撤消一直返回到第一行。这种方法的问题在于它很快变得很慢(尽管您可以使用webGL提高渲染速度)

Buffer paths as they are draw, basicly recording mouse movements and clicks, then re-rendering all visible paths each time you update the display. This will let you zoom and rotate without pixel artifacts, give you a draw area as large as you like (within limit of 64bit doubles) and a bonus undo all the way back to the first line. The problem with this method is that it quickly becomes very slow (though you can improve rendering speed with webGL)

上述两种方法的组合方法。在绘制路径时记录它们,但也将它们渲染到屏幕外的画布上。使用屏幕外的画布更新显示并保持较高的刷新率。仅在需要时才重新渲染屏幕外的画布,即,在撤消或缩放时,在平移或旋转时不需要重新渲染。

A combination of the above two methods. Record the paths as they are drawn, but also render them to an offscreen canvas/s. Use the offscreen canvas to update the display and keep the refresh rate high. You only re-render the offscreen canvas when you need to, ie when you undo or if you zoom, you will not need to re-render when you pan or rotate.


Demo


我不会做完整的绘图包,所以这只是一个使用屏幕外缓冲区保存可见内容的示例路径。绘制的所有路径都记录在path数组中。当用户更改视图,平移,缩放,旋转时,路径会重新绘制到屏幕外的画布上以匹配新视图。

Demo

I am not going to do a full drawing package so this is just an example that uses an offscreen buffer to hold the visible paths. All paths that are drawn are recorded in a paths array. When the user changes the view, pan, zoom, rotate, the paths are redrawn to the offscreen canvas to match the new view.

有一些样板可以处理设置和鼠标,忽略了。因为有很多代码,而且时间很短,所以由于注释很短,您将不得不从中选择所需的内容。

There is some boilerplate to handle setup and mouse that can be ignored. As there is a lot of code and time is short you will have to pick out what you need from it as the comments are short.

这里有路径路径对象。 视图包含转换和相关功能。一些用于平移,缩放和旋转的功能。还有一个显示功能,可以渲染和处理所有鼠标和用户IO。通过按住鼠标修饰符ctrl,alt,shift

There is a paths object for paths. view holds the transform and associated functions. Some functions for pan, zoom, rotate. And a display function that renders and handles all mouse and user IO. The pan,zoom and scale controls are accessed via holding the mouse modifiers ctrl, alt, shift

var drawing = createImage(100,100); // offscreen canvas for drawing paths

// the onResize is a callback used by the boilerplate code at the bottom of this snippet
// it is called whenever the display size has changed (including starting app). It is
// debounced by 100ms to prevent needless calls
var onResize = function(){
    drawing.width = canvas.width;
    drawing.height = canvas.height;
    redrawBuffers = true; // flag that drawing buffers need redrawing
    ctx.font = "18px arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    view.pos.x = cw;  // set origin at center of screen
    view.pos.y = ch;
    view.update();
}
const paths = [];  // array of all recorded paths
const path = {   // descriptor of a path object
    addPoint(x,y){   // adds a point to the path
        this.points.push({x,y});
    },
    draw(ctx){   // draws this path on context ctx
        var i = 0;
        ctx.beginPath();
        ctx.moveTo(this.points[i].x,this.points[i++].y);
        while(i < this.points.length){
            ctx.lineTo(this.points[i].x,this.points[i++].y);
        }
        ctx.stroke();
    }
}
// creates a new path and adds it to the array of paths.
// returns the new path
function addPath(){
    var newPath;
    newPath = Object.assign({points : []},path);
    paths.push(newPath)
    return newPath;
}
// draws all recorded paths onto context cts using the current view
function drawAll(ctx){
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,w,h);
    var m = view.matrix;
    ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
    var i = 0;
    for(i = 0; i < paths.length; i ++){
        paths[i].draw(ctx);
    }
}

// this controls the view
const view = {
    matrix : [1,0,0,1,0,0],  // current view transform
    invMatrix : [1,0,0,1,0,0], // current inverse view transform
    rotate : 0,  // current x axis direction in radians
    scale : 1,   // current scale
    pos : {      // current position of origin
        x : 0,
        y : 0,
    },
    update(){ // call to update transforms
        var xdx = Math.cos(this.rotate) * this.scale;
        var xdy = Math.sin(this.rotate) * this.scale;
        var m = this.matrix;
        var im = this.invMatrix;
        m[0] = xdx;
        m[1] = xdy;
        m[2] = -xdy;
        m[3] = xdx;
        m[4] = this.pos.x;
        m[5] = this.pos.y;
        // calculate the inverse transformation
        cross = m[0] * m[3] - m[1] * m[2];
        im[0] =  m[3] / cross;
        im[1] = -m[1] / cross;
        im[2] = -m[2] / cross;
        im[3] =  m[0] / cross;
    },
    mouseToWorld(){  // conver screen to world coords
        var xx, yy, m;
        m = this.invMatrix;
        xx = mouse.x - this.matrix[4];     
        yy = mouse.y - this.matrix[5];     
        mouse.xr =  xx * m[0] + yy * m[2]; 
        mouse.yr =   xx * m[1] + yy * m[3];
    },        
    toWorld(x,y,point = {}){  // convert screen to world coords
        var xx, yy, m;
        m = this.invMatrix;
        xx = x - this.matrix[4];     
        yy = y - this.matrix[5];     
        point.x =  xx * m[0] + yy * m[2]; 
        point.y = xx * m[1] + yy * m[3];
        return point;
    },        
    toScreen(x,y,point = {}){  // convert world coords to  coords
        var m;
        m = this.matrix;
        point.x =  x * m[0] + y * m[2] + m[4]; 
        point.y = x * m[1] + y * m[3] + m[5];
        return point;
    },        
    clickOrigin : {  // used to hold coords to deal with pan zoom and rotate
        x : 0,
        y : 0,
        scale : 1,
    },
   dragging : false, // true is dragging 
   startDrag(){  // called to start a Orientation UI input such as rotate, pan and scale
        if(!view.dragging){
            view.dragging = true;
            view.clickOrigin.x = mouse.xr;
            view.clickOrigin.y = mouse.yr;
            view.clickOrigin.screenX = mouse.x;
            view.clickOrigin.screenY = mouse.y;
            view.clickOrigin.scale = view.scale;
        }
   }
}

// functions to do pan zoom and scale
function panView(){  // pans the view
    view.startDrag();  // set origins as referance point
    view.pos.x -= (view.clickOrigin.screenX - mouse.x);
    view.pos.y -= (view.clickOrigin.screenY - mouse.y);
    view.update();
    view.mouseToWorld(); // get the new mouse pos
    view.clickOrigin.screenX = mouse.x; // save the new mouse coords
    view.clickOrigin.screenY = mouse.y;
}   
// scales the view
function scaleView(){
    view.startDrag();
    var y = view.clickOrigin.screenY - mouse.y;
    if(y !== 0){
        view.scale = view.clickOrigin.scale + (y/ch);
        view.update();
    }
}   
// rotates the view by setting the x axis direction
function rotateView(){
    view.startDrag();
    workingCoord = view.toScreen(0,0,workingCoord); // get location of origin
    var x = workingCoord.x - mouse.x;
    var y = workingCoord.y - mouse.y;
    var dist = Math.sqrt(x * x + y * y);
    if(dist > 2 / view.scale){
        view.rotate = Math.atan2(-y,-x);
        view.update();
    }
}
var currentPath; // Holds the currently drawn path
var redrawBuffers = false; // if true this indicates that all paths need to be redrawn
var workingCoord; // var to use as a coordinate

// main loop function called from requestAnimationFrame callback in boilerplate code
function display() {
    var showTransform = false;  // flags that view is being changed
    // clear the canvas and set defaults
    ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
    ctx.globalAlpha = 1; // reset alpha
    ctx.clearRect(0, 0, w, h);
    view.mouseToWorld();  // get the mouse world coords
    
    // get the transform matrix
    var m = view.matrix;
    // show feedback
    if(mouse.shift || mouse.alt || mouse.ctrl){
        if(mouse.shift){
            ctx.fillText("Click drag to pan",cw, 20);
        }else if(mouse.ctrl){
            ctx.fillText("Click drag to rotate",cw, 20);
        }else{
            ctx.fillText("Click drag to scale : " + view.scale.toFixed(4),cw, 20);
        }
    }else{
          ctx.fillText("Click drag to draw.",cw, 20);
          ctx.fillText("Hold [shift], [ctrl], or [alt] and use mouse to pan, rotate, scale",cw, 40);
    }
    if(mouse.buttonRaw === 1){ // when mouse is down
        if(mouse.shift || mouse.alt || mouse.ctrl){ // pan zoom rotate
            if(mouse.shift){
                panView();
            }else if(mouse.ctrl){
                rotateView();
            }else{
                scaleView();
            }            
            m = view.matrix;
            showTransform = true;
            redrawBuffers = true;
        }else{ // or add a path
            if(currentPath === undefined){
                currentPath = addPath();
            }
            currentPath.addPoint(mouse.xr,mouse.yr)
        }
    }else{
        // if there is a path then draw it onto the offscreen canvas and
        // reset the path to undefined
        if(currentPath !== undefined){
            currentPath.draw(drawing.ctx);
            currentPath = undefined;
        }
        view.dragging = false; // incase there is a pan/zoom/scale happening turn it off
    }
    if(showTransform){  // redraw all paths when pan rotate or zoom 
        redrawBuffers = false;
        drawAll(drawing.ctx);
        ctx.drawImage(drawing,0,0);
    }else{  // draws the sceen when normal drawing mode.
        if(redrawBuffers){
            redrawBuffers = false;
            drawAll(drawing.ctx);
        }
        ctx.drawImage(drawing,0,0);
        ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
        drawing.ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
        
        // draw a cross hair.
        if(mouse.buttonRaw === 0){
            var invScale = 1 / view.scale; // get inverted scale
            ctx.beginPath();
            ctx.moveTo(mouse.xr - 10 * invScale,mouse.yr);
            ctx.lineTo(mouse.xr + 10 * invScale,mouse.yr);
            ctx.moveTo(mouse.xr ,mouse.yr - 10 * invScale);
            ctx.lineTo(mouse.xr ,mouse.yr + 10 * invScale);
            ctx.lineWidth = invScale;
            ctx.stroke();
            ctx.lineWidth = 1;
        }
    }

    // draw a new path if being drawn
    if(currentPath){
        currentPath.draw(ctx);
    }
    // If rotating or about to rotate show feedback
    if(mouse.ctrl){
        ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
        view.mouseToWorld();  // get the mouse world coords
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(0,0,3,0,Math.PI * 2);
        ctx.moveTo(0,0);
        ctx.lineTo(mouse.xr,mouse.yr);
        ctx.stroke();
        ctx.lineWidth = 1.5;
        ctx.strokeStyle = "red";
        ctx.beginPath();
        ctx.arc(0,0,3,0,Math.PI * 2);
        ctx.moveTo(0,0);
        ctx.lineTo(mouse.xr,mouse.yr);
        ctx.stroke();
        ctx.strokeStyle = "black";
        ctx.beginPath();
        ctx.moveTo(0,0);
        ctx.lineTo(200000 / view.scale,0);
        ctx.stroke();
        ctx.scale(1/ view.scale,1 / view.scale);
        ctx.fillText("X axis",100 ,-10  );
    }
}

/******************************************************************************/
// end of answer code
/******************************************************************************/







//Boiler plate from here down and can be ignored.
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
    const RESIZE_DEBOUNCE_TIME = 100;
    var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
    createCanvas = function () {
        var c,
        cs;
        cs = (c = document.createElement("canvas")).style;
        cs.position = "absolute";
        cs.top = cs.left = "0px";
        cs.zIndex = 1000;
        document.body.appendChild(c);
        return c;
    }
    resizeCanvas = function () {
        if (canvas === undefined) {
            canvas = createCanvas();
        }
        canvas.width = innerWidth;
        canvas.height = innerHeight;
        ctx = canvas.getContext("2d");
        if (typeof setGlobals === "function") {
            setGlobals();
        }
        if (typeof onResize === "function") {
            if(firstRun){
                onResize();
                firstRun = false;
            }else{
                resizeCount += 1;
                setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
            }
        }
    }
    function debounceResize() {
        resizeCount -= 1;
        if (resizeCount <= 0) {
            onResize();
        }
    }
    setGlobals = function () {
        cw = (w = canvas.width) / 2;
        ch = (h = canvas.height) / 2;
    }
    mouse = (function () {
        function preventDefault(e) {
            e.preventDefault();
        }
        var mouse = {
            x : 0,
            y : 0,
            w : 0,
            alt : false,
            shift : false,
            ctrl : false,
            buttonRaw : 0,
            over : false,
            bm : [1, 2, 4, 6, 5, 3],
            active : false,
            bounds : null,
            crashRecover : null,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left;
            m.y = e.pageY - m.bounds.top;
            m.alt = e.altKey;
            m.shift = e.shiftKey;
            m.ctrl = e.ctrlKey;
            if (t === "mousedown") {
                m.buttonRaw |= m.bm[e.which - 1];
            } else if (t === "mouseup") {
                m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") {
                m.buttonRaw = 0;
                m.over = false;
            } else if (t === "mouseover") {
                m.over = true;
            } else if (t === "mousewheel") {
                m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") {
                m.w = -e.detail;
            }
            if (m.callbacks) {
                m.callbacks.forEach(c => c(e));
            }
            e.preventDefault();
        }
        m.addCallback = function (callback) {
            if (typeof callback === "function") {
                if (m.callbacks === undefined) {
                    m.callbacks = [callback];
                } else {
                    m.callbacks.push(callback);
                }
            }
        }
        m.start = function (element) {
            if (m.element !== undefined) {
                m.removeMouse();
            }
            m.element = element === undefined ? document : element;
            m.mouseEvents.forEach(n => {
                m.element.addEventListener(n, mouseMove);
            });
            m.element.addEventListener("contextmenu", preventDefault, false);
            m.active = true;
        }
        m.remove = function () {
            if (m.element !== undefined) {
                m.mouseEvents.forEach(n => {
                    m.element.removeEventListener(n, mouseMove);
                });
                m.element.removeEventListener("contextmenu", preventDefault);
                m.element = m.callbacks = undefined;
                m.active = false;
            }
        }
        return mouse;
    })();

    function update(timer) { // Main update loop
        globalTime = timer;
        display(); // call demo code
        requestAnimationFrame(update);
    }
    setTimeout(function(){
        resizeCanvas();
        mouse.start(canvas, true);
        window.addEventListener("resize", resizeCanvas);
        requestAnimationFrame(update);
    },0);
})();
/** SimpleFullCanvasMouse.js end **/
// creates a blank image with 2d context
function createImage(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}

更新


  • 添加了更多评论。

  • 添加了 toScreen(x,y)函数来查看对象。从世界坐标转换为屏幕坐标。

  • 改进的旋转方法以设置绝对x轴方向。

  • 添加了带有指示器的旋转反馈,以显示旋转原点

  • 在帮助文本显示中显示比例。

  • Added many more comments.
  • Added toScreen(x,y) function to view object. Converts from world coordinates to screen coordinates.
  • Improved rotation method to set absolute x Axis direction.
  • Added rotation feed back with indicators to show rotation origin and the current x Axis direction and a red line to indicate new x Axis direction if mouse button down.
  • Showing scale in help text display.

这篇关于在旋转的CANVAS上绘制-第2部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆