撤销重做命令栈
Canvas 没有 undo()。历史记录的是 文档操作,必要时才对某层位图做快照。把每次 getImageData 整张压栈,内存按帧数线性爆炸,而且和相机/DPR 缠死。
1. 命令
export interface Command {
readonly label: string;
apply(): void;
revert(): void;
}
export class History {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private coalescing: Command | undefined;
execute(cmd: Command): void {
cmd.apply();
this.undoStack.push(cmd);
this.redoStack.length = 0;
}
/** 拖手柄期间把多次 translate 合成一条 */
coalesce(cmd: Command): void {
if (this.coalescing) {
this.coalescing.revert();
}
cmd.apply();
this.coalescing = cmd;
}
commitCoalesce(): void {
if (!this.coalescing) return;
this.undoStack.push(this.coalescing);
this.coalescing = undefined;
this.redoStack.length = 0;
}
undo(): void {
const cmd = this.undoStack.pop();
if (!cmd) return;
cmd.revert();
this.redoStack.push(cmd);
}
redo(): void {
const cmd = this.redoStack.pop();
if (!cmd) return;
cmd.apply();
this.undoStack.push(cmd);
}
}
interface Stroke {
id: string;
}
export class AddStrokeCommand implements Command {
readonly label = "stroke";
constructor(
private readonly doc: { strokes: Stroke[] },
private readonly stroke: Stroke,
) {}
apply(): void {
this.doc.strokes.push(this.stroke);
}
revert(): void {
const i = this.doc.strokes.findIndex((s) => s.id === this.stroke.id);
if (i >= 0) this.doc.strokes.splice(i, 1);
}
}
拖节点:pointerdown 记下矩阵,move 用 coalesce(new SetLocalCommand(node, next)),up 时 commitCoalesce()。这样撤销一步回到拖之前,而不是几十步。
2. 什么时候才快照像素
| 操作 | 历史 |
|---|---|
| 加/删/改矢量节点 | 命令 |
| 提交一笔墨迹 | 命令(stroke 对象) |
| 像素橡皮、涂抹、液化 | 该层 ImageBitmap 前后各一份,或命令里存 region patch |
| 滤镜应用到整图 | 替换 source bitmap,命令持有 old/new 引用 |
patch 比整图便宜:只存脏 AABB 的 ImageData。仍要 willReadFrequently 工作表面。
上限:undo 50 步;超出丢栈底,并 oldBitmap.close()。
3. 和协作
本地 undo 不能无脑改已广播的操作。OT/CRDT 下撤销是「再发一条逆操作」,不是弹本地栈。单机编辑器和多人白板不要共用一个 History 类当同步源。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 撤销一次回到空白 | 快照的是 clear 之后的表面 |
| 拖一下 undo 50 次 | 没 coalesce |
| 内存涨到几百 MB | 全画布 ImageData × 步数 × DPR |
| redo 丢了 | execute 时没清 redo |
| 协作对方乱跳 | 把本地 history 当了文档 |
权威资料
- 无 Canvas 专用 undo 规范。模式来自命令式编辑器;位图层参考 ImageData 管线。
核对日期:2026-08-26