跳到主要内容

getScreenCTM与坐标变换

生产只维护一套换算:client ↔ 局部用户空间,矩阵来自 getScreenCTM()getCTM() 不够用来对齐 HTML overlay,也不够用来接 clientX

SVG 2 IDL:getCTM() / getScreenCTM() 返回 DOMMatrix | nulldisplay: none、在 <defs>、尚未挂载,都可能是 null。逆矩阵在 scale=0 时是 NaN

1. 两套 CTM

API到哪含外层 HTML 的 CSS transform含滚动后的 client
getCTM()最近 viewport(通常是祖先 <svg> 用户空间)不含不含
getScreenCTM()屏幕 / clientclientX/Y 对齐

包裹 <div style="transform: scale(1.2)"> 时,getCTM 当没这回事,offsetXgetCTM 组合会漂。指针、overlay、截图对齐一律 getScreenCTM

getCTM 仍有用:只要 SVG 内部父子变换(把局部点变到根 svg 用户空间),且你确定没有 HTML CSS transform。一旦跨出 &lt;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,嵌套 guse、stroke 时与用户单位无关。

iframe、滤镜、部分 foreignObjectgetScreenCTM 可能 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