矢量白板与无限画布
白板的文档是 对象列表(形状、便签、提交后的笔画 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