使用 SVG 制作加载动画

公子   09月20, 2020

最近我们设计师反馈,他想要做如下的一个加载动画。但是要么效果好的导出的 GIF 体积特别大,看了下有 8M 多了,要么体积小的 GIF 效果又特别不清楚。然后我看了下效果,发现其实用 SVG 动画来实现应该比较简单,于是就和设计师要了一下原始的稿子导出成 SVG 后处理了下。

将 AE 动效稿子转成 SVG 动画的话 Airbnb 有出过一款 Lottie 的工具。通过它的 AE 插件 Bodymovin 能够以 JSON 的形式导出动画信息和素材。然后在网页上使用 bodymovin.js 动画播放库载入该 JSON 素材即可完成动效的转换。具体的使用教程可以参考 Youtube 视频《How to export an animation with Bodymovin》

使用 Bodymovin 是真的非常方便,不过介于设计师需要的效果比较简单,为了这个效果而每次去加载一个几十KB的基础库和JSON文件实在是没有必要。所以我这里就基于 SVG + CSS 动画来实现了下,最终的效果如下。最终体积也就 6KB,gzip 后会更小。 下面就来跟着我一块一步步的实现它吧!这里我不会特别详细的描述每一步的基本原理,如果大家想了解 SVG 动画的基础知识的话可以先看看我之前写的文章《SVG 动画实践》

动画拆分分析

通过观察发现该动画主要用到了平移、旋转、透明度,宽度和颜色等属性变化等动画效果。这些都可以通过 CSS3 动画来实现,剩下的我们需要对这些动画进行拆分,先分别实现它们。最后将他们组合,通过一定的时间配合实现完整的效果。

在这里我将该动效最终拆分成了以下几个部分:

  • 外圈的波纹效果
    • 外圈1的波纹效果
    • 外圈2的波纹效果
    • 外圈3的波纹效果
  • 主体的伸展运动
    • 主体绿色部分的平移伸展
      • 主体绿色部分上的平移伸展
      • 主体绿色部分下的平移伸展
    • 主体白色圆球的渐隐效果
    • 主体蓝色部分的平移伸展
    • 主体白色横条的渐隐效果
    • 主体部分的自转
  • 蓝球的公转效果

每一个单独的动画效果我们都需要对其进行处理,所以我们需要对导出的 SVG 进行元素的整理,将我们需要进行操作的元素进行分组标记。由于 Sketch 导出的 SVG 文件会带有比较多的冗余元素,所以我一般会在手工处理 SVG 之前在走一遍 svgo 这类工具对内容进行优化。这里推荐张鑫旭老师写的 SVG 在线压缩合并工具,直接粘贴 SVG 代码过去即可,非常简单。下面是 SVG 整体结构的示意代码。

<svg width="552px" height="552px" viewBox="0 0 552 552" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <!--外圈-->
  <g id="track-list">
    <!--外圈1-->
    <circle id="track-circle-1" />
    <!--外圈2-->
    <circle id="track-circle-2" />
    <!--外圈3-->
    <circle id="track-circle-3" />
  </g>
  <!--中间主体-->
  <g id="main">
    <!--主体绿色部分下-->
    <g id="bottom-triangel">
      <path d="..." />
    </g>
    <!--主体绿色部分上-->
    <g id="top-triangel">
      <path id="shadow" d="..." />
      <path d="..." />
      <!--主体白色圆球-->
      <circle id="white-ball" />
    </g>
    <!--主体蓝色部分-->
    <g id="right-triangel">
      <path d="..." />
      <!--主体白色横条-->
      <rect id="white-line" />
    </g>
   </g>
   <!--外圈蓝球-->
   <circle id="blue-ball" />
</svg>

可以看到我对 SVG 内的元素进行了重新的整理,将需要操作的元素都加上了 id 属性,方便后续直接使用 CSS 选择器选择对象进行操作。另外所有需要一块进行操作的元素也都使用 <g /> 分组标签进行了包裹。

外圈的波纹效果

外圈的波纹效果本质上就是圆的半径慢慢放大,效果里还伴随了圆的边框变窄的一个过程。其中三个圆最内层的那个是不需要动的,只需要动后面两个即可。从设计稿中拿到结束帧的状态之后这个动画做起来就比较容易了。

#track-circle-2 {
  animation-name: wave1;
  animation-timing-function: ease-in-out;
  animation-duration: 6s;
  animation-iteration-count: infinite;
}

#track-circle-3 {
  animation-name: wave2;
  animation-timing-function: ease-in-out;
  animation-duration: 6s;
  animation-iteration-count: infinite;
}
@keyframes wave1 {
  50% {
    stroke-width: 4;
    r: 219px;
  }
}

@keyframes wave2 {
  50% {
    stroke-width: 3;
    r: 274.5px;
  }
}

主体的伸展运动

这块是整个里面比较复杂的一部分了,不过通过拆分,我们发现实现起来也比较简单,先实现内部元素的平移,然后再补充上整体的自转效果即可。平移这块没有什么多说的,唯一麻烦的就是通过起始帧和结束帧的位置计算出需要移动的距离而已。如图最终白线标记的位置就是我们需要的平移位置啦。

#top-triangel { animation: topmove ease-in-out 2s infinite; }
#shadow { animation: shadowhide linear 2s infinite; }
#bottom-triangel { animation: bottommove ease-in-out 2s infinite; }
#right-triangel { animation-name: rightmove ease-in-out 2s infinite; }
@keyframes topmove {
  from, to { transform: translate(0, 0); }
  50% { transform: translate(-31px, -30px); }
}
@keyframes bottommove {
  from, to { transform: translate(0, 0); }
  50% { transform: translate(-31px, 30px); }
}
@keyframes rightmove {
  from, to { transform: translate(0); }
  50% { transform: translate(29px); }
}
@keyframes shadowhide {
  30%, 70% { opacity: 0; }
}

对了,别忘记我们刚才的动画拆分里还有白色圆球和白色横条的渐隐效果。渐隐效果可以使用 opacity 透明度来实现,不过这里除了渐隐之外还有一个大小的变化,可能使用呼吸效果来表述会更合适一点。圆的大小就是修改半径,横条的大小我们直接修改宽度就可以了。

#white-ball { animation: balltransparent ease-in-out 2s infinite; }
#white-line { animation: linetransparent ease-in-out 2s infinite; }
@keyframes balltransparent {
  50% {
    opacity: 0;
    transform: scale(0);
  }
}
@keyframes linetransparent {
  50% {
    opacity: 0;
    width: 0px;
  }
}

根据刚才的动画拆分,主体部分我们就还剩下一个自转没有实现了。在做这一部分的时候需要注意两点。第一,旋转默认是基于 SVG 画布的左上角进行旋转的,自转的话一般都是基于中心旋转,所以一定要记得设置 transform-origin 为中心点。第二,Sketch 导出的 SVG 会存在大量的 translate() 平移属性操作,有可能是最开始设计师画的时候是在某个位置,后来觉得不合适进行了移动,在 SVG 里就会以平移变换体现出来。这个时候如果我们直接使用 transform 进行变换的话实际上是会复写掉它们原本的平移的,这样就导致了之前设置的旋转圆心不正确的问题。

所以这种情况下需要使用联合变换,将之前的平移变换补充到 CSS 的变换中来就可以了,这也是为什么代码中会多出两个 translate() 的原因。

#main {
  animation: mainrotate linear 6s infinite;
  transform-origin: center center;
}

@keyframes mainrotate {
  from {
    transform: translate(0, 0) rotateZ(0deg) translate(-72px, -42px);
  }
  to {
    transform: translate(0, 0) rotateZ(360deg) translate(-72px, -42px);
  }
}

下面就是最终的实现效果。怎么样,是不是感觉已经离胜利不远了!

蓝球的公转效果

动画拆分里的最后一步就是蓝球的公转效果了。从上文我们知道,旋转我们是可以设置旋转圆心的。自转是围绕自己转的,所以旋转圆心是自己的中心,公转则是围绕太阳转的,所以旋转圆心是太阳的圆心,对应到我们的动效里其实就是整个画布的中心。

在这里我还使用了 CSS 表达角度的另外一个单位 turn,它表示的是圈数,转 360° 就表示 1turn。除了 turn 之外,CSS 角度还有 grad 梯度和 rad 弧度这两个单位。grad 则是将一个圆划分成了400等分,转 360° 就表示 400grad。而 rad 弧度则和我们数学上的弧度表示基本一致,一个圆总共是 2πrad

#blue-ball {
  animation: spin linear 6s infinite;
  transform-origin: center center;
}

@keyframes spin {
  from { transform: rotate(0turn); }
  to { transform: rotate(1turn); }
}

后记

最后将上面的代码拼凑起来就可以实现文章开头的动画效果了,是不是还挺简单的。另外在 SVG 标签中也支持内嵌 <style><script> 标签,所以我们可以直接将样式内嵌在 SVG 文件中,这样我们就可以和引用 GIF 一样直接使用 <img> 或者背景图片的形式使用 SVG 而不需要其他负担,在一些不支持内嵌样式的 Markdown 网站比如 Github 中效果奇佳哦!