Recentemente tenho trabalhado em um projeto relacionado ao processamento de imagens de páginas web, que pode ser considerado minha primeira experiência com canvas. Os requisitos do projeto incluem uma função para adicionar marcas d'água às imagens. Sabemos que a maneira usual de adicionar marcas d'água a imagens no navegador é usar o método drawImage
do canvas
. Para síntese comum (como a síntese de uma imagem base e uma imagem de marca d'água PNG), o princípio geral de implementação é o seguinte:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: imagem base // watermarkImg: imagem de marca d'água // x, y são as coordenadas de colocação de img na tela ctx. drawImage(img, x, y);ctx.drawImage(marca d'águaImg, x, y);
Basta usar drawImage()
direta e continuamente para desenhar a imagem correspondente na canvas
.
O texto acima é a introdução básica. Mas o que é um pouco problemático é que existe outra função que precisa ser implementada na necessidade de adicionar uma marca d'água, que é a de que o usuário pode mudar a posição da marca d'água. Naturalmente, pensamos se podemos implementar a função undo
de canvas
. Quando o usuário muda a posição da marca d'água, primeiro desfaça a operação drawImage
anterior e, em seguida, redesenhe a posição da imagem da marca d'água.
restore
/ save
?
A maneira mais eficiente e conveniente é verificar se canvas 2D
possui essa função. Depois de alguma pesquisa, o par de restore
/ save
de APIs apareceu. Vamos primeiro dar uma olhada nas descrições dessas duas APIs:
CanvasRenderingContext2D.restore() é o método da API Canvas 2D para restaurar a tela ao seu estado salvo mais recentemente, exibindo o estado superior na pilha de estados de desenho. Se não houver estado salvo, este método não fará alterações.
CanvasRenderingContext2D.save() é o método da API Canvas 2D para salvar todo o estado da tela, colocando o estado atual na pilha.
À primeira vista parece atender às necessidades. Vamos dar uma olhada no código de exemplo oficial:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // Salva o estado padrão ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Restaura o último estado padrão salvo ctx.fillRect(150, 75, 100, 100);
O resultado é mostrado abaixo:
Estranho, parece ser inconsistente com os resultados que esperávamos. O resultado que queremos é poder salvar um instantâneo da tela atual após chamar o método save
e poder retornar completamente ao estado do último instantâneo salvo após chamar o método resolve
.
Vamos dar uma olhada mais de perto na API. Acontece que perdemos um conceito importante: drawing state
, que é o estado de desenho. O estado do desenho salvo na pilha contém as seguintes partes:
Os valores atuais das seguintes propriedades: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
Bem, as alterações na tela após drawImage
não existem no estado draw. Portanto, usar resolve
/ save
não pode alcançar a função de desfazer que precisamos.
Como a pilha da API nativa para salvar o status do desenho não pode atender às necessidades, naturalmente pensaremos em simular nós mesmos uma pilha para salvar as operações. A questão que se segue é: Quais dados devem ser salvos na pilha após cada operação de desenho? Como mencionado antes, o que queremos é poder salvar um instantâneo da tela atual após cada operação de desenho. Se pudermos obter os dados do instantâneo e usar os dados do instantâneo para restaurar a tela, o problema será resolvido.
Felizmente, canvas 2D
fornece APIs nativamente para obter snapshots e restaurar o canvas por meio de snapshots - getImageData
/ putImageData
. A seguir está a descrição da API:
/* * @param { Número } sx A coordenada x do canto superior esquerdo da área retangular dos dados da imagem a ser extraída * @param { Número } sy A coordenada y do canto superior esquerdo da área retangular de dados de imagem a serem extraídos * @param { Número } sw Será a largura da área retangular dos dados de imagem a serem extraídos * @param { Número } sh A altura da área retangular de dados de imagem a serem extraídos * @return { Object } ImageData contém tela Dados de imagem retangulares */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } objeto imagedata contendo valores de pixel * @param { Number } dados de imagem de origem dx na tela de destino O deslocamento de posição (o deslocamento na direção do eixo x) * @param { Número } dy O deslocamento de posição dos dados da imagem de origem na tela de destino (o deslocamento na direção do eixo y) */ void ctx.putImageData(imagedata, dx, dy);
Vejamos uma aplicação simples:
class WrappedCanvas { construtor (canvas) { 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); (); this.ctx.putImageData(imgData, 0, 0);
Encapsulamos o método drawImage
de canvas
e salvamos um instantâneo do estado anterior na pilha simulada antes de cada chamada para esse método. Ao executar uma operação undo
, remova o último instantâneo salvo da pilha e redesenhe a tela para implementar a operação de desfazer. Os testes reais também atenderam às expectativas.
Na seção anterior, implementamos a função de desfazer do canvas
de maneira bastante aproximada. Por que você diz áspero? Uma razão óbvia é que esta solução tem um desempenho insatisfatório. Nossa solução equivale a redesenhar toda a tela todas as vezes. Supondo que haja muitas etapas de operação, salvaremos muitos dados de imagem pré-armazenados na pilha de simulação, que é a memória. Além disso, quando desenhar imagens é muito complexo, os dois métodos getImageData
e putImageData
causarão sérios problemas de desempenho. Há uma discussão detalhada sobre stackoverflow: Por que putImageData é tão lento? Também podemos verificar isso a partir dos dados deste caso de teste no jsperf. O Taobao FED também mencionou nas melhores práticas do Canvas que tentam não usar o método putImageData
em animações. Além disso, o artigo também mencionou que APIs com menor sobrecarga de renderização devem ser chamadas tanto quanto possível. Podemos começar a partir daqui para pensar em como otimizar.
Como mencionado anteriormente, registramos cada operação salvando um instantâneo de toda a tela. Pensando nisso de outro ângulo, se salvarmos cada ação de desenho em um array, cada vez que uma operação de desfazer for executada, a tela será primeiro limpa e depois. Redesenhar esta matriz de ação de desenho também pode implementar a função de desfazer a operação. Em termos de viabilidade, em primeiro lugar, isso pode reduzir a quantidade de dados salvos na memória e, em segundo lugar, evita o uso de putImageData
possui maior sobrecarga de renderização. Tomando drawImage
como objeto de comparação e observando este caso de teste no jsperf, há uma diferença de ordem de magnitude no desempenho entre os dois.
Portanto, acreditamos que esta solução de otimização é viável.
O método de aplicação aprimorado é aproximadamente o seguinte:
class WrappedCanvas { construtor (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { this.executionArray.push({método: 'drawImage', params: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height } desfazer () { if (this.executionArray. length > 0) { // Limpa a tela this.clearCanvas(); // Exclui a operação atual this.executionArray.pop(); (deixe exe de this.executionArray) { this[exe.method](...exe.params) } } }}
Se você é novo no canvas, aponte quaisquer erros ou deficiências. O texto acima é todo o conteúdo deste artigo. Espero que seja útil para o estudo de todos. Também espero que todos apoiem a Rede VeVb Wulin.