您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)canvas繪圖中如何實(shí)現(xiàn)撤銷功能的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
最近在做網(wǎng)頁版圖片處理相關(guān)的項(xiàng)目,也算是初入了 canvas 的坑。項(xiàng)目需求中有一個(gè)給圖片添加水印的功能。我們知道,在瀏覽器端實(shí)現(xiàn)圖片添加水印功能,通常的做法就是使用 canvas
的 drawImage
方法。對于普通的合成(比如一張底圖和一張 PNG 水印圖片合成)來說,其大致實(shí)現(xiàn)原理如下:
var canvas = document.getElementById("canvas"); var ctx = canvas.getContext('2d'); // img: 底圖 // watermarkImg: 水印圖片 // x, y 是畫布上放置 img 的坐標(biāo) ctx.drawImage(img, x, y); ctx.drawImage(watermarkImg, x, y);
直接連續(xù)使用 drawImage()
把對應(yīng)的圖片繪制到 canvas
畫布上就行。
以上就是背景介紹。但是略麻煩的是添加水印的需求中還有一個(gè)需要實(shí)現(xiàn)的功能是用戶能夠切換水印的位置。我們自然會(huì)想到能否實(shí)現(xiàn) canvas
的 undo
功能,當(dāng)用戶切換水印位置時(shí),先撤銷上一步 drawImage
操作,然后再重新繪制水印圖片位置。
restore
/ save
?
效率最高也是最方便的肯定是查閱 canvas 2D
原生 API 是否有此功能。經(jīng)過一番搜索, restore
/ save
這一對 API 進(jìn)入視線。我們先看一下這兩個(gè) API 的描述:
CanvasRenderingContext2D.restore() 是 Canvas 2D API 通過在繪圖狀態(tài)棧中彈出頂端的狀態(tài),將 canvas 恢復(fù)到最近的保存狀態(tài)的方法。 如果沒有保存狀態(tài),此方法不做任何改變。
CanvasRenderingContext2D.save() 是 Canvas 2D API 通過將當(dāng)前狀態(tài)放入棧中,保存 canvas 全部狀態(tài)的方法。
乍看起來可以滿足需求。我們看一下官方示例代碼:
var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); ctx.save(); // 保存默認(rèn)的狀態(tài) ctx.fillStyle = "green"; ctx.fillRect(10, 10, 100, 100); ctx.restore(); // 還原到上次保存的默認(rèn)狀態(tài) ctx.fillRect(150, 75, 100, 100);
結(jié)果如下圖所示:
奇怪,好像和我們預(yù)期的結(jié)果不太一致。我們想要的結(jié)果是 save
方法調(diào)用后能夠保存當(dāng)前畫布的快照, resolve
方法調(diào)用后能夠完全回到上一個(gè)保存的快照處的狀態(tài)。
再仔細(xì)研究一下 API。原來我們遺漏一個(gè)重要概念: drawing state
,也就是繪制狀態(tài)。保存到棧中的繪制狀態(tài)包含以下幾個(gè)部分:
當(dāng)前的變換矩陣
當(dāng)前的剪切區(qū)域
當(dāng)前的虛線列表
以下屬性當(dāng)前的值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
好吧, drawImage
操作后對畫布的改變根本不存在于繪制狀態(tài)中。所以,使用 resolve
/ save
無法實(shí)現(xiàn)我們需要的 undo 功能。
模擬棧實(shí)現(xiàn)
既然原生的 API 保存繪制狀態(tài)的棧無法滿足需求,那么自然我們會(huì)想到自己模擬一個(gè)保存操作的棧。隨之而來的問題就是:每次繪制操作之后,應(yīng)該保存什么數(shù)據(jù)進(jìn)棧?前面說過,我們想要的是每步繪制操作之后能夠保存當(dāng)前畫布的 快照 ,如果能拿到快照數(shù)據(jù),同時(shí)能利用快照數(shù)據(jù)恢復(fù)畫布的話,問題也就迎刃而解了。
幸運(yùn)的是 canvas 2D
原生提供了獲取快照和通過快照恢復(fù)畫布的 API —— getImageData
/ putImageData
。以下是 API 說明:
/* * @param { Number } sx 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的左上角 x 坐標(biāo) * @param { Number } sy 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的左上角 y 坐標(biāo) * @param { Number } sw 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的寬度 * @param { Number } sh 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的高度 * @return { Object } ImageData 包含 canvas 給定的矩形圖像數(shù)據(jù) */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } imagedata 包含像素值的對象 * @param { Number } dx 源圖像數(shù)據(jù)在目標(biāo)畫布中的位置偏移量(x 軸方向的偏移量) * @param { Number } dy 源圖像數(shù)據(jù)在目標(biāo)畫布中的位置偏移量(y 軸方向的偏移量) */ void ctx.putImageData(imagedata, dx, dy);
我們來看一個(gè)簡單的應(yīng)用方式:
class WrappedCanvas { constructor (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; 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
方法,每次調(diào)用該方法之前都會(huì)保存上一個(gè)狀態(tài)的快照到模擬的棧中。在執(zhí)行 undo
操作時(shí),從棧中取出最新保存的快照,然后重新繪制畫布,即可實(shí)現(xiàn)撤銷操作。實(shí)際測試也符合預(yù)期。
性能優(yōu)化
上一節(jié)中我們很粗獷地實(shí)現(xiàn)了 canvas
的撤銷功能。為什么說粗獷呢?一個(gè)很顯而易見的原因就是此方案性能不好。我們的方案相當(dāng)于每次都是重新繪制整個(gè)畫布。假設(shè)操作步驟很多,我們在模擬棧也就是內(nèi)存中就會(huì)保存很多預(yù)存的圖片數(shù)據(jù)。此外,在繪制圖片過于復(fù)雜時(shí), getImageData
和 putImageData
這兩個(gè)方法會(huì)產(chǎn)生比較嚴(yán)重的性能問題。stackoverflow 上有詳細(xì)的討論: Why is putImageData so slow? 。我們還可以從 jsperf 上這個(gè)測試用例的數(shù)據(jù)來驗(yàn)證這一點(diǎn)。淘寶 FED 在Canvas 最佳實(shí)踐中也提到了盡量“不在動(dòng)畫中使用 putImageData
方法”。另外,文章里還提到一點(diǎn),“盡可能調(diào)用那些渲染開銷較低的 API”。我們可以從這里入手思考如何進(jìn)行優(yōu)化。
之前說過,我們通過對整個(gè)畫布保存快照的方式來記錄每個(gè)操作,換個(gè)角度思考,如果我們把每次繪制的動(dòng)作保存到一個(gè)數(shù)組中,在每次執(zhí)行撤銷操作時(shí),首先清空畫布,然后重繪這個(gè)繪圖動(dòng)作數(shù)組,也可以實(shí)現(xiàn)撤銷操作的功能??尚行苑矫妫紫冗@樣可以減少保存到內(nèi)存的數(shù)據(jù)量,其次還避免了使用渲染開銷較高的 putImageData
。以 drawImage
為比較對象,看 jsperf 上這個(gè)測試用例,二者的性能存在數(shù)量級(jí)的差距。
因此,我們認(rèn)為此優(yōu)化方案是可行的。
改進(jìn)后的應(yīng)用方式大致如下:
class WrappedCanvas { constructor (canvas) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; this.executionArray = []; } 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) { // 清空畫布 this.clearCanvas(); // 刪除當(dāng)前操作 this.executionArray.pop(); // 逐個(gè)執(zhí)行繪圖動(dòng)作進(jìn)行重繪 for (let exe of this.executionArray) { this[exe.method](...exe.params) } } } }
感謝各位的閱讀!關(guān)于“canvas繪圖中如何實(shí)現(xiàn)撤銷功能”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。