Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw on rotated CANVAS - Part 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 (updated version of original in linked question/answer)

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

like image 294
Matías Cánepa Avatar asked Mar 27 '17 14:03

Matías Cánepa


People also ask

How do I rotate the image on my Canvas?

The image on your canvas will be rotated by that angle. Press and hold the Shift + Space keys and then click and drag the image to rotate it in whichever direction you want. Alternatively, press the “ 4 ” key to rotate it in 15-degree increments counter-clockwise or “ 6 ” to rotate it in 15-degree increments clockwise.

How do I use HTML5 canvas to draw?

Using HTML5 Canvas effectively requires a strong foundation in drawing, coloring, and transforming basic two-dimensional shapes. While the selection of built-in shapes is relatively limited, we can draw any shape we desire using a series of line segments called paths , which we will discuss in the upcoming section Using Paths to Create Lines.

How do I draw rectangles in canvas?

On Canvas, basic rectangle shapes can be drawn in three different ways: filling, stroking, or clearing. We can also build rectangles (or any other shape) by using paths, which we will cover in the next section. First, let’s look at the API functions used for these three operations:

How many radians does it take to rotate an image?

Well, the entire co-ordinate system rotated by 0.5 radians (roughly 30º) around the top-left corner of the canvas before we drew the image. So if you think about it, 50 across and 35 down isn’t the same as place as it used to be. So how do we rotate our image and keep it in the same place?


1 Answers

Record all paths, use canvas buffer to keep interface smooth

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

  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

  2. 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)

  3. 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

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.

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

UPDATE

  • 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.
like image 61
Blindman67 Avatar answered Oct 14 '22 22:10

Blindman67