在之前的九篇文章中,我们学会了绘制各种 SVG 图形、用分组组织它们、用矩阵操控它们的位置和姿态,用 SVGStringList 与 SVGNumberList 读写和操作“批量数据”。但到目前为止,我们做的所有事情都是静态的——图形画好了就定在那里,不会动。
而真实世界是动态的。一个产品 Logo 应该有微弱的呼吸感、一个加载指示器应该不停旋转、一个数据图表应该有平滑的入场动画、一个交互按钮应该在 hover 时产生流畅的反馈。
让图形“动起来”的方式有很多种:CSS Animation、JavaScript + requestAnimationFrame、Web Animation API……但在 SVG 的世界里,有一种最原生、最纯粹的方式—— SMIL(Synchronized Multimedia Integration Language,同步多媒体集成语言)。
SMIL 是一种基于 XML 的语言,专门用于编写交互式多媒体演示。在 SVG 中,SMIL 通过一组动画元素(<animate>、<animateTransform>、<animateMotion>、<set>)来实现声明式的动画定义。
什么是“声明式动画”?
声明式意味着你 不需要写任何 JavaScript——你只需要在 SVG 标签中声明“这个属性要从 A 变到 B,花 3 秒钟”,浏览器就会自动完成中间的插值和渲染。这就像 CSS Transition 一样——你只管声明终点,浏览器帮你算中间过程。
本文将从最基础的 <animate> 开始,逐步深入到 <animateTransform> 和 <animateMotion>,最后通过 JavaScript 实现对 SMIL 动画的精确控制。
一、SMIL 动画的“家族谱系”
在深入具体元素之前,先了解一下 SMIL 动画元素的继承体系:
EventTarget
↓
Node
↓
Element
↓
SVGElement
↓
SVGAnimationElement ← 所有动画元素的基类
↓ ↓ ↓ ↓
SVGAnimateElement SVGAnimateTransformElement SVGAnimateMotionElement SVGSetElement
(<animate>) (<animateTransform>) (<animateMotion>) (<set>)SVGAnimationElement 是所有动画元素接口的基接口,提供了所有动画共有的属性和方法。这意味着 <animate>、<animateTransform>、<animateMotion> 和 <set> 都共享一套核心能力——比如 beginEvent、endEvent、repeatEvent 等事件。
二、<animate>:属性动画的“基本粒子”
<animate> 是 SMIL 动画中最基础的元素。它的作用非常简单:在指定时间内,将目标元素的某个属性从起始值平滑变化到结束值。
2.1 属性一览
属性 | 说明 | 示例 |
|---|---|---|
attributeName | 要动画化的属性名 | cx、r、fill、opacity |
attributeType | 目标属性的命名空间: CSS 或 XML,默认自动判定 | attributeType="CSS" |
from | 动画起始值 | 0 |
to | 动画结束值 | 100 |
by | 相对偏移量(与 to 二选一,不能同时使用) | 50 |
values | 分号分隔的多个值(替代 from/to) | 0;50;100;50;0 |
dur | 动画持续时间 | 5s(5 秒)、500ms(500 毫秒) |
repeatCount | 重复次数 | indefinite(无限循环)、3(3 次) |
repeatDur | 重复总时长 | 10s |
fill | 动画结束后的状态 | freeze(保持最后一帧)、remove(恢复初始值) |
begin | 动画开始时间 | 0s、2s、click(点击触发) |
end | 动画结束时刻 | 5s |
restart | 重新开始策略:always(默认)/ whenNotActive / never | never |
calcMode | 值插值算法:discrete / linear(默认)/ paced / spline | spline |
keyTimes | 与 values 配合使用,定义每个关键帧的时间点(0~1) | 0;0.5;1 |
keySplines | calcMode="spline" 时的贝塞尔控制点,分号分隔 | 0.42 0 0.58 1; 0.42 0 0.58 1 |
additive | 动画值是否与基础值叠加:replace(默认)/ sum | sum |
accumulate | 多次重复时是否累积:none(默认)/ sum | sum |
2.2 基本示例:让圆“呼吸”
<svg viewBox="0 0 400 200">
<circle cx="200" cy="100" r="30" fill="#4A90D9">
<!-- 让半径在 30 和 80 之间来回变化,周期 2 秒,无限循环 -->
<animate attributeName="r" values="30;80;30" dur="2s" repeatCount="indefinite" />
</circle>
</svg>这个例子中,<animate> 被放在 <circle> 内部,它会自动动画化父元素的 r 属性。values="30;80;30" 定义了三个关键帧——从 30 到 80 再到 30,形成一个完整的呼吸周期。
2.3 多属性同时动画
如果你想同时动画化多个属性,只需要在同一个元素内添加多个 <animate> 子元素:
<circle cx="200" cy="100" r="30" fill="#4A90D9">
<!-- 半径变化 -->
<animate attributeName="r" values="30;80;30" dur="2s" repeatCount="indefinite" />
<!-- 颜色变化 -->
<animate attributeName="fill" values="#4A90D9;#E67E22;#4A90D9" dur="2s" repeatCount="indefinite" />
<!-- 透明度变化 -->
<animate attributeName="opacity" values="1;0.5;1" dur="2s" repeatCount="indefinite" />
</circle>2.4 values vs from/to
values 属性比 from/to 更强大——它支持多个关键帧,而 from/to 只能定义起点和终点。
<!-- 只有起点和终点 -->
<animate attributeName="cx" from="0" to="300" dur="3s" />
<!-- 多个关键帧:0 → 150 → 300 → 150 → 0 -->
<animate attributeName="cx" values="0;150;300;150;0" dur="3s" repeatCount="indefinite" />2.5 keyTimes 与 calcMode:精细控制节奏
keyTimes 属性可以精确控制每个关键帧在时间轴上的位置:
<!-- 前 10% 时间走完 0→100,后 90% 时间走完 100→200 -->
<animate attributeName="cx" values="0;100;200" keyTimes="0;0.1;1" dur="5s" />calcMode 控制插值方式:
linear(默认):线性插值
discrete:离散跳跃(不插值)
paced:均匀速度
spline:贝塞尔曲线插值(需配合 keySplines)
三、<animateTransform>:变换属性的“专属通道”
<animateTransform> 是 <animate> 的专门版本,用于动画化 transform 属性。
你可能会有疑问:为什么不用 <animate attributeName="transform">?
原因是 transform 属性的值不是简单的数字(如 cx="100"),而是一个复合值——比如 rotate(45, 100, 100) 包含角度和坐标。<animateTransform> 专门处理这种复杂性,让你可以分别动画化平移、旋转、缩放和斜切。
3.1 核心属性
属性 | 说明 | 示例 |
|---|---|---|
attributeName | 固定为 transform | — |
type | 变换类型 | translate、rotate、scale、skewX、skewY |
from / to / values | 变换参数值 | 见下方示例 |
3.2 旋转动画:让图形“转起来”
<svg viewBox="0 0 400 300">
<rect x="150" y="100" width="100" height="60" fill="#4A90D9" rx="4">
<!-- 绕矩形中心 (200, 130) 持续旋转 -->
<animateTransform attributeName="transform" type="rotate"
from="0 200 130" to="360 200 130"
dur="4s" repeatCount="indefinite" />
</rect>
</svg>from="0 200 130" 表示从 0 度开始,旋转中心在 (200, 130);to="360 200 130" 表示旋转到 360 度,同样的旋转中心。
3.3 平移与缩放组合
你可以在同一个元素上使用多个 <animateTransform> 来实现复合动画:
<rect x="0" y="100" width="80" height="60" fill="#E67E22" rx="4">
<!-- 水平来回移动 -->
<animateTransform attributeName="transform" type="translate"
values="0,0; 200,0; 0,0" dur="4s" repeatCount="indefinite" />
<!-- 同时持续旋转 -->
<animateTransform attributeName="transform" type="rotate"
from="0 40 130" to="360 40 130"
dur="2s" repeatCount="indefinite" additive="sum"
/>
</rect>注意:多个 <animateTransform> 实现叠加效果,需要添加 additive="sum"。在上面的例子中,矩形实现既在水平移动,又在自转效果,需要将其中一个动画的 additive 属性设置为 sum。这会告诉 SVG 渲染器将两个变换效果叠加起来,而不是替换。
3.4 additive 属性:叠加 vs 替换
additive 属性控制动画值是叠加在基础值之上,还是替换基础值。
<!-- 叠加模式:在原有 transform 基础上增加偏移 -->
<animateTransform attributeName="transform" type="translate"
from="0,0" to="100,0" dur="2s"
additive="sum" />四、<animateMotion>:沿路径行走
如果说 <animate> 动画化的是属性,<animateTransform> 动画化的是变换,那么 <animateMotion> 动画化的就是位置和方向——它让一个元素沿着一条路径运动。
4.1 基本用法
<svg viewBox="0 0 400 300">
<!-- 定义路径 -->
<path id="track" d="M 50 150 C 150 50 250 250 350 150" fill="none" stroke="#ddd" stroke-width="2" />
<!-- 沿路径运动的小车 -->
<rect x="-20" y="-10" width="40" height="20" fill="#4A90D9" rx="4">
<animateMotion dur="4s" repeatCount="indefinite" rotate="auto">
<mpath href="#track" />
</animateMotion>
</rect>
</svg><mpath> 仅仅是为动画提供了运动的“轨道”,而元素在轨道上的姿态(是否旋转:就像一辆车在弯道上行驶时会自动转向)则需要通过 <animateMotion> 的 rotate 属性来明确控制。这是一个需要开发者显式开启的功能。
4.2 控制旋转行为
rotate 属性控制元素是否跟随路径方向:
rotate 值 | 效果 |
|---|---|
0 | 默认值,仅平移,不旋转;不设置rotate默认rotate="0" |
auto | 自动跟随路径方向(默认) |
auto-reverse | 反向跟随 |
<angle> | 固定旋转角度(如 45) |
<angle> <cx> <cy> | 固定角度 + 自定义旋转中心 |
<!-- 固定为 0 度 ,不跟随路径方向 -->
<animateMotion dur="4s" rotate="0">
<mpath href="#track" />
</animateMotion>五、<set>:瞬间赋值(无过渡)
<set> 是最简单的动画元素——它不产生过渡动画,而是在指定时间瞬间将属性设置为某个值。
<circle cx="100" cy="100" r="30" fill="blue">
<!-- 3 秒后瞬间变成红色 -->
<set attributeName="fill" to="red" begin="3s" />
</circle><set> 常用于控制动画的触发时机或实现状态切换效果。
六、JavaScript 控制 SMIL 动画
SMIL 动画虽然是“声明式”的,但你完全可以通过 JavaScript 来控制它们——开始、暂停、跳转、监听事件。
6.1 获取动画元素
<circle cx="200" cy="100" r="30" fill="#4A90D9">
<animate id="myAnim" attributeName="r" values="30;80;30" dur="2s" repeatCount="indefinite" />
</circle>const anim = document.getElementById('myAnim');
// anim 是一个 SVGAnimateElement 实例,继承自 SVGAnimationElement6.2 控制播放
SVGAnimationElement 提供了以下控制方法:
方法 | 说明 |
|---|---|
beginElement() | 开始动画(如果已结束则重新开始) |
pauseElement() | 暂停动画 |
unpauseElement() | 恢复暂停的动画 |
getCurrentTime() | 获取当前动画时间(秒) |
setCurrentTime(seconds) | 跳转到指定时间点 |
getStartTime() | 获取动画的开始时间 |
endElement() | 立即结束动画 |
const anim = document.getElementById('myAnim');
// 暂停
anim.pauseElement();
// 恢复
anim.unpauseElement();
// 跳转到 1 秒处
anim.setCurrentTime(1);
// 重新开始
anim.beginElement();应用场景:这些方法让你可以实现“播放/暂停”按钮、进度条拖拽、动画重置等交互功能。
6.3 监听动画事件
SVGAnimationElement 触发三种事件:
事件 | 触发时机 | 事件类型 |
|---|---|---|
beginEvent | 动画开始播放时 | TimeEvent |
repeatEvent | 每次重复时(第一次重复后触发) | TimeEvent |
endEvent | 动画到达有效结束时 | TimeEvent |
const anim = document.getElementById('myAnim');
// 监听开始事件
anim.addEventListener('beginEvent', (e) => {
console.log(`动画开始了!重复次数: ${e.detail}`);
});
// 监听重复事件
anim.addEventListener('repeatEvent', (e) => {
console.log(`动画第 ${e.detail} 次重复`);
});
// 监听结束事件
anim.addEventListener('endEvent', () => {
console.log('动画结束了!');
});
// 也可以使用事件处理器属性
anim.onbegin = (e) => console.log('开始!');TimeEvent 接口提供了两个关键属性:
detail:对于 repeatEvent,表示当前的重复次数。
view:生成事件的 Window 对象。
七、综合实战:可交互的“播放器控制面板”
让我们综合运用所有知识,构建一个可交互的动画播放器——包含播放、暂停、重置、进度条拖拽和事件日志。
7.1 运行效果图:
播放/继续:开始或继续动画。
暂停:冻结动画在当前帧。
重置:将动画跳回起点。
进度条:拖拽到任意时间点,动画会跳转到对应位置。
事件日志:实时记录动画的开始、重复和结束事件。
所有的控制都是通过 SVGAnimationElement 的方法实现的——beginElement()、pauseElement()、unpauseElement()、setCurrentTime() 和 getCurrentTime()。并做了浏览器兼容性处理。

7.2 HTML 结构
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; font-family: sans-serif; }
.container { background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 24px; width: 90%; max-width: 600px; }
svg { border: 1px solid #ddd; border-radius: 8px; width: 100%; display: block; }
.controls { display: flex; gap: 12px; margin-top: 16px; flex-wrap: wrap; align-items: center; }
.controls button { padding: 8px 20px; border: none; border-radius: 6px; background: #4A90D9; color: white; cursor: pointer; font-size: 14px; transition: background 0.2s; }
.controls button:hover { background: #357ABD; }
.controls button:disabled { background: #ccc; cursor: not-allowed; }
.controls button.danger { background: #E74C3C; }
.controls button.danger:hover { background: #C0392B; }
.controls button.success { background: #27AE60; }
.controls button.success:hover { background: #1E8449; }
.progress-bar { flex: 1; min-width: 150px; display: flex; align-items: center; gap: 12px; }
.progress-bar input[type="range"] { flex: 1; cursor: pointer; }
.progress-bar span { font-size: 13px; color: #666; font-family: monospace; min-width: 40px; }
#log { margin-top: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; font-size: 13px; font-family: monospace; max-height: 80px; overflow-y: auto; color: #333; border: 1px solid #eee; }
.info { margin-top: 8px; font-size: 14px; color: #555; display: flex; gap: 20px; flex-wrap: wrap; }
.info span { background: #f0f0f0; padding: 4px 12px; border-radius: 12px; }
</style><div class="container">
<!-- SVG主画布:定义视口大小500x300,引入SVG和xlink命名空间以支持高级特性 -->
<svg id="mainSvg" viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- defs标签用于定义可复用的图形元素,这些元素不会直接显示,需要通过use引用 -->
<defs>
<!-- 定义运动路径motionPath:使用贝塞尔曲线创建一个闭合的运动轨迹,fill="none"表示路径本身不填充颜色 -->
<!-- M=起点坐标 C=三次贝塞尔曲线控制点 Q=二次贝塞尔曲线控制点,最终回到起点形成闭合路径 -->
<path id="motionPath" d="M 50 150 C 200 50 300 50 350 250 Q 200 150 50 150" fill="none" />
</defs>
<!-- 创建SVG画布背景:覆盖整个视口的矩形,使用浅灰白色填充 -->
<rect width="500" height="300" fill="#fafafa" />
<!-- 使用use标签引用之前定义的motionPath,绘制灰色虚线轨迹作为运动参考线 -->
<!-- stroke-dasharray="6,4"表示6像素实线+4像素空白的虚线模式 -->
<use xlink:href="#motionPath" stroke="#ddd" stroke-width="2" stroke-dasharray="6,4" />
<!-- 创建运动组motion-group:将所有需要运动的元素包裹在一个组内,方便统一控制 -->
<g id="motion-group">
<!-- 创建旋转组rotation-group:专门用于承载需要旋转的图形元素,实现自转效果 -->
<g id="rotation-group">
<!-- 绘制主体矩形:中心对齐原点,所以x/y为负的一半尺寸,蓝色填充,圆角rx=6 -->
<rect x="-30" y="-40" width="60" height="80" fill="#4A90D9" rx="6" />
<!-- 在矩形上方绘制一个圆形作为装饰:位于组原点正上方50像素处,橙色填充 -->
<circle cx="0" cy="-50" r="12" fill="#E67E22" />
</g>
<!-- 定义旋转动画rotationAnim:让rotation-group绕原点(0,0)从0度旋转到360度 -->
<!-- 持续时间3秒,无限循环,立即开始播放 -->
<animateTransform id="rotationAnim" attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="3s" repeatCount="indefinite" begin="indefinite" />
<!-- 定义路径运动动画motionAnim:让整个motion-group沿motionPath路径运动 -->
<!-- 持续时间6秒(是旋转动画的两倍,确保每完整路径运动一圈,图形自转两圈),无限循环 -->
<animateMotion id="motionAnim" dur="6s" repeatCount="indefinite" begin="indefinite">
<!-- mpath标签指定动画要跟随的路径,引用之前定义的motionPath -->
<mpath xlink:href="#motionPath" />
</animateMotion>
</g>
</svg>
<!-- 控制面板容器:包含所有交互按钮和进度条 -->
<div class="controls">
<button id="playBtn" class="success">▶ 播放</button>
<button id="pauseBtn">⏸ 暂停</button>
<button id="resetBtn" class="danger">⏹ 重置</button>
<div class="progress-bar">
<label for="progress"></label><input type="range" id="progress" min="0" max="6" step="0.01" value="0" />
<span id="timeDisplay">0.0s</span>
</div>
</div>
<div class="info"><span>🔄 循环次数: <b id="loopCountDisplay">0</b></span></div>
<div id="log">📋 事件日志:等待动画开始...</div>
</div>7.3 JavaScript交互逻辑
<script>
(function() {
'use strict';
const svg = document.getElementById('mainSvg');
const motionAnim = document.getElementById('motionAnim');
const rotAnim = document.getElementById('rotationAnim');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
const progress = document.getElementById('progress');
const timeDisplay = document.getElementById('timeDisplay');
const logEl = document.getElementById('log');
const loopCountDisplay = document.getElementById('loopCountDisplay');
// 省略部分代码,需要请关注微信公众号 sharing pi 留言 获取。
})();
</script>八、SMIL vs CSS Animation:如何选择?
很多开发者会问:“SMIL 和 CSS Animation 都能做动画,我该用哪个?”
对比维度 | SMIL | CSS Animation |
|---|---|---|
适用对象 | SVG 元素专用 | 所有 HTML/SVG 元素 |
路径动画 | 原生支持(<animateMotion>) | 需要 JS 辅助 |
多属性同步 | 精细控制 | 精细控制 |
JS 控制 | beginElement()、pauseElement()等 | 通过 animation-play-state 等控制 |
声明式 | 在 SVG 中声明 | 在 CSS 中声明 |
学习曲线 | 较陡(元素多、属性多) | 较平缓 |
浏览器支持 | 广泛(2020 年起全面支持) | 广泛 |
选择建议:
需要路径动画(物体沿曲线运动)→ 选 SMIL(<animateMotion>)。
需要精细控制 SVG 特定属性(如路径的 d 属性)→ 选 SMIL。
需要与 HTML 元素统一动画 → 选 CSS Animation。
需要复杂的 JS 交互控制(进度条、播放/暂停)→ 两者都可以,SMIL 的 API 更直接。
备注:Chrome 45 曾弃用 SMIL 而偏向 CSS 动画和 Web 动画,但之后 Chrome 开发者中止了对 SMIL 的弃用。SMIL 仍然是 SVG 动画的可靠选择。
九、无障碍与性能考量
9.1 减少运动偏好
对于患有前庭障碍、癫痫或偏头痛的用户,闪烁和快速运动的动画可能引发不适。你应该尊重用户的减少运动偏好:
@media (prefers-reduced-motion: reduce) {
animate, animateTransform, animateMotion {
animation: none;
}
/* 或者直接暂停所有 SMIL 动画 */
}你也可以通过 Sec-CH-Prefers-Reduced-Motion 客户端提示在服务端做出相应处理。
9.2 性能建议
SMIL 动画由浏览器底层渲染引擎驱动,性能通常优于同等的 JavaScript 动画。
避免同时运行大量(>50 个)独立的 SMIL 动画。
对于复杂的路径动画,考虑简化路径的顶点数量。
十、常见错误与避坑指南
错误一:用 <animate> 动画化 transform 属性
<!-- 错误:transform 需要用 animateTransform -->
<animate attributeName="transform" from="rotate(0)" to="rotate(360)" dur="2s" />正确做法:使用 <animateTransform>。
错误二:忘记设置 attributeName
<!-- 错误:没有指定要动画化哪个属性 -->
<animate from="0" to="100" dur="2s" />正确做法:始终设置 attributeName。
错误三:values 中值数量与 keyTimes 不匹配
<!-- 错误:values 有 3 个值,keyTimes 有 4 个值 -->
<animate values="0;100;200" keyTimes="0;0.3;0.6;1" dur="3s" />正确做法:keyTimes 的数量必须与 values 的数量一致。
错误四:混淆 beginElement() 和 begin
beginElement() 是 JavaScript 方法,用于从代码中启动动画。begin 是属性,用于声明动画的开始时间(如 begin="2s" 或 begin="click")。它们功能相似,但使用场景不同。
十一、本章总结
元素 | 接口 | 用途 | 关键属性 |
|---|---|---|---|
<animate> | SVGAnimateElement | 动画化任意属性 | attributeName、from/to/values、dur |
<animateTransform> | SVGAnimateTransformElement | 动画化 transform 属性 | type(translate/rotate/scale等) |
<animateMotion> | SVGAnimateMotionElement | 沿路径运动 | <mpath href="..."> |
<set> | SVGSetElement | 瞬间设置属性值 | to、begin |
SVGAnimationElement | 所有动画元素的基类 | 控制播放、监听事件 | beginElement()、pauseElement()、setCurrentTime() |
TimeEvent | 时间事件 | 携带动画事件的上下文信息 | detail(重复次数)、view(Window对象) |
三个核心认知:
SMIL 是声明式动画的“原住民”——你不需要写一行 JavaScript 就能让 SVG 动起来。<animate>、<animateTransform>、<animateMotion> 分别对应属性动画、变换动画和路径动画三种核心需求。
JavaScript 让 SMIL 更强大——通过 SVGAnimationElement 接口,你可以精确控制动画的播放、暂停、跳转,并通过 beginEvent、repeatEvent、endEvent 监听动画的生命周期。
SMIL 没有被淘汰——虽然曾经被 Chrome 弃用,但后来开发者中止了这一计划。SMIL 仍然是 SVG 动画中路径动画和精细属性控制的最佳选择。
下篇预告
在下一篇文章中,我们将深入探索 SMIL 动画中最具视觉冲击力的特性——<animateMotion> 与路径漫游。你将学会:
用 <animateMotion> 让物体沿任意复杂路径自动行驶。
用 rotate="auto" 实现物体方向与路径切线对齐。
用 <mpath> 引用外部路径。
结合 TimeEvent 监听路径动画的进度。
综合实战:制作一个 SVG 小车的轨迹导航动画。
互动话题:尝试用 <animate> 和 <animateTransform> 组合出一个你喜欢的交互动画效果(比如跳动的心、旋转的风车、摆动的钟摆)。欢迎在评论区分享你的 SVG 代码和效果描述!