双效合一的SVG多色描边变形动画

2,000 阅读12分钟

今天在codepen看到一个效果如下:


觉得十分有趣,作者是SVG结合canvas完成的,里面所有的路径部分是SVG完成的,但动画效果是canvas完成的。canvas能看懂一丢丢,能改一丢丢,但不会写(硬伤~),那就用最熟悉的SVG+CSS3看看能不能完成咯。

1.路径变形动画

先来个拆分,动画是两部分的结合,流动的描边和变形动画。为了和原作者有点区别,我准备做四个形状的动画,哦吼吼,升级版!在绘制时恍恍惚惚有种儿童简笔画的感觉……

基础图形变形过程
基础图形变形过程

如果没有任何的变形动画基础,请先移步这三篇文章,了解一下变形动画的实现原理和实操方法(自己推自己的文章,我是该有多脸皮厚呀):

juejin.cn/post/684490…
juejin.cn/post/684490…
juejin.cn/post/684490…

在AI中如何处理只在这里简单概括:
圆形:闭合路径剪开(顶点),转成开放路径,轻微拖动除起始锚点外的三个锚点,消除导出路径<path>中小s的存在,使路径变成标准的小c开头的路径。
三角形:闭合路径剪开(顶点),转成开放路径,轻微拖动除起始锚点外的两个锚点,使路径变成标准的小c开头的路径。(没错,我就是传说中的复读机君,我有什么办法,处理方法满满的都是套路啊)
矩形:闭合路径剪开(左下角),转成开放路径,轻微拖动除起始锚点外的两个锚点,使路径变成标准的小c开头的路径。(关于剪开路径的位置?这个嘛?没有为什么,我就想看看有什么不同效果啦)
五边形:闭合路径剪开(左下角)……(此处省略重复步骤,巴拉巴拉……)

2.多边形<polygon>转成<path>小c标准路径

突然插入这么一段小直播,是我发现在这个动画效果中,因为除了圆形,剩下的都是多边形,其实<polygon points="X1,Y1 X2,Y2 …… "/>这个绘制方法是很容易理解的,都是多边形顶点对应的绝对坐标,但因为有圆形存在,我们不得已才要把很简单的事情复杂化,然后在AI里手柄拖来拖去的真的好烦的好嘛,而且有可能导出的SVG还有大C开头的,反反复复,不胜其烦,那么有没有一种简单的方法可以把这种多边形路径直接转成小c绘制的标准路径的方法呢?有!
我以五边形为例,图示一下:

多边形与路径的转换
多边形与路径的转换

我的五边形的五个顶点坐标依次为X1,Y1 X2,Y2 X3,Y3 X4,Y4 X5,Y5,注意,这里说的坐标都是绝对坐标,即在AI中选中锚点之后的X值和Y值。关于具体的转换,我拿其中一段路径举例。我们先看三次贝塞尔曲线绘制路径的指令,也就是右侧绿色的曲线,每一段曲线都由起点和终点两个端点以及对应的两个控制点(也就是我们AI中手柄的位置)组成的,而当我们的控制点坐标越接近路径端点,曲线越平,当控制点与端点重合时,就得到了直线。
有了这个概念基础,理解起来就方便多了,我需要把<polygon>转换成<path>,首先,起点M的坐标(绝对坐标)显而易见就是多边形顶点的坐标,当用绝对路径C表示路径1时,起点A控制点坐标就是起点A坐标,终点B控制点坐标就是终点B坐标。这样还不够,我们需要的是相对坐标表示方法的c指令,也就是我喜欢称之为“标准曲线”的东西。对于小c绘制方法指令而言,起点和终点控制点的相对坐标最简单,就是0,0,但最后一组相对坐标则要经过计算,B相对于A的移动距离,也就是终点B的绝对坐标与起点A的相对坐标差。

当然了,如果你懒得看原理,觉得很烦的话,就可以直接看解决方法,即c0,0 0,0 X(终点-起点),Y(终点-起点)。坐标点可以在AI里面直接获得,但计算公式还是少不了的。
所以,最终我的五边形成功的转换成了<path d="MX1,Y1 c0,0 0,0 X2-X1,Y2-Y1 c0,0 0,0 X3-X2,Y3-Y2 c0,0 0,0 X4-X3,Y4-Y3 c0,0 0,0 X5-X4,Y5-Y4 c0,0 0,0 X5-X1,Y5-Y1">路径表示方法,这里说明一下,如果剪开路径时不错开,最后一段路径是大C对应的绝对路径绘制方法,也就是CX5,Y5 X1,Y1 X1,Y1。

3.添加虚拟曲线

做完上面的工作仍然没有算完,对于变形动画而言,曲线的数量要相等才能完成,而我们的这四个图形,曲线数量分别是:圆→4,三角形→3,矩形→4,五边形→5,还好,没有选择太复杂的图形,那就给圆和矩形加1个虚拟曲线,给三角形加两个虚拟曲线,大家全部补齐成5个咯。(什么?你问我什么是虚拟曲线?打滚……上面的文章链接你没看,没看)
好啦,加过虚拟曲线,处理过的四个图形的<path>路径已经统一起来了,这样就可以套用我们的变形动画了。
来看一下变形动画的定义部分:

@keyframes deform{
0% {d:path('');} /*圆形路径*/
25% {d:path('');}/*三角形路径*/
50% {d:path('');}/*矩形路径*/
75% {d:path('');}/*五边形路径*/
100%
}
#deform {animation:deform 3s ease infinite};

然后我们的<path>引用这个动画就好了。就得到了变形动画:

单纯的变形动画
单纯的变形动画

嗯,只是动了,但起来看上去不是很炫,没事,go on。

4.流光溢彩动效

关于这种不同颜色沿着描边路径流动的效果,我起了个名字叫“流光溢彩”。先拿五边形为例,看一下单色流动动画的设置,之所以没有拿圆形举例,是怕你想用旋转来实现啊:

<style>
@keyframes animate {
0%{stroke-dashoffset:0}
100%{stroke-dashoffset:1356} /*五边形的周长*/
}

#animate{
animation:animate 2s linear infinite;
stroke-dasharray:678;  /*五边形1/2周长*/
}
</style>

得到效果如下:

单色描边动画
单色描边动画

原理我简单解释一下,dashoffset为虚线偏移位置,dasharray定义了虚线的样式,只有一个值的话,则表示线长和间距等长,如下图示:

dasharray和dashoffset
dasharray和dashoffset

当我们把stroke-dasharray定义成1/2周长时,相当于让图形实现了一半描边效果,而CSS中stroke-dashoffset的值的变化,则对应生成了动效,定义差值为周长是为了实现首尾相接连绵不断的效果。注意一下,这里说差值为周长,也就是说如果初始0%对应的 stroke-dashoffset如果不是0, 那结束时100%对应的也要变化,这是我们下面实现四个颜色流动的基础。
这里如果把stroke-dashoffset的值改成等值负数,会得到相反方向的动画效果,感兴趣的话可以自己试一下。

好了,逐步推进,实现了单色流动,那双色怎么办?要再定义一个动态单色流动动画,然后进行叠加么,哎,我们这种懒人总是想方设法偷懒,因为我只要给这个单色流动的动效的底层加一个相同路径实色描边,就得到了这种效果:

双色描边动画
双色描边动画

嗯,双色流动已完成(此为懒人法,非正解,无需掌握,看过算完)。
好了正式进阶开始了,上面偷懒法只能解决两个颜色的问题,当我需要多个颜色,肿么办?
嗯,乖乖的多定义几个描边动画设置,去写CSS属性吧。因为每个<path>路径只识别一个描边效果,那这种多色的只能用多条相同路径叠加来实现了。我用图示来表示一下:

多色拼接原理
多色拼接原理

当然了,针对我们四个颜色,如果把相同的五边形路径重复四遍是惨绝人寰的,这里我们可以用<defs>元素或者<symbol>元素来定义需要重复的路径,然后用<use>元素来引用,推荐<symbol>,是由于<symbol>支持的属性更多,虽然在这个案例中无法体现出来,但养成好习惯,需要用<defs>的都可以用<symbol>来代替。这里因为dasharray的定义相同,所以统一到了路径内联属性里。
来看看代码部分:

<svg>
<style>
@keyframes animate1 {
0%{stroke-dashoffset:0}
100%{stroke-dashoffset:1356}/*1356是路径的长度*/
}
@keyframes animate2 {
0%{stroke-dashoffset:339}/*定义了四个颜色,所以339是1/4周长*/
100%{stroke-dashoffset:1695}/*需要dashoffset变化值是一个周长来实现首尾相接*/
}
@keyframes animate3 {
0%{stroke-dashoffset:678}
100%{stroke-dashoffset:2034}
}
@keyframes animate4 {
0%{stroke-dashoffset:1017}
100%{stroke-dashoffset:2373}
}
#animate1 {
animation:animate1 2s linear infinite;
stroke:#ffb850;
}
#animate2 {
animation:animate2 2s linear infinite;
stroke:#ff7e5d;
}
#animate3 {
animation:animate3 2s linear infinite;
stroke:#8cd2a4;
}
#animate4 {
animation:animate4 2s linear infinite;
stroke:#62adea;
} 
</style>
<symbol><!--用symbol来定义需要重复引用的相同路径-->
<path id="pentagon" d="" stroke-width="10" stroke-dasharray="339 1017" fill="none"/></symbol>
<use xlink:href="#pentagon" id="animate1"/>
<use xlink:href="#pentagon" id="animate2"/>  
<use xlink:href="#pentagon" id="animate3"/>
<use xlink:href="#pentagon" id="animate4"/>  
</svg>

还算是很清晰的,而且如果用五个颜色,那就初始的dashoffset递增1/5周长,然后改一下dasharray为线长1/5 间距4/5 就可以了。

得到的效果如下:

四种颜色流动
四种颜色流动

5.双效合一

独立设计形状之间的变形动画和同一形状的不同颜色描边的动画都已经实现了,现在要做的就是把这两个效果合在一起了。在我们上面实现“流光溢彩”动效时把需要重复定义的路径用<symbol>进行了定义,定义的<path>的id值不是被赋予用了某个属性,而是作为标签存在,便于被<use xlink:href="#">反复引用,但当这个效果运用到变形动画中时,会发现<path>路径的id对应的是绘制路径的变形动画,那我们来换个思路,把这四个路径当做独立的存在,每个路径在进行变形动画的同时也在进行描边动画,此时我们的SVG定义的变形动画deform的关键帧不变,四个不同颜色的描边动画的定义animate1-4的关键帧也不变,需要变化的是动画属性:

#animate1 {
animation:deform  4s ease  infinite, animate1 2s linear infinite;
stroke:#ffb850;
}
#animate2 {
animation:deform  4s ease  infinite, animate2 2s linear infinite;
stroke:#ff7e5d;
}
#animate3 {
animation:deform  4s ease  infinite, animate3 2s linear infinite;
stroke:#8cd2a4;
}
#animate4 {
animation:deform  4s ease  infinite, animate4 2s linear infinite;
stroke:#62adea;
} 

即每个路径的动画属性同时赋予了两种动效,一个是变形的deform动画,一个是对应的描边动画。
为了尽可能的优化代码,我把相同定义的<path>属性统一定义到了CSS里面,如下:

path{stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-width:10;stroke-dasharray:339 1017;fill:none}

这样,我们的对应的四条路径的代码就简化成了如下:

<path id="animate1" />
<path id="animate2" />
<path id="animate3"/>
<path id="animate4"/>

这里如果有好奇的小伙伴可能会提出疑问,我们的描边动画在定义时用的周长是五边形的周长,但这个动画里的几个形状并不是等长,肿么办?
其实不要理会,只要选一个最长的路径进行定义就可以了。因为我们的路径是一层层叠加的,如果图形的周长比定义时选择的短,出现的结果就是最顶层的路径会略长一些,但对于这类动画而言,很难看出差别。

双效合一动画
双效合一动画

另外这里如果对变形的效果不满意,可以自行调整路径的方向和起点位置,以前的文章里都有详细的方法,不再赘述。
当然了,手痒痒的我还是改了一下各个参数,看了一下效果,比如我定义了stroke-dasharry:100 300 (线长100 间距300的虚线),同时改了其他的stroke-dashoffset的值,依次差阶100,然后得到了一个效果:

重新定义虚线样式后的效果
重新定义虚线样式后的效果

即使得到了相要的动画效果,但积极努力追求上进的我却依然不满意啊,因为我想让变形动画在完成一个变形之后略作停留之后再进行下一个变换。而不是像现在这种唰唰唰一气呵成,于是乎,我改进了一下,得到了下面这种效果:

修改关键帧之后动画效果
修改关键帧之后动画效果

我是用了偷懒的效果,把变形动画的关键帧改成了下面这种:

@keyframes deform{
0% {d:path('')} /*圆形路径*/
15% {d:path('')} /*三角形路径*/
25% {d:path('')} /*三角形路径*/
40% {d:path('')} /*矩形形路径*/
50% {d:path('')} /*矩形形路径*/
65% {d:path('')} /*五边形路径*/
75%{d:path('')} /*五边形路径*/
90% {d:path('')} /*圆形路径*/
100% {d:path('')} /*圆形路径*/
}

嗯,满意。
直接附上codepen的地址,
codepen.io/yangyangbei…
小伙伴们自行查看咯。