在旋转的CANVAS上绘制 - 第2部分

时间:2017-03-27 14:05:30

标签: javascript html5 canvas html5-canvas

作为对此question and answer的跟进......我还有另外一个问题需要解决:

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

要测试此项,请使用鼠标绘制内容,然后单击“旋转”。

这就是我正在尝试的,但画布会被删除。

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(链接问题/答案中原件的更新版本)

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

1 个答案:

答案 0 :(得分:6)

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

您有几个选项取决于要求是什么。

  1. 屏幕外缓冲区用于保存渲染线条。渲染到屏幕外缓冲区,然后将缓冲区绘制到显示画布。这是最快的方法,但你正在使用像素,因此如果你缩放你会得到像素伪像,它将限制绘图区域的大小(仍然很大但不是伪无限)并严重限制你可以提供的undos数量内存限制

  2. 绘制缓冲路径,基本记录鼠标移动和点击,然后在每次更新显示时重新渲染所有可见路径。这将让您在没有像素伪影的情况下进行缩放和旋转,为您提供任意大小的绘制区域(在64位双精度范围内)和奖励撤消一直回到第一行。这种方法的问题在于它很快变得非常慢(尽管你可以通过webGL提高渲染速度)

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

  4. 演示

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

    有一些样板来处理可以忽略的设置和鼠标。由于代码很多而且时间很短,因此评论很短,你必须从中挑选出你需要的东西。

    路径有paths个对象。 view包含转换和相关函数。一些平移,缩放,旋转功能。还有一个显示功能,可以呈现和处理所有鼠标和用户IO。通过按住鼠标修改器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轴方向。
    • 添加旋转反馈,其中指示器显示旋转原点和当前x轴方向,红色线表示鼠标按钮按下时的新x轴方向。
    • 在帮助文本显示中显示比例。