Récemment, j'ai travaillé sur un projet lié au traitement d'images de pages Web, qui peut être considéré comme ma première expérience avec Canvas. Les exigences du projet incluent une fonction permettant d'ajouter des filigranes aux images. Nous savons que la manière habituelle d'implémenter la fonction d'ajout de filigranes aux images côté navigateur est d'utiliser la méthode drawImage
de canvas
. Pour une synthèse ordinaire (telle que la synthèse d'une image de base et d'une image de filigrane PNG), le principe général de mise en œuvre est le suivant :
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img : image de base // watermarkImg : image en filigrane // x, y sont les coordonnées de placement d'img sur le canevas ctx. drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
Utilisez simplement drawImage()
directement et continuellement pour dessiner l'image correspondante sur canvas
.
Ce qui précède est l’introduction de base. Mais ce qui est un peu gênant, c'est qu'il existe une autre fonction qui doit être implémentée dans l'exigence d'ajout d'un filigrane, à savoir que l'utilisateur peut changer la position du filigrane. Nous réfléchissons naturellement à la possibilité d'implémenter la fonction undo
de canvas
. Lorsque l'utilisateur change la position du filigrane, annulez d'abord l'opération drawImage
précédente, puis redessinez la position de l'image du filigrane.
restore
/ save
?
Le moyen le plus efficace et le plus pratique consiste à vérifier si canvas 2D
dispose de cette fonction. Après quelques recherches, la paire d'API restore
/ save
est apparue. Jetons d’abord un coup d’œil aux descriptions de ces deux API :
CanvasRenderingContext2D.restore() est la méthode API Canvas 2D permettant de restaurer le canevas à son état le plus récemment enregistré en faisant apparaître l'état supérieur dans la pile d'état de dessin. S’il n’y a aucun état enregistré, cette méthode n’apporte aucune modification.
CanvasRenderingContext2D.save() est la méthode de l'API Canvas 2D pour enregistrer l'intégralité de l'état du canevas en plaçant l'état actuel dans la pile.
À première vue, cela semble répondre aux besoins. Jetons un coup d'œil à l'exemple de code officiel :
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // Enregistre l'état par défaut ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Restaurer le dernier état par défaut enregistré ctx.fillRect(150, 75, 100, 100 );
Le résultat est présenté ci-dessous :
Étrangement, cela semble incompatible avec les résultats attendus. Le résultat que nous souhaitons est de pouvoir enregistrer un instantané du canevas actuel après avoir appelé la méthode save
, et de pouvoir revenir complètement à l'état du dernier instantané enregistré après avoir appelé la méthode resolve
.
Examinons de plus près l'API. Il s'avère que nous avons manqué un concept important : drawing state
, qui est l'état de dessin. L'état du dessin enregistré dans la pile contient les éléments suivants :
Les valeurs actuelles des propriétés suivantes : StrokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
Eh bien, les modifications apportées au canevas après drawImage
n'existent pas du tout dans l'état de dessin. Par conséquent, l’utilisation resolve
/ save
ne peut pas obtenir la fonction d’annulation dont nous avons besoin.
La pile de sauvegarde de l'état des dessins de l'API native ne pouvant pas répondre aux besoins, nous penserons naturellement à simuler nous-mêmes une pile de sauvegarde des opérations. La question qui suit est : Quelles données doivent être sauvegardées sur la pile après chaque opération de dessin ? Comme mentionné précédemment, ce que nous voulons, c'est pouvoir enregistrer un instantané du canevas actuel après chaque opération de dessin. Si nous pouvons obtenir les données de l'instantané et utiliser les données de l'instantané pour restaurer le canevas, le problème sera résolu.
Heureusement, canvas 2D
fournit nativement des API pour obtenir des instantanés et restaurer le canevas via des instantanés - getImageData
/ putImageData
. Voici la description de l'API :
/* * @param { Number } sx Coordonnée x du coin supérieur gauche de la zone rectangulaire des données d'image à extraire * @param { Number } sy Coordonnée y du coin supérieur gauche de la zone rectangulaire de données d'image à extraire * @param { Number } sw Sera La largeur de la zone rectangulaire des données d'image à extraire * @param { Number } sh La hauteur de la zone rectangulaire des données d'image à extraire * @return { Object } ImageData contient un canevas Étant donné des données d'image rectangulaires */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } objet imagedata contenant des valeurs de pixels * @param { Number } données d'image source dx dans le canevas cible Le décalage de position (le décalage dans la direction de l'axe x) * @param { Number } dy Le décalage de position des données d'image source dans le canevas cible (le décalage dans la direction de l'axe y) */ void ctx.putImageData(imagedata, dx, dy);
Regardons une application simple :
class WrappedCanvas { constructeur (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); } annuler () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0);
Nous encapsulons la méthode drawImage
de canvas
et enregistrons un instantané de l'état précédent dans la pile simulée avant chaque appel à cette méthode. Lorsque vous effectuez une opération undo
, supprimez le dernier instantané enregistré de la pile, puis redessinez le canevas pour implémenter l'opération d'annulation. Les tests réels ont également répondu aux attentes.
Dans la section précédente, nous avons implémenté très grossièrement la fonction d'annulation de canvas
. Pourquoi tu dis dur ? Une raison évidente est que cette solution fonctionne mal. Notre solution équivaut à redessiner la toile entière à chaque fois. En supposant qu'il existe de nombreuses étapes opérationnelles, nous enregistrerons de nombreuses données d'image pré-stockées dans la pile de simulation, qui est la mémoire. De plus, lorsque dessiner des images est trop complexe, les deux méthodes getImageData
et putImageData
entraîneront de sérieux problèmes de performances. Il y a une discussion détaillée sur stackoverflow : Pourquoi putImageData est-il si lent ? Nous pouvons également le vérifier à partir des données de ce scénario de test sur jsperf. Taobao FED a également mentionné dans Canvas les meilleures pratiques qui tentent de ne pas utiliser la méthode putImageData
dans les animations. En outre, l'article mentionne également que les API avec une surcharge de rendu inférieure doivent être appelées autant que possible. Nous pouvons commencer à partir de là pour réfléchir à la manière d’optimiser.
Comme mentionné précédemment, nous enregistrons chaque opération en enregistrant un instantané de l'ensemble du canevas. En y réfléchissant sous un autre angle, si nous enregistrons chaque action de dessin dans un tableau, chaque fois qu'une opération d'annulation est effectuée, le canevas est d'abord effacé, puis. Redessiner ce tableau d'actions de dessin peut également implémenter la fonction d'annulation de l'opération. En termes de faisabilité, tout d’abord, cela peut réduire la quantité de données enregistrées en mémoire, et deuxièmement, cela évite l’utilisation de putImageData
entraîne une surcharge de rendu plus élevée. En prenant drawImage
comme objet de comparaison et en regardant ce cas de test sur jsperf, il existe une différence de performance d'un ordre de grandeur entre les deux.
Nous pensons donc que cette solution d’optimisation est réalisable.
La méthode d’application améliorée est à peu près la suivante :
class WrappedCanvas { constructeur (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { this.executionArray.push({ méthode : 'drawImage', params : params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); undo() { if (this.executionArray. length > 0) { // Effacer le canevas this.clearCanvas(); // Supprimer l'opération en cours this.executionArray.pop(); // Exécuter les actions de dessin une par une pour redessiner (laisser l'exe de this.executionArray) { this[exe.method](...exe.params) } } }}
Si vous êtes nouveau sur Canvas, veuillez signaler toute erreur ou lacune. Ce qui précède représente l’intégralité du contenu de cet article. J’espère qu’il sera utile à l’étude de chacun. J’espère également que tout le monde soutiendra le réseau VeVb Wulin.