Kürzlich habe ich an einem Projekt zur Bildverarbeitung von Webseiten gearbeitet, das als meine erste Erfahrung mit Canvas angesehen werden kann. Zu den Projektanforderungen gehört eine Funktion zum Hinzufügen von Wasserzeichen zu Bildern. Wir wissen, dass die übliche Methode zum Hinzufügen von Wasserzeichen zu Bildern auf der Browserseite die Verwendung der Methode drawImage
von canvas
ist. Für die normale Synthese (z. B. die Synthese eines Basisbilds und eines PNG-Wasserzeichenbilds) lautet das allgemeine Implementierungsprinzip wie folgt:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: Basisbild // watermarkImg: Wasserzeichenbild // x, y sind die Koordinaten der Platzierung von img auf der Leinwand ctx. drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
Verwenden Sie einfach drawImage()
direkt und kontinuierlich, um das entsprechende Bild auf canvas
zu zeichnen.
Das Obige ist die Hintergrundeinführung. Was jedoch ein wenig problematisch ist, ist, dass beim Hinzufügen eines Wasserzeichens eine weitere Funktion implementiert werden muss, nämlich dass der Benutzer die Position des Wasserzeichens ändern kann. Wir denken natürlich darüber nach, ob wir die undo
-Funktion von canvas
implementieren können. Wenn der Benutzer die Position des Wasserzeichens ändert, machen Sie zuerst den vorherigen drawImage
-Vorgang rückgängig und zeichnen Sie dann die Position des Wasserzeichenbilds neu.
restore
/ save
?
Der effizienteste und bequemste Weg besteht darin, zu überprüfen, ob canvas 2D
über diese Funktion verfügt. Nach einigem Suchen wurde das API-Paar „ restore
/ save
angezeigt. Werfen wir zunächst einen Blick auf die Beschreibungen dieser beiden APIs:
CanvasRenderingContext2D.restore() ist die Canvas-2D-API-Methode, um den Canvas in seinem zuletzt gespeicherten Zustand wiederherzustellen, indem der oberste Status im Zeichnungsstatusstapel angezeigt wird. Wenn kein gespeicherter Status vorhanden ist, nimmt diese Methode keine Änderungen vor.
CanvasRenderingContext2D.save() ist die Methode der Canvas 2D-API, um den gesamten Zustand der Leinwand zu speichern, indem der aktuelle Zustand in den Stapel gelegt wird.
Auf den ersten Blick scheint es den Bedürfnissen gerecht zu werden. Werfen wir einen Blick auf den offiziellen Beispielcode:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // Standardzustand speichern ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //Zuletzt gespeicherten Standardzustand wiederherstellen ctx.fillRect(150, 75, 100, 100);
Das Ergebnis ist unten dargestellt:
Seltsamerweise scheint es nicht mit den von uns erwarteten Ergebnissen übereinzustimmen. Das gewünschte Ergebnis besteht darin, nach dem Aufruf der save
-Methode einen Snapshot der aktuellen Leinwand speichern zu können und nach dem Aufruf der resolve
-Methode vollständig zum Status des zuletzt gespeicherten Snapshots zurückkehren zu können.
Schauen wir uns die API genauer an. Es stellt sich heraus, dass wir ein wichtiges Konzept übersehen haben: drawing state
, also den Zeichnungszustand. Der im Stapel gespeicherte Zeichnungsstatus enthält die folgenden Teile:
Die aktuellen Werte der folgenden Eigenschaften: StrokeStyle, FillStyle, GlobalAlpha, LineWidth, LineCap, LineJoin, MiterLimit, LineDashOffset, ShadowOffsetX, ShadowOffsetY, ShadowBlur, ShadowColor, GlobalCompositeOperation, Font, TextAlign, TextBaseline, Direction, ImageSmoothingEnabled.
Nun, die Änderungen an der Leinwand nach drawImage
-Vorgang sind im Zeichenzustand überhaupt nicht vorhanden. Daher kann die Verwendung von resolve
/ save
nicht die von uns benötigte Rückgängig-Funktion erreichen.
Da der Stack der nativen API zum Speichern des Zeichnungsstatus die Anforderungen nicht erfüllen kann, denken wir natürlich darüber nach, selbst einen Stack zum Speichern von Vorgängen zu simulieren. Die folgende Frage lautet: Welche Daten sollen nach jedem Zeichenvorgang auf dem Stapel gespeichert werden? Wie bereits erwähnt, möchten wir nach jedem Zeichenvorgang einen Schnappschuss der aktuellen Leinwand speichern können. Wenn wir die Schnappschussdaten abrufen und die Schnappschussdaten zum Wiederherstellen der Leinwand verwenden können, ist das Problem gelöst.
Glücklicherweise bietet canvas 2D
nativ APIs zum Abrufen von Snapshots und zum Wiederherstellen des Canvas über Snapshots – getImageData
/ putImageData
. Das Folgende ist die API-Beschreibung:
/* * @param { Number } sx Die x-Koordinate der oberen linken Ecke des rechteckigen Bereichs der zu extrahierenden Bilddaten. * @param { Number } sy Die y-Koordinate der oberen linken Ecke des rechteckigen Bereichs von Zu extrahierende Bilddaten * @param { Number } sw Wird sein Die Breite des rechteckigen Bereichs der zu extrahierenden Bilddaten * @param { Number } sh Die Höhe des rechteckigen Bereichs der zu extrahierenden Bilddaten * @return { Object } ImageData enthält Canvas Gegebene rechteckige Bilddaten */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } imagedata-Objekt mit Pixelwerten * @param { Number } dx Quellbilddaten in der Zielleinwand Positionsversatz (der Versatz in Richtung der x-Achse) * @param { Number } dy Der Positionsversatz der Quellbilddaten im Ziel-Canvas (der Versatz in Richtung der y-Achse) */ void ctx.putImageData(imagedata, dx, dy);
Schauen wir uns eine einfache Anwendung an:
class WrappedCanvas { constructionor (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width = 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); } undo () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0);
Wir kapseln die Methode drawImage
von canvas
und speichern vor jedem Aufruf dieser Methode einen Schnappschuss des vorherigen Zustands im simulierten Stapel. Wenn Sie einen undo
-Vorgang durchführen, entfernen Sie den zuletzt gespeicherten Snapshot aus dem Stapel und zeichnen Sie dann die Leinwand neu, um den Rückgängig-Vorgang zu implementieren. Auch die tatsächlichen Tests entsprachen den Erwartungen.
Im vorherigen Abschnitt haben wir die Rückgängig-Funktion von canvas
sehr grob implementiert. Warum sagst du grob? Ein offensichtlicher Grund ist, dass diese Lösung schlecht funktioniert. Unsere Lösung ist gleichbedeutend damit, die gesamte Leinwand jedes Mal neu zu zeichnen. Unter der Annahme, dass es viele Betriebsschritte gibt, speichern wir viele vorab gespeicherte Bilddaten im Simulationsstapel, dem Speicher. Wenn das Zeichnen von Bildern zu komplex ist, führen die beiden Methoden getImageData
und putImageData
außerdem zu ernsthaften Leistungsproblemen. Es gibt eine ausführliche Diskussion zum Stackoverflow: Warum ist putImageData so langsam? Wir können dies auch anhand der Daten dieses Testfalls auf jsperf überprüfen. Taobao FED erwähnte in den Canvas-Best Practices auch, dass versucht wird, die putImageData
-Methode in Animationen nicht zu verwenden. Darüber hinaus wurde in dem Artikel auch erwähnt, dass APIs mit geringerem Rendering-Overhead so oft wie möglich aufgerufen werden sollten. Von hier aus können wir darüber nachdenken, wie wir optimieren können.
Wie bereits erwähnt, zeichnen wir jeden Vorgang auf, indem wir einen Schnappschuss der gesamten Leinwand speichern. Wenn wir jede Zeichenaktion in einem Array speichern, wird die Leinwand bei jedem Rückgängigmachen zuerst gelöscht und dann Durch das Neuzeichnen dieses Zeichnungsaktionsarrays kann auch die Funktion zum Rückgängigmachen des Vorgangs implementiert werden. Was die Machbarkeit betrifft, kann dies erstens die im Speicher gespeicherte Datenmenge reduzieren und zweitens die Verwendung von putImageData
einen höheren Rendering-Overhead verursacht. Nimmt man drawImage
als Vergleichsobjekt und betrachtet diesen Testfall auf jsperf, so gibt es einen Leistungsunterschied zwischen den beiden um eine Größenordnung.
Daher glauben wir, dass diese Optimierungslösung machbar ist.
Die verbesserte Anwendungsmethode sieht ungefähr wie folgt aus:
class WrappedCanvas { constructionor (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width = this.ctx.canvas.height; ]; } drawImage (...params) { this.executionArray.push({ method: 'drawImage', params: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); } undo () { if (this.executionArray. length > 0) { // Löschen Sie die Leinwand this.clearCanvas(); // Löschen Sie die aktuelle Operation this.executionArray.pop(); Führen Sie die Zeichenaktionen nacheinander aus, um sie neu zu zeichnen (let exe of this.executionArray) { this[exe.method](...exe.params) } } }}
Wenn Sie neu bei Canvas sind, weisen Sie uns bitte auf eventuelle Fehler oder Mängel hin. Das Obige ist der gesamte Inhalt dieses Artikels. Ich hoffe, dass er für das Studium aller hilfreich ist. Ich hoffe auch, dass jeder das VeVb Wulin Network unterstützt.