Overlay与HTML叠加
双击文字要输入框、右键要菜单、协作要标牌:用 HTML 叠在 SVG 上,用 getScreenCTM 对齐。不要先上 foreignObject(那章只留给必须跟用户空间一起旋转的短表单)。
1. 用 screen CTM 钉盒子
export function placeOverlayAtLocal(
overlay: HTMLElement,
target: SVGGraphicsElement,
localX: number,
localY: number,
): void {
const ctm = target.getScreenCTM();
if (!ctm) return;
const p = new DOMPoint(localX, localY).matrixTransform(ctm);
overlay.style.position = "fixed";
overlay.style.left = `${p.x}px`;
overlay.style.top = `${p.y}px`;
overlay.style.transformOrigin = "0 0";
}
position: fixed + client 点,自动含滚动。若 overlay 放在 position: relative 的画布容器里,改用容器 getBoundingClientRect 做差,改 absolute。两种布局选一种。
对齐的是 叶子元素 的 CTM,不是根 svg 的。文字在旋转的 <g> 里时,用 text 自己的 getScreenCTM。getCTM 不含外层 HTML CSS transform,overlay 会漂,见 getScreenCTM。
正在 IME 的 textarea 不要 transform: scale(...) 去跟相机——系统候选框按 CSS 像素摆。只更新 left/top,字号用固定 px。未编辑的只读标牌可以跟着视觉 scale。
相机、滚动、resize、字体加载完成,都要再跑一遍 placeOverlayAtLocal。绑在 rAF 里合并,不要每个 wheel 同步重排。卸 overlay 时去掉这些监听。
2. 事件所有权
| 区域 | 谁收指针 |
|---|---|
| SVG 空白、节点拖拽 | svg,touch-action: none,setPointerCapture |
| 叠加的按钮、输入 | HTML;svg 不要 capture 到它们头上 |
| 拖节点经过标牌 | 拖拽期间 overlay pointer-events: none,编辑中的 input 除外 |
拖到 HTML 上就 pointerout 是没 capture、也没把 overlay 设 none。结束拖拽再恢复。
elementFromPoint 会先命中 overlay。命中 SVG 节点前先看点是否落在「正在交互的 HTML」上。
3. 文本编辑拆法
双击 text → 在锚点挂 <textarea>,内容来自 scene,SVG 那一行 visibility: hidden(仍占几何,避免跳)。Enter / blur 写回 textContent,卸 DOM。不要在 SVG 里画 caret。
宽度用 getBBox({ fill: true, stroke: false, markers: false, clipped: false }).width 经 CTM 变到 CSS 像素,给 textarea 一个 minWidth。不要用 getComputedTextLength() 当 HTML 字号——字体、letter-spacing 和 CSS 盒子不是同一套。
export function userWidthToCssPx(el: SVGGraphicsElement, userWidth: number): number {
const a = new DOMPoint(0, 0);
const b = new DOMPoint(userWidth, 0);
const ctm = el.getScreenCTM();
if (!ctm) return userWidth;
const pa = a.matrixTransform(ctm);
const pb = b.matrixTransform(ctm);
return Math.hypot(pb.x - pa.x, pb.y - pa.y);
}
多行、选区、IME:这是 overlay 存在的理由。foreignObject 会把 HTML 一起塞进 viewBox 缩放,Safari 上候选框和裁切更差,见 foreignObject。
z-index:overlay 高于 SVG 装饰层、低于应用模态。协作光标标牌可 pointer-events: none,避免永远挡命中。
4. 适用 / 不适用
适用:属性气泡、临时输入、右键菜单、协作光标。
不适用:把整页 UI 画进 SVG;也不要把可点击工具栏做成 <image> 热区。按钮必须是 HTML,才有 Tab 和读屏。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 缩放后输入框漂 | 用了 getCTM 或手写 viewBox 映射 |
| 旋转文字对不齐 | overlay 相对根 svg,没用叶子 CTM |
| 拖到气泡就丢拖 | overlay 抢走 pointer,没 capture / 没 none |
| 中文候选框飞 | textarea 被 scale,或放进了 foreignObject |
| 滚动后 overlay 留在原地 | 用了相对容器的 absolute,却按 fixed 的 client 写 |
权威资料
核对日期:2026-08-26