getScreenCTM与坐标变换
生产只维护一套换算:client ↔ 局部用户空间,矩阵来自 getScreenCTM()。getCTM() 不够用来对齐 HTML overlay,也不够用来接 clientX。
SVG 2 IDL:getCTM() / getScreenCTM() 返回 DOMMatrix | null。display: none、在 <defs>、尚未挂载,都可能是 null。逆矩阵在 scale=0 时是 NaN。
1. 两套 CTM
| API | 到哪 | 含外层 HTML 的 CSS transform | 含滚动后的 client |
|---|---|---|---|
getCTM() | 最近 viewport(通常是祖先 <svg> 用户空间) | 不含 | 不含 |
getScreenCTM() | 屏幕 / client | 含 | 与 clientX/Y 对齐 |
包裹 <div style="transform: scale(1.2)"> 时,getCTM 当没这回事,offsetX 和 getCTM 组合会漂。指针、overlay、截图对齐一律 getScreenCTM。
getCTM 仍有用:只要 SVG 内部父子变换(把局部点变到根 svg 用户空间),且你确定没有 HTML CSS transform。一旦跨出 <svg> 盒子,换 getScreenCTM。
2. 生产换算
export function clientToLocal(
el: SVGGraphicsElement,
clientX: number,
clientY: number,
): DOMPoint {
const ctm = el.getScreenCTM();
if (!ctm) {
throw new Error("element is not in the rendering tree");
}
return new DOMPoint(clientX, clientY).matrixTransform(ctm.inverse());
}
export function localToClient(
el: SVGGraphicsElement,
x: number,
y: number,
): DOMPoint {
const ctm = el.getScreenCTM();
if (!ctm) {
throw new Error("element is not in the rendering tree");
}
return new DOMPoint(x, y).matrixTransform(ctm);
}
export function clientToSvgUser(
svg: SVGSVGElement,
clientX: number,
clientY: number,
): DOMPoint {
return clientToLocal(svg, clientX, clientY);
}
isPointInFill({ x, y }) 用 clientToLocal(被测元素, …),不是 clientToSvgUser。根 svg 的用户空间和带 transform 的叶子不是同一套数。
不要用 svg.createSVGPoint() + matrixTransform 当新代码模板,DOMPoint 就是替换。不要用 event.offsetX:它相对的是事件 target 的 padding box,嵌套 g、use、stroke 时与用户单位无关。
iframe、滤镜、部分 foreignObject 下 getScreenCTM 可能 null 或错。命中路径要允许「这次不算」,不要把 inverse() 无保护地铺在 mousemove 上。
3. 和 viewBox 的关系
用户空间 = viewBox 用户单位。getScreenCTM 已经含 viewport 对齐、preserveAspectRatio 的 meet/slice、元素自身 transform。不要再手写一遍「client 减 getBoundingClientRect 再乘 viewBox 宽高比」——meet 留白时你一定会算错。手写公式只留给 相机 里 你自己 存的 viewBox 状态,且绘制也是你改的那份。
4. 适用 / 不适用
适用:指针、框选、overlay、打印裁切、把 HTML 盒子钉到某个 text 锚点。
不适用:在 requestAnimationFrame 里对一万个节点各调一次 getScreenCTM。相机矩阵自己持有,叶子用相对矩阵乘;只在 HTML 边界对一次 screen CTM。
5. 失败形态
| 症状 | 原因 |
|---|---|
| overlay 对不齐,差一个缩放 | 用了 getCTM,外层 HTML 有 CSS transform |
| 旋转节点点偏 | 用根 svg 的逆,没用叶子的 getScreenCTM |
inverse() 得到 NaN | 缩放到 0 或未挂载 |
| 宽屏两侧点不准 | 手写 viewBox 映射,忽略了 meet 留白 |
offsetX 放大后乱跳 | 把它当用户单位 |
权威资料
核对日期:2026-08-26