跳到主要内容

用户坐标系与变换

每个 SVGGraphicsElement 有从 局部用户空间 走到祖先 viewport 的 CTM。命中、overlay、打印裁切都靠它,不要心算一串 translate/rotate

SVG 2 上 getCTM() / getScreenCTM() 返回 DOMMatrix | null。元素不在渲染树(display: none、在 <defs> 里)时可能拿到 null 或无意义矩阵。

1. 三套坐标

空间谁产生典型 API
局部用户空间元素自己的 x/y/path + 自身 transformcxdisPointInFill 的点
viewport 用户空间层层 CTM 乘到最近 <svg>getCTM()
屏幕 / client含滚动、CSS transform、视口缩放getScreenCTM()clientX
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());
}

isPointInFill({ x, y }) 的点必须是 该元素局部用户空间。把 clientX 直接丢进去,旋转节点上永远点不准。

2. 属性 transform vs CSS transform

两者会组合。生产只选一条写入通道:

  • 编辑器 / 场景图:把 DOMMatrix 写成 attribute transform="matrix(a b c d e f)",CSS transform 保持 none。
  • 过渡动画:用 CSS / WAAPI 动 transform,此时必须显式设 transform-boxtransform-origin

SVG 上 CSS transform-origin 默认曾经是 0 0,后来向 HTML 的 50% 50% 靠。再叠加 transform-box: view-box | fill-box | stroke-box,不同引擎仍有历史包袱。动画章见 CSS 与 WAAPI

不要 attribute 转一半、CSS 转一半,还用 getCTM() 以为自己算得清。

3. 写入矩阵

export function setMatrix(el: SVGGraphicsElement, m: DOMMatrix): void {
const { a, b, c, d, e, f } = m;
el.setAttribute("transform", `matrix(${a} ${b} ${c} ${d} ${e} ${f})`);
}

export function worldMatrix(el: SVGGraphicsElement): DOMMatrix {
const ctm = el.getCTM();
if (!ctm) {
throw new Error("element is not in the rendering tree");
}
return DOMMatrix.fromMatrix(ctm);
}

getCTM() 含祖先 <g> 的变换,但不含外层 HTML 的 CSS transform;跨 HTML/SVG 边界对齐 overlay 必须用 getScreenCTM()

4. 不可逆

缩放到 0、退化矩阵上 inverse() 会得到 NaN。缩放手柄夹最小 scale。getScreenCTM() 在 iframe / 某些过滤器效果下也可能失败,命中路径要允许「这次不算」。

5. 失败形态

症状原因
旋转后点偏一块用了 client 坐标去比局部 path
overlay 对不齐overlay 用了 getCTM,忽略了页面滚动或外层 CSS transform
动画绕错锚点transform-box / transform-origin 和 illustrator 导出的 0,0 假设冲突
越转越飞每帧 transform() 叠乘,从不 setAttribute 成绝对 matrix

权威资料

核对日期:2026-08-26