在线矢量编辑器
编辑器的真相是 文档模型(对象树、样式、图层),DOM 里的 <svg> 是投影。打开文件、撤销、协作、导出都对这份模型说话。禁止 innerHTML 往回灌用户文件。世界单位 = 文档用户空间;相机 = 根 viewBox。
1. 场景怎么切
Document:nodes / groups / z-order / assets
投影:createElementNS 同步到 SVG(可虚拟化)
g.content 用户对象(必须是节点)
g.chrome 选框、手柄、网格、参考线(不进 serialize)
canvas.preview 复杂滤镜 / 位图笔刷预览(可以光栅,确认后写回对象)
DOM:属性面板、IME 文本
必须是节点:可选中对象、剪贴蒙版的引用目标、文字框(几何)。手柄是 chrome,命中要能区分「点到对象」和「点到手柄」。
可以光栅:滤镜烘焙预览、超大位图对象的视口重采样、历史缩略图。确认滤镜后替换 source asset,不要让实时 filter 挂在每个对象上当文档。
export type EditorNode =
| { id: string; type: "path"; d: string; fill: string }
| { id: string; type: "group"; children: string[] };
export interface EditorDoc {
view: { minX: number; minY: number; width: number; height: number };
nodes: Record<string, EditorNode>;
root: string[];
}
export function serializeDocument(doc: EditorDoc, svg: SVGSVGElement): string {
for (const el of svg.querySelectorAll("[data-chrome]")) {
el.remove();
}
svg.setAttribute(
"viewBox",
`${doc.view.minX} ${doc.view.minY} ${doc.view.width} ${doc.view.height}`,
);
return new XMLSerializer().serializeToString(svg);
}
更好:从 EditorDoc 生成 XML,不读 DOM。DOM 可能含 chrome、hover 描边、错误的 style。打开用户文件:DOMParser → 消毒 → 收成 EditorDoc,再投影。建树必须 createElementNS,见 命名空间。
2. 手柄与相机
变换在世界空间做。getScreenCTM 只用于指针 → 世界,见 坐标变换。旋转/缩放手柄不要写进对象的用户 path,用 chrome 的局部坐标系画,逻辑仍改文档的 transform 矩阵。框选与多选见 手柄。
文本:短标签可用 <text>;段落用 HTML overlay,基线位置跟世界走。不要用 foreignObject 吃用户 HTML,见 foreignObject。
布尔运算、偏移、简化在世界单位做,结果写回 d。屏幕空间运算会随相机变。撤销是文档命令,见 撤销。
适用:对象要成组、布尔、导出 SVG、对齐吸附。流程图只是子集,见 流程图;本篇多了任意 path 与资源。
3. 不适用
- 用 Canvas 当唯一文档还做对齐/吸附/布尔运算:像素没有稳定几何。需要像素工具就 hybrid,确认后变成 path / image 对象。
- 把库的
toJSON()当 schema:业务字段、协作 clock、权限过不了。库最多当投影。 - Snap.svg 新项目,见 09。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 导出含选框和网格 | serialize 了 chrome |
| 打开用户文件 XSS | innerHTML 或消毒漏 foreignObject |
| 缩放后手柄不跟对象 | 手柄用了 CSS 像素布局 |
| 布尔运算每次差 0.5px | 在屏幕空间做几何,没在世界空间 |
| 撤销打乱图层 | 命令改了 DOM 没改文档,或反过来 |
权威资料
核对日期:2026-08-26