# 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地址
});
1
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的时候,需要调用该方法
1
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');
});
1
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;
...
1
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;
}
1
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;
    }
1
2
3
4
5
6
7

# setParams

setParams 是 AnimationItem 实例的一个方法,删掉了一些无关紧要的地方,核心就是两点

  1. 确定渲染方式,并创建相应的渲染器

  2. 确定数据源,如果有 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));
    }
1
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);
    }
};
1
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层:

  1. 结构层:可以读取到动画画布的宽高,帧数,背景色,时间,起始关键帧,结束帧等信息。

  2. asset:图片资源信息集合,这里放置的是 制作动画时引用的图片资源。

  3. layers:图层集合,这里可以获取到多少图层,每个图层的开始帧 结束帧等。

  4. 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": [] // 蒙层集合
}
1
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
}
1
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);
};
1
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
    }
}
1
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 表达式标记 
1
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
  }]
}]
1
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;
        }
    }
}
1
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

# 总结

网上大佬整理的流程图

参考:

lottie系列文章

Lottie - 轻松实现复杂的动画效果

Lottie 动画原理剖析

Lottie Web SVG 动画源码浅析以及对应原生实现

Lottie原理与源码解析