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