作者:vivo 互联网前端团队- ZhaoJie
本文将从各个角度来对动画整个体系进行分类,并且介绍各种前端动画的实现方法,最后我们将总结在实际开发中的各个场景的动画选择方案。
前端动画场景需求多
对众多动画场景的技术实现方案选择上比较模糊
各动画方案的优劣及适用场景认识模糊
现有动画库太多,不知道选哪个
主流动画库的适用场景认识模糊
下面首先让我们从各个角度来对动画整个体系进行分类,让我们清晰的了解动画整个体系。
首先我们从动画的用途或者说是业务的角度来进行区分,将我们平时的动画分为展示型动画和交互型动画。
类似于一张GIF图,或者一段视频。比如在开启宝箱的时候,我们会加入一个切场过渡动画,来替代原有的生硬等待结果。
展示型动画在实际使用的场景中,实现的方法很多,比如用GIF图,canvas,CSS3动画等,但是最终输出的结果是不带有交互的,也就是从动画起始状态到结束状态一气呵成,这个过程用户可以感知,但是无法参与。
用户自已参与的,对于交互性动画而言,我们可以在动画播放的某个时间节点触发相应的操作,进而让用户参与到其中,最常见的例子红包雨,不仅仅能提升用户的体验,还能提升我们的产品的多元性。
然而交互性动画经常面临的一个问题就是,通过原生代码实现交互动画是很复杂的,同时性能和兼容性是不得不认真考虑的问题,比较好的解决方案还是寻求相关的框架。
不管采用什么方式来制作动画,最终呈现到前端页面的无非是以下三种形式:
PS:为了简单也可以用视频,但除非动画的播放场景固定,不然移动端视频在不同app、不同机型、不同系统的播放显示都不太一样,容易踩不少坑。
Canvas
div
SVG
一句话总结:都是2D做图,svg是矢量图,canvas是位图。canvas 是逐像素进行渲染的,适合游戏。
SVG
Canvas
性能比较
前端动效开发,首先应该确定的是
动画用途->确认动画类型->确认绘制技术->确认动画的实现方式。
虽然最终呈现动画的载体(绘制技术)就三种,但实现动画的方式却很多,得从动画类型出发讨论动画的实现方式:
(1)逐帧动画(序列帧动画)
(2)补间动画(Tween动画\关键帧动画)
(3)SVG动画
(4)骨骼动画
(5)3D动画
逐帧动画是在时间帧上逐帧绘制帧内容,由于是一帧一帧的画,所以逐帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容。
由于逐帧动画的帧序列内容不一样,不仅增加制作负担而且最终输出的文件量也很大,但它的优势也很明显:因为它相似与电影播放模式,很适合于表演很细腻的动画,如3D效果、人物或动物急剧转身等等效果。
所以逐帧动画的实现核心是什么,就是将我们的这些静态的图片进行快速的循环播放,形成了一个动态的动画效果。这就是帧动画。
2.3.1.1 GIF实现
我们可以将帧动画导出成GIF图,GIF图会连续播放,无法暂停,它往往用来实现小细节动画,成本较低、使用方便。但其缺点也是很明显的:
画质上,GIF 支持颜色少(最大256色)、Alpha 透明度支持差,图像锯齿毛边比较严重;
交互上,不能直接控制播放、暂停、播放次数,灵活性差;
性能上,GIF 会引起页面周期性的绘画,性能较差。
2.3.1.2 CSS实现
CSS3帧动画是我们今天需要重点介绍的方案,最核心的是利用CSS3中Animation动画,确切的说是使用animation-timing-function 的阶梯函数 steps(number_of_steps, direction) 来实现逐帧动画的连续播放。
帧动画的实现原理是不断切换视觉内图片内容,利用视觉滞留生理现象来实现连续播放的动画效果,下面我们来介绍制作CSS3帧动画的几种方案。
(1)连续切换动画图片地址src(不推荐)
我们将图片放到元素的背景中(background-image),通过更改 background-image 的值实现帧的切换。但是这种方式会有以下几个缺点,所以该方案不推荐。
(2)连续切换雪碧图位置(推荐)我们将所有的帧动画图片合并成一张雪碧图,通过改变 background-position 的值来实现动画帧切换。分两步进行:
步骤一:
将动画帧合并为雪碧图,雪碧图的要求可以看上面素材准备,比如下面这张帧动画雪碧图,共20帧。
步骤二:
使用steps阶梯函数切换雪碧图位置
写法一:
.sprite {
width: 300px;
height: 300px;
background-repeat: no-repeat;
background-image: url(frame.png);
animation: frame 333ms steps(1,end) both infinite;
}
@keyframes frame {
0% {background-position: 0 0;}
5% {background-position: -300px 0;}
10% {background-position: -600px 0;}
15% {background-position: -900px 0;}
20% {background-position: -1200px 0;}
25% {background-position: -1500px 0;}
30% {background-position: -1800px 0;}
35% {background-position: -2100px 0;}
40% {background-position: -2400px 0;}
45% {background-position: -2700px 0;}
50% {background-position: -3000px 0;}
55% {background-position: -3300px 0;}
60% {background-position: -3600px 0;}
65% {background-position: -3900px 0;}
70% {background-position: -4200px 0;}
75% {background-position: -4500px 0;}
80% {background-position: -4800px 0;}
85% {background-position: -5100px 0;}
90% {background-position: -5400px 0;}
95% {background-position: -5700px 0;}
100% {background-position: -6000px 0;}
}
针对以上动画有疑问?
问题一:既然都详细定义关键帧了,是不是可以不用steps函数了,直接定义linear变化不就好了吗?
animation: frame 10s linear both infinite;
如果我们定义成这样,动画是不会阶梯状,一步一步执行的,而是会连续的变化背景图位置,是移动的效果,而不是切换的效果,如下图:
问题二: 不是应该设置为20步吗,怎么变成了1?
这里我们先来了解下animation-timing-function属性。CSS animation-timing-function属性定义CSS动画在每一动画周期中执行的节奏。
综上我们可以知道,因为我们详细定义了一个动画周期,也就是说0% ~ 5%之间变化一次,5% ~ 10%变化一次,所以我们这样写才能达到想要的效果。
写法二:
.sprite { width: 300px;
height: 300px;
background-repeat: no-repeat;
background-image: url(frame.png);
animation: frame 333ms steps(20) both infinite;
}
@keyframes frame { 0% {background-position: 0 0;}//可省略
100% {background-position: -6000px 0;}
}
这里我们定义了关键帧的开始和结束,也就是定义了一个关键帧周期,但因为我们没有详细的定义每一帧的展示,所以我们要将0%~100%这个区间分成20步来阶段性展示。
(3)连续移动雪碧图位置(移动端推荐)
跟第二种基本一致,只是切换雪碧图的位置过程换成了transform:translate3d()来实现,不过要加多一层overflow: hidden;的容器包裹,这里我们以只定义初始和结束帧为例,使用transform可以开启GPU加速,提高机器渲染效果,还能有效解决移动端帧动画抖动的问题。
.sprite-wp {
width: 300px;
height: 300px;
overflow: hidden;
}
.sprite {
width: 6000px;
height: 300px;
will-change: transform;
background: url(frame.png) no-repeat center;
animation: frame 333ms steps(20) both infinite;
}
@keyframes frame {
0% {transform: translate3d(0,0,0);}
100% {transform: translate3d(-6000px,0,0);}
}
steps() 函数详解
从上面的代码我们可以发现,CSS实现的核心就是使用animation-timing-function缓动函数的阶梯函数steps(number_of_steps, direction)来实现逐帧动画的连续播放的。
接着我们来了解下steps() 函数:
steps 指定了一个阶梯函数,包含两个参数:
除了 steps 函数,animation-timing-function 还有两个与逐帧动画相关的属性值 step-start 与 step-end:
2.3.1.3 JS实现
(1)通过JS来控制img的src属性切换(不推荐)
和上面CSS3帧动画里面切换元素background-image属性一样,会存在多个请求等问题,所以该方案我们不推荐,但是这是一种解决思路。
(2)通过JS来控制canvas图像绘制
通过canvas制作帧动画的原理是用drawImage方法将图片绘制到canvas上,不断擦除和重绘就能得到我们想要的效果。
(function () { var timer = null,
canvas = document.getElementById("canvas"),
context = canvas.getContext('2d'),
img = new Image(),
width = 300,
height = 300,
k = 20,
i = 0;
img.src = "frame.png"; function drawImg() {
context.clearRect(0, 0, width, height);
i++; if (i == k) {
i = 0;
}
context.drawImage(img, i * width, 0, width, height, 0, 0, width, height); window.requestAnimationFrame(drawImg);
}
img.onload = function () { window.requestAnimationFrame(drawImg);
}
})();
上面是通过改变裁剪图像的X坐标位置来实现动画效果的,也可以通过改变画布上放置图像的坐标位置实现,如下:
context.drawImage(img, 0, 0, width*k, height,-i*width,0,width*k,height);
(3)通过JS来控制CSS属性值变化
这种方式和前面CSS3帧动画一样,有三种方式,一种是通过JS切换元素背景图片地址background-image,一种是通过JS切换元素背景图片定位background-position,最后一种是通过JS移动元素transform:translate3d(),第一种不做介绍,因为同样会存在多个请求等问题,不推荐使用,这里实现后面两种。
切换元素背景图片位置 background-position
.sprite { width: 300px;
height: 300px;
background: url(frame.png) no-repeat 0 0;
}
(function(){ var sprite = document.getElementById("sprite"),
picWidth = 300,
k = 20,
i = 0,
timer = null; // 重置背景图片位置
sprite.style = "background-position: 0 0"; // 改变背景图位置
function changePosition(){
sprite.style = "background-position: "+(-picWidth*i)+"px 0";
i++; if(i == k){
i = 0;
} window.requestAnimationFrame(changePosition);
} window.requestAnimationFrame(changePosition);
})();
移动元素背景图片位置 transform:translate3d()
.sprite-wp { width: 300px;
height: 300px;
overflow: hidden;
}
.sprite { width: 6000px;
height: 300px;
will-change: transform;
background: url(frame.png) no-repeat center;
}
(function () {
var sprite = document.getElementById("sprite"),
picWidth = 300,
k = 20,
i = 0,
timer = null;
// 重置背景图片位置
sprite.style = "transform: translate3d(0,0,0)";
// 改变背景图移动
function changePosition() {
sprite.style = "transform: translate3d(" + (-picWidth * i) + "px,0,0)";
i++;
if (i == k) {
i = 0;
}
window.requestAnimationFrame(changePosition);
}
window.requestAnimationFrame(changePosition);
})();
2.3.1.4 性能分析
我们通过Chrome浏览器的各种工具,查看了每种方案的 FPS、CPU占用率、GPU占用、Scripting、Rendering、Painting、内存的使用情况,得到以下数据:
通过分析以上数据我们可以得出以下几点:
结论:我们看到,在7个指标中,CSS transform:translate3d() 方案将其中的4个指标做到了最低,从这点看,我们完全有理由选择这种方案来实现CSS帧动画。
2.3.2 补间动画(Tween动画\关键帧动画)
补间动画是动画的基础形式之一,又叫做中间帧动画,渐变动画,指的是人为设定动画的关键状态,也就是关键帧,而关键帧之间的过渡过程只需要由计算机处理渲染的一种动画形式。
说白了,就是我们在做动画的时候,只需要指定几个特殊时刻动画的状态,其余的状态由计算机自动计算补充。
实现补间动画常见的手段主要由以下几种:
2.3.2.1 CSS实现
(1)transition 动画
transition允许CSS的属性值在一定的时间区间内平滑地过渡,即指定元素的初始状态 和末尾状态,既可以完成一个动画,中间的变化完全有浏览器自己决定。动画的效果主要还是看transition相关属性即可。
然而利用transition制作的动画也有着显著的缺点:
(2)animation 动画
利用animation可以完成一个完整的CSS补间动画,如上面所说,我们只需要定义几个特殊时刻的动画状态即可。这个特殊时刻通常我们叫做关键帧。
keyframes 关键帧
Keyframes具有其自己的语法规则,他的命名是由"@keyframes"开头,后面紧接着是这个“动画的名称”加上一对花括号“{}”,括号中就是一些不同时间段样式规则,有点像我们CSS的样式写法一样。
对于一个"@keyframes"中的样式规则是由多个百分比构成的,如“0%”到"100%"之间,我们可以在这个规则中创建多个百分比,我们分别给每一个百分比中给需要有动画效果的元素加上不同的属性,从而让元素达到一种在不断变化的效果,比如说移动,改变元素颜色,位置,大小,形状等。
不过有一点需要注意的是,我们可以使用“fromt”“to”来代表一个动画是从哪开始,到哪结束,也就是说这个 "from"就相当于"0%"而"to"相当于"100%",值得一说的是,其中"0%"不能像别的属性取值一样把百分比符号省略,我们在这里必须加上百分符号(“%”)如果没有加上的话,我们这个keyframes是无效的,不起任何作用。因为keyframes的单位只接受百分比值。看一下具体的代码:
@keyframes IDENT {
from {
Properties:Properties value;
}
Percentage {
Properties:Properties value;
}
to {
Properties:Properties value;
}
}
/*或者全部写成百分比的形式:*/
@keyframes IDENT {
0% {
Properties:Properties value;
}
Percentage {
Properties:Properties value;
}
100% {
Properties:Properties value;
}
}
其中IDENT是一个动画名称,你可以随便取,当然语义化一点更好,Percentage是百分比值,我们可以添加许多个这样的百分比,Properties为CSS的属性名,比如说left,background等,value就是相对应的属性的属性值。
2.3.2.2 JS实现
利用JavaScript实现动画,可以采用开源的JavaScript动画库或框架进行实现,例如:Anime.js或者TweenJS 下面我们以Anime.js为例进行演示如何实现一个补间动画。
一定程度上,anime.js也是一个CSS3动画库,适用所有的CSS属性,并且实现的@keyframes 能更方便的实现帧动画,替代CSS3复杂的定义方式。使用对象数组的形式定义每一帧。
戳我:keyframes实例
anime({
targets: 'div',
translateX: [
{ value: 250, duration: 1000, delay: 500, elasticity: 0 }, //第一帧
{ value: 0, duration: 1000, delay: 500, elasticity: 0 } //第二帧
]
}) //这个例子实现了目标元素在两帧中实现水平位移
提供的Timeline能实现更为复杂的动画效果,通过这个Timeline,我们可以维护不同的动画之间的关系,进而通过多个不同的动画组成一个更为复杂的动画。
戳我:Timeline实例
var myTimeline = anime.timeline();
//通过.add()方法添加动画
myTimeline
.add({
targets: '.square',
translateX: 250
})
.add({
targets: '.circle',
translateX: 250
})
.add({
targets: '.triangle',
translateX: 250
});
当我们在实现动画的时候,慢慢会发现,大部分的元素都是图片,而且图片是提前预设好的,不能更改,只能用新的图片替换,例如当我们要实现微笑动画的时候,需要画两张图,一幅是闭着嘴的,一幅是张嘴笑的,然后逐帧播放。这样的画面当你有足够多帧图片的时候,并不会看出生硬,一旦低于 24 帧就是变得不自然了,那怎么在不增加工作量的前提下,实现流畅的变化呢?我们将关键帧动画的思维嫁接到元素自身扭曲变化上,就催生出了「柔性动画」的概念。
2.3.3.1 SVG动画讲解
从上图可以看出,元素之间是可以相互变化的,而且非常的流畅,这样的动画并不需要 canvas 这种重武器,简单的 DOM 就可以实现,SVG 真的是一个神器,不仅在实现图标,字体上特点鲜明,在实现柔性动画方面也独树一帜。
SVG 依然是 DOM ,他有自己独有的 Animation 标签,但也支持 CSS 的属性,其实现动画的本质是依赖于线条和填充,线条的变化,导致填充区域的改变,从而引起形状的变化。而线条则依赖于路径和锚点,路径和锚点的改变,直接影响了线条的变化。
可以用AI等SVG编辑工具生成SVG图片后,配合anime.js、GSAP等现有库进行动画制作。
下面我们通过anime.js来实现一个SVG路径动画.
SVG 绘制路径
戳我:SVG实例
var path = anime.path('.motion-path-demo path');
anime({
targets: '.motion-path-demo .el',
translateX: path('x'),
translateY: path('y'),
rotate: path('angle'),
easing: 'linear',
duration: 2000,
loop: true
});
SVG 实现的动画比较局部和小巧,使用范围也比较狭窄,但是当我们实现复杂的柔性动画,甚至游戏的时候,就还是需要用骨骼动画来实现。
从上图我们可以看到龙的翅膀是一张图片,但是可以通过图片的局部的扭曲和变形,来实现煽动翅膀时带来的肌肉收缩和舒张。这样的动画是怎么实现的呢?这就要引出骨骼动画中,一个非常重要的概念:网格。
这里我们比较浅显的讨论下这个概念,要实现图片的局部变化,我们就要把图片分块,分的每一块就称为网格,每个网格都有自己的顶点和边,顶点的位移会引起网格形状的变化,形状的变化就会带来所附属的图片的变化。网格的概念是不是很像路径和锚点,不论怎样的技术,在实现逻辑上都大同小异,重要的不是一直盯着不同和变化的部分,而是发现那些不变的地方,才能达到触类旁通的效果。
制作这样的动画并不复杂,你可以使用类似 Spine 和 DragonBones 这样的工具,但是做动画真的是一个体力活,你需要不断的调试,以求达到一种让人看起来舒服的状态。
2.3.4.1 骨骼动画讲解
骨骼动画就是把角色的各部分身体部件图片绑定到一根根互相作用连接的“骨头”上,通过控制这些骨骼的位置、旋转方向和放大缩小而生成的动画。
我们常说的骨骼动画一般分为两个部分:
骨架涉及的数据包括两个:
蒙皮则表达的是依附在骨骼上的顶点的信息。
骨骼绑定的过程就是确定每个顶点受哪几根骨骼的影响,每根骨骼影响的权重有多大,譬如肘部的皮肤可能同时受大臂和小臂两根骨头的影响,而远离手肘的部分可能就只受小臂骨头影响。一般在3D骨骼动画里,每个顶点最多支持4-8根骨骼同时影响它就已经可以很精确地表达整个蒙皮的效果了。
骨骼动画比传统的逐帧动画要求更高的处理器性能,但同时它也具有更多的优势:
动画更加生动逼真。
图片资源占最小的存储空旷:骨骼动画的图片容量可以减少90%(配置文件H5的压缩方案后面详解)。
动画切换自动补间:过渡动画自动生成,让动作更加灵动。
骨骼可控 :可以通过代码控制骨骼,轻松实现角色装备更换,甚至可对某骨骼做特殊控制或事件监听。
骨骼事件帧:动画执行到某个动作或某个帧,触发自定义事件行为。
动作数据继承:多角色可共用一套动画数据。
可结合物理引擎和碰撞检测。
2.3.4.2 骨骼动画制作
首先我们来了解一下,骨骼动画是如何进行制作的:
制作骨骼动画主要是使用 Spine 和 DragonBones 这样的工具进行制作。
DragonBones
DragonBones是从Flash动画开始创作的,初衷是减小资源量,同时实现更为细粒度的动作(比如交互式的),让美术从繁琐的逐帧绘制Sprie Sheet的工作中解放出来,所以它把一个角色每一帧的sprite sheet拆分成一个个更小的基本图块,譬如胳膊,腿,躯干等等,而每个基本图块仍然是最小的可控制单位。
以下游戏&渲染引擎都支持渲染DragonBones导出的文件:
Spine
Spine 是一款针对游戏开发的 2D 骨骼动画编辑工具。Spine 旨在提供更高效和简洁 的工作流程,以创建游戏所需的动画。
业界收费专业2D骨骼动画编辑工具,动画设计师推荐易用稳定,以下游戏&渲染引擎都支持渲染Spine导出的文件:
下面我们来制作一个骨骼动画小案例
创建骨骼
首先我们需要创建手部的骨骼,如下图所示:
创建蒙皮网格
然后我们需要给手部创建蒙皮网格(MESH),如下图所示:
首先,单击创建骨骼的Create按钮,退出骨骼创建模式
设置网格点权重
我们需要给网格顶点设置各个骨骼的权重,整个过程如下图所示:
首先,关闭Edit Mesh菜单
动起来!
现在我们要让手动起来了,我们只展示一个弯曲手臂的动画即可。
首先,我们需要设置关键帧,让我们在第1帧和第30帧设置好关键帧,这两个关键帧对应的手臂位置是完全一样的,因为我们需要循环播放动画。
具体步骤如下图:
接下来我们只需轻轻旋转手臂,并在0-30帧中间找一个帧当做关键帧即可:我们选择第15帧作为中间的关键帧。
额外的,我给另一只手、嘴巴、脸部和头发都做了MESH,以下是动画的效果图:
2.3.4.3 前端展示骨骼动画
用Spine将制作好的骨骼动画进行导出输出资源(合图信息文件:atlas;动画信息文件:json,图片合图:png),将这些资源交由前端进行展示。
前端开发根据Spine或者DragonBones能够支持的渲染引擎,在项目中导入渲染引擎进行展示骨骼动画。
前端3D动画实现可以通过perspective属性操作用CSS 3D来实现,或者直接借助开源的Three.js开源库进行实现。
由于3D动画涉及的内容较多,篇幅有限,后面我们将专门开一章来讲解前端3D动画。
适合场景: 简单的展示型动画
使用transition\animation属性,设置相应的关键帧状态,并且借助一些缓动函数来进行实现一些简单化的动画。
优点:开发成本低,不需要导入任何额外的依赖包
缺点与不足:只能够胜任做一些比较简单化的动画,无法实现一些过于负责的动画。
适用场景: 简单的展示型动画+弱交互型动画
Anime.js是一个轻量级的js驱动的动画库,主要的功能有:
GitHub:
https://github.com/juliangarn...
codepen仓库:
https://codepen.io/collection...
文档演示:
http://animejs.com/documentat...
功能介绍:
一定程度上,anime.js也是一个CSS3动画库,适用所有的CSS属性,并且实现的@keyframes能更方便的实现帧动画,替代CSS3复杂的定义方式。使用对象数组的形式定义每一帧。
戳我:keyframes实例
anime({
targets: 'div',
translateX: [
{ value: 250, duration: 1000, delay: 500, elasticity: 0 }, //第一帧
{ value: 0, duration: 1000, delay: 500, elasticity: 0 } //第二帧
]
}) //这个例子实现了目标元素在两帧中实现水平位移
提供的Timeline能实现更为复杂的动画效果,通过这个Timeline,我们可以维护不同的动画之间的关系,进而通过多个不同的动画组成一个更为复杂的动画。
戳我:Timeline实例
var myTimeline = anime.timeline();
//通过.add()方法添加动画
myTimeline
.add({
targets: '.square',
translateX: 250
})
.add({
targets: '.circle',
translateX: 250
})
.add({
targets: '.triangle',
translateX: 250
});
动画播放的控制,常见的有暂停,重播,继续,动画状态的跟踪,自动播放,循环次数,抖动效果
戳我:playback controls实例
为动画提供了回调函数,在动画或时间线完成的开始,期间或之时执行回调函数。
戳我:callback实例
var myAnimation = anime({
targets: '#begin .el',
translateX: 250,
delay: 1000,
begin: function(anim) { // callback
console.log(anim.began); // true after 1000ms
}
});
支持promise,动画结束后,调用anime.finished会返回一个promise对象。
戳我:promise实例
支持svg绘制路径,目前不支持canvas绘制。
戳我:SVG实例
对于input这样带有数值的元素标签,也可以通过anime实例来设置动画。
戳我:DOM ATTRIBUTES实例
anime({
targets: input,
value: 1000, // Animate the input value to 1000
round: 1 // Remove decimals by rounding the value
});
优点:
显而易见,anime.js不仅实现了CSS3动画的深度封装,更多的是通过js驱动来实现操作动画的状态,timeline实现了对于多个分支动画的管理,对于实现更为复杂的动画提供了可能。
通过anime.js提供的playback controls和callback,同时对于promise的支持,让我们对于动画的简单交互有了操作的空间。
虽然不支持canvas,但是支持svg绘制路径。
浏览器兼容性比较好,Android 4以上全部支持。
复杂的展示型动画:
强交互&互动小游戏&骨骼动画: