SMIL与属性动画
SMIL 在 SVG 里主要是 animate / animateTransform / animateMotion(以及 set)。它写在文档里,不依赖页面 JS 或外部 CSS。Chrome 2015 年对 SMIL 发过弃用意向,2016-08 已暂停;到 2026,Chromium / Gecko / WebKit 仍能跑。生产口径是 仍可用,岗位是自包含文件,不是应用 UI 的默认时间轴。
应用内首选 CSS 与 WAAPI。SMIL 的岗位是自包含 .svg 文件。
1. 适用 / 不适用
适用: 作为 <img src="*.svg">、CSS background-image、无运行时的静态资源;需要 animateMotion 沿 path,而文件里不许带 JS。
不适用: 要暂停、seek、和按钮状态机同步的产品 UI;要进用户上传 / 富文本的任意 SVG(扩大可执行面);要三引擎像素级对拍的复杂 syncbase 时间图。引擎边角见 SMIL、foreignObject 与文本。
2. 自包含文件怎么写
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="8" fill="steelblue">
<animate
attributeName="r"
values="8;12;8"
dur="1.2s"
repeatCount="indefinite"
/>
</circle>
</svg>
<img> 不执行脚本,但 会跑文件内 SMIL——这正是 2016 暂停弃用的理由之一:CSS / WAAPI 进不去这层沙箱。attributeName 动的是 XML 属性,不依赖「该属性是否已升级成 CSS 几何属性」。
animateTransform 改 transform 属性,和页面 CSS transform 会抢。自包含文件里不要混用。animateMotion 见 路径动画。
3. begin / end 事件与时间图
<rect id="a" x="0" y="0" width="20" height="20">
<animate id="go" attributeName="x" from="0" to="40" dur="0.4s" begin="indefinite" fill="freeze"/>
</rect>
export function startSmil(anim: SVGAnimationElement): void {
anim.beginElement();
}
export function onSmilEnd(
anim: SVGAnimationElement,
handler: () => void,
): () => void {
const wrapped = (): void => {
handler();
};
anim.addEventListener("endEvent", wrapped);
return () => {
anim.removeEventListener("endEvent", wrapped);
};
}
规范事件是 beginEvent / endEvent / repeatEvent(注意不是 HTML 的 begin)。begin="go.end+0.2s" 这种 syncbase 在 Gecko 上最完整,Chromium 有历史边角,不要当跨引擎编排总线。
begin="click" / mouseover 让「静态图」变成可交互时间线。自包含装饰用时钟 0s;交互应发生在 有 JS 的 inline 文档 里,用 WAAPI。
4. 用户上传:SMIL 按可执行面处理
用户 SVG 的威胁模型和 HTML 同类,见 08 安全。消毒如果只剥 <script> 和 on*,留下 animate* 等于留下:
- 事件
begin(点击、焦点)驱动的时间线 - 与漏网
script/foreignObject的组合面 - 无限
repeatCount的 CPU / 电池消耗
产品规则:用户上传白名单不要包含 SMIL 元素。 illustrator / 设计稿进编辑器,先烘焙成静态几何,动画由应用自己的 WAAPI 加。不要「看起来只是图,就 inline」。
const SMIL_TAGS = new Set([
"animate",
"animateTransform",
"animateMotion",
"animateColor",
"set",
]);
export function rejectSmil(root: SVGElement): boolean {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let node: Node | null = walker.currentNode;
while (node) {
if (node instanceof SVGElement && SMIL_TAGS.has(node.localName)) {
return true;
}
node = walker.nextNode();
}
return false;
}
发现即拒收或剥掉整棵动画子树,不要尝试「安全地解释」用户时间线。
5. 失败形态
| 症状 | 原因 |
|---|---|
文档写了 CSS 关键帧,<img> 里不动 | SMIL 才是无 JS 通道;页面 CSS 进不去 img 文档 |
| 点一下才播、消毒后仍能播 | begin="click" 等事件 begin 没剥 |
| Chrome / Firefox 衔接差 1 拍 | syncbase / repeatCount 细节当可移植协议用了 |
| 和 WAAPI 互相打架 | 同一 transform 两条写入通道 |
| 上传 SVG 吃满 CPU | 未拦截 indefinite SMIL |
权威资料
核对日期:2026-08-26