立即模式与场景图
Canvas 2D 每次 fill / stroke / drawImage 都把结果 立即写入 backing store。调用返回后,引擎不保留「刚才那条贝塞尔」的对象句柄。这和 SVG/DOM 的保留模式相反。
1. 两种模式
保留模式(SVG / DOM)
scene graph ──► 渲染器按树重绘
改 node.x,下一帧自动反映
立即模式(Canvas 2D)
你的代码 ──► 像素
改「逻辑 x」如果自己不重绘,屏幕上什么都不变
生产后果:
- 拖拽一个矩形,必须 清 backing store(或脏区)再把整棵场景画一遍。
- 撤销不是
ctx.undo(),而是回放命令或恢复场景快照后重绘。 - 从像素里「点选颜色」可以得到 RGBA,得不到「点到了哪个业务对象」。
2. 最小场景图
下面这个结构是绝大多数 2D 编辑器/可视化的底座。它不依赖框架。
type Mat2D = DOMMatrix;
interface SceneNode {
id: string;
/** 相对父节点的局部变换 */
local: Mat2D;
/** 局部坐标下的几何,用于绘制和命中 */
path: Path2D;
fill?: string;
stroke?: string;
lineWidth?: number;
children: SceneNode[];
/** 跳过命中(装饰层、已烘焙背景) */
pickable?: boolean;
}
function worldMatrix(node: SceneNode, parent: Mat2D): Mat2D {
return parent.multiply(node.local);
}
function paintNode(
ctx: CanvasRenderingContext2D,
node: SceneNode,
parent: Mat2D,
): void {
const world = worldMatrix(node, parent);
ctx.save();
ctx.setTransform(world);
if (node.fill) {
ctx.fillStyle = node.fill;
ctx.fill(node.path);
}
if (node.stroke) {
ctx.strokeStyle = node.stroke;
ctx.lineWidth = node.lineWidth ?? 1;
ctx.stroke(node.path);
}
ctx.restore();
for (const child of node.children) {
paintNode(ctx, child, world);
}
}
export function paintScene(
ctx: CanvasRenderingContext2D,
root: SceneNode,
): void {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
paintNode(ctx, root, new DOMMatrix());
}
关键点:
- 几何活在
Path2D里,不要每帧beginPath+ 一堆全局lineTo,除非路径每帧都变。 - 变换活在矩阵里,不要用一串
translate/rotate指望人脑跟踪当前 CTM。 setTransform(world)是绝对设置,不会叠乘脏 CTM。热路径里不要transform()连乘却从不重置。
3. 什么时候可以不建场景图
适用:一次性海报、服务端出图、每帧数据完全来自外部(音频频谱、监控波形)。这时「数据数组 → 直接画」即可,数组本身就是场景。
不适用:对象要被点选、对齐、成组、做动画插值。没有场景图你会在第一周之后把状态拆进 8 个平行数组,然后无法做父子变换。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 拖一下花屏/残影 | 没 clearRect 或没覆盖脏区 |
| 越画越慢 | 每帧重建巨大 Path2D,或 getImageData 读全画布 |
| 点不准 | 命中用了 CSS 坐标,绘制用了 backing store 坐标 |
| 撤销错乱 | 把 backing store 当 source of truth 做了 putImageData 栈 |
权威资料
核对日期:2026-08-26