svg 实现 MG 动画

1,190 阅读9分钟

上一篇文章我们使用 svg 绘制好了静态的图形,本篇文章将通过给静态图形添加动画效果,以实现如下所示的 MG 动画:

svg 实现动画的方式不止一种,我将会通过多种方式给不同的元素添加动画效果以展示它们的用法。

使用 css

从 SVG2 开始,原本在元素上使用的形变属性 transform 可以通过 css 来设置了,不过要注意用法有些许差别,比如以 css 的形式设置时值需要添加单位。顺便说一句,一旦某个元素应用了形变,该元素内部就会建立一个新的坐标系统,其后续的变化会基于新创建的坐标系。(关于 css 中 transform 与坐标系的更多细节,可浏览《css3 实现一个旋转的掘金方块》

十字

给定义了十字形的 <g> 元素一个 id,然后在 <style> 中定义动画,通过 transform-origin 将形变的中心点从默认的用户坐标系左上角移动到中心点,然后添加帧动画 rotate-ani。给 rotate-ani 设置动画时间为 2s,同时在 25% 时就逆时针旋转 45°,保持到 100%,这样就有了旋转后停顿的效果,forwards 则是让动画停止时保持最后一帧的状态,因为动画设置了 infinite 是循环的,所以此处其实可以不设置 forwards

<!-- 代码片段 1.1 -->
<style>
  #cross {
    transform-origin: 400px 300px;
    animation: rotate-ani 2s ease-out forwards infinite;
  }

  /* 逆时针旋转 45° */
  @keyframes rotate-ani {
    25%,
    100% {
      transform: rotate(-45deg);
    }
  }
</style>

<svg width="100%" height="600" viewBox="0 0 800 600">
  <!-- 十字 -->
  <g id="cross" stroke-width="8" stroke="#FAB748">
    <!-- 省略 -->
  </g>
</svg>

中心圆

将之前绘制好的位于图形中心的 4 个半圆组成一组,包裹在 <g id="mid-circle"> 内,统一添加样式,同样是先将形变中心移到用户坐标系的中心点,然后通过 transform: scale(0) rotate(90deg); 让中心圆先缩小到看不见,并呈现顺时针旋转 90° 的状态,然后在帧动画中将 25% 时的状态设置成动画开始时状态,以保证等待十字形的旋转结束后再开始中心圆动画,结束状态为 scale(1) 以及默认的 rotate(0),即恢复原来的大小和旋转角度:

<!-- 代码片段 1.2 -->
<style>
  #mid-circle {
    transform-origin: 400px 300px;
    transform: scale(0) rotate(90deg);
    animation: rotation-occurs 2s ease-out infinite;
  }

  /* 旋转变大出现 */
  @keyframes rotation-occurs {
    25% {
      transform: scale(0) rotate(90deg);
    }
    50%,
    100% {
      transform: scale(1);
    }
  }
</style>

<!-- 中间的圆 -->
<g id="#mid-circle">
  <!-- 紫色大半圆 -->
  <!-- 蓝色大半圆 -->
  <!-- 紫色小半圆 -->
  <!-- 蓝色小半圆 -->
</g>

目前实现的动画效果如下:

GIF 2023-9-27 14-44-12.gif

左右两端大三角形

以左上角黄色三角为例,剩余 3 个三角形的动画以此类推。通过 transform-origin 将形变的起始点移动到三角形的顶点,然后在动画的前 25% 的范围内保持隐藏,25% ~ 50% 时进行伸展动画,之后保持动画结束时的状态:

<!-- 代码片段 1.3 -->
<style>
  #left-top-triangle {
    transform-origin: 316px 216px;
    transform: scale(0);
    animation: extend 2s ease-out infinite;
  }

  /* 伸展出现 */
  @keyframes extend {
    25% {
      transform: scale(0);
    }
    50%,
    100% {
      transform: scale(1);
    }
  }
</style>
<path id="left-top-triangle" d="M 316 300 l 0 -84 l -84 84 Z"></path>

上下两端小正方形

以左上角蓝色正方形为例,添加一个从正方形左下角开始的放大出现效果,extend 动画在代码片段 1.3 中已定义,hide-to-appear 只是设置了一个透明度的变化:

<!-- 代码片段 1.4 -->
<style>
  #left-top-square {
    opacity: 0;
    /* x = 400 - 42; y = 300 - 84 */
    transform-origin: 358px 216px;
    animation: extend 2s ease-out infinite,
      hide-to-appear 2s ease-out infinite;
  }
  /* 隐藏到出现 */
  @keyframes hide-to-appear {
    25% {
      opacity: 0;
    }
    50%,
    100% {
      opacity: 1;
    }
  }
</style>

<!-- 左上角蓝色正方形  -->
<use id="left-top-square" x="358" y="174" href="#blueRect"></use>

给其它 3 个小正方形添加动画的方法类似,无非是调整下形变的原点坐标,不再赘述。

剪切小三角形

同样以左上角的橘色小三角形为例,添加的动画效果是从左上方渐显移入:

<!-- 代码片段 1.5 -->
<style>
  #left-top-s-triangle {
    /* x = 400 - 84 - 42; y = 300 - 84 - 42 */
    opacity: 0;
    transform: translate(-42px, -42px);
    animation: hide-to-appear 2s ease-out infinite,
      move-to-right-bottom 2s ease-out infinite;
  }
  @keyframes move-to-right-bottom {
    25% {
      transform: translate(-42px, -42px);
    }
    50%,
    100% {
      transform: translate(0, 0);
    }
  }
</style>
<!-- 左上角剪切三角 -->
<g
  id="left-top-s-triangle"
  clip-path="url(#cut-off-left-top)"
  fill="#FAB748"
  >
  <!-- 省略 -->
</g>

translate(x, y) 如果只设置 x,则 y 默认为 0

至此,动画效果如下:

GIF 2023-9-28 10-21-43.gif

使用 svg 内置的动画元素

四周小圆

现在介绍如何使用 svg 内置的动画元素来实现位于四周的小圆动画。svg 的内置动画元素除了包括本次案例中会使用的 <animate><animatetransform><animateMotion>,还有个 <set>,它们都是基于 SMIL(Synchronized Multimedia Integration Language,同步多媒体集成语言) 实现的。SMIL 是 W3C 推荐的可扩展标记语言,用于描述多媒体演示,由 XML 编写。使用动画元素时,直接在需要添加动画效果的元素内使用即可:

animate

左上角的小圆我使用 <animate> 来实现动画:

<!-- 代码片段 2.1 -->
<!-- 左上小圆 -->
<use x="232" y="132" href="#blueCircle">
  <animate
    attributeName="x"
    values="316; 232; 232"
    keyTimes="0; 0.2; 1"
    dur="2s"
    repeatCount="indefinite"
    fill="freeze"
    ></animate>
  <animate
    attributeName="y"
    values="216; 132; 132"
    keyTimes="0; 0.2; 1"
    dur="2s"
    repeatCount="indefinite"
    fill="freeze"
    ></animate>
</use>

介绍下用到的属性:

  • attributeName:指定要让哪个属性发生变动从而产生动画;
  • values:动画期间被修改的属性的值,通过 ; 分割。如果只有初始和最终值,可以用 from/ to 属性替代;
  • keyTimes:用于控制动画的快慢,它的值与 values 的值一一对应,并且为 [0, 1] 之间的浮点数。比如在 values 中,定义了 2 个 232,与之对应的 keyTimes 中的值为 0.21,代表动画在 2s * 0.2 即 0.4s 内就完成了,剩下的 1.6s(2s * 0.8)内 x 都维持 232 数值不变;
  • dur:动画的持续时间;
  • repeatCount:动画的重复次数,为 indefinite 则代表无限重复;
  • fill:定义动画的最终状态,为 freeze 则表示保持最后一帧的状态,如果是 remove 则代表回到第一帧。

另外,还有个没有用到但比较有用的 begin 属性,可以控制动画的开始条件,值可以是时间,比如 1s,或是某个事件,比如 cross.click,代表点击了 idcross 元素后触发。

animateTransform

小圆的移动除了可以通过改变属性 xy 实现,也可以通过像代码片段 1.5 中的剪切三角形动画那样,通过 transformtranslate 实现,只不过这里我选择使用专门用来定义平移、旋转、缩放等形变动画的元素 <animateTransform>,相比 <animate>,它多了个 type 属性用以指定是那种形变,并且需注意一个元素内只能添加一次 <animateTransform>,如果定义多个,后定义会覆盖之前的定义 :

<!-- 代码片段 2.2 -->
<!-- 左下小圆 -->
<use x="232" y="468" href="#blueCircle">
  <animateTransform
    attributeName="transform"
    type="translate"
    values="84, -84; 0, 0; 0, 0"
    keyTimes="0; 0.2; 1"
    dur="2s"
    repeatCount="indefinite"
    ></animateTransform>
</use>

可以对比下代码片段 1.5,看看使用 css 和 svg 内置动画元素在的区别。

掘金 logo

以左边的 logo 为例,介绍如何使用 <animateMotion> 添加沿着指定路径移动的动画。注意,添加动画的元素的坐标原点,会影响到运动路径,所以我将 <image>xy 的值均设置为了 0

<!-- 代码片段 2.3 -->
<!-- 左边 logo -->
<image x="0" y="0" href="../imgs/juejin.png" width="40">
  <animateMotion
    path="M 0 400, 50 340, 100 360, 150 300, 200 350, 230 300, 214 286"
    dur="2s"
    keyPoints="0; 0.5; 1; 1"
    keyTimes="0; 0.6; 0.8; 1"
    repeatCount="indefinite"
    ></animateMotion>
</image>
  • path 指定运动的路径,其值和 <path> 中的 d 属性是一样的。如果想查看路径的轨迹,可以专门定义一个 <path>
<!-- 代码片段 2.3.1 -->
<path
  d="M 0 400, 50 340, 100 360, 150 300, 200 350, 230 300, 214 286"
  fill="none"
  stroke="red"
></path>

下图中的红色折线即为动画的路径:

image.png

如果路径已经在其它地方定义好了,则也可以通过 <mpath> 直接引用:

<!-- 代码片段 2.3.2 -->
<path
  id="logo-path"
  d="M 0 400, 50 340, 100 360, 150 300, 200 350, 230 300, 214 286"
  fill="none"
  stroke="red"
  ></path>

<image x="0" y="0" href="../imgs/juejin.png" width="40">
  <animateMotion
    dur="2s"
    keyPoints="0; 0.5; 1; 1"
    keyTimes="0; 0.6; 0.8; 1"
    repeatCount="indefinite"
    >
    <mpath href="#logo-path"></mpath>
  </animateMotion>
</image>
  • keyPoints 配合前面介绍过的 keyTimes 可以对运动的快慢进行管理,它的值也是一个列表,列表中的值处于 0 到 1 之间,以 ; 间隔。比如 keyPoints 设置的 0.5 对应 keyTimes0.6,意为完成路径的前半段动画所用的时间占全程(2s)的 60%,比较慢;之后 keyPoints 设置了两个 1,对应的 keyTimes0.81,意味着在 1.6s(2s * 0.8) 时,logo 就来到了路径终点,然后停留 0.4s(2s * 0.2)。

动画和添加动画的元素也可以分开定义,通过 href 建立关联:

<!-- 代码片段 2.3.3 -->
<image id="logo" x="0" y="0" href="../imgs/juejin.png" width="40"></image>

<animateMotion
  href="#logo"
  path="M 0 400, 50 340, 100 360, 150 300, 200 350, 230 300, 214 286"
  dur="2s"
  keyPoints="0; 0.5; 1; 1"
  keyTimes="0; 0.6; 0.8; 1"
  repeatCount="indefinite"
  ></animateMotion>

至此,动画效果如下:

GIF 2023-9-28 15-48-06.gif

使用 js

最后,我使用 js 库 snap.svg 给右下角的签名添加上动画,需要先引入库文件(snap.svg 也可用于创建 svg):

<!-- 代码片段 3 -->
<svg id="ma-canvas" width="100%" height="600" viewBox="0 0 800 600">
   <text id="signature" x="800" y="580" font-size="12" fill="#26AAD6">
        <!-- 省略 -->
    </text>
 </svg>

<script src="../js/snap.svg-min.js"></script>
<script>
  window.onload = function () {
    const svg = Snap('#ma-canvas')
    const signature = svg.select('#signature')

   Snap.animate(
      [1000, 0],
      [800, 1],
      val => {
        signature.attr({
          x: val[0],
          opacity: val[1]
        })
      },
      2000,
      mina.easeout,
      () => {
        console.log('动画结束')
      }
    )
  }
</script>

<svg> 和需要添加动画的 <text> 增加 id 属性后,就可以先通过 Snap() 获取到 svg,再通过 svg.select() 获取到签名。之后使用 Snap.animate() 这个 api,就可以添加让签名从右向左平移出现的动画了。 介绍下 Snap.animate() 的参数:

  • 前 2 个参数可以直接是个数字,或是如上所示的数组,分别代表要改变的属性的初始值和最终值;
  • 第 3 个参数是个回调函数 setter,Snap.animate 的底层实现依靠的是之前在制作 canvas 动画时介绍过的 requestAnimationFrame,所以每秒钟会调用 60 次 setter,每次调用会传入参数 val,是在对应时刻的第 1 个参数到第 2 个参数的过渡值,我们在 setter 中通过设置属性的方法将 val 的值赋给签名,就可以得到动画了;
  • 第 4 个参数为动画持续时间,单位为 ms;
  • 第 5 个参数为可选参数,是动画曲线,其值可参见官网关于 mina 的内容;
  • 第 6 个参数为可选参数,为动画结束时执行的回调。

感谢.gif 点赞.png