最近は Web ページの画像処理に関するプロジェクトに取り組んでおり、これが Canvas の初体験とも言えます。プロジェクト要件には、画像にウォーターマークを追加する機能が含まれます。ブラウザ側で画像に透かしを追加する通常の方法は、 canvasのdrawImageメソッドを使用することです。通常の合成 (ベース画像と PNG 透かし画像の合成など) の場合、一般的な実装原理は次のとおりです。
var Canvas = document.getElementById(canvas);var ctx = Canvas.getContext('2d');// img: ベース画像 // WatermarkImg: ウォーターマーク画像 // x, y は、img をキャンバス ctx 上に配置する座標です。 drawImage(img, x, y);ctx.drawImage(watermarkImg, x, y); drawImage()直接継続的に使用して、対応する画像をcanvasに描画するだけです。
以上が背景の紹介です。ただし、少し面倒なのは、ウォーターマークを追加する必要がある場合に、ユーザーがウォーターマークの位置を切り替えることができる別の機能を実装する必要があることです。 canvasのundo機能を実装できないかということは当然考えられます。ユーザーがウォーターマークの位置を切り替えたとき、まず以前のdrawImage操作を取り消してから、ウォーターマーク画像の位置を再描画します。
restore / save ?
最も効率的で便利な方法は、 canvas 2Dネイティブ API にこの機能があるかどうかを確認することです。いくつか検索した結果、 restore / saveの API のペアが見えてきました。まず、これら 2 つの API の説明を見てみましょう。
CanvasRenderingContext2D.restore() は、描画状態スタックの最上位の状態をポップすることで、キャンバスを最後に保存された状態に復元する Canvas 2D API メソッドです。 保存された状態がない場合、このメソッドは変更を加えません。
CanvasRenderingContext2D.save() は、現在の状態をスタックに入れることでキャンバス全体の状態を保存する Canvas 2D API のメソッドです。
一見、ニーズを満たしているように見えます。公式のサンプルコードを見てみましょう。
var Canvas = document.getElementById(canvas);var ctx = Canvas.getContext(2d);ctx.save(); // デフォルト状態を保存 ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //最後に保存したデフォルト状態に戻す ctx.fillRect(150, 75, 100, 100);
結果を以下に示します。
奇妙なことに、それは私たちが期待した結果と矛盾しているようです。私たちが望む結果は、 saveメソッドを呼び出した後に現在のキャンバスのスナップショットを保存できることと、 resolveメソッドを呼び出した後に最後に保存されたスナップショットの状態に完全に戻ることができることです。
API を詳しく見てみましょう。描画drawing stateである描画状態という重要な概念を見逃していたことがわかりました。スタックに保存される描画状態には次の部分が含まれます。
次のプロパティの現在値: ストロークスタイル、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、lineDashOffset、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、globalCompositeOperation、font、textAlign、textBaseline、direction、imageSmoothingEnabled。
そうですね、 drawImage操作後のキャンバスへの変更は、描画状態にはまったく存在しません。したがって、 resolve / save使用しても、必要な元に戻す機能を実現できません。
ネイティブ API の描画状態保存スタックでは対応できないため、当然、保存操作のスタックを自分でシミュレートすることを考えます。次の質問は、各描画操作の後にどのデータをスタックに保存する必要があるかということです。前に述べたように、描画操作のたびに現在のキャンバスのスナップショットを保存できるようにする必要があります。スナップショット データを取得し、そのスナップショット データを使用してキャンバスを復元できれば、問題は解決されます。
幸いなことに、 canvas 2D 、スナップショットを取得し、スナップショットを通じてキャンバスを復元するための API ( getImageData / putImageDataをネイティブに提供します。 API の説明は次のとおりです。
/* * @param { Number } sx 抽出する画像データの矩形領域の左上隅の x 座標 * @param { Number } sy 抽出する画像データの矩形領域の左上隅の y 座標抽出する画像データ * @param { Number } sw 抽出する画像データの矩形領域の幅 * @param { Number } sh 抽出する画像データの矩形領域の高さ* @return { Object } ImageData にはキャンバスが含まれます与えられた長方形の画像データ */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } ピクセル値を含む imagedata オブジェクト * @param { Number } ターゲット キャンバス内の dx ソース画像データPosition offset (x 軸方向のオフセット) * @param { Number } dy ターゲット キャンバス内のソース画像データの位置オフセット (y 軸方向のオフセット) */ void ctx.putImageData(画像データ, dx, dy);簡単なアプリケーションを見てみましょう。
class WrappedCanvas { コンストラクター (キャンバス) { this.ctx = Canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.imgStack = [ ]; }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); canvasのdrawImageメソッドをカプセル化し、このメソッドを呼び出すたびに、以前の状態のスナップショットをシミュレートされたスタックに保存します。 undo操作を実行する場合は、最後に保存されたスナップショットをスタックから削除し、キャンバスを再描画して元に戻す操作を実装します。実際のテストも期待通りでした。
前節では、 canvasの undo 機能を非常に大まかに実装しました。なぜ荒いと言われるのですか?明らかな理由の 1 つは、このソリューションのパフォーマンスが低いことです。私たちの解決策は、毎回キャンバス全体を再描画することに相当します。操作ステップが多いことを想定して、事前に保存された大量の画像データをシミュレーション スタック (メモリ) に保存します。さらに、画像の描画が複雑すぎる場合、 getImageDataとputImageData 2 つのメソッドによって重大なパフォーマンスの問題が発生します。 stackoverflow については、「 putImageData が遅いのはなぜですか? 」で詳細に説明されています。これは、jsperf 上のこのテスト ケースのデータからも確認できます。 Taobao FED は、アニメーションでputImageDataメソッドを使用しないようにする Canvas のベスト プラクティスにも言及しています。さらに、この記事では、レンダリングのオーバーヘッドが低い API をできるだけ呼び出す必要があるとも述べられています。ここから最適化する方法を考えてみましょう。
前述したように、キャンバス全体のスナップショットを保存することで各操作を記録します。別の角度から考えると、各描画アクションを配列に保存すると、元に戻す操作が実行されるたびに、最初にキャンバスがクリアされ、次にクリアされます。この描画アクション配列を再描画すると、操作を元に戻す機能も実装できます。実現可能性の観点から言えば、まず第一に、これによりメモリに保存されるデータの量を減らすことができ、第二に、レンダリングのオーバーヘッドが高いputImageDataの使用を回避できます。比較オブジェクトとしてdrawImage取り上げ、jsperf上のこのテストケースを見ると、2つの間のパフォーマンスには桁違いの違いがあります。
したがって、この最適化ソリューションは実現可能であると考えています。
改良された適用方法は大まかに以下のとおりです。
class WrappedCanvas { コンストラクタ (canvas) { this.ctx = Canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.executionArray = [ ]; }drawImage (...params) { this.executionArray.push({ メソッド: 'drawImage', params: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height); } undo () { if (this.executionArray. length > 0) { // キャンバスをクリアします this.clearCanvas() // 現在の操作を削除します this.executionArray.pop(); // 再描画する描画アクションを 1 つずつ実行します。 (this.executionArray の exe を実行させます) { this[exe.method](...exe.params) } } }}Canvas を初めて使用する場合は、間違いや不備があれば指摘してください。以上がこの記事の全内容です。皆様の学習のお役に立てれば幸いです。また、VeVb Wulin Network をご支援いただければ幸いです。