跳到主要内容

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 自己的 getScreenCTMgetCTM 不含外层 HTML CSS transform,overlay 会漂,见 getScreenCTM

正在 IME 的 textarea 不要 transform: scale(...) 去跟相机——系统候选框按 CSS 像素摆。只更新 left/top,字号用固定 px。未编辑的只读标牌可以跟着视觉 scale。

相机、滚动、resize、字体加载完成,都要再跑一遍 placeOverlayAtLocal。绑在 rAF 里合并,不要每个 wheel 同步重排。卸 overlay 时去掉这些监听。

2. 事件所有权

区域谁收指针
SVG 空白、节点拖拽svgtouch-action: nonesetPointerCapture
叠加的按钮、输入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