Skip to content

SVG 动画(SMIL / CSS)

SVG 支持多种动画方式:SMIL 动画(<animate><animateTransform><animateMotion>)、CSS 动画和 JavaScript 控制。SMIL 动画声明在 SVG 内部,无需额外脚本;CSS 动画利用 Web 动画标准;JavaScript 则提供最大灵活性。本节将对比介绍这三种方式的用法和适用场景。

前置知识

阅读本节前,建议先了解:SVG 渐变与滤镜

基础概念

SVG 动画的三种实现方式各有优势:

方式优点缺点适用场景
SMIL声明式、无需 JSChrome 曾弃用后恢复、功能有限简单路径动画、循环动画
CSS性能好、GPU 加速无法操作 SVG 特有属性变换、透明度、颜色过渡
JavaScript完全控制、可交互需要编写代码复杂动画、交互式图形

SMIL 动画

animate - 属性动画

<animate> 用于对 SVG 属性进行动画。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SVG SMIL 动画</title>
  <style>
    svg { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>SVG SMIL 动画</h1>

  <svg width="500" height="300" viewBox="0 0 500 300">
    <!-- 圆形半径动画 -->
    <circle cx="100" cy="100" r="20" fill="#4285f4">
      <animate attributeName="r" values="20;50;20" dur="2s" repeatCount="indefinite"/>
    </circle>

    <!-- 矩形颜色动画 -->
    <rect x="200" y="50" width="80" height="80" rx="10" fill="#34a853">
      <animate attributeName="fill" values="#34a853;#ea4335;#fbbc05;#34a853" dur="4s" repeatCount="indefinite"/>
    </rect>

    <!-- 透明度动画 -->
    <circle cx="380" cy="100" r="40" fill="#ea4335">
      <animate attributeName="opacity" values="1;0.3;1" dur="1.5s" repeatCount="indefinite"/>
    </circle>

    <!-- 位移动画 -->
    <rect x="50" y="220" width="40" height="40" fill="#9c27b0">
      <animate attributeName="x" values="50;400;50" dur="3s" repeatCount="indefinite"/>
    </rect>
  </svg>
</body>
</html>

animate 常用属性

属性说明示例
attributeName要动画的属性rfillopacityx
from起始值20
to结束值50
values多个关键帧值20;50;20
dur持续时间2s500ms
repeatCount重复次数indefinite3
begin开始时间0sclick
fill动画结束后状态freezeremove
keyTimes关键帧时间点0;0.5;1
calcMode插值模式linearsplinediscrete

animateTransform - 变换动画

<animateTransform> 用于动画变换操作。

html
<svg width="400" height="300" viewBox="0 0 400 300">
  <!-- 旋转动画 -->
  <g transform="translate(100, 100)">
    <rect x="-30" y="-30" width="60" height="60" fill="#4285f4">
      <animateTransform attributeName="transform" type="rotate"
                        from="0" to="360" dur="3s" repeatCount="indefinite"/>
    </rect>
  </g>

  <!-- 缩放动画 -->
  <g transform="translate(250, 100)">
    <circle cx="0" cy="0" r="30" fill="#ea4335">
      <animateTransform attributeName="transform" type="scale"
                        values="1;1.5;1" dur="2s" repeatCount="indefinite"/>
    </circle>
  </g>

  <!-- 平移动画 -->
  <rect x="50" y="220" width="40" height="40" fill="#34a853">
    <animateTransform attributeName="transform" type="translate"
                      values="0,0;300,0;0,0" dur="4s" repeatCount="indefinite"/>
  </rect>
</svg>

animateMotion - 沿路径运动

<animateMotion> 使元素沿指定路径运动。

html
<svg width="400" height="300" viewBox="0 0 400 300">
  <!-- 定义运动路径 -->
  <defs>
    <path id="motionPath" d="M 50,200 C 100,50 300,50 350,200 S 200,350 50,200"
          fill="none" stroke="#ccc" stroke-dasharray="5,5"/>
  </defs>

  <!-- 沿路径运动的圆形 -->
  <circle r="15" fill="#ea4335">
    <animateMotion dur="4s" repeatCount="indefinite" rotate="auto">
      <mpath href="#motionPath"/>
    </animateMotion>
  </circle>

  <!-- 沿路径运动的矩形 -->
  <rect x="-15" y="-10" width="30" height="20" rx="5" fill="#4285f4">
    <animateMotion dur="4s" repeatCount="indefinite" begin="2s" rotate="auto">
      <mpath href="#motionPath"/>
    </animateMotion>
  </rect>
</svg>

SMIL 动画控制

html
<!-- 点击开始/暂停动画 -->
<circle cx="100" cy="100" r="30" fill="#4285f4">
  <animate attributeName="r" values="30;60;30" dur="2s"
           begin="click" repeatCount="indefinite" fill="freeze"/>
</circle>

<!-- 嵌套动画:先等待 1 秒,然后开始 -->
<circle cx="250" cy="100" r="20" fill="#34a853">
  <animate attributeName="opacity" values="1;0.2;1" dur="1s"
           begin="1s" repeatCount="indefinite"/>
</circle>

CSS 动画

CSS transition

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SVG CSS 动画</title>
  <style>
    svg { border: 1px solid #ccc; display: block; margin: 10px 0; }

    /* 鼠标悬停效果 */
    .hover-circle {
      transition: all 0.3s ease;
      cursor: pointer;
    }
    .hover-circle:hover {
      fill: #ea4335;
      transform: scale(1.2);
      transform-origin: center;
    }

    /* 脉冲动画 */
    .pulse {
      animation: pulse 2s ease-in-out infinite;
    }
    @keyframes pulse {
      0%, 100% { r: 30; opacity: 1; }
      50% { r: 45; opacity: 0.7; }
    }

    /* 旋转动画 */
    .spin {
      animation: spin 3s linear infinite;
      transform-origin: center;
    }
    @keyframes spin {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    /* 颜色循环 */
    .colorCycle circle {
      animation: colorChange 4s ease-in-out infinite;
    }
    @keyframes colorChange {
      0% { fill: #4285f4; }
      33% { fill: #ea4335; }
      66% { fill: #34a853; }
      100% { fill: #4285f4; }
    }

    /* 虚线动画(蚂蚁线) */
    .dash-anim {
      stroke-dasharray: 8 4;
      stroke-dashoffset: 0;
      animation: dashMove 1s linear infinite;
    }
    @keyframes dashMove {
      to { stroke-dashoffset: -12; }
    }
  </style>
</head>
<body>
  <h1>SVG CSS 动画</h1>

  <svg width="500" height="300" viewBox="0 0 500 300">
    <!-- 悬停效果 -->
    <circle class="hover-circle" cx="60" cy="60" r="30" fill="#4285f4"/>

    <!-- 脉冲动画 -->
    <circle class="pulse" cx="180" cy="60" r="30" fill="#34a853"/>

    <!-- 旋转动画 -->
    <g class="spin" transform="translate(300, 60)">
      <rect x="-25" y="-25" width="50" height="50" rx="5" fill="#ea4335"/>
    </g>

    <!-- 颜色循环 -->
    <g class="colorCycle" transform="translate(420, 60)">
      <circle cx="0" cy="0" r="30"/>
    </g>

    <!-- 蚂蚁线动画 -->
    <rect class="dash-anim" x="30" y="150" width="200" height="100" rx="8"
          fill="none" stroke="#9c27b0" stroke-width="2"/>

    <!-- 描边动画 -->
    <rect x="280" y="150" width="180" height="100" rx="8"
          fill="none" stroke="#4285f4" stroke-width="2"
          stroke-dasharray="560" stroke-dashoffset="560"
          style="animation: drawLine 2s ease forwards;">
    </rect>
    <style>
      @keyframes drawLine {
        to { stroke-dashoffset: 0; }
      }
    </style>
  </svg>
</body>
</html>

CSS 动画 vs SMIL 的属性支持

属性SMIL animateCSS animationCSS transition
fill / stroke支持支持支持
opacity支持支持支持
transform支持支持支持
r / cx / cy支持部分支持不支持
d (路径)支持不支持不支持
points支持不支持不支持
stroke-dashoffset支持支持支持

JavaScript 控制

SVG DOM 操作

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SVG JavaScript 动画</title>
  <style>
    svg { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>SVG JavaScript 控制</h1>

  <svg id="jsSvg" width="400" height="300" viewBox="0 0 400 300">
    <circle id="jsCircle" cx="200" cy="150" r="40" fill="#4285f4"/>
  </svg>

  <script>
    const circle = document.getElementById('jsCircle');

    // 使用 requestAnimationFrame 做动画
    let time = 0;
    function animate() {
      time += 0.02;

      // 修改属性
      const x = 200 + Math.sin(time) * 100;
      const y = 150 + Math.cos(time * 0.7) * 60;
      const r = 30 + Math.sin(time * 1.5) * 15;
      const hue = (time * 30) % 360;

      circle.setAttribute('cx', x);
      circle.setAttribute('cy', y);
      circle.setAttribute('r', Math.max(5, r));
      circle.setAttribute('fill', `hsl(${hue}, 70%, 55%)`);

      requestAnimationFrame(animate);
    }
    animate();

    // SVG 元素也支持 CSS 属性操作
    // circle.style.fill = 'red';
    // circle.style.transform = 'scale(1.5)';
  </script>
</body>
</html>

使用 Web Animations API

javascript
// SVG 元素也支持 Web Animations API
const circle = document.getElementById('jsCircle');

circle.animate([
  { r: 40, fill: '#4285f4' },
  { r: 60, fill: '#ea4335' },
  { r: 40, fill: '#34a853' },
  { r: 40, fill: '#4285f4' }
], {
  duration: 3000,
  iterations: Infinity
});

交互式 SVG 动画

javascript
// 鼠标跟随效果
const svg = document.getElementById('jsSvg');
const circles = svg.querySelectorAll('.particle');

svg.addEventListener('mousemove', (e) => {
  const rect = svg.getBoundingClientRect();
  const x = ((e.clientX - rect.left) / rect.width) * 400;
  const y = ((e.clientY - rect.top) / rect.height) * 300;

  circles.forEach((circle, i) => {
    const angle = (i / circles.length) * Math.PI * 2;
    const distance = 40 + Math.sin(Date.now() / 500 + i) * 20;
    const cx = x + Math.cos(angle) * distance;
    const cy = y + Math.sin(angle) * distance;

    circle.setAttribute('cx', cx);
    circle.setAttribute('cy', cy);
  });
});

注意事项

1. SMIL 在 Chrome 中的历史

Chrome 曾在 2015 年宣布弃用 SMIL,但后来恢复了支持。SMIL 目前仍然有效,但推荐对于简单动画优先使用 CSS。

2. CSS transform 在 SVG 中的差异

css
/* SVG 元素的 transform-origin 默认是 SVG 画布的原点 (0,0) */
/* 需要显式设置 transform-origin */
.my-element {
  transform-origin: center; /* 可能不生效 */
  transform-origin: 50% 50%; /* 推荐使用百分比 */
  transform-box: fill-box; /* 让 transform-origin 相对于元素边界框 */
}

3. 动画性能

css
/* 推荐使用 transform 和 opacity(GPU 加速) */
.fast-animation {
  animation: move 1s linear infinite;
  /* 触发 GPU 合成层 */
  will-change: transform;
}

/* 避免动画化触发重排的属性 */
/* 不好的做法:动画化 width、height、x、y */

最佳实践

1. 动画方案选择指南

需求推荐方案
悬停/焦点反馈CSS transition
循环装饰动画CSS animation
沿路径运动SMIL animateMotion
路径变形动画SMIL animate(d 属性)
交互式动画JavaScript
复杂编排JavaScript + Web Animations API
入场动画CSS animation + delay

2. 描边动画模板

css
.draw-path {
  stroke-dasharray: var(--path-length);
  stroke-dashoffset: var(--path-length);
  animation: draw 2s ease forwards;
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}
javascript
// 动态获取路径长度
const path = document.querySelector('.draw-path');
const length = path.getTotalLength();
path.style.setProperty('--path-length', length);

下一节

继续学习:SVG Sprite

参考链接