用 <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