跳到主要内容

矢量白板与无限画布

白板的文档是 对象列表(形状、便签、提交后的笔画 path),不是一张越画越大的位图。无限感来自相机:改根 viewBox,世界可以任意大。不要用 CSS scale 放大整棵 DOM。

1. 场景怎么切

世界:任意大平面,单位自定
相机:viewBox(x, y, cssW*scale, cssH*scale)
g.shapes 矢量对象,必须是节点(进视口的才挂载)
canvas.ink 正在画的笔迹、压感预览(可以光栅)
g.committed pointerup 后把 stroke 变成 path 进文档
DOM:工具栏、远程光标、便签 HTML

必须是节点:可选择的形状、文字框锚点、已提交且还要编辑的笔画(简化后的 path)。虚拟化:视口 AABB 外的节点不进 DOM,只留在文档数组。

可以光栅:当前这一笔墨迹、网格、他人笔迹的低清预览。压感采样点不要每个 circle。Canvas 层和 SVG 必须共用 Camera,见 选型混用

export interface Camera {
x: number;
y: number;
/** 世界单位 / CSS 像素;越大看得越广 */
scale: number;
}

export interface BoardObject {
id: string;
kind: "rect" | "path" | "note";
x: number;
y: number;
data: string;
}

export interface BoardDoc {
objects: BoardObject[];
}

export function viewToWorld(
cam: Camera,
viewX: number,
viewY: number,
): { x: number; y: number } {
return { x: cam.x + viewX * cam.scale, y: cam.y + viewY * cam.scale };
}

锚点缩放:光标下的世界点在改 scale 前后不变,调 x/y。实现与 相机 同一套,不要对 <svg> 设 CSS transform

正在画的笔:pointerdown 在 canvas.ink 采样世界点,pointerup 拟合成一条 path 写入 BoardDoc。预览层可丢,文档不能丢。

2. 虚拟化

queryVisible(cam, cssW, cssH) 用空间索引取出对象,diff 出要 append / remove 的 DOM。对象 10⁴ 时,DOM 里只应有视口附近几百个。见 虚拟化。滤镜、阴影只给选中项。

提交笔画:把采样点拟合成 path d(Douglas-Peucker 或固定步长),一条 path 进文档。撤销弹这条对象,不是 putImageData,见 撤销。指针捕获见 状态机

3. 不适用

  • 把整板 getImageData 当存盘:相机一变就废,内存按 DPR 爆炸。
  • 协作把本地 canvas 快照当 CRDT:对端要对齐的是对象 id。
  • 便签用 SVG text 写长文:换行与 IME 用 HTML overlay,位置仍跟世界坐标走。

4. 失败形态

症状原因
放大发糊相机其实是 CSS scale,或墨迹只活在 canvas 且没按世界重画
一拖就掉帧视口外节点全挂在 DOM,或每笔都是未简化的一万个命令
命中偏移ink canvas 与 SVG viewBox 不是同一相机
撤销空白快照了 clear 后的像素而不是对象
导出没有笔迹笔迹还停在 overlay,没 commit 成 path

权威资料

核对日期:2026-08-26