用户坐标系与变换
每个 SVGGraphicsElement 有从 局部用户空间 走到祖先 viewport 的 CTM。命中、overlay、打印裁切都靠它,不要心算一串 translate/rotate。
SVG 2 上 getCTM() / getScreenCTM() 返回 DOMMatrix | null。元素不在渲染树(display: none、在 <defs> 里)时可能拿到 null 或无意义矩阵。
1. 三套坐标
| 空间 | 谁产生 | 典型 API |
|---|---|---|
| 局部用户空间 | 元素自己的 x/y/path + 自身 transform | cx、d、isPointInFill 的点 |
| 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写成 attributetransform="matrix(a b c d e f)",CSStransform保持 none。 - 过渡动画:用 CSS / WAAPI 动
transform,此时必须显式设transform-box和transform-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