Draw on rotated CANVAS - Part 2

As an answer to this question ... I have another problem to solve:

When I draw on the canvas and then apply some transformations, such as rotation, I would like to save what was painted and continue drawing.

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

This is what I am trying, but the canvas is 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 (updated version of the original in a related question / answer)

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

+6
source share
1 answer

Record all paths, use storage buffer to maintain a smooth interface

, .

  • /s . , . , , , , , ( , ) ,

  • , , , . , , ( 64- ), . , ( webGL)

  • . , . . , , , , .

Demo

, , . . , , , , .

, . , , , , .

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;}
Hide result

UPDATE

  • .
  • ​​ toScreen(x,y) . .
  • x.
  • ​​ x , x, .
  • .
+6

Source: https://habr.com/ru/post/1016076/


All Articles