SVG use 影子树 CSS 动画的两个陷阱 (Two Traps of CSS Animation in SVG use Shadow Tree)
用 <defs> 定义图形组件、再用 <use> 实例化,是 SVG 复用几何的标准做法;配合 CSS 动画时会踩到两个反直觉的坑。两者都源自同一件事:<use> 会把模板元素克隆进一棵影子树,而 DOM 查询拿到的永远是 <defs> 里的模板本身。
陷阱一:class 同时挂在模板和宿主上,动画叠加两次
触发条件
模板带 class,实例的包装组又带了同一个 class:
<defs><g id="tail" class="tail" ...>...</g></defs> <!-- 模板上也挂了 -->
<g class="tail"><use href="#tail"/></g> <!-- 宿主上也挂了 -->症状
transform: rotate(-8deg → 8deg) 实际渲染成 ±16°。幅度凭空翻倍,但每一条 CSS 规则单独看都完全正确。
根因
影子树里的克隆节点保留原元素的 class,会被文档样式表再次匹配;而外层包装组也匹配一次。两次旋转沿父子链复合,角度相加。
排查
document.getAnimations().map(a => a.animationName + " -> " + a.effect.target.getAttribute("class"))
// 同一个 animationName 对同一个 class 出现两次 = 中招本文对应成品修前 22 条动画、修后 21 条,差额就是那条重复的 sway -> tail。
解法
模板上不挂动画 class,只挂在 <use> 的包装组或 <use> 自身:
<defs><g id="tailG" ...>...</g></defs>
<g class="tail"><use href="#tailG"/></g>嵌套动画(父组转、子组再转)本身是想要的,关键区别在于同一条规则被匹配了几次,而不是嵌套了几层。
陷阱二:getScreenCTM() 测到的是模板,缺了宿主的变换
触发条件
写测试脚本验证"子元素有没有正确跟随父元素"时,用 document.getAnimations() 拿到动画目标,再 getScreenCTM() 换算坐标。
症状
测出的偏差大得离谱(本例 145px),但画面肉眼看着大致正常。最容易导致的错误结论:动画写错了,然后去改根本没写错的代码。
根因
a.effect.target 返回的是 <defs> 里的模板元素,它的父链是 g.sN < g#thighN < defs——不经过 <use> 宿主。所以 getScreenCTM() 只包含它自身的变换,不含外层包装组的旋转(大腿、曲柄等)。
同样的坑对 <use> 间接实例化的任何元素都成立(曲柄组里的踏板也一样)。
解法
手动按渲染顺序复合宿主与模板的矩阵(先作用内层):
const chainPt = (wrappers, tmpl, x, y) => {
const q = svg.createSVGPoint(); q.x = x; q.y = y;
let p = q.matrixTransform(tmpl.getScreenCTM()); // 内层:模板自身变换
for (let i = wrappers.length - 1; i >= 0; i--) { // 外层:宿主包装组,由内向外
const m = wrappers[i].getScreenCTM();
const r = svg.createSVGPoint(); r.x = p.x; r.y = p.y;
p = r.matrixTransform(m);
}
return p;
};
// 踝点 = 大腿宿主组 ∘ 小腿模板,作用在绘制坐标上
const ankle = chainPt([T.tN], T.sN, 424, 452);改写后同一份动画的实测偏差从 145px 降到 3.3px——动画从头到尾都是对的,错的是测量。
下次如何快速识别
- 测出的误差接近某个父级变换的量级(几十到上百 px),而不是几 px 的数值误差 → 先怀疑父链没复合上。
- 一个特征判据:膝盖位置完全吻合、脚踝却大幅偏离——膝盖只由父级(大腿)决定,脚踝还依赖内层(小腿)。凡是"父级对、子级错"的测量结果,都该先检查矩阵是不是少乘了一层,再怀疑关键帧。
附带结论
- 文档
<style>里的 CSS 会生效于<use>影子树内的元素,这是嵌套动画能工作的前提。 - 但模板的 class 被所有实例共享,所以没法用 class 给"某一个实例的影子内容"单独设样式或变换原点。需要逐实例差异时,只能把差异放到实例侧的包装元素上(本例远腿的半周期延迟
animation-delay:-0.6s就挂在宿主包装组上,而不是模板上)。
相关
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend