Overlay与DOM叠加
Canvas 不参与 CSS 布局,也没有可访问树。要把输入框、右键菜单、协作光标标牌叠上去,必须用 同一套 world ↔ view 函数 定位 DOM,而不是再写一套坐标。
1. 定位
export function placeOverlay(
el: HTMLElement,
canvas: HTMLCanvasElement,
camera: Camera,
worldX: number,
worldY: number,
): void {
const view = worldToView(camera, worldX, worldY);
const rect = canvas.getBoundingClientRect();
el.style.position = "absolute";
el.style.left = `${rect.left + window.scrollX + view.x}px`;
el.style.top = `${rect.top + window.scrollY + view.y}px`;
el.style.transformOrigin = "0 0";
el.style.transform = `scale(${camera.scale})`;
}
若 overlay 已在与 canvas 同坐标的 wrapping position: relative 容器里,不要加 rect.left,只用 view 坐标。两种布局选一种。
缩放文字:transform: scale(camera.scale) 让 DOM 与世界物体同步;IME 输入框通常 不要 跟着 scale(否则系统候选框错位)。正在编辑的 textarea 固定 CSS 像素,只更新 left/top。
2. 事件所有权
| 区域 | 谁收指针 |
|---|---|
| 画布空白、节点拖拽 | canvas,touch-action: none |
| 叠加的按钮、输入 | DOM,canvas 不要 capture 到它们头上 |
| 拖节点经过 overlay | 仍应走 canvas capture,overlay pointer-events: none,编辑中的输入除外 |
拖拽开始时给 overlay 加 pointer-events: none,结束时去掉。否则拖到标牌上会 pointerout 打断。
3. 文本编辑的标准拆法
双击文字节点 → 在世界盒位置挂 <textarea>,内容来自 scene,canvas 那一行改成不画(或画占位)。Enter/blur 写回 scene,卸 DOM。不要在 canvas 里自己做 caret 闪烁和 IME。
4. 适用 / 不适用
适用:属性气泡、协作头像、临时输入、右键菜单。
不适用:把整页 UI 画进 canvas。按钮、表单、Tab 焦点必须是 DOM。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 缩放后输入框漂 | 一套用 backing 坐标,一套用 CSS |
| 拖到气泡就丢拖 | overlay 抢走 pointer |
| 中文输入候选框飞 | textarea 被 scale |
| 滚动页面后 overlay 留在原地 | 用了 client 当 page,没加 scroll |
权威资料
核对日期:2026-08-26