加入收藏 | 设为首页 | 会员中心 | 我要投稿 湖南网 (https://www.hunanwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程 > 正文

探究 canvas 绘图中撤销(undo)功能的实现方式详解

发布时间:2020-05-12 06:37:36 所属栏目:编程 来源:站长网
导读:副问题#e# 最近在做网页国界片处理赏罚相干的项目,也算是初入了 canvas 的坑。项目需求中有一个给图片添加水印的成果。我们知道,在赏识器端实现图片添加水印成果,凡是的做法就是行使 canvas 的 drawImage 要领。对付平凡的合成(好比一张底图和一张 PNG 水印
副问题[/!--empirenews.page--]

最近在做网页国界片处理赏罚相干的项目,也算是初入了 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 进入视线。我们先看一下这两个 API 的描写:

CanvasRenderingContext2D.restore() 是 Canvas 2D API 通过在画图状态栈中弹出顶端的状态,将 canvas 规复到最近的生涯状态的要领。 假如没有生涯状态,此要领不做任何改变。

CanvasRenderingContext2D.save() 是 Canvas 2D API 通过将当前状态放入栈中,生涯 canvas 所有状态的要领。

乍看起来可以满意需求。我们看一下官方示例代码:

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 ,也就是绘制状态。生涯到栈中的绘制状态包括以下几个部门:

当前的调动矩阵

当前的剪切地区

当前的虚线列表

以部属性当前的值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.

好吧, drawImage 操纵后对画布的改变基础不存在于绘制状态中。以是,行使 resolve / save 无法实现我们必要的 undo 成果。

模仿栈实现

既然原生的 API 生涯绘制状态的栈无法满意需求,那么天然我们会想到本身模仿一个生涯操纵的栈。随之而来的题目就是T媚课绘制操纵之后,应该生涯什么数据进栈?前面说过,我们想要的是每步绘制操纵之后可以或许生涯当前画布的 快照 ,假如能拿到快照数据,同时能操作快照数据规复画布的话,题目也就迎刃而解了。

荣幸的是 canvas 2D 原生提供了获取快照和通过快照规复画布的 API —— getImageData / putImageData 。以下是 API 声名:

/* * @param { Number } sx 将要被提取的图像数据矩形地区的左上角 x 坐标 * @param { Number } sy 将要被提取的图像数据矩形地区的左上角 y 坐标 * @param { Number } sw 将要被提取的图像数据矩形地区的宽度 * @param { Number } sh 将要被提取的图像数据矩形地区的高度 * @return { Object } ImageData 包括 canvas 给定的矩形图像数据 */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } imagedata 包括像素值的工具 * @param { Number } dx 源图像数据在方针画布中的位置偏移量(x 轴偏向的偏移量) * @param { Number } dy 源图像数据在方针画布中的位置偏移量(y 轴偏向的偏移量) */ void ctx.putImageData(imagedata, dx, dy);

我们来看一个简朴的应用方法:

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 要领,每次挪用该要领之前城市生涯上一个状态的快照到模仿的栈中。在执行 undo 操纵时,从栈中取出最新生涯的快照,然后从头绘制画布,即可实现取消操纵。现实测试也切合预期。

机能优化

上一节中我们很粗犷地实现了 canvas 的取消成果。为什么说粗犷呢?一个很显而易见的缘故起因就是此方案机能欠好。我们的方案相等于每次都是从头绘制整个画布。假设操纵步调许多,我们在模仿栈也就是内存中就会生涯许多预存的图片数据。另外,在绘制图片过于伟大时, getImageData 和 putImageData 这两个要了解发生较量严峻的机能题目。stackoverflow 上有具体的接头: Why is putImageData so slow? 。我们还可以从 jsperf 上这个测试用例的数据来验证这一点。淘宝 FED 在Canvas 最佳实践中也提到了只管“不在动画中行使 putImageData 要领”。其它,文章里还提到一点,“尽也许挪用那些渲染开销较低的 API”。我们可以从这里入手思索怎样举办优化。

之前说过,我们通过对整个画布生涯快照的方法来记录每个操纵,换个角度思索,假如我们把每次绘制的举措生涯到一个数组中,在每次执行取消操纵时,起首清空画布,然后重绘这个画图举措数组,也可以实现取消操纵的成果。可行性方面,起首这样可以镌汰生涯到内存的数据量,其次还停止了行使渲染开销较高的 putImageData 。以 drawImage 为较量工具,看 jsperf 上这个测试用例,二者的机能存在数目级的差距。

因此,我们以为此优化方案是可行的。

改造后的应用方法大抵如下:

(编辑:湖南网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读