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

2019-01-28 21:00:56于丽

好吧, drawImage 操作后对画布的改变根本不存在于绘制状态中。所以,使用 resolve / save 无法实现我们需要的 undo 功能。

模拟栈实现

既然原生的 API 保存绘制状态的栈无法满足需求,那么自然我们会想到自己模拟一个保存操作的栈。随之而来的问题就是:每次绘制操作之后,应该保存什么数据进栈?前面说过,我们想要的是每步绘制操作之后能够保存当前画布的 快照 ,如果能拿到快照数据,同时能利用快照数据恢复画布的话,问题也就迎刃而解了。

幸运的是 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); } } }

我们封装了一下 canvasdrawImage 方法,每次调用该方法之前都会保存上一个状态的快照到模拟的栈中。在执行 undo 操作时,从栈中取出最新保存的快照,然后重新绘制画布,即可实现撤销操作。实际测试也符合预期。

性能优化

上一节中我们很粗犷地实现了 canvas 的撤销功能。为什么说粗犷呢?一个很显而易见的原因就是此方案性能不好。我们的方案相当于每次都是重新绘制整个画布。假设操作步骤很多,我们在模拟栈也就是内存中就会保存很多预存的图片数据。此外,在绘制图片过于复杂时, getImageData