6
作者:vivo 互联网前端团队- ZhaoJie

本文将从各个角度来对动画整个体系进行分类,并且介绍各种前端动画的实现方法,最后我们将总结在实际开发中的各个场景的动画选择方案。

一、背景

前端动画场景需求多

对众多动画场景的技术实现方案选择上比较模糊

各动画方案的优劣及适用场景认识模糊

现有动画库太多,不知道选哪个

主流动画库的适用场景认识模糊

下面首先让我们从各个角度来对动画整个体系进行分类,让我们清晰的了解动画整个体系。

二、分类

2.1 用途角度

首先我们从动画的用途或者说是业务的角度来进行区分,将我们平时的动画分为展示型动画和交互型动画。

2.1.1 展示型动画

类似于一张GIF图,或者一段视频。比如在开启宝箱的时候,我们会加入一个切场过渡动画,来替代原有的生硬等待结果。

展示型动画在实际使用的场景中,实现的方法很多,比如用GIF图,canvas,CSS3动画等,但是最终输出的结果是不带有交互的,也就是从动画起始状态到结束状态一气呵成,这个过程用户可以感知,但是无法参与

2.1.2 交互型动画

用户自已参与的,对于交互性动画而言,我们可以在动画播放的某个时间节点触发相应的操作,进而让用户参与到其中,最常见的例子红包雨,不仅仅能提升用户的体验,还能提升我们的产品的多元性。

然而交互性动画经常面临的一个问题就是,通过原生代码实现交互动画是很复杂的,同时性能和兼容性是不得不认真考虑的问题,比较好的解决方案还是寻求相关的框架。

2.2 绘制技术角度

不管采用什么方式来制作动画,最终呈现到前端页面的无非是以下三种形式:

  1. Canvas
  2. div
  3. SVG
PS:为了简单也可以用视频,但除非动画的播放场景固定,不然移动端视频在不同app、不同机型、不同系统的播放显示都不太一样,容易踩不少坑。

2.2.1 不同绘制技术的性能差异

Canvas

  • 效率高、性能好、可控性高,只能处理位图,内存占用恒定
  • 依赖分辨率
  • 不支持事件处理器
  • 弱的文本渲染能力
  • 能够以 .png 或 .jpg 格式保存结果图像
  • 最适合图像密集型的游戏,其中的许多对象会被频繁重绘

div

  • 包括CSS控制的DOM动画、JS控制的DOM动画
  • 比较适合简单的数量较少的复杂度较低的动画

SVG

  • 处理矢量图,不失真
  • 不依赖分辨率
  • 支持事件处理器
  • 最适合带有大型渲染区域的应用程序(比如谷歌地图)
  • 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
  • 不适合游戏应用

2.2.2 Canvas和SVG比较

一句话总结:都是2D做图,svg是矢量图,canvas是位图。canvas 是逐像素进行渲染的,适合游戏。

SVG

  • SVG绘制的是矢量图,缩放不影响显示,所以最适合带有大型渲染区域的应用程序(比如谷歌地图)
  • SVG 是一种使用 XML 描述 2D 图形的语言。
  • SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的。您可以为某个元素附加 JavaScript 事件处理器。
  • 在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。

Canvas

  • Canvas 通过 JavaScript 来绘制 2D 图形。
  • Canvas 是逐像素进行渲染的。
  • 在 Canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。
  • Canvas只占用一个DOM节点,在做一些烟花、飘雪等运动元素很多的动画时,会比CSS/SVG性能好。

性能比较

  • 一般情况下,随着屏幕大小的增大,canvas将开始降级,因为需要绘制更多的像素。
  • 随着屏幕上的对象数目增多,SVG 将开始降级,因为我们正不断将这些对象添加到 DOM 中。
  • 这些度量不一定准确,以下方面的不同一定会引起变化:实现和平台、是否使用完全硬件加速的图形,以及 JavaScript 引擎的速度。

2.3 动画类型角度

前端动效开发,首先应该确定的是

动画用途->确认动画类型->确认绘制技术->确认动画的实现方式。

虽然最终呈现动画的载体(绘制技术)就三种,但实现动画的方式却很多,得从动画类型出发讨论动画的实现方式:

(1)逐帧动画(序列帧动画)

  • GIF实现
  • CSS实现(animation)
  • JS+DOM实现
  • JS+canvas实现

(2)补间动画(Tween动画\关键帧动画)

  • CSS实现(transition、animation等)使用一些缓动函数
  • JS实现

(3)SVG动画

  • 使用 XML 格式定义图形
  • 可以用AI等SVG编辑工具生成SVG图片后,配合anime.js、GSAP等现有库进行动画制作

(4)骨骼动画

  • 一般采用Spine、DragonBones等工具导出相应资源图片和JSON动画配置资源后使用。

(5)3D动画

  • DOM操作用CSS 3D实现。(perspective属性、css3d-engine
  • 场景搭建用webGL(Three.js等)
  • 3D模型动画用Blender或maya等制作完成后导出使用

2.3.1 逐帧动画(序列帧动画)

逐帧动画是在时间帧上逐帧绘制帧内容,由于是一帧一帧的画,所以逐帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容。

由于逐帧动画的帧序列内容不一样,不仅增加制作负担而且最终输出的文件量也很大,但它的优势也很明显:因为它相似与电影播放模式,很适合于表演很细腻的动画,如3D效果、人物或动物急剧转身等等效果。

所以逐帧动画的实现核心是什么,就是将我们的这些静态的图片进行快速的循环播放,形成了一个动态的动画效果。这就是帧动画。
2.3.1.1 GIF实现

我们可以将帧动画导出成GIF图,GIF图会连续播放,无法暂停,它往往用来实现小细节动画,成本较低、使用方便。但其缺点也是很明显的:

  1. 画质上,GIF 支持颜色少(最大256色)、Alpha 透明度支持差,图像锯齿毛边比较严重;
  2. 交互上,不能直接控制播放、暂停、播放次数,灵活性差;
  3. 性能上,GIF 会引起页面周期性的绘画,性能较差。
2.3.1.2 CSS实现

CSS3帧动画是我们今天需要重点介绍的方案,最核心的是利用CSS3中Animation动画,确切的说是使用animation-timing-function 的阶梯函数 steps(number\_of\_steps, direction) 来实现逐帧动画的连续播放。

帧动画的实现原理是不断切换视觉内图片内容,利用视觉滞留生理现象来实现连续播放的动画效果,下面我们来介绍制作CSS3帧动画的几种方案。

(1)连续切换动画图片地址src(不推荐)

我们将图片放到元素的背景中(background-image),通过更改 background-image 的值实现帧的切换。但是这种方式会有以下几个缺点,所以该方案不推荐。

  • 多张图片会带来多个 HTTP 请求
  • 每张图片首次加载会造成图片切换时的闪烁
  • 不利于文件的管理

(2)连续切换雪碧图位置(推荐)我们将所有的帧动画图片合并成一张雪碧图,通过改变 background-position 的值来实现动画帧切换。分两步进行:

步骤一:

将动画帧合并为雪碧图,雪碧图的要求可以看上面素材准备,比如下面这张帧动画雪碧图,共20帧。

(图片来源于:帧动画的多种实现方式与性能对比)

步骤二:

使用steps阶梯函数切换雪碧图位置

写法一:

<div class="sprite"></div>


.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%变化一次,所以我们这样写才能达到想要的效果。

写法二:

<div class="sprite"></div>.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加速,提高机器渲染效果,还能有效解决移动端帧动画抖动的问题。

<div class="sprite-wp">    <div class="sprite"></div></div>

.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 指定了一个阶梯函数,包含两个参数:

  • 第一个参数指定了函数中的间隔数量(必须是正整数);
  • 第二个参数可选,指定在每个间隔的起点或是终点发生阶跃变化,接受 start 和 end 两个值,默认为 end。
  • start 第一帧是第一步动画的结束,end 第一帧是第一步动画的开始。

除了 steps 函数,animation-timing-function 还有两个与逐帧动画相关的属性值 step-start 与 step-end:

  • step-start 等同于 steps(1,start)
  • step-end 等同于 steps(1,end)
2.3.1.3 JS实现

(1)通过JS来控制img的src属性切换(不推荐)

和上面CSS3帧动画里面切换元素background-image属性一样,会存在多个请求等问题,所以该方案我们不推荐,但是这是一种解决思路。

(2)通过JS来控制canvas图像绘制

通过canvas制作帧动画的原理是用drawImage方法将图片绘制到canvas上,不断擦除和重绘就能得到我们想要的效果。

<canvas id="canvas" width="300" height="300"></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;
}

<div class="sprite" id="sprite"></div>(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;
}

<div class="sprite-wp">    <div class="sprite" id="sprite"></div></div>

(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、内存的使用情况,得到以下数据:

通过分析以上数据我们可以得出以下几点:

  1. 除了CSS transform:translate3d() 方案,其他方案的FPS都能达到60FPS的流畅程度,但该方案的FPS 也不是很低。
  2. CPU占用率最低的方案是
    CSS transform:translate3d() 方案。
  3. GPU占用最低的方案是JS canvas 绘制方案。
  4. CSS 方案没有脚本开销。
  5. Rendering 最少的是
    CSS transform:translate3d() 方案。
  6. Painting 最少的是
    CSS transform:translate3d() 方案。
  7. 各方案内存占用区别不大。

结论:我们看到,在7个指标中,CSS transform:translate3d() 方案将其中的4个指标做到了最低,从这点看,我们完全有理由选择这种方案来实现CSS帧动画。

2.3.2 补间动画(Tween动画\关键帧动画)

补间动画是动画的基础形式之一,又叫做中间帧动画,渐变动画,指的是人为设定动画的关键状态,也就是关键帧,而关键帧之间的过渡过程只需要由计算机处理渲染的一种动画形式。

说白了,就是我们在做动画的时候,只需要指定几个特殊时刻动画的状态,其余的状态由计算机自动计算补充。

实现补间动画常见的手段主要由以下几种:

  • CSS3 Animation:通过animation(除steps()以外的时间函数)属性在每个关键帧之间插入补间动画。
  • CSS3 Transition:区别于animation,transition只能设定初始和结束时刻的两个关键帧状态。
  • 利用JavaScript实现动画:例如JavaScript动画库或框架,Anime.js 或者TweenJS,它是CreateJS的其中一个套件。另外,在Flash业界久负盛名的GreenSock推出的GSAP(GreenSock Animation Platform)也新引入了对Javascript动画的支持。
2.3.2.1 CSS实现

(1)transition 动画

transition允许CSS的属性值在一定的时间区间内平滑地过渡,即指定元素的初始状态 和末尾状态,既可以完成一个动画,中间的变化完全有浏览器自己决定。动画的效果主要还是看transition相关属性即可。

然而利用transition制作的动画也有着显著的缺点:

  1. transition需要事件触发,所以没法在网页加载时自动发生。
  2. transition是一次性的,不能重复发生,除非一再触发。
  3. transition只能定义开始状态和结束状态,不能定义中间状态,也就是说只有两个状态。
  4. 一条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 
});

2.3.3 SVG动画

当我们在实现动画的时候,慢慢会发现,大部分的元素都是图片,而且图片是提前预设好的,不能更改,只能用新的图片替换,例如当我们要实现微笑动画的时候,需要画两张图,一幅是闭着嘴的,一幅是张嘴笑的,然后逐帧播放。这样的画面当你有足够多帧图片的时候,并不会看出生硬,一旦低于 24 帧就是变得不自然了,那怎么在不增加工作量的前提下,实现流畅的变化呢?我们将关键帧动画的思维嫁接到元素自身扭曲变化上,就催生出了「柔性动画」的概念。

2.3.3.1 SVG动画讲解

(图片来源于:GSAP官网)

从上图可以看出,元素之间是可以相互变化的,而且非常的流畅,这样的动画并不需要 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
});

(图片来源于:animejs官网)

2.3.4 骨骼动画

SVG 实现的动画比较局部和小巧,使用范围也比较狭窄,但是当我们实现复杂的柔性动画,甚至游戏的时候,就还是需要用骨骼动画来实现。

(图片来源于:DragonBones官网)

从上图我们可以看到龙的翅膀是一张图片,但是可以通过图片的局部的扭曲和变形,来实现煽动翅膀时带来的肌肉收缩和舒张。这样的动画是怎么实现的呢?这就要引出骨骼动画中,一个非常重要的概念:网格

这里我们比较浅显的讨论下这个概念,要实现图片的局部变化,我们就要把图片分块,分的每一块就称为网格,每个网格都有自己的顶点和边,顶点的位移会引起网格形状的变化,形状的变化就会带来所附属的图片的变化。网格的概念是不是很像路径和锚点,不论怎样的技术,在实现逻辑上都大同小异,重要的不是一直盯着不同和变化的部分,而是发现那些不变的地方,才能达到触类旁通的效果。

制作这样的动画并不复杂,你可以使用类似 Spine 和 DragonBones 这样的工具,但是做动画真的是一个体力活,你需要不断的调试,以求达到一种让人看起来舒服的状态。

2.3.4.1 骨骼动画讲解

骨骼动画就是把角色的各部分身体部件图片绑定到一根根互相作用连接的“骨头”上,通过控制这些骨骼的位置、旋转方向和放大缩小而生成的动画。

我们常说的骨骼动画一般分为两个部分:

  1. 骨架(Skeleton)
  2. 蒙皮(Skin)

骨架涉及的数据包括两个:

  • 一是骨架的拓扑结构(连接、父子关系)。
  • 二是骨架的各种pose,也就是每个动作对应的整个骨架的位置信息。

蒙皮则表达的是依附在骨骼上的顶点的信息。

骨骼绑定的过程就是确定每个顶点受哪几根骨骼的影响,每根骨骼影响的权重有多大,譬如肘部的皮肤可能同时受大臂和小臂两根骨头的影响,而远离手肘的部分可能就只受小臂骨头影响。一般在3D骨骼动画里,每个顶点最多支持4-8根骨骼同时影响它就已经可以很精确地表达整个蒙皮的效果了。

  • 骨骼动画的优势:

骨骼动画比传统的逐帧动画要求更高的处理器性能,但同时它也具有更多的优势:

  1. 动画更加生动逼真。
  2. 图片资源占最小的存储空旷:骨骼动画的图片容量可以减少90%(配置文件H5的压缩方案后面详解)。
  3. 动画切换自动补间:过渡动画自动生成,让动作更加灵动。
  4. 骨骼可控 :可以通过代码控制骨骼,轻松实现角色装备更换,甚至可对某骨骼做特殊控制或事件监听。
  5. 骨骼事件帧:动画执行到某个动作或某个帧,触发自定义事件行为。
  6. 动作数据继承:多角色可共用一套动画数据。
  7. 可结合物理引擎和碰撞检测。
2.3.4.2 骨骼动画制作

首先我们来了解一下,骨骼动画是如何进行制作的:

制作骨骼动画主要是使用 Spine 和 DragonBones 这样的工具进行制作。
  • DragonBones

(图片来源于:DragonBones官网)

DragonBones是从Flash动画开始创作的,初衷是减小资源量,同时实现更为细粒度的动作(比如交互式的),让美术从繁琐的逐帧绘制Sprie Sheet的工作中解放出来,所以它把一个角色每一帧的sprite sheet拆分成一个个更小的基本图块,譬如胳膊,腿,躯干等等,而每个基本图块仍然是最小的可控制单位。

以下游戏&渲染引擎都支持渲染DragonBones导出的文件:

(图片来源于:DragonBones官网)

  • Spine

(图片来源于:Spine官网)

Spine 是一款针对游戏开发的 2D 骨骼动画编辑工具。Spine 旨在提供更高效和简洁 的工作流程,以创建游戏所需的动画。

业界收费专业2D骨骼动画编辑工具,动画设计师推荐易用稳定,以下游戏&渲染引擎都支持渲染Spine导出的文件:

(图片来源于:Spine官网)

下面我们来制作一个骨骼动画小案例

  • 创建骨骼

首先我们需要创建手部的骨骼,如下图所示:

  1. 1确保左上角为SETUP模式
  2. 确保选中右边视图中的根骨骼,创建骨骼时必须要选中父骨骼
  3. 单击左下角的Create按钮
  4. 开始依次创建出5根骨骼
  5. 创建蒙皮网格

然后我们需要给手部创建蒙皮网格(MESH),如下图所示:

首先,单击创建骨骼的Create按钮,退出骨骼创建模式

  1. 选中手部贴图(Attachment)
  2. 勾选其底部的Mesh选项
  3. 单击右下角的Edit按钮
  4. 呼出了Edit Mesh菜单
  5. 勾选Edit Mesh菜单中的Deformed选项
  6. 单击Edit Mesh菜单中的Create按钮
  7. 开始在手部创建网格顶点
  8. 可以单击Edit Mesh菜单中的Modify按钮对顶点进行位移
  9. 设置网格点权重

我们需要给网格顶点设置各个骨骼的权重,整个过程如下图所示:

首先,关闭Edit Mesh菜单

  1. 确认勾选的还是手部的贴图
  2. 单击左下角的Weights按钮,呼出Weights菜单
  3. 单击Weights菜单底部的Bind按钮,来绑定骨骼
  4. 选择手部的五根骨骼,直到它们都出现Weights菜单里,注意不同的骨骼颜色是不一样的
  5. 单击Weights菜单的Auto按钮或者按esc键,来触发Spine的自动权重计算
  6. 勾选Weights菜单的Overlay,我们可以看到绑定后的权重热力图
  7. 动起来!

现在我们要让手动起来了,我们只展示一个弯曲手臂的动画即可。

首先,我们需要设置关键帧,让我们在第1帧和第30帧设置好关键帧,这两个关键帧对应的手臂位置是完全一样的,因为我们需要循环播放动画。

具体步骤如下图:

  1. 确保左上角的模式处于ANIMATE模式
  2. 选中手部的五根骨骼(按住cmd键或control键依次点选)
  3. 选中第0帧
  4. 单击Rotate下的钥匙按钮,我们对手臂的旋转属性设置关键帧
  5. 选择第30帧
  6. 重复第4步的操作,使第30帧的关键帧与第0帧完全相同

接下来我们只需轻轻旋转手臂,并在0-30帧中间找一个帧当做关键帧即可:我们选择第15帧作为中间的关键帧。

  1. 选择第15帧
  2. 确保Rotate按钮被选中
  3. 向上旋转5根骨骼到一个角度
  4. 按下K帧按钮进行关键帧设置
  5. 按下播放按钮来预览动画

额外的,我给另一只手、嘴巴、脸部和头发都做了MESH,以下是动画的效果图:

2.3.4.3 前端展示骨骼动画

用Spine将制作好的骨骼动画进行导出输出资源(合图信息文件:atlas;动画信息文件:json,图片合图:png),将这些资源交由前端进行展示。

前端开发根据Spine或者DragonBones能够支持的渲染引擎,在项目中导入渲染引擎进行展示骨骼动画。

2.3.5 3D动画

前端3D动画实现可以通过perspective属性操作用CSS 3D来实现,或者直接借助开源的Three.js开源库进行实现。

由于3D动画涉及的内容较多,篇幅有限,后面我们将专门开一章来讲解前端3D动画。

三、现有方案总结

3.1 纯CSS实现

适合场景: 简单的展示型动画

使用transition\animation属性,设置相应的关键帧状态,并且借助一些缓动函数来进行实现一些简单化的动画。

优点:开发成本低,不需要导入任何额外的依赖包

缺点与不足:只能够胜任做一些比较简单化的动画,无法实现一些过于负责的动画。

3.2 Anime.js

适用场景: 简单的展示型动画+弱交互型动画

Anime.js是一个轻量级的js驱动的动画库,主要的功能有:

  1. 支持keyframes,连接多个动画
  2. 支持Timeline,为实现更为复杂的动画提供了可能
  3. 支持动画状态的控制playback control,播放,暂停,重新启动,搜索动画或时间线。
  4. 支持动画状态的callback,在动画开始,执行中,结束时提供回调函数
  5. 支持SVG动画
  6. 可以自定义贝塞尔曲线
  7. 任何包含数值的DOM属性都可以设置动画
  8. GitHub:
    https://github.com/juliangarn...
  9. codepen仓库:
    https://codepen.io/collection...
  10. 文档演示:
    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以上全部支持。

缺点:

Anime.js做展示型动画是可以胜任的,但是对于特别复杂的动画也是不太能够实现,在做交互性动画方面还是需要看场景,它更多适合做一些小型的交互动画,类似于通过触摸屏幕踢足球这种强交互的,anime.js就不是很有优势了。

3.3 Lottie

适用场景: 复杂的展示型动画

通过 AE 上的 Bodymovin 插件将 AE 中制作好的动画导出成一个 json 文件,通过Lottie对JSON进行解析,最后以SVG/canvas/html的方式渲染动画。

能够完好的展示设计师设计的各种各样复杂的动画。

优点:

  • 跨平台,一次绘制、一次转换、随处可用。
  • 文件更小,获取AE导出的JSON,最后通过lottie渲染为canvas/svg/html格式。
  • 可以通过api操纵动画的一些属性,比如动画速度;添加动画各个状态的回调函数。
  • 动画都是在After Effects中创建的,使用Bodymovin导出,并且本机渲染无需额外的工程工作。
  • 解放前端工程师的生产力,提高设计师做动效的自由度。

缺点:

  • Bodymovin 插件待完善,仍然有部分 AE 效果无法成功导出。
  • 对于交互方面支持的还不是很好,更多的是用来展示动画。
  • Lottie 对 json 文件的支持待完善,目前有部分能成功导出成 json 文件的效果在移动端上无法很好的展现。
  • 很多AE的效果是不支持的 查看支持的特性:Supported Features

3.4 PixiJs

适用场景: 交互型动画,动画小游戏

PixiJS是一个2D 渲染引擎, Pixi 主要负责渲染画面。可以创建丰富的交互式图形,动画和游戏,而无需深入了解WebGL API或处理浏览器和设备兼容性的问题。与此同时,PixiJS具有完整的WebGL支持,如果需要,可以无缝地回退到HTML5的canvas。PixiJs默认使用WebGL渲染,也可以通过声明指定canvas渲染,WebGL在移动端Android 4.4 browser并不支持,不过可以使用canvas优雅降级。

特性(摘自官方DOCS):

  • 支持WebGL渲染
  • 支持canvas 渲染(官方称PixiJS在canvas渲染方面现在是最快的)
  • 非常简单易用的API
  • 丰富的交互事件,比如完整的鼠标和移动端的触控事件
  • Pixi使用和 canvas Drawing几乎一致的 api,但不同于 canvas 的绘画 api,使用 Pixi 绘制的图形是通过 WebGL 在 GPU 上渲染
  • 还有一系列特性需要在学习PixiJs之后了解

优点:

  • 最大优势莫过于通过WebGL来调用GPU渲染动画,这样极大的提升了性能。
  • 无需深入了解WebGL API或者是浏览器兼容性(因为下面这条原因)。
  • 支持canvas回退,当前设备不支持WebGL时,PixiJs会使用canvas渲染动画。
  • 完整的DOCS,比较活跃的社区,有利于深入的学习。不过我感觉PixiJs学习成本相对来说还是很高的。

缺点:

  • 首先是兼容的问题,WebGL在Android 4.4 是不支持的,只能使用canvas进行降级。
  • Pixi 主要负责渲染画面,很多其它功能开发者得自己写或搭配其它库来使用,不过按照目前来看,是满足我们的需求的。

性能:

对于手机版本Android4.4 以上的手机,除了代码层面造成的性能不足,通过WebGL调用GPU渲染,性能还是有保障的。然而对于Android4.4只能使用canvas渲染,性能还是要看动画的复杂度,以及代码的优化

3.5 总结

简单的展示型动画:

对于比较简单的动画,我们可以先尝试使用原生CSS的transition\animation属性来进行实现。

简单的展示型动画+弱交互:

对于简单的动画展示并且需要有简单的交互行为,比如用户点击一下暂停执行相应操作,待操作完成继续播放动画,交互方面比较偏弱,可以采用Anime.js的方案。

Anime.js不仅仅支持所有的CSS属性,而且可以通过Timeline,callback, playback controls来控制动画执行的各个状态,并且Anime.js可以配合实现SVG动画。

复杂的展示型动画:

  1. 如果所需的资源很小,可以先考虑使用GIF动图或者逐帧动画CSS实现;
  2. 如果所需的资源较大,可以使用Lottie方案,然后设计同学用AE到处动画json,将动画还原为svg/canvas/html。

强交互&互动小游戏&骨骼动画:

  1. 对于交互场景比较负责或者需要做一个小游戏,可以采用PixiJs,通过WebGL来渲染,利用硬件资源,极大的提升性能,在兼容性方面,对于不支持WebGL的浏览器,可以使用canvas渲染来平稳回退;
  2. 如果是需要展示骨骼动画,可以通过PixiJs方案进行渲染由Spine或DragonBones输出的文件。


vivo互联网技术
3.3k 声望10.2k 粉丝