# lottie原理探索
# 什么是Lottie
Lottie 是 Airbnb 开源的一个动画渲染库,它可以解析 Adobe After Effects 使用 Bodymovin 插件把做的动画导出的 json 文件,并在 Android/iOS、React Native 和 web 端实现相同的动画效果。
简单讲就是UI同学使用AE作出好看的特效,再通过Bodymovin插件转成json文件,我们通过对应的库(前端使用lottie-web)将文件解析并展示。
# lottie优点
动画由UI同学使用AE实现,动画的实现更加方便,效果也更好
前端可以直接调用动画,并对动画进行控制,减少前端直接写动画的工作量
视觉效果提升明显,从以前的简单动效到支持复杂动态
文件大小相比较于gif缩小60%-90%,CPU占用率也大幅降低
# lottie缺点
lottie-web 文件本身比较大,大小为513k,轻量版压缩后也有144k,经过gzip后,大小为39k
使用的json文件是由AE导出,如果UI在设计的时候创建了很多图层,可能会导致json文件较大
有部分AE动画的效果还不支持,lottie无法实现
# Lottie-web使用方法
直接使用 npm 引入,创建lottie实例
import lottie from 'lottie-web';
const animation = lottie.loadAnimation({
container: element, // document.getElementById("app") 容器
renderer: 'svg', // 渲染方式 可以选择 svg/canvas/html
loop: true, // 循环播放
autoplay: true, // 自动播放
path: 'data.json' // 读取动画json的路径,可以是CDN地址
});
2
3
4
5
6
7
8
9
lottie-web 提供了很多操控动画的方法,可以根据需要对动画进行控制
animation.play(); // 播放该动画,从目前停止的帧开始播放
animation.stop(); // 停止播放该动画,回到第0帧
animation.pause(); // 暂停该动画,在当前帧停止并保持
animation.goToAndStop(value, isFrame); // 跳到某个时刻/帧并停止。isFrame(默认false)指示value表示帧还是时间(毫秒)
animation.goToAndPlay(value, isFrame); // 跳到某个时刻/帧并进行播放
animation.goToAndStop(30, true); // 跳转到第30帧并停止
animation.goToAndPlay(300); // 跳转到第300毫秒并播放
animation.playSegments(arr, forceFlag); // arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段
animation.playSegments([10,20], false); // 播放完之前的片段,播放10-20帧
animation.playSegments([[0,5],[10,18]], true); // 直接播放0-5帧和10-18帧
animation.setSpeed(speed); // 设置播放速度,speed为1表示正常速度
animation.setDirection(direction); // 设置播放方向,1表示正向播放,-1表示反向播放
animation.destroy(); // 删除该动画,移除相应的元素标签等。在unmount的时候,需要调用该方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lottie-web还提供了一些监听事件
complete: 播放完成(循环播放下不会触发)
loopComplete: 当前循环下播放(循环播放/非循环播放)结束时触发
enterFrame: 每进入一帧就会触发,播放时每一帧都会触发一次,stop方法也会触发
segmentStart: 播放指定片段时触发,playSegments、resetSegments等方法刚开始播放指定片段时会发出,如果playSegments播放多个片段,多个片段最开始都会触发。
data_ready: 动画json文件加载完毕触发
DOMLoaded: 动画相关的dom已经被添加到html后触发
destroy: 将在动画删除时触发
animation.addEventListener('data_ready', () => {
console.log('animation data has loaded');
});
2
3
# 源码摸索
从 git仓库 将源码clone下来,在player/js 目录下即为播放器代码,module.js 为入口文件,下面以一个文件被加载到播放为例,看看都做了什么工作。
入口文件 module.js 进入,定义了 lottie 对象,并将各个模块方法都挂载在对象上,其中 loadAnimation 方法是用来初始化动画的。源码中可以看到,大部分的静态方法被委托到了 animationManager 上。
var lottie = {};
...
function loadAnimation(params) {
if (standalone === true) {
params.animationData = JSON.parse(animationData);
}
return animationManager.loadAnimation(params);
}
...
lottie.loadAnimation = loadAnimation;
lottie.play = animationManager.play;
lottie.pause = animationManager.pause;
lottie.setLocationHref = setLocationHref;
lottie.togglePause = animationManager.togglePause;
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
下面跟着断点进行调试,看一下具体的执行过程。执行了初始化动画的方法 loadAnimation 之后,返回了 animationManager.loadAnimation,就是我们刚才看到的入口文件中被挂载的静态方法
继续向下,进入到源码中的 animation/AnimationManager.js 文件
下面就是几个比较重要的方法了,咱们一个一个看
function loadAnimation(params){
var animItem = new AnimationItem(); // 实例化
setupAnimation(animItem, null);// 绑定自定义事件
animItem.setParams(params);// 设置参数
return animItem;
}
2
3
4
5
6
# setupAnimation
首先进行了实例化,AnimationItem 是动画容器的基类, setupAnimation 方法中是一些自定义的事件
function setupAnimation(animItem, element){
animItem.addEventListener('destroy',removeElement);
animItem.addEventListener('_active',addPlayingCount);
animItem.addEventListener('_idle',subtractPlayingCount);
registeredAnimations.push({elem: element,animation:animItem});
len += 1;
}
2
3
4
5
6
7
# setParams
setParams 是 AnimationItem 实例的一个方法,删掉了一些无关紧要的地方,核心就是两点
确定渲染方式,并创建相应的渲染器
确定数据源,如果有 animationData 参数就直接用这个数据,如果没有就用 path ,之后会调用 assetLoader.load 来初始化数据,最终数据都被传给了 configAnimation 方法
demo中使用的是path的方式将文件路径传了进去,所以会执行 assetLoader.load ,assetLoader中主要就是建立了 XML 连接,将 path 中的内容下载下来,在将传入的callback 也就是 configAnimation 执行掉
AnimationItem.prototype.setParams = function(params) {
...
var animType = params.animType ? params.animType : params.renderer ? params.renderer : 'svg';
// 确定渲染方式并创建渲染器
switch(animType){
case 'canvas':
this.renderer = new CanvasRenderer(this, params.rendererSettings);
break;
// demo中使用的是svg方式,所以会进入进行svg渲染器的创建
case 'svg':
this.renderer = new SVGRenderer(this, params.rendererSettings);
break;
default:
this.renderer = new HybridRenderer(this, params.rendererSettings);
break;
}
...
// 判断数据源
if (params.animationData) {
this.configAnimation(params.animationData);
} else if(params.path){
// 对path进行处理
if( params.path.lastIndexOf('\\') !== -1){
this.path = params.path.substr(0,params.path.lastIndexOf('\\')+1);
} else {
this.path = params.path.substr(0,params.path.lastIndexOf('/')+1);
}
this.fileName = params.path.substr(params.path.lastIndexOf('/')+1);
this.fileName = this.fileName.substr(0,this.fileName.lastIndexOf('.json'));
// 初始化path的数据,并传给 configAnimation
assetLoader.load(params.path, this.configAnimation.bind(this), function() {
this.trigger('data_failed');
}.bind(this));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# configAnimation
configAnimation 中会初始化很多动画对象的属性,然后加载各种资源,像图像、字体等,也会去加载预先定义的片段segments(默认有一个),片段是lottie提供的一种动画控制手段,可以将多个动画统一在一个动画对象中,其本质上是不断调用loadNextSegments方法,获取片段路径然后不断调用assetsLoader.load方法去加载新的片段。
AnimationItem.prototype.configAnimation = function (animData) {
if(!this.renderer){
return;
}
try {
this.animationData = animData;
if (this.initialSegment) {
this.totalFrames = Math.floor(this.initialSegment[1] - this.initialSegment[0]);
this.firstFrame = Math.round(this.initialSegment[0]);
} else {
this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip);
this.firstFrame = Math.round(this.animationData.ip);
}
this.renderer.configAnimation(animData);
if(!animData.assets){
animData.assets = [];
}
this.assets = this.animationData.assets;
this.frameRate = this.animationData.fr;
this.frameMult = this.animationData.fr / 1000;
this.renderer.searchExtraCompositions(animData.assets);
this.trigger('config_ready');
this.preloadImages();
this.loadSegments();
this.updaFrameModifier();
this.waitForFontsLoaded();
} catch(error) {
this.triggerConfigError(error);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
一切都结束后会将"data_ready"事件触发
至此,动画的load的全部结束,动画效果也被渲染出来。然后就会进入一个持续监听的状态,首先进入first,然后进入resume,在 resume 中会计算当前时间和初始化时间的一个diff elapsedTime,之后将 elapsedTime 依次传入所有动画对象的advanceTime方法中,然后更新初始时间并根据状态看是否要进行下一次循环。可见,这里在首帧后lottie做的事情其实就是不断和上一次RAF的时间算diff,并不断利用这个diff去进行下一步操作。
# 数据结构
lottie要解析的数据结构,可以在线上看 json文件。乍一看内容非常多😳,实际上捋一捋就好了😌
Lottie动画Json结构分为4层:
结构层:可以读取到动画画布的宽高,帧数,背景色,时间,起始关键帧,结束帧等信息。
asset:图片资源信息集合,这里放置的是 制作动画时引用的图片资源。
layers:图层集合,这里可以获取到多少图层,每个图层的开始帧 结束帧等。
shapes:元素集合,可以获取到每个图层都包含多个动画元素。
# 最外层json结构层
{
"v": "5.1.13", // bodymovin 版本
"fr": 25, // 帧率
"ip": 0, // 起始关键帧
"op": 75, // 结束关键帧
"w": 710, // 视图宽
"h": 760, // 视图高
"nm": "test3-13", // 名称
"ddd": 0, // 3d
"assets": [], // 资源集合
"layers": [], // 图层集合
"masker": [] // 蒙层集合
}
2
3
4
5
6
7
8
9
10
11
12
13
这是最外层的json文件,assets、layers、masker里的内容较多先合起来,看看其他的东西。其中ip表示其实关键帧,一般为0,op表示动画的结束关键帧,fr表示帧率,所以动画时间等于:(op-ip)/fr 。w和h分别表示视图的宽和高。
# layers层
{
"ddd": 0, // 是否为3d
"ind": 1, // layer的ID,唯一
"ty": 0, // 图层类型。
"nm": "立即领取", // 图层名称
"refId": "comp_0", // 引用的资源,图片/预合成层
"sr": 1,
"ks": {}, // 变换。对应AE中的变换设置,下面有详细介绍
layer: [], // 该图层包含的子图层
shaps: [], // 形状图层
"ao": 0,
"w": 1334,
"h": 750,
"ip": 0, // 该图层开始关键帧
"op": 60, // 该图层结束关键帧
"st": 0, // 该图层
"bm": 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nm 表示AE中对应的图层名称
ty 表示类型
0: comp,合成图层
1: solid;
2: image,图片
3: null;
4: shape,形状图层
5: text,文字
在源码中对layers图层数据相应的处理
// 在player/js/renderers/BaseRenderer.js
BaseRenderer.prototype.buildAllItems = function(){
var i, len = this.layers.length;
for(i=0;i<len;i+=1){
this.buildItem(i);
}
this.checkPendingElements();
};
BaseRenderer.prototype.createItem = function(layer){
switch(layer.ty){
case 2:
return this.createImage(layer);
case 0:
return this.createComp(layer);
case 1:
return this.createSolid(layer);
case 3:
return this.createNull(layer);
case 4:
return this.createShape(layer);
case 5:
return this.createText(layer);
case 13:
return this.createCamera(layer);
}
return this.createNull(layer);
};
// 在 player/js/renderers/SVGRenderer.js 中,以svg为例
SVGRenderer.prototype.createNull = function (data) {
return new NullElement(data,this.globalData,this);
};
SVGRenderer.prototype.createShape = function (data) {
return new SVGShapeElement(data,this.globalData,this);
};
SVGRenderer.prototype.createText = function (data) {
return new SVGTextElement(data,this.globalData,this);
};
SVGRenderer.prototype.createImage = function (data) {
return new IImageElement(data,this.globalData,this);
};
SVGRenderer.prototype.createComp = function (data) {
return new SVGCompElement(data,this.globalData,this);
};
SVGRenderer.prototype.createSolid = function (data) {
return new ISolidElement(data,this.globalData,this);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# ks变换
在layers 中,ks是一个非常重要的属性,代表着AE图层中的变换属性。
"ks": { // 变换。对应AE中的变换设置
"o": { // 透明度
"a": 0,
"k": 100,
"ix": 11
},
"r": { // 旋转
"a": 0,
"k": 0,
"ix": 10
},
"p": { // 位置
"a": 0,
"k": [-167, 358.125, 0],
"ix": 2
},
"a": { // 锚点
"a": 0,
"k": [667, 375, 0],
"ix": 1
},
"s": { // 缩放
"a": 0,
"k": [100, 100, 100],
"ix": 6
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
lottie-web会把ks处理成transform的属性,用于对元素进行变换操作。transform包含了translate(平移)、scale(缩放)、rotate(旋转)、skew(倾斜)等几种。lottie-web中处理ks(变换)的相关代码在 /player/js/utils/TransformProperty.js,内容比较多就不往上贴了。ks下面又有对应的不同的具体属性,下面是opacity的详细信息。
# opacity
"a": 1, // 是否存在关键帧数量
"k": [ // property value 储存变量的地方
{
"i": { // 贝塞尔曲线 入值 这里是一个一次函数曲线
"x": [
0.833
],
"y": [
0.833
]
},
"o": { // 贝塞尔曲线 出值
"x": [
0.167
],
"y": [
0.167
]
},
"n": [
"0p833_0p833_0p167_0p167" // 猜测是备注,代码里没用
],
"t": 0, // 当前关键帧的开始时间
"s": [ // 开始的opacity
100
],
"e": [ // 结束的opacity
57
]
},
{
"t": 89.0000036250443 // 持续的帧数
}
],
"ix": 11 // propertyIndex 表达式标记
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# shape层
shape参数的值对应的是AE中图层的形状设置,用于绘制图形。
"shapes": [{
"ty": "gr", // 类型。混合图层
"it": [{ // 各图层json
"ind": 0,
"ty": "sh", // 类型,sh表示图形路径
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [ // 内切线点集合
[0, 0],
[0, 0]
],
"o": [ // 外切线点集合
[0, 0],
[0, 0]
],
"v": [ // 顶点坐标集合
[182, -321.75],
[206.25, -321.75]
],
"c": false // 贝塞尔路径闭合
},
"ix": 2
},
"nm": "路径 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},{
"ty": "st", // 类型。图形描边
"c": { // 线的颜色
"a": 0,
"k": [0, 0, 0, 1],
"ix": 3
},
"o": { // 线的不透明度
"a": 0,
"k": 100,
"ix": 4
},
"w": { // 线的宽度
"a": 0,
"k": 3,
"ix": 5
},
"lc": 2, // 线段的头尾样式
"lj": 1, // 线段的连接样式
"ml": 4, // 尖角限制
"nm": "描边 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
}]
}]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
ty字段表示shape的类型
gr: 图形合并
st: 图形描边
fl: 图形填充
tr: 图形变换
sh: 图形路径
el: 椭圆路径
rc: 矩形路径
tm: 剪裁路径
/player/js/elements/helpers/shapes/SVGElementsRenderer.js
var SVGElementsRenderer = (function() {
function createRenderFunction(data) {
var ty = data.ty;
switch(data.ty) { // 根据类型,进行相应的渲染
case 'fl':
return renderFill;
case 'gf':
return renderGradient;
case 'gs':
return renderGradientStroke;
case 'st':
return renderStroke;
case 'sh':
case 'el':
case 'rc':
case 'sr':
return renderPath;
case 'tr':
return renderContentTransform;
}
}
// 渲染path元素
function renderPath(styleData, itemData, isFirstFrame) {
var j, jLen,pathStringTransformed,redraw,pathNodes,l, lLen = itemData.styles.length;
var lvl = itemData.lvl;
var paths, mat, props, iterations, k;
for(l=0;l<lLen;l+=1){
redraw = itemData.sh._mdf || isFirstFrame;
if(itemData.styles[l].lvl < lvl){ // 设置transform等属性
mat = _matrixHelper.reset();
iterations = lvl - itemData.styles[l].lvl;
k = itemData.transformers.length-1;
while(!redraw && iterations > 0) {
redraw = itemData.transformers[k].mProps._mdf || redraw;
iterations --;
k --;
}
if(redraw) {
iterations = lvl - itemData.styles[l].lvl;
k = itemData.transformers.length-1;
while(iterations > 0) {
props = itemData.transformers[k].mProps.v.props;
mat.transform(props[0],props[1],props[2],props[3],props[4],props[5],props[6],props[7],props[8],props[9],props[10],props[11],props[12],props[13],props[14],props[15]);
iterations --;
k --;
}
}
} else {
mat = _identityMatrix;
}
paths = itemData.sh.paths;
jLen = paths._length;
if(redraw){ // 设置path的d的值,即路径绘制形状
pathStringTransformed = '';
for(j=0;j<jLen;j+=1){
pathNodes = paths.shapes[j];
if(pathNodes && pathNodes._length){
pathStringTransformed += buildShapeString(pathNodes, pathNodes._length, pathNodes.c, mat);
}
}
itemData.caches[l] = pathStringTransformed;
} else {
pathStringTransformed = itemData.caches[l];
}
itemData.styles[l].d += pathStringTransformed;
itemData.styles[l]._mdf = redraw || itemData.styles[l]._mdf;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 总结
网上大佬整理的流程图
参考: