乐趣区

关于前端:剖析-lottieweb-动画实现原理

图片起源:https://aescripts.com/bodymovin/

本文作者:青舟

前言

Lottie 是一个简单帧动画的解决方案,它提供了一套从设计师应用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师通过 AE 实现动画后,能够应用 AE 的扩大程序 Bodymovin 导出一份 JSON 格局的动画数据,而后开发同学能够通过 Lottie 将生成的 JSON 数据渲染成动画。

1、如何实现一个 Lottie 动画

  1. 设计师应用 AE 制作动画。
  2. 通过 Lottie 提供的 AE 插件 Bodymovin 把动画导出 JSON 数据文件。
  3. 加载 Lottie 库联合 JSON 文件和上面几行代码就能够实现一个 Lottie 动画。
import lottie from 'lottie-web';
import animationJsonData from 'xxx-demo.json';  // json 文件

const lot = lottie.loadAnimation({container: document.getElementById('lottie'), 
   renderer: 'svg',
   loop: true,
   autoplay: false,
   animationData: animationJsonData,
 });

// 开始播放动画
lot.play();

更多动画 JSON 模板能够查看 https://lottiefiles.com/

2、解读 JSON 文件数据格式

笔者本人制作了 Lottie Demo -> 点我预览

  • 0s 至 3s,scale 属性值从 100% 变到 50%。
  • 3s 至 6s,scale 属性值从 50% 变到 100%,实现动画。

通过 Bodymovin 插件导出 JSON 数据结构如下图所示:

具体 JSON 信息能够通过 Demo 查看,JSON 信息命名比拟简洁,第一次看可能难以了解。接下来联合笔者本人制作的 Demo 进行解读。

2.1 全局信息

左侧为应用 AE 新建动画合成须要填入的信息,和右面第一层 JSON 信息对应如下:

  • wh:宽 200、高 200
  • v:Bodymovin 插件版本号 4.5.4
  • fr:帧率 30fps
  • ipop:开始帧 0、完结帧 180
  • assets:动态资源信息(如图片)
  • layers:图层信息(动画中的每一个图层以及动作信息)
  • ddd:是否为 3d
  • comps:合成图层

其中 fripop 在 Lottie 动画过程中尤为重要,后面提到咱们的动画 Demo 是 0 – 6s,然而 Lottie 是以帧率计算动画工夫的。Demo 中设置的帧率为 30fps,那么 0 – 6s 也就等同于 0 – 180 帧。

2.2 图层相干信息

了解 JSON 外层信息后,再来开展看下 JSON 中 layers 的具体信息,首先 demo 制作动画细节如下:

次要是 3 个区域:

  • 内容区域,蕴含形态图层的大小、地位、圆度等信息。
  • 变动区域,蕴含 5 个变动属性(锚点、地位、缩放、旋转、不透明度)。
  • 缩放 3 帧(图中绿色区域),在 0 帧、90 帧、180 帧对缩放属性进行了批改,其中图中所示为第 90 帧,图层缩放至 50%。

对应上图动画制作信息,便能够对应到 JSON 中的 layers 了。如下图所示:

2.3 属性变动信息

接下来再看 ks(变动属性)中的 s 开展,也就是缩放信息。

其中:

  • t 代表关键帧数
  • s 代表变动前(图层为二维,所以第 3 个值 固定为 100)。
  • e 代表变动后(图层为二维,所以第 3 个值 固定为 100)。

3、Lottie 如何把 JSON 数据动起来

后面简略了解了 JSON 的数据意义,那么 Lottie 是如何把 JSON 数据动起来的呢?接下来联合 Demo 的 Lottie 源码浏览,只会展现局部源码,重点是理清思路即可,不要执着源代码。

以下源码介绍次要分为 2 大部分:

  • 动画初始化(3.1 大节 – 3.3 大节)
  • 动画播放(3.4 大节)

3.1 初始化渲染器

如 Demo 所示,Lottie 通过 loadAnimation 办法来初始化动画。渲染器初始化流程如下:

function loadAnimation(params){
    // 生成以后动画实例
    var animItem = new AnimationItem();
    // 注册动画
    setupAnimation(animItem, null);
    // 初始化动画实例参数
    animItem.setParams(params);
    return animItem;
}

function setupAnimation(animItem, element) {
    // 监听事件
    animItem.addEventListener('destroy', removeElement);
    animItem.addEventListener('_active', addPlayingCount);
    animItem.addEventListener('_idle', subtractPlayingCount);
    // 注册动画
    registeredAnimations.push({elem: element, animation:animItem});
    len += 1;
}
  • AnimationItem 这个类是 Lottie 动画的基类,loadAnimation 办法会学生成一个 AnimationItem 实例并返回,开发者应用的 配置参数和办法 都是来自于这个类。
  • 生成 animItem 实例后,调用 setupAnimation 办法。这个办法首先监听了 destroy_active_idle 三个事件期待被触发。因为能够多个动画并行,因而定义了全局的变量 lenregisteredAnimations 等,用于判断和缓存已注册的动画实例。
  • 接下来调用 animItem 实例的 setParams 办法初始化动画参数,除了初始化 loopautoplay 等参数外,最重要的是抉择渲染器。如下:
AnimationItem.prototype.setParams = function(params) {
    // 依据开发者配置抉择渲染器
    switch(animType) {
        case 'canvas':
            this.renderer = new CanvasRenderer(this, params.rendererSettings);
            break;
        case 'svg':
            this.renderer = new SVGRenderer(this, params.rendererSettings);
            break;
        default:
            // html 类型
            this.renderer = new HybridRenderer(this, params.rendererSettings);
            break;
    }

    // 渲染器初始化参数
    if (params.animationData) {this.configAnimation(params.animationData);
    }
}

Lottie 提供了 SVG、Canvas 和 HTML 三种渲染模式,个别应用第一种或第二种。

  • SVG 渲染器反对的个性最多,也是应用最多的渲染形式。并且 SVG 是可伸缩的,任何分辨率下不会失真。
  • Canvas 渲染器就是依据动画的数据将每一帧的对象一直重绘进去。
  • HTML 渲染器受限于其性能,反对的个性起码,只能做一些很简略的图形或者文字,也不反对滤镜成果。

每个渲染器均有各自的实现,复杂度也各有不同,然而动画越简单,其对性能的耗费也就越高,这些要看理论的情况再去判断。渲染器源码在 player/js/renderers/ 文件夹下,本文 Demo 只剖析 SVG 渲染动画的实现。因为 3 种 Renderer 都是基于 BaseRenderer 类,所以下文中除了 SVGRenderer 也会呈现 BaseRenderer 类的办法。

3.2 初始化动画属性,加载动态资源

确认应用 SVG 渲染器后,调用 configAnimation 办法初始化渲染器。

AnimationItem.prototype.configAnimation = function (animData) {if(!this.renderer) {return;}
    
    // 总帧数
    this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip);
    this.firstFrame = Math.round(this.animationData.ip);
    
    // 渲染器初始化参数
    this.renderer.configAnimation(animData);

    // 帧率
    this.frameRate = this.animationData.fr;
    this.frameMult = this.animationData.fr / 1000;
    this.trigger('config_ready');
    
    // 加载动态资源
    this.preloadImages();
    this.loadSegments();
    this.updaFrameModifier();
    
    // 期待动态资源加载结束
    this.waitForFontsLoaded();};

在这个办法中将会初始化更多动画对象的属性,比方总帧数 totalFrames、帧率 frameMult 等。而后加载一些其余资源,比方图像、字体等。如下图所示:

同时在 waitForFontsLoaded 办法中期待动态资源加载结束,加载结束后便会调用 SVG 渲染器的 initItems 办法绘制动画图层,也就是将动画绘制进去。

AnimationItem.prototype.waitForFontsLoaded = function(){if(!this.renderer) {return;}
    // 查看加载结束
    this.checkLoaded();}

AnimationItem.prototype.checkLoaded = function () {
    this.isLoaded = true;

    // 初始化所有元素
    this.renderer.initItems();
    setTimeout(function() {this.trigger('DOMLoaded');
    }.bind(this), 0);

    // 渲染第一帧
    this.gotoFrame();
    
    // 自动播放
    if(this.autoplay){this.play();
    }
};

checkLoaded 办法中能够看到,通过 initItems 初始化所有元素后,便通过 gotoFrame 渲染第一帧,如果开发者配置了 autoplaytrue,则会间接调用 play 办法播放。这里有个印象就好,会在前面具体讲。接下来还是先看 initItems 实现细节。

3.3 绘制动画初始图层

initItems 办法次要是调用 buildAllItems 创立所有图层。buildItem 办法又会调用 createItem 确定具体图层类型,这里的办法源码中拆分较细,本文只保留了 createItem 办法,其余感兴趣能够查看源码细节。

在制作动画时,设计师操作的图层元素有很多种,比方图片、形态、文字等等。所以 layers 中每个图层会有一个字段 ty 来辨别。联合 createItem 办法来看,一共有以下 8 中类型。

BaseRenderer.prototype.createItem = function(layer) {
    // 依据图层类型,创立相应的 svg 元素类的实例
    switch(layer.ty){
        case 0:
            // 合成
            return this.createComp(layer);
        case 1:
            // 固态
            return this.createSolid(layer);
        case 2:
            // 图片
            return this.createImage(layer);
        case 3:
            // 兜底空元素
            return this.createNull(layer);
        case 4:
            // 形态
            return this.createShape(layer);
        case 5:
            // 文字
            return this.createText(layer);
        case 6:
            // 音频
            return this.createAudio(layer);
        case 13:
            // 摄像机
            return this.createCamera(layer);
    }
    return this.createNull(layer);
};

因为笔者以及大多数开发者,都不是业余的 AE 玩家,因而不用不过纠结每种类型是什么,理清次要思路即可。联合笔者的 Demo,只有一个图层,并且图层的 ty 为 4。是一个 Shape 形态图层,因而在初始化图层过程中只会执行 createShape 办法。

其余图层类型的渲染逻辑,如 ImageTextAudio 等等,每一种元素的渲染逻辑都实现在源码 player/js/elements/ 文件夹下,具体实现逻辑这里就不进行开展了,感兴趣的同学自行查看。

接下来便是执行 createShape 办法,初始化元素相干属性。

除了一些细节的初始化办法,其中值得注意的是 initTransform 办法。

initTransform: function() {
    this.finalTransform = {
        mProp: this.data.ks
            ? TransformPropertyFactory.getTransformProperty(this, this.data.ks, this)
            : {o:0},
        _matMdf: false,
        _opMdf: false,
        mat: new Matrix()};
},

利用 TransformPropertyFactorytransform 初始化,联合 Demo 第 0 帧,对应如下:

  • 不透明度 100%
  • 缩放 100%
transform: scale(1);
opacity: 1;

那么为什么在初始化渲染图层时,须要初始化 transformopacity 呢?这个问题会在 3.4 大节中进行答复。

3.4 Lottie 动画播放

在剖析 Lottie 源码动画播放前,先来回顾下。笔者 Demo 的动画设置:

  • 0s 至 3s,scale 属性值从 100% 变到 50%。
  • 3s 至 6s,scale 属性值从 50% 变到 100%。

如果依照这个设置,3s 进行一次扭转的话,那动画就过于僵硬了。因而设计师设置了帧率为 30fps,意味着每隔 33.3ms 进行一次 变动 ,使得动画不会过于生硬。那么如何实现这个 变动,便是 3.3 大节提到的 transformopacity

在 2.2 大节中提到的 5 个变动属性(锚点、地位、缩放、旋转、不透明度)。其中不透明度通过 CSS 的 opacity 来管制,其余 4 个(锚点、地位、缩放、旋转)则通过 transformmatrix 来管制。笔者的 Demo 中实际上初始值如下:

transform: matrix(1, 0, 0, 1, 100, 100);
/* 上文的 transform: scale(1); 只是为了不便了解 */
opacity: 1;

这是因为无论是旋转还是缩放等属性,实质上都是利用 transformmatrix() 办法实现的,因而 Lottie 对立应用 matrix 解决。平时开发者应用的相似于 transform: scale 这种表现形式,只是因为更容易了解,记忆与上手。matrix 相干知识点能够学习张鑫旭老师的 了解 CSS3 transform 中的 Matrix。

所以 Lottie 动画播放流程可 临时 小结为:

  1. 渲染图层,初始化所有图层的 transformopacity
  2. 依据帧率 30fps,计算每一帧(每隔 33.3ms)对应的 transformopacity 并批改 DOM

然而 Lottie 如何管制 30fps 的工夫距离呢?如果设计师设置 20fps or 40fps 怎么解决?能够通过 setTimeoutsetInterval 实现吗?带着这个问题看看源码是如何解决的,如何实现一个通用的解决方案。

Lottie 动画播放次要是应用 AnimationItem 实例的 play 办法。如果开发者配置了 autoplaytrue,则会在所有初始化工作筹备结束后(3.2 大节有提及),间接调用 play 办法播放。否则由开发者被动调用 play 办法播放。

接下来从 play 办法理解一下整个播放流程的细节:

AnimationItem.prototype.play = function (name) {this.trigger('_active');  
};

去掉多余代码,play 办法次要是触发了 _active 事件,这个 _active 事件便是在 3.1 大节初始化时注册的。

animItem.addEventListener('_active', addPlayingCount);

function addPlayingCount(){activate();
}

function activate(){
    // 触发第一帧渲染
    window.requestAnimationFrame(first);
}

触发后通过调用 requestAnimationFrame 办法,一直的调用 resume 办法来管制动画。

function first(nowTime){
    initTime = nowTime;
    // requestAnimationFrame 每次都进行计算批改 DOM
    window.requestAnimationFrame(resume);
}

前文提到的动画参数:

  • 开始帧为 0
  • 完结帧为 180
  • 帧率为 30 fps

requestAnimationFrame 在失常状况下能达到 60 fps(每隔 16.7ms 左右)。那么 Lottie 如何保障动画依照 30 fps(每隔 33.3ms)晦涩运行呢。这个时候咱们要转化下思维,设计师心愿依照每隔 33.3ms 去计算变动,那也能够通过 requestAnimationFrame 办法,每隔 16.7ms 去计算,也能够计算动画的变动。只不过计算的更粗疏而已,而且还会使得动画更晦涩,这样无论是 20fps or 40fps 都能够解决了,来看下源码是如何解决的。

在一直调用的 resume 办法中,次要逻辑如下:

function resume(nowTime) {
    // 两次 requestAnimationFrame 间隔时间
    var elapsedTime = nowTime - initTime;

    // 下一次计算帧数 = 上一次执行的帧数 + 本次距离的帧数
    // frameModifier 为帧率(fr / 1000 = 0.03)
    var nextValue = this.currentRawFrame + value * this.frameModifier;
    
    this.setCurrentRawFrameValue(nextValue);
    
    initTime = nowTime;
    if(playingAnimationsNum && !_isFrozen) {window.requestAnimationFrame(resume);
    } else {_stopped = true;}
}

AnimationItem.prototype.setCurrentRawFrameValue = function(value){
    this.currentRawFrame = value;
    // 渲染以后帧
    this.renderFrame();};

resume 办法:

  • 首先会计算以后工夫和上次工夫的 diff 工夫。
  • 之后计算动画开始到当初的工夫的以后帧数。留神这里的 帧数 只是绝对 AE 设置的一个计算单位,能够有小数。
  • 最初通过 renderFrame() 办法更新以后帧对应的 DOM 变动。

举例说明:

假如上一帧为 70.25 帧,本次 requestAnimationFrame 间隔时间为 16.78 ms,那么:

以后帧数:70.25 +  16.78 * 0.03 =  70.7534 帧

因为 70.7534 帧在 Demo 中的 0 – 90 帧动画范畴内,因而帧比例(代表动画运行工夫百分比)的计算如下:

帧比例:70.7534 / 90 = 0.786148889

0 – 90 帧的动画为图层从 100% 缩放至 50%,因为仅计算 50% 的变动,所以缩放到如下:

缩放比例:100 -(50 * 0.781666)= 60.69255555%

对应计算代码在 TransformPropertyFactory 类中:

// 计算百分比
perc = fnc((frameNum - keyTime) / (nextKeyTime - keyTime));
endValue = nextKeyData.s || keyData.e;
// 计算值
keyValue = keyData.s[i] + (endValue[i] - keyData.s[i]) * perc;

其中 fnc 为计算函数,如果设置了贝塞尔静止曲线函数,那么 fnc 也会相应批改计算规定。以后 Demo 为了不便了解,采纳的是线性变动。具体源码感兴趣的同学能够自行查看。

计算好以后 scale 的值后,再利用 TransformPropertyFactory 计算好以后对应的 transformmatrix 值,而后批改对应 DOM 元素上的 CSS 属性。这样通过 requestAnimationFrame 不停的计算帧数,再计算对应的 CSS 变动,在肯定的工夫内,便实现了动画。播放流程如下:

帧数计算这里须要时刻记住,在 Lottie 中,把 AE 设置的帧数作为一个计算单位,Lottie 并不是依据设计师设置的 30fps(每隔 33.3ms)进行每一次变动,而是依据 requestAnimationFrame 的距离(每隔 16.7ms 左右)计算了更粗疏的变动,保障动画的晦涩运行。

没有通过 setTimeoutsetInterval 实现,是因为它们都有各自的毛病,这里就不开展了,大家自行查阅材料。requestAnimationFrame 采纳零碎工夫距离,放弃最佳绘制效率,让动画可能有一个对立的刷新机制,从而节俭系统资源,进步零碎性能,改善视觉效果。

4、总结

尽管咱们理解了 Lottie 的实现原理,然而在理论利用中也有一些劣势和有余,要依照理论状况进行取舍。

4.1 Lottie 的劣势

  1. 设计师通过 AE 制作动画,前端能够间接还原,不会呈现买家秀卖家秀的状况。
  2. SVG 是可伸缩的,任何分辨率下不会失真。
  3. JSON 文件,能够多端复用(Web、Android、iOS、React Native)。
  4. JSON 文件大小会比 GIF 以及 APNG 等文件小很多,性能也会更好。

4.2 Lottie 的有余

  1. Lottie-web 文件自身依然比拟大,未压缩大小为 513k,轻量版压缩后也有 144k,通过 Gzip 后,大小为 39k。所以,须要留神 Lottie-web 的加载。
  2. 不必要的序列帧。Lottie 的次要动画思维是绘制某一个图层一直的扭转 CSS 属性,如果设计师偷懒用了一些插件实现的动画成果,可能会造成每一帧都是一张图,如下图所示,那就会造成这个 JSON 文件十分大,留神和设计师提前进行沟通。

  1. 局部 AE 特效不反对。有大量的 AE 动画成果,Lottie 无奈实现,有些是因为性能问题,有些是没有做,留神和设计师提前沟通,点我查看。

5、参考资料

  • https://github.com/airbnb/lot…
  • http://airbnb.io/lottie/#/
  • 了解 CSS3 transform 中的 Matrix
  • window.requestAnimationFrame

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版