您现在的位置是:网站首页> 编程资料编程资料
探究 canvas 绘图中撤销(undo)功能的实现方式详解html5实现点击弹出图片功能html5 录制mp3音频支持采样率和比特率设置html5表单的required属性使用html5调用摄像头实例代码HTML5页面音频自动播放的实现方式Html5大屏数据可视化开发的实现html实现弹窗的实例HTML5来实现本地文件读取和写入的实现方法HTML 罗盘式时钟的实现HTML5简单实现添加背景音乐的几种方法
2023-10-16
343人已围观
简介 这篇文章主要介绍了探究 canvas 绘图中撤销(undo)功能的实现方式详解的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
最近在做网页版图片处理相关的项目,也算是初入了 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 保存绘制状态的栈无法满足需求,那么自然我们会想到自己模拟一个保存操作的栈。随之而来的问题就是:每次绘制操作之后,应该保存什么数据进栈?前面说过,我们想要的是每步绘制操作之后能够保存当前画布的 快照 ,如果能拿到快照数据,同时能利用快照数据恢复画布的话,问题也就迎刃而解了。
幸运的是 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 上这个测试用例,二者的性能存在数量级的差距。

因此,我们认为此优化方案是可行的。
改进后的应用方式大致如下:
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(); // 删除当前操作 this.executionArray.pop(); // 逐个执行绘图动作进行重绘 for (let exe of this.executionArray) { this[exe.method](...exe.params) } } } }新人入坑 canvas,如有错误与不足,欢迎指出。以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
相关内容
- html5调用摄像头功能的实现代码Html5调用手机摄像头并实现人脸识别的实现HTML5混合开发二维码扫描以及调用本地摄像头HTML5调用手机摄像头拍照的实现思路及代码HTML5 Canvas+JS控制电脑或手机上的摄像头实例html5调用摄像头实例代码
- HTML5新增的标签和属性归纳总结html5实现点击弹出图片功能html5 录制mp3音频支持采样率和比特率设置html5表单的required属性使用html5调用摄像头实例代码HTML5页面音频自动播放的实现方式Html5大屏数据可视化开发的实现html实现弹窗的实例HTML5来实现本地文件读取和写入的实现方法HTML 罗盘式时钟的实现HTML5简单实现添加背景音乐的几种方法
- 在HTML5 canvas里用卷积核进行图像处理的方法canvas 基础之图像处理的使用
- 如何使用localstorage代替cookie实现跨域共享数据问题html5的localstorage详解HTML5 LocalStorage 本地存储刷新值还在HTML5 localStorage使用总结HTML5本地存储localStorage、sessionStorage基本用法、遍历操作、异常HTMl5的存储方式sessionStorage和localStorage详解
- 教你如何一步一步用Canvas写一个贪吃蛇H5 canvas实现贪吃蛇小游戏
- html5/css3响应式页面开发总结 CSS3移动端vw+rem不依赖JS实现响应式布局的方法CSS banner图响应式居中显示的方法详解使用CSS3的@media来编写响应式的页面 jQuery和CSS3响应式轮播插件jcSlider纯CSS3大转盘抽奖示例代码(响应式、可配置)CSS3 media queries + jQuery实现响应式导航CSS 响应式布局系统的实例代码
- 传统HTML页面实现模块化加载的方法在HTML里加载摄像头的方法基于HTML代码实现图片碎片化加载功能html5用video标签流式加载的实现HTML5 图片预加载的示例代码HTML页面缩小后显示滚动条的示例代码h5页面背景图很长要有滚动条滑动效果的实现HTML5实现直播间评论滚动效果的代码html+css实现滚动到元素位置显示加载动画效果
- HTML5 和小程序实现拍照图片旋转、压缩和上传功能HTML5页面嵌入小程序没有返回按钮及返回页面空白的问题微信小程序之html5 canvas绘图并保存到系统相册基于Jscex +HTML5 Canvas 制作的抽奖小程序HTML5跳转小程序wx-open-launch-weapp的示例代码
- 详解canvas drawImage()方法绘制图片不显示的问题HTML5 Canvas API中drawImage()方法的使用实例canvas绘制图片drawImage使用方法
- 全民英雄 仙女龙怎么样 仙女龙技能详解_手机游戏_游戏攻略_
