Búsqueda de sitios web

javascript - Dibujar en CANVAS rotado - Parte 2


Como seguimiento a esta pregunta y respuesta... tengo otro problema que resolver:

Cuando dibujo en un lienzo y luego aplico algunas transformaciones como la rotación, me gustaría mantener lo que se dibujó y continuar el dibujo.

Para probar esto, usa el ratón para dibujar algo y luego haz clic en "rotar".

Esto es lo que estoy intentando, pero el lienzo se borra.

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 (versión actualizada del original en la pregunta/respuesta enlazada)

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

Respuesta #1

Graba todos los caminos, usa el búfer del lienzo para mantener la interfaz fluida

Tienes varias opciones que dependerán de cuáles sean los requisitos.

  1. Búfer(es) fuera de pantalla para almacenar las líneas renderizadas. Renderiza en el búfer fuera de pantalla y luego dibuja el búfer en el lienzo de visualización. Este es el método más rápido, pero estás trabajando con píxeles, por lo que si haces zoom obtendrás artefactos de píxeles y se limitará el tamaño del área de dibujo (todavía grande pero no pseudo infinito) y restringirá severamente la cantidad de deshacer que puedes proporcionar debido a los límites de memoria.

  2. Almacena en búfer los trazos a medida que se dibujan, básicamente registrando los movimientos y clics del ratón, y luego volviendo a renderizar todos los trazos visibles cada vez que actualizas la pantalla. Esto te permitirá hacer zoom y rotar sin artefactos de píxeles, te dará un área de dibujo tan grande como desees (dentro del límite de los dobles de 64 bits) y un bono de deshacer hasta llegar a la primera línea. El problema con este método es que rápidamente se vuelve muy lento (aunque puedes mejorar la velocidad de renderizado con webGL).

  3. Una combinación de los dos métodos anteriores. Registra los caminos a medida que se dibujan, pero también represéntalos en un lienzo/s fuera de pantalla. Utiliza el lienzo fuera de pantalla para actualizar la pantalla y mantener alta la tasa de refresco. Solo necesitas volver a renderizar el lienzo fuera de pantalla cuando sea necesario, es decir, cuando deshagas una acción o si haces zoom; no necesitarás volver a renderizar al desplazar o rotar.

Demostración

No voy a hacer un paquete completo de dibujo, así que esto es solo un ejemplo que utiliza un búfer fuera de pantalla para contener las rutas visibles. Todas las rutas que se dibujan se registran en un arreglo de rutas. Cuando el usuario cambia la vista, desplaza, hace zoom o rota, las rutas se vuelven a dibujar en el lienzo fuera de pantalla para coincidir con la nueva vista.

Hay algo de código estándar para manejar la configuración y el ratón que se puede ignorar. Dado que hay mucho código y el tiempo es corto, tendrás que seleccionar lo que necesitas, ya que los comentarios son breves.

Hay un objeto paths para rutas. view contiene la transformación y las funciones asociadas. Algunas funciones para desplazar, hacer zoom y rotar. Y una función de visualización que renderiza y maneja toda la entrada del ratón y del usuario. Los controles de desplazamiento, zoom y escala se acceden manteniendo presionados los modificadores del ratón 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;}

ACTUALIZACIÓN

  • Agregué muchos más comentarios.
  • Se añadió la función toScreen(x,y) para ver el objeto. Convierte de coordenadas del mundo a coordenadas de pantalla.
  • Método de rotación mejorado para establecer la dirección absoluta del eje x.
  • Se agregó retroalimentación de rotación con indicadores para mostrar el origen de la rotación y la dirección actual del eje X, y una línea roja para indicar la nueva dirección del eje X si el botón del ratón está presionado.
  • Mostrando la escala en la visualización del texto de ayuda.