Recientemente he estado trabajando en un proyecto relacionado con el procesamiento de imágenes de páginas web, que puede considerarse como mi primera experiencia con el lienzo. Los requisitos del proyecto incluyen una función para agregar marcas de agua a las imágenes. Sabemos que la forma habitual de agregar marcas de agua a imágenes en el lado del navegador es utilizar el método de canvas
drawImage
. Para la síntesis ordinaria (como la síntesis de una imagen base y una imagen de marca de agua PNG), el principio de implementación general es el siguiente:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: imagen base // watermarkImg: imagen de marca de agua // x, y son las coordenadas para colocar img en el lienzo ctx. drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
Simplemente use drawImage()
directa y continuamente para dibujar la imagen correspondiente en canvas
.
Lo anterior es la introducción de antecedentes. Pero lo que es un poco problemático es que hay otra función que debe implementarse cuando es necesario agregar una marca de agua, que es que el usuario puede cambiar la posición de la marca de agua. Naturalmente, pensamos si podemos implementar la función undo
del canvas
. Cuando el usuario cambia la posición de la marca de agua, primero deshaga la operación drawImage
anterior y luego vuelva a dibujar la posición de la imagen de la marca de agua.
restore
/ save
?
La forma más eficiente y conveniente es verificar si canvas 2D
tiene esta función. Después de algunas búsquedas, apareció el par de API restore
/ save
. Primero echemos un vistazo a las descripciones de estas dos API:
CanvasRenderingContext2D.restore() es el método API de Canvas 2D para restaurar el lienzo a su estado guardado más recientemente al abrir el estado superior en la pila de estados de dibujo. Si no hay ningún estado guardado, este método no realiza cambios.
CanvasRenderingContext2D.save() es el método de la API Canvas 2D para guardar todo el estado del lienzo colocando el estado actual en la pila.
A primera vista parece satisfacer las necesidades. Echemos un vistazo al código de muestra oficial:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // Guarda el estado predeterminado ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Restaurar al último estado predeterminado guardado ctx.fillRect(150, 75, 100, 100);
El resultado se muestra a continuación:
Es extraño, pero parece inconsistente con los resultados que esperábamos. El resultado que queremos es poder guardar una instantánea del lienzo actual después de llamar al método save
y poder volver completamente al estado de la última instantánea guardada después de llamar al método resolve
.
Echemos un vistazo más de cerca a la API. Resulta que nos perdimos un concepto importante: drawing state
, que es el estado de dibujo. El estado del dibujo guardado en la pila contiene las siguientes partes:
Los valores actuales de las siguientes propiedades: StrokeStyle, FillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, ShadowOffsetX, ShadowOffsetY, ShadowBlur, ShadowColor, globalCompositeOperation, Font, TextAlign, TextBaseline, Direction, ImageSmoothingEnabled.
Bueno, los cambios en el lienzo después de drawImage
no existen en absoluto en el estado de dibujo. Por lo tanto, usar resolve
/ save
no puede lograr la función de deshacer que necesitamos.
Dado que la pila de la API nativa para guardar el estado del dibujo no puede satisfacer las necesidades, naturalmente pensaremos en simular una pila para guardar las operaciones nosotros mismos. La pregunta que sigue es: ¿Qué datos se deben guardar en la pila después de cada operación de dibujo? Como se mencionó anteriormente, lo que queremos es poder guardar una instantánea del lienzo actual después de cada operación de dibujo. Si podemos obtener los datos de la instantánea y utilizarlos para restaurar el lienzo, el problema se resolverá.
Afortunadamente, canvas 2D
proporciona de forma nativa API para obtener instantáneas y restaurar el lienzo a través de instantáneas: getImageData
/ putImageData
. La siguiente es la descripción de la API:
/* * @param { Número } sx La coordenada x de la esquina superior izquierda del área rectangular de datos de imagen que se extraerán * @param { Número } sy La coordenada y de la esquina superior izquierda del área rectangular de datos de imagen que se extraerán * @param { Number } sw Será El ancho del área rectangular de datos de imagen que se extraerán * @param { Number } sh La altura del área rectangular de datos de imagen que se extraerán * @return {Objeto} ImageData contiene lienzo Datos de imagen rectangulares dados */ ImageData ctx.getImageData(sx, sy, sw, sh); desplazamiento de posición (el desplazamiento en la dirección del eje x) * @param { Número } dy El desplazamiento de posición de los datos de la imagen de origen en el lienzo de destino (el desplazamiento en la dirección del eje y) */ void ctx.putImageData(imagedata, dx, dy);
Veamos una aplicación sencilla:
clase WrappedCanvas { constructor (lienzo) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { const imgData = this.ctx.getImageData(0, 0, this.width, this.height); this.imgStack.push(imgData);this.ctx.drawImage(...params); } deshacer () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0);
Encapsulamos el método drawImage
de canvas
y guardamos una instantánea del estado anterior en la pila simulada antes de cada llamada a este método. Al realizar una operación undo
, elimine la última instantánea guardada de la pila y luego vuelva a dibujar el lienzo para implementar la operación de deshacer. Las pruebas reales también cumplieron con las expectativas.
En la sección anterior, implementamos la función de deshacer del canvas
de manera muy aproximada. ¿Por qué dices duro? Una razón obvia es que esta solución funciona mal. Nuestra solución equivale a volver a dibujar todo el lienzo cada vez. Suponiendo que hay muchos pasos de operación, guardaremos una gran cantidad de datos de imágenes previamente almacenados en la pila de simulación, que es la memoria. Además, cuando dibujar imágenes es demasiado complejo, los dos métodos getImageData
y putImageData
causarán graves problemas de rendimiento. Hay una discusión detallada sobre stackoverflow: ¿Por qué putImageData es tan lento? También podemos verificar esto a partir de los datos de este caso de prueba en jsperf. Taobao FED también mencionó en Canvas las mejores prácticas que intentan no utilizar el método putImageData
en las animaciones. Además, el artículo también menciona que las API con menor sobrecarga de renderizado deben llamarse tanto como sea posible. Podemos empezar desde aquí a pensar en cómo optimizar.
Como se mencionó anteriormente, registramos cada operación guardando una instantánea de todo el lienzo. Pensándolo desde otro ángulo, si guardamos cada acción de dibujo en una matriz, cada vez que se realiza una operación de deshacer, el lienzo primero se borra y luego. Volver a dibujar esta matriz de acciones de dibujo también puede implementar la función de deshacer la operación. En términos de viabilidad, en primer lugar, esto puede reducir la cantidad de datos guardados en la memoria y, en segundo lugar, evita el uso de putImageData
tiene una mayor sobrecarga de renderizado. Tomando drawImage
como objeto de comparación y observando este caso de prueba en jsperf, hay una diferencia de orden de magnitud en el rendimiento entre los dos.
Por tanto, creemos que esta solución de optimización es factible.
El método de aplicación mejorado es aproximadamente el siguiente:
clase WrappedCanvas { constructor (lienzo) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; this.executionArray = [ ]; } drawImage (...params) { this.executionArray.push({ método: 'drawImage', params: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height } deshacer () { if (this.executionArray. length > 0) { // Limpiar el lienzo this.clearCanvas(); // Eliminar la operación actual this.executionArray.pop() // Ejecutar las acciones de dibujo una por una para volver a dibujarlas (deje exe de this.executionArray) { this[exe.method](...exe.params) } } }}
Si es nuevo en Canvas, indique cualquier error o deficiencia. Lo anterior es el contenido completo de este artículo. Espero que sea útil para el estudio de todos. También espero que todos apoyen VeVb Wulin Network.