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

标签 javascript html canvas html5-canvas

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

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

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

这就是我正在尝试的,但是 Canvas 被删除了。

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/

最佳答案

记录所有路径,使用canvas buffer保持界面流畅

您有多种选择,具体取决于要求。

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

  2. 在绘制路径时缓冲路径​​,基本上记录鼠标移动和点击,然后在每次更新显示时重新渲染所有可见路径。这将使您可以缩放和旋转而不会出现像素伪影,为您提供任意大的绘制区域(在 64 位 double 限制内)以及一直返回到第一行的奖励撤消。这种方法的问题是它很快就会变得非常慢(尽管您可以使用 webGL 提高渲染速度)

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

演示

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

有一些可以忽略的样板来处理设置和鼠标。由于代码很多而且时间很短,您必须从中挑选出您需要的内容,因为注释很短。

路径有一个 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 轴方向。
  • 在帮助文本显示中显示比例。

关于javascript - 在旋转的 CANVAS 上绘图 - 第 2 部分,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43048634/

相关文章:

javascript - AddRegions 不是函数

Javascript 一次打印一个输出而不是一次打印整个页面?

html - 正文溢出时无法滚动 : hidden and an fixed element

html - Flexbox 不随内容扩展

javascript - 如何在 fabricjs 中向后发送和向前发送一个活跃的组

javascript - 使用 HTML5 canvas 生成动画 GIF

javascript - 获取 Lodash 中排序属性的键

javascript - 如何向 jquery 过滤器添加全选选项

javascript - 如何使用canvas js使图像上的多边形可点击?

javascript - Jest 遇到了意外的标记 - 用 jest 和 enzyme 进行 React