如何借助SVG+CSS用2个小时撸完一个网易云音乐的动效海报(可控制速度)

7,671 阅读12分钟

因为平时也关注网易UEDC的订阅号,前几天就看到了这么一个动效,主题是《网易云音乐2018年度听歌报告》,内容是一个人在努力蹬车因为构图简单,创意又不错,所以就试了下用SVG+CSS动画实现起来的难度,大概费时两个小时左右,效率还是蛮高的,总比用AE实现起来快的多得多,下面就捋一捋实现的过程。

1. 第一步 先构图,费时1小时

画图这种事情,怎么能难得到一个美工呢。在Ai中完成,不考虑太多细节(原效果中有渐变和噪点,这里就省略了,用纯色填充来代替,毕竟人家是一个team,而我是一个人在摸索着战斗)。

图做完之后,就要进行拆解了,需要把四个部分拆出来,这四个部分就是要做动效的部分,左大腿,左小腿,右大腿,右小腿。

这四个部分为了方便下面做动效,每一个都是要放到单独的图层中的,至于其他图层顺序,就不是那么重要了。

2.第二步 给腿部增加旋转动效,费时0.5小时

做这种涉及到关节类的实现效果,其实是个通路,因为简而言之都是旋转动画,不过是增加了连动,也就是说大腿围绕关节处(即原点)的旋转时要带动小腿围绕大腿末端(小腿动画对应的原点)的旋转。因为有做舞动的机器人的基础,这里就简单了很多,不过仍然做了一点点小的优化,没有像做机器人时拆分成很多SVG,而是直接进行了嵌套。

首先,最最重要的一点,要获取下面图中这三个坐标值。这是做旋转动画时不可或缺的tranform-origin值。

大腿的旋转是非常简单的旋转动画设置了,以左腿为例,通过对比左右腿,差不多得到的数据就是大概顺时针旋转了60度,只要在CSS中定义动画参数如下:

/*大腿旋转运动的动画规则*/
@keyframes legLMove{
0% {transform: rotate(0deg)}
100% {transform: rotate(-60deg)}
}
#legL{
animation: legLMove 1s  infinite  alternate;
transform-origin:455px 235px; /*大腿旋转运动的原点*/
}

这样就轻松的得到了大腿运动的动效。
相比之下,小腿就要复杂的多,步步拆解嘛,动画设置的思路也是解题思路,所以,先解决的第一个问题,就是小腿跟随大腿运动的问题。因为在导出SVG之前,在Ai中已经把需要增加动效的部分放置再来不同的图层里,因此,简化的腿部对应的DOM结构是下面这种

<!--左大腿-->
<g id="legL">
	<path d="M……z"/>
</g>
<!--左小腿-->
<g id="calfL">
    <path d="M……z"/>
</g>

既然要跟随大腿运动,那好办,直接把左小腿整体部分放到大腿所在的组合<g>标签中,就能保证相同的运动了。修改后的DOM结构就变成了:

<!--左大腿-->
<g id="legL">
	<path d="M……z"/>
	<!--左小腿-->
	<g id="calfL">
    	<path d="M……z"/>
	</g>
</g>

现在,这一步已经解决了,那么来解决下一个问题吧,如何让小腿自由的摆动。这里,为了确定小腿的旋转参数,我画了一张示意图。

这张图中,绿色的弧线就是大腿的运动轨迹,红色的弧线是小腿的运动轨迹,最终小腿部分和大腿部分几乎是平直状态,因为小腿目前是跟随大腿运动的,换句话说,它们两个处于同一个参考系中,因此,在确定小腿的摆动幅度时,不用考虑大腿的状态。(半透明的小腿部分就是最终小腿的状态)。目测,逆时针旋转,100度左右(不用那么精确)。因此,小腿部分的动画规则就出来了:

@keyframes calfLMove{
0% {transform: rotate(0deg)}
100% {transform: rotate(100deg)}
}
#calfL{
animation: calfLMove 1s  infinite alternate;
/*小腿对应的旋转原点 也就是大腿的末端*/
transform-origin:343px 220px; 
}

这样,小腿在跟随大腿进行旋转运动的同时实现了自己的旋转动效。(这里实现的这种方法要比之前在做舞动的机械人时优化了很多)。

左腿完成了,右腿就水到渠成了,不过是一个逆向而已。记得修改DOM结构。

当然,这个蹬自行车的效果实现的并不好,看上去更像是踩踏板,有时间的话准备开一篇新的文章通过定义offset-rotate:var(--degMove);这种在CSS中设置随路径曲率的旋转方向为变量的方法,然后javascript设置定时器改变这个值来实现(这个方法已经测试过了,js同样也可以通过document.documentElement.style.setProperty的方法来改变SVG的CSS样式)。此为后话。

3. 第三步 增加音符的动效,费时0.5小时

为了尽可能的和原效果保持一致,这里把音符的动效一并加上了。为了方便查看,我先把无关元素暂时隐藏掉,只保留音符,大概的效果是下面这种:

音符逐渐变大并沿路径移动后淡化消失的过程。从这句话中,我们提炼出三个元素:沿路径移动 | 变大 | 变淡,也就是说对应三种动效设置。 第一步,为了实现路径动画,我先绘制了路径,并确保每个音符落在路径上。

路径动画的CSS参数写一写:

@keyframes notePath{
0% {offset-distance:0%;}
100% {offset-distance:100%;}
}
#notePath{
offset-path:path('  '); /*绘制的路径path对应的d属性*/
animation:notePath 2s ease infinite;
}

音符先不做缩放的处理,以中间状态的为基准,调用这个动画属性后,就可以得到沿路径运动的音符了**(再次强调,在路径动画中,一定要把元素放到画布零位置后再导出。)**:

继续,下面叠加尺寸变化,animation属性不需要做任何修改,但动画规则中要增加关于缩放,也就是transform:scale()的定义

@keyframes notePath{
0% {
offset-distance:0%;
transform:scale(0.2); /*增加关于缩放的定义,起点缩小至原尺寸0.2*/
}
100% {
offset-distance:100%;
transform:scale(1.5); /*增加关于缩放的定义,终点放大至原尺寸1.5倍*/
}
}

此时,音符在沿路径移动的基础上已经叠加了缩放的效果:

只差最后一步,就是透明度的变化了。通过分析动效,音符并不是在整个动画的时间过程中遵循透明度的变小的规律,而是在变化的中途开始逐渐变淡。这里的处理有两个思路,正好都说一下。第一种,直接定义在统一的动画规则中。既然是中途的变化,那我们CSS可以改成下面这种:

@keyframes notePath{
0% {
offset-distance:0%;
transform:scale(0.2);
}
50%{opacity: 1} /*在中途增加一个关于透明度的定义*/
100% {
offset-distance:100%;
transform:scale(1.5);
opacity: 0.1
}
}

来个好玩的吧,试试骗过浏览器。 其实浏览器是很傻的,它只会乖乖的看着你的动画规则进行解析,比如透明度这种问题,我们知道opacity:1就是完全不透明的了,但浏览器对于opacity定义大于1时,也会按照完全不透明来解析。想到了什么没?
也就是说,我们完全不用增加一个50%的关键帧,而是写成下面这种:

@keyframes notePath{
0% {
offset-distance:0%;
transform:scale(0.2);
opacity: 2; /*透明度设置>1*/
}
100% {
offset-distance:100%;
transform:scale(1.5);
opacity: 0.1;
}
}

对浏览器来说,它要执行的过程中,透明度opacity是这样来的,整个时间段,2→0.1,所以差不多前半段是2→1(对应透明度不发生变化),后半段1→0.1(对于透明度变小),也就满足了我们的设定。 第二种方法,虽然略显麻烦,却是很重要的一种方法。在上面的设置中,有一个问题,就是因为animation属性是一个统一定义,这里面包含的参数非常多,比如说速率,也就是我定义为ease值的参数。如果我想让透明度线性速率变化,怎么办呢?来试试给动画属性叠加多个规则

/*增加一个设置透明度变化的动画规则*/
@keyframes noteOpacity{
	50%{opacity:1}
	100%{opacity:0.1}
}

这时,我们要把这个透明度变化的动画规则追加到animation的定义中,追加的方式很简单,逗号后面增加上新的动画规则的属性就可以了:

animation:notePath 2s ease infinite ,noteOpacity 2s linear infinite;
/*新的动画规则noteOpacity可以自由定义其他动画属性*/

这种叠加多个动画规则(当然了,你得有个前提,这些动画规则分别定义了不同的属性变化)在一些场景下非常有用的。以这个来做一个非常简单的尝试。当我把noteOpacity这个动画规则的动画时间由2s改成0.5s,为了看出差别,路径动画的时间延迟到了5s,猜猜动效会发生什么变化?

正如图中所示,这是两个不同的动画规则的叠加效果,因此整个路径运动的过程中会有数次透明度的变化。 好了,让律动的音符和我们骑自行车的动效进行合成吧!

粗制滥造的仿作到此为止。来个小结吧,这篇文章中两个重要的部分:

  1. 对于连动效果,先把整体部分放入需要连动的元素的同级中,以便保持相同的动画设置,再另行设置单独的动画属性。
  2. 给动画属性animation增加多个不同的动画规则的方法非常非常有用。

我也是在做CSS动画的过程中,不断来优化原来总结的知识体系,以后遇到好玩有趣的案例都会做一做。至于CSS变量这个,虽然是2年前的功能,对我来说,却是新的知识点,也准备试一下和js结合之后能怎么玩起来,毕竟要比通过innerHTML方法向CSS文件中添加<style>标签和内容要简单明朗的多。

4. 加强版 来做一个可以任意加速减速的效果

本来这个支持交互增减速度的效果没想在这个案例中实现的,但有人留言提到了嘛,那就试一下。先明确一下,加速和减速改变的是animation属性中的动画周期中需要的时间。在这个案例中,四个动画时间是统一的,那既然如此,这里不用具体的时间定义,而是写成var(--speed)这种定义变量的方式。举个例子,原来左腿小腿的动画属性如下:

animation: legLMove 1s  infinite  alternate;

用全局变量定义之后改成了:

animation: legLMove var(--speed)  infinite  alternate; 
/*用变量代替具体的时间定义*/

定了变量之后,在页面加载时,首先要给**--speed一个初始值,所以直接定义了一个变量time并将其初始值设为1,然后通过document.documentElement.style.setProperty("--speed",time+"s")这种方法给--speed**变量定义了一个初始值1s。

这里还有方法二,就是写成var(--speed, 1s)这种增加默认值但定义方式,但因为需要重复写四遍,实在嫌弃麻烦,索性放弃了。

下面是最重要的部分,设置键盘按下的监听事件。不考虑浏览器兼容的话,键盘的监听事件document.onkeydown在svg中同样适用。因为只是简单的加速和减速,所以我只指定了两个按键,左右箭头,想要得到的效果就是按下左箭头减速,按下右箭头加速。

对于动画效果的加速减速,对应到动画时间上正好相反,时间越长,速度越慢,因此,每次按下左箭头后让time+1就可以了,速度会相应变慢;而加速时,因为time只能是正数,所以设置成了time/(time+1)。(这里定义的方法有很多,只是随便选了一种)。最终的javascript部分如下:

var time=1;
//在键盘事件触发之前先给一个初始设置。
document.documentElement.style.setProperty("--speed",time+"s");
document.onkeydown= function(e){
    //左键头
    if(e.keyCode==37){time+=1;}
    //右箭头
    if(e.keyCode==39){time=time/(time+1);}
    document.documentElement.style.setProperty("--speed",time+"s");
    }

好了,现在可以试一下了,为了方便看出来按下键盘的效果,我给原来SVG的结构中增加了一个文本标签<text>,并让其显示按键的key值。

从动效中可以看出来,重点观察右下角按键值的key值,通过左右箭头可以明显的控制脚踏的速度。当然了,因为转gif导致的卡顿,效果并不是那么流畅。在控制台输出一下time值,会更加一目了然。因为受制于所传动图的尺寸,我分别按了右键3次和左键3次。

好了,案例终于圆满的结束了,因为这是我第一次使用CSS变量,有用的不合时宜的地方,还请各位大神予以指正。javascript慢慢入门之后,真的发现CSS动画愈发好玩儿了,毕竟有交互才有操纵感。在留言中,有人问过这种和Gif但区别,我想,区别大概就是在这里吧。

CSS定义变量属性值的方法实在是比通过创建<style>标签的方式,通过innerHTML的方法追加样式属性的方法好用好用太多了。