用 SVG 来做你自己的动画!

2,112 阅读14分钟

废话不多说,今天来了解 svg 矢量元素的原理和动画实现!!

在做动画之前,我们先来复习一下 svg 基础,相信大家也都完全掌握了😏~~

0. 基础知识回顾

如果你已经掌握了基础,可以直接跳到下面动画章节

svg 文件引入方式

  • 直接使用 svg标签 内嵌到 html,这样可以完全控制 svg。
  • 通过 object 加载,适用于需要交互性和内部执行脚本的 svg:
<object data="image.svg" type="image/svg+xml"></object>
  • 使用 img 标签引入,不存在交互性,会产生样式隔离,加载速度快。
  • iframe 加载
  • 使用框架
// 你可以封装为组件,然后引入
<template>
  <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
    ...
  </svg>
</template>

...
import SvgIcon from '@/components/icons/SvgIcon.vue';

// 也可以使用插件,比如 vue-svg-loader、@svgr/webpack、react-svg-loader 等直接当做组件引入
import { ReactComponent as IconComponent } from './assets/image.svg';

当然你也可以这样创建一个 svg:const svg = document.createElementNS("www.w3.org/2000/svg", "svg");

参考坐标系

浏览器标准中,页面共用一套坐标系统,示意图如下:

image.png

svg 元素常用属性

  • width/height: 定义的画布尺寸
  • viewBox: 定义画布上可以显示的区域

比如下面的例子:

<svg width="200" height="200" viewBox="0 0 100 100"></svg>

这里定义的画布尺寸是 200 * 200px。但是,viewBox 属性定义了画布上可以显示的区域:从 (0,0) 点开始,100 宽、100 高的区域。这个 100 * 100 的区域,会放到 200 * 200 的画布上显示。于是就形成了放大两倍的效果(这样虽然放大了,但是图片可能会显示不全了)。

  • stroke-* 族属性,设置线条样式,在形状中会讲
  • opacity、fill-opacity:透明度与填充透明度设置
  • fill-rule:填充规则,确定填充封闭区域的判断算法
  • transform;设置位移样式,同css使用
  • role 和 aria-label:快捷访问参数,用于屏幕阅读器等
  • xmlns 命名空间。他是 XML 文档的一部分,用于避免命名冲突。对于 SVG,标准命名空间是 www.w3.org/2000/svg 用于正确解析 svg
  • version svg 的版本

注意,svg 定义的长度都是不带 px 的,他是相对于上级 viewbox、width、height 而言的。

常用的形状

image.png

  • 路径

通用的形状元素,可以换出任意形状。

// fill 和 stroke 是通用填充和线条属性,下面讲属性会提到
<path
    fill="none"
    stroke="red"
    d="M 10,30
       A 20,20 0,0,1 50,30
       A 20,20 0,0,1 90,30
       Q 90,60 50,90
       Q 10,60 10,30 z" />

重点讲解一下 d 属性,他是一个表现属性,可以用作 css 来用,其声明一个字符串,其中包含一组路径指令。指令组如下:

path 指令描述示例
M、m移动到x,y坐标M x1,y1
L、l到x,y画直线L x1,y1
H、h到x位置画水平线H x
V、v到x位置画垂直线V y
C、c三次方贝塞尔曲线C x1 y1, x2 y2, x y。x1,y1是控制点起点,x2,y2是控制点终点,x,y是曲线的终点
S、s三次方贝塞尔曲线(简写)S x2 y2, x y,如果 S 命令跟在一个 C 或 S 命令后面,则它的第一个控制点会被假设成前一个命令曲线的第二个控制点的中心对称点,如果 S 命令单独使用,前面没有 C 或 S 命令,那当前点将作为第一个控制点
Q、q二次方贝塞尔曲线Q x1 y1, x y 二次函数就只有一对控制点
T、t二次方贝塞尔曲线(简写)T x1,y1 快捷命令 T 会通过前一个控制点,推断出一个新的控制点
A、a椭圆A x轴半径 y轴半径 x轴相对于当前坐标系的旋转角度 使用大弧(1)或小弧(0) 绘制方向(1顺时针 0逆时针) x y
Z、z封闭路径(大小写同等作用)通常用于指令的最后,确保路径是闭合的,通常用在需要填充路径的地方

我们会单独出一期贝塞尔曲线的数学推导计算的讲解,也可以参照这个 贝塞尔曲线的数学知识

大小写分别表示绝对坐标位置和相对当前位置的坐标位置

你还可以使用 css 控制 path

// 在 hover 时显示一个 path,在 css 中可声明 d
#svg_css_ex1:hover path {
  d: path(
    "M10,30 A20,20 0,0,1 50,30 A20,20 0,0,1 90,30 Q90,60 50,90 Q10,60 10,30 z M5,5 L90,90"
  );
}
  • 矩形
// x,y 表示左上角的坐标, rx/ry 表示圆角的半径(这里不能特殊化只配置一个圆角)
<rect x="60" y="10" rx="10" ry="10" width="30" height="30"/>

如果要支持独立圆角的矩形,需要用到路径自定义:

<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M0 96C0 42.9807 42.9807 0 96 0H254C279.405 0 300 20.5949 300 46V222C300 265.078 265.078 300 222 300H122C54.6213 300 0 245.379 0 178V96Z" fill="#0286FF" fill-opacity="0.75"/>
</svg>
  • 圆、椭圆
// cx cy 圆中心的位置  r 半径 
<circle cx="0" cy="0" r="50" fill="blue" />

// cx cy 椭圆中心的位置  rx ry 两个半径 
<ellipse cx="75" cy="75" rx="20" ry="5"/>
  • 直线
// 定义起始位置和结束位置即可
<line x1="10" x2="50" y1="110" y2="150" stroke="black" stroke-width="5"/>
  • 折线
// points 是坐标的集合,(x,y) 为一组,表示多个点的连线
<polyline points="60, 110 65, 120 70, 115 75, 130 80, 125 85, 140 90, 135 95, 150 100, 145"/>

常用的属性

  • 颜色

下面的属性都可以使用 css 代替

// stroke 边框颜色 fill 封闭区域的填充颜色 opacity表示对应的透明度
<rect x="10" y="10" width="100" height="100" stroke="blue" fill="purple"
       fill-opacity="0.5" stroke-opacity="0.8"/>
  • 描边
// stroke-linecap="butt",端点是平齐的。stroke-linecap="round",端点是圆形的。 stroke-linecap="square",端点是方形的。
<line x1="40" x2="120" y1="20" y2="20" stroke="black" stroke-width="20" stroke-linecap="butt"/>

image.png

// stroke-linejoin属性,用来控制两条描边线段之间,用什么方式连接。stroke-linejoin="miter",连接处是尖锐的。 stroke-linejoin="round",连接处是圆润的。stroke-linejoin="bevel",连接处是平坦的。
<polyline points="40 60 80 20 120 60" stroke="black" stroke-width="20"
      stroke-linecap="butt" fill="none" stroke-linejoin="miter"/>

image.png

// stroke-dasharray 定义虚线,其属性的参数,是一组用逗号分割的数字组成的数列。第一个数字是实线长度,第二个是空白长度,第三个是实线长度,以此类推往复。
<path d="M 10 75 Q 50 10 100 75 T 190 75" stroke="black"
    stroke-linecap="round" stroke-dasharray="5,10,5" fill="none"/>

此外,还有 fill-rule 配置填充规则, stroke-miterlimit 约束路径连接处的尖角长度, stroke-dashoffset 定义从路径起始点到虚线图案开始点的偏移量(往右偏移是负数,往左是正的)

常用 defs

<defs> 标签用于定义可以重复使用的元素和图形片段。任何在 <defs> 中定义的内容不会直接呈现,而是可以在 SVG 文档的其他地方通过引用来使用。这样可以减少重复代码,提高 SVG 文件的可维护性和效率。

  • 内置 css

svg 内部可以内置 css:

<defs>
<style><![CDATA[
   #MyRect {
     stroke: black;
     fill: red;
   }
]]></style>
</defs>

一个 svg 与 css 结合的例子:developer.mozilla.org/zh-CN/docs/…

  • 渐变
// 线性渐变
<defs>
<linearGradient id="Gradient1">
  <stop class="stop1" offset="0%" />
  <stop class="stop2" offset="50%" />
  <stop class="stop3" offset="100%" />
</linearGradient>
<style type="text/css">
  <![CDATA[
          #rect1 { fill: url(#Gradient1); }
          .stop1 { stop-color: red; }
          .stop2 { stop-color: black; stop-opacity: 0; }
          .stop3 { stop-color: blue; }
        ]]>
</style>
</defs>

上面的代码定义了一套线性渐变模式,并通过 css 的 fill 关联,作用于 id 为 rect1 的元素上。

stop 是渐变元素的内置组件,有两个专用属性:stop-color 和 stop-opacity

可以使用 xlink:href="#Gradient1" 来引用另一个渐变定义

// 径向渐变,定义从圆心到外部向量方向的扩展样式 cx 表示渐变区域的中心点,而 fx="0.25 表示渐变的焦点在x长度 25%的地方
 <radialGradient id="RadialGradient1" cx="0.5" cy="0.5" r="0.5" fx="0.25" fy="0.25">
  <stop offset="0%" stop-color="red" />
  <stop offset="100%" stop-color="blue" />
</radialGradient>

焦点与中心点是不一样的,比如:

image.png

  • 剪切

它是一种遮罩效应,我们来看下面的例子:

<defs>
<clipPath id="cut-off-bottom">
  <rect x="0" y="0" width="200" height="100" />
</clipPath>
</defs>

<circle cx="100" cy="100" r="100" clip-path="url(#cut-off-bottom)" />

上面的例子,在一个正圆的基础上,加一个矩形遮罩,矩形高度只有一半,利用 clip-path 属性与 ClipPath 元素结合来使用,这招覆盖的地方就显示,没有遮住的地方就被剪裁了:

image.png

  • 遮罩 mask
<defs>
<mask id="Mask">
  <rect x="0" y="0" width="200" height="200" fill="url(#Gradient)" />
</mask>
</defs>

<rect x="0" y="0" width="200" height="200" fill="red" mask="url(#Mask)" />

上面的例子,在一个矩形额基础上,加了一个带渐变(渐变省略)的 rect 遮罩,而且大小一样,这个遮罩就会盖在目标元素上面。

注意:同剪裁一样,遮罩超出目标元素的部分会被剪裁

  • 滤镜

滤镜功能比较复杂,简单来讲,基本上是通过定义 filter 后,在 svg元素上 filter="url(#blur)" 来应用的。比如下面的高斯模糊:

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="blur">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
    </filter>
  </defs>
  <circle cx="100" cy="100" r="50" fill="blue" filter="url(#blur)" />
</svg>

或者你可以写一个模拟阴影效果:

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="dropshadow">
      <feDropShadow dx="2" dy="2" stdDeviation="2" flood-color="gray" />
    </filter>
  </defs>
  <rect x="50" y="50" width="100" height="100" fill="blue" filter="url(#dropshadow)" />
</svg>

滤镜还有如下过滤算子:

<feBlend> <feColorMatrix> <feComponentTransfer> <feComposite> <feConvolveMatrix> <feDiffuseLighting> <feDisplacementMap> <feFlood> <feGaussianBlur> <feImage> <feMerge> <feMorphology> <feOffset> <feSpecularLighting> <feTile> <feTurbulence>,这个就不一一介绍了。

  • symbol

symbol 元素用来定义图形模板对象,不会直接渲染,需要通过 use 标签去实例化:

<svg>
  <!-- symbol definition  NEVER draw -->
  <symbol id="sym01" viewBox="0 0 150 110">
    <circle cx="50" cy="50" r="40" stroke-width="8" stroke="red" fill="red" />
    <circle
      cx="90"
      cy="60"
      r="40"
      stroke-width="8"
      stroke="green"
      fill="white" />
  </symbol>

  <!-- actual drawing by "use" element -->
  <use xlink:href="#sym01" x="0" y="0" width="100" height="50" />
  <use xlink:href="#sym01" x="0" y="50" width="75" height="38" />
  <use xlink:href="#sym01" x="0" y="100" width="50" height="25" />
</svg>

上面的例子,symbol 里定义了一组圆形,但是界面没有渲染出来,在下面的 use 调用后,才渲染在界面上:

image.png

如果想要多个 svg 文件公用一个 symbol,可以将这个 symbol 单独封装为一个 svg:

<svg style="display: none;">
  <symbol id="icon-heart" viewBox="0 0 24 24">
    <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
  </symbol>
</svg>

而在使用 <symbol> 元素时,你不需要在HTML中显式地“引入”包含 <symbol> 的 SVG 文件,只要确保 <symbol> 已经定义在相同的HTML文档中即可。所以在全局中引入:

<object type="image/svg+xml" data="上面的svg文件.svg" width="0" height="0" style="position: absolute; width: 0; height: 0;"></object>

然后在任意位置均可使用:

<svg width="100" height="100">
  <use href="上面的svg文件.svg#icon-heart" fill="red"></use>
</svg>

文本

用来标记大块文本的子部分,它必须是一个text元素或别的tspan元素的子元素。

text 系列元素通用属性:font-family、font-style、font-weight、font-variant、font-stretch、font-size、font-size-adjust、kerning、letter-spacing、word-spacing 和 text-decoration

比如下面的例子,将字体换成红色:

<text>
  // tspan 支持 x, dx, rotate, textLength 等属性
  <tspan font-weight="bold" fill="red">This is bold and red</tspan>
</text>

类似的文本元素还有:

  • tref: <tref xlink:href="#example" /> 引用另一个id的text标签
  • textPath: 利用 xlink:href 属性取得一个任意路径,把字符对齐到路径,于是字体会环绕路径、顺着路径走,如下图:

image.png

<textPath xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#my_path">
  This text follows a curve.
</textPath>

元素集合

我们来看一个例子,他画出了两个红色的矩形:

<g fill="red">
  <rect x="0" y="0" width="10" height="10" />
  <rect x="20" y="0" width="10" height="10" />
</g>

他们统一配置了 fill 为红色,包裹在 g 标签里,这样的 g 标签就叫做元素集合。集合的好处在于可以统一样式、统一管理、统一动画加载等,还可以统一调用,我们来看一个例子:

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义一个defs区域来存放可复用的元素 -->
  <defs>
    <!-- 使用g标签组合两个图形 -->
    <g id="myShape">
      <circle cx="20" cy="20" r="15" fill="red" />
      <rect x="10" y="10" width="20" height="20" fill="blue" />
    </g>
  </defs>
  
  <!-- 使用use标签引用之前定义的组合图形 -->
  <use href="#myShape" x="0" y="0" />
  <use href="#myShape" x="50" y="0" />
  <use href="#myShape" x="0" y="50" />
  <use href="#myShape" x="50" y="50" />
</svg>

上面的例子,定义了一个叫做 myShape 的组,但是仅仅在 defs 里证明,没有实际渲染,下面结合 use 来动态调用,达到组件复用的效果。这里类似 symbol,而 symbol 兼具 <g>的分组功能和 <defs>初始不可见的特性,同时可使用viewbox与preserveAspectRatio,如果想要复用样式,建议使用 symbol,如果只是单独分组,可使用 g。

在 use 中使用 xlink:href 属性引用相关复用元素id,use中 x、y 坐标是相对于原图形定义的初始坐标的位移坐标,相当于使用了 transform="translate(x, y)"

H5 API

1. pathLength

使用 pathLength 属性为路径指定一个预期的长度,然后利用此信息来调整路径动画或其他视觉效果的表现方式。

<path id="myPath" d="M10 80 Q 95 10 180 80 T 350 80" stroke="black" stroke-width="2" fill="none" />

...
const myPath = document.getElementById("myPath");
console.log("Path length:", myPath.pathLength.baseVal); // 获取路径长度

2. getTotalLength()

返回路径的总长度

3. getPointAtLength()

返回路径上指定长度处的坐标信息:

<svg width="360" height-"200" viewbox="0 0 360 200">
  <path id="myPath" d="M10 80 Q 95 10 180 80 T 350 80" stroke="black" stroke-width="4" fill="none" />
  <circle id="point" cx="0" cy="0" r="10" fill="red" />
</svg>

<script>
  const myPath = document.getElementById("myPath");
  const point = document.getElementById("point");
  const length = myPath.getTotalLength(); // 获取总路径长度

  const position = myPath.getPointAtLength(length * 0.5); // 取中点位置
  point.setAttribute("cx", position.x);
  point.setAttribute("cy", position.y);
</script>

上面的例子,使用小圆点定位给定长度的路径位置:

image.png

4. isPointInFill()

用于判断指定点是否位于路径的填充区域内

  console.log("Point in fill:", myPath.isPointInFill(new DOMPoint(pointX, pointY))); // 判断指定点是否在路径的填充区域内

5. isPointInStroke()

用于判断指定点是否位于路径的描边区域内

  console.log("Point in stroke:", myPath.isPointInStroke(new DOMPoint(pointX, pointY))); // 判断指定点是否在路径的描边区域内

1. 描边动画

描边动画主要是利用了 stroke-* 族属性的特点。

比如我们画一个圈圈来模拟 loading 的动画:

<svg stroke-width="20" class="icon fill-none" width="200" height="200">
  // cx="50%" cy="50%" 相对于 svg 居中
  <circle class="p" cx="50%" cy="50%" r="30%" />
</svg>

这样就只是一个静态的图片:

image.png

想让 stroke 动起来,就先看两个属性。

  • stroke-dasharray:定义描边虚线,如果合理使用就能做到线段移动的效果。它是一个由数字组成的数组,每两个数字表示一个虚线段的长度和间隙的长度。例如,stroke-dasharray="5,3" 表示虚线段的长度为 5 个单位,间隙的长度为 3 个单位。

  • stroke-dashoffset:用于控制虚线的起始位置,默认情况下,虚线的起始位置是从路径的起点开始的,而 stroke-dashoffset 可以用来偏移虚线的起点位置。它的值是一个长度值,表示虚线的起始点与路径起点的距离。

如果想实现这样的动画:

1.gif

看了上面的两个属性,相信你肯定有点想法吧,给你 10 分钟思考时间😏!

好了,时间到了,说答案吧。

一开始让边框偏移整个周长的量,这样就是白屏状态,然后设置一个 keyframes,设定时间内将平移量stroke-dashoffset设置为圆周长的长度。

但是有个问题,设置stroke-dashoffset前必须得设置描边虚线stroke-dasharray,所以我们就这样:设置stroke-dasharray为整个周长,然后让他偏移整个周长,这样初始状态就显示为白屏了,最后播放偏移量慢慢变为0的动画:

stroke-dasharray: 376;
stroke-dashoffset: 376;
animation: stroke 5s forwards;
...

@keyframes stroke {
  to {
    stroke-dashoffset: 0;
  }
}

当然直接写死周长不太适合,可以用js动态获取:

stroke-dasharray: var(--l);

...
path.style.setProperty('--l', path.getTotalLength();

从网上下载的 svg 图标都可以用这样的方式来设置描边动画,下面是例子:

2. 动画元素

我们直接上例子:

<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
  <rect width="10" height="10">
    <animate
      attributeName="rx"
      values="0;5;0"
      dur="10s"
      repeatCount="indefinite" />
  </rect>
</svg>

动画元素 <animate> 可以在 svg 中直接使用,attributeName 定义了自变量是哪个,常用选择项同 css 的 transition,values 是一组数据,类似于 keyframes,表示值在整个动画周期内的变化路径,dur是动画时间定义,repeatCount 定义重复次数,可以是一个 number 或者是 indefinite。

上面的例子表示这个矩形的圆角半径在 10 秒内从 0 变到 5 再到 0,(5 和 0 相对于 rect 的宽高而言),周期往复。

我们再看一下路径动画:

<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
  <path fill="none" stroke="lightgrey"
    d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />

  <circle r="5" fill="red">
    <animateMotion dur="10s" repeatCount="indefinite"
      path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </circle>
</svg>

animateMotion 内部的path,可以使用内置元素 <mpath xlink:href="#path" /> 复用定义好的路径

首先画一个封闭的路径,然后画一个圆圈,其内部定义一个路径动画,这个动画也可以定义一个 path,如果和上面的 path 一致,那就会出现小圆在这个路径上移动的动画:

再来看看 transform 动画:

<svg
  width="120"
  height="120"
  viewBox="0 0 120 120"
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  xmlns:xlink="http://www.w3.org/1999/xlink">
  <polygon points="60,30 90,90 30,90">
    <animateTransform
      attributeName="transform"
      attributeType="XML"
      type="rotate"
      from="0 60 70"
      to="360 60 70"
      dur="10s"
      repeatCount="indefinite" />
  </polygon>
</svg>

先使用多边形元素定义一个三角形,然后加入 animateTransform 来定义动画。其中 type 可选:

  • rotate:旋转。
  • translate:平移。
  • scale:缩放。
  • skewX:沿 X 轴倾斜。
  • skewY:沿 Y 轴倾斜。

至于不是像素变量的 from 和 to,比如 rotate,0 60 70 --> 360 60 70 的意思是围绕点(60, 70)从0旋转360deg。

3. 使用 css 控制 svg 动画

当然了,你也可以将 svg 当做一个普通的 dom 行内元素,通过添加自定义 css 来控制动画:

<style>
@keyframes move {
  0% { transform: translateX(0); }
  50% { transform: translateX(200px); }
  100% { transform: translateX(0); }
}

#myCircle {
  animation: move 2s ease infinite; /* 应用名为 move 的动画 */
}
</style>

<svg width="500" height="100">
  <circle id="myCircle" cx="50" cy="50" r="20" fill="red" />
</svg>

效果:

1.gif


完!!