在之前的九篇文章中,我们学会了绘制各种 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 实例,继承自 SVGAnimationElement
6.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对象)

三个核心认知:

  1. SMIL 是声明式动画的“原住民”——你不需要写一行 JavaScript 就能让 SVG 动起来。<animate>、<animateTransform>、<animateMotion> 分别对应属性动画、变换动画和路径动画三种核心需求。

  2. JavaScript 让 SMIL 更强大——通过 SVGAnimationElement 接口,你可以精确控制动画的播放、暂停、跳转,并通过 beginEvent、repeatEvent、endEvent 监听动画的生命周期。

  3. SMIL 没有被淘汰——虽然曾经被 Chrome 弃用,但后来开发者中止了这一计划。SMIL 仍然是 SVG 动画中路径动画和精细属性控制的最佳选择。

下篇预告

在下一篇文章中,我们将深入探索 SMIL 动画中最具视觉冲击力的特性——<animateMotion> 与路径漫游。你将学会:

  • 用 <animateMotion> 让物体沿任意复杂路径自动行驶。

  • 用 rotate="auto" 实现物体方向与路径切线对齐。

  • 用 <mpath> 引用外部路径。

  • 结合 TimeEvent 监听路径动画的进度。

  • 综合实战:制作一个 SVG 小车的轨迹导航动画。

互动话题:尝试用 <animate> 和 <animateTransform> 组合出一个你喜欢的交互动画效果(比如跳动的心、旋转的风车、摆动的钟摆)。欢迎在评论区分享你的 SVG 代码和效果描述!