Canvas画布快照需区分上下文状态与像素内容:save()/restore()仅保存样式和变换,不保存像素;toDataURL()或getImageData()才能真正捕获像素快照;撤销/重做需清空redo栈、用指针管理步进、限制栈长度防内存爆炸。

Canvas 实现画布快照,关键在于区分两种“状态”:绘图上下文状态(context state) 和 画布像素内容状态(canvas content snapshot)。很多人混淆这两者,导致撤销功能失效或性能低下。下面直接讲清楚怎么做、为什么这么做、以及容易踩的坑。
save() / restore() 只管样式和变换,不管画了什么
调用 ctx.save() 保存的是当前上下文的配置快照,比如:
• fillStyle/strokeStyle 颜色
• lineWidth、lineCap、font 等绘制属性
• 平移、旋转、缩放等变换矩阵
• 剪辑路径(clip)
• globalAlpha、globalCompositeOperation 等合成设置
它不保存任何已绘制的像素。所以你画了一条线再 save(),然后清空画布,restore() 后颜色和粗细会回来,但那条线不会重现。
适用场景:封装绘图函数时做局部隔离,比如画一个带旋转的图标,内部 save/restore,不影响外部画布状态。
toDataURL() 或 getImageData() 才能真正“拍照”
要实现用户意义上的“撤销”,必须保存画布当前的像素内容。主流做法是:
- toDataURL('image/png'):生成 base64 字符串,体积稍大但兼容性好,适合中小画布(如 1200×800 以内)
- ctx.getImageData(0, 0, width, height):返回 ImageData 对象(含像素数组),内存占用低、还原快,但不能跨域导出,且需手动管理内存
注意:频繁调用 toDataURL 会触发大量内存分配和编码开销,建议只在用户结束一笔操作(如 mouseup 或 touchend)后保存,而不是每画一笔都存。
用户要生成可打印的中文字帖/练习纸、导出多页 A4 PDF 报告,或把 SVG 设计稿零误差还原到 Canvas 时使用。本技能是「Canvas 内容工厂闭环」的总控,编排:网格渲染引擎(13 种教育网格+拼音标注) → 多页 PDF 导出(A4 合成) → SVG 精准复刻(坐标误差<0.001px)。触发词:生成字帖、练习纸、导出 PDF、SVG 转 Canvas、印刷级还原、A4 报告、米字格田字格。
撤销/重做栈的设计要点
一个健壮的 undo/redo 功能不是简单 push/pop 数组,要注意三个细节:
- 每次新操作前,清空 redo 栈(因为历史分支被覆盖)
- 用指针(如 cStep)而非单纯 pop,支持任意步数回退 + 再前进
- 限制栈长度(如最多存 50 张快照),避免内存爆炸;可配合 LRU 策略淘汰旧快照
示例逻辑片段:
cPush() {
if (cStep
cPushArray.push(canvas.toDataURL());
cStep++;
}
还原快照时别直接 drawImage 覆盖整个画布
用 ctx.drawImage(img, 0, 0) 恢复快照看似简单,但可能带来两个问题:
- 如果画布尺寸动态调整过(比如响应式缩放),drawImage 会拉伸失真
- 若画布设置了 transform(如 scale 缩放视图),直接绘制会叠加变换,导致错位
稳妥做法:先 ctx.setTransform(1, 0, 0, 1, 0, 0) 重置变换,再清空画布(ctx.clearRect(0, 0, canvas.width, canvas.height)),最后 drawImage。

















