图片起源:https://aescripts.com/bodymovin/本文作者:青舟
前言
Lottie 是一个简单帧动画的解决方案,它提供了一套从设计师应用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师通过 AE 实现动画后,能够应用 AE 的扩大程序 Bodymovin 导出一份 JSON 格局的动画数据,而后开发同学能够通过 Lottie 将生成的 JSON 数据渲染成动画。
1、如何实现一个 Lottie 动画
- 设计师应用 AE 制作动画。
- 通过 Lottie 提供的 AE 插件 Bodymovin 把动画导出 JSON 数据文件。
- 加载 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 信息对应如下:
w
和h
: 宽 200、高 200v
:Bodymovin 插件版本号 4.5.4fr
:帧率 30fpsip
和op
:开始帧 0、完结帧 180assets
:动态资源信息(如图片)layers
:图层信息(动画中的每一个图层以及动作信息)ddd
:是否为 3dcomps
:合成图层
其中 fr
、ip
、op
在 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
三个事件期待被触发。因为能够多个动画并行,因而定义了全局的变量len
、registeredAnimations
等,用于判断和缓存已注册的动画实例。 - 接下来调用
animItem
实例的setParams
办法初始化动画参数,除了初始化loop
、autoplay
等参数外,最重要的是抉择渲染器。如下:
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
渲染第一帧,如果开发者配置了 autoplay
为 true
,则会间接调用 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
办法。
其余图层类型的渲染逻辑,如 Image
、Text
、Audio
等等,每一种元素的渲染逻辑都实现在源码 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() };},
利用 TransformPropertyFactory
对 transform
初始化,联合 Demo 第 0 帧,对应如下:
- 不透明度 100%
- 缩放 100%
transform: scale(1);opacity: 1;
那么为什么在初始化渲染图层时,须要初始化 transform
和 opacity
呢?这个问题会在 3.4 大节中进行答复。
3.4 Lottie 动画播放
在剖析 Lottie 源码动画播放前,先来回顾下。笔者 Demo 的动画设置:
- 0s 至 3s,
scale
属性值从 100% 变到 50%。 - 3s 至 6s,
scale
属性值从 50% 变到 100%。
如果依照这个设置,3s 进行一次扭转的话,那动画就过于僵硬了。因而设计师设置了帧率为 30fps ,意味着每隔 33.3ms 进行一次变动,使得动画不会过于生硬。那么如何实现这个变动,便是 3.3 大节提到的 transform
和 opacity
。
在 2.2 大节中提到的 5 个变动属性(锚点、地位、缩放、旋转、不透明度)。其中不透明度通过 CSS 的 opacity
来管制,其余 4 个(锚点、地位、缩放、旋转)则通过 transform
的 matrix
来管制。笔者的 Demo 中实际上初始值如下:
transform: matrix(1, 0, 0, 1, 100, 100);/* 上文的 transform: scale(1); 只是为了不便了解*/opacity: 1;
这是因为无论是旋转还是缩放等属性,实质上都是利用 transform
的 matrix()
办法实现的,因而 Lottie 对立应用 matrix
解决。平时开发者应用的相似于 transform: scale
这种表现形式,只是因为更容易了解,记忆与上手。 matrix
相干知识点能够学习张鑫旭老师的 了解CSS3 transform中的Matrix。
所以 Lottie 动画播放流程可临时小结为:
- 渲染图层,初始化所有图层的
transform
和opacity
- 依据帧率 30fps,计算每一帧(每隔 33.3ms )对应的
transform
和opacity
并批改 DOM
然而 Lottie 如何管制 30fps 的工夫距离呢?如果设计师设置 20fps or 40fps 怎么解决?能够通过 setTimeout
、setInterval
实现吗?带着这个问题看看源码是如何解决的,如何实现一个通用的解决方案。
Lottie 动画播放次要是应用 AnimationItem
实例的 play
办法。如果开发者配置了 autoplay
为 true
,则会在所有初始化工作筹备结束后(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
计算好以后对应的 transform
的 matrix
值,而后批改对应 DOM 元素上的 CSS 属性。这样通过 requestAnimationFrame
不停的计算帧数,再计算对应的 CSS 变动,在肯定的工夫内,便实现了动画。播放流程如下:
帧数计算这里须要时刻记住,在 Lottie 中,把 AE 设置的帧数作为一个计算单位,Lottie 并不是依据设计师设置的 30fps(每隔 33.3ms) 进行每一次变动,而是依据 requestAnimationFrame
的距离(每隔 16.7ms 左右)计算了更粗疏的变动,保障动画的晦涩运行。
没有通过 setTimeout
、setInterval
实现,是因为它们都有各自的毛病,这里就不开展了,大家自行查阅材料。requestAnimationFrame
采纳零碎工夫距离,放弃最佳绘制效率,让动画可能有一个对立的刷新机制,从而节俭系统资源,进步零碎性能,改善视觉效果。
4、总结
尽管咱们理解了 Lottie 的实现原理,然而在理论利用中也有一些劣势和有余,要依照理论状况进行取舍。
4.1 Lottie 的劣势
- 设计师通过 AE 制作动画,前端能够间接还原,不会呈现买家秀卖家秀的状况。
- SVG 是可伸缩的,任何分辨率下不会失真。
- JSON 文件,能够多端复用(Web、Android、iOS、React Native)。
- JSON 文件大小会比 GIF 以及 APNG 等文件小很多,性能也会更好。
4.2 Lottie 的有余
- Lottie-web 文件自身依然比拟大,未压缩大小为 513k,轻量版压缩后也有 144k,通过 Gzip 后,大小为39k。所以,须要留神 Lottie-web 的加载。
- 不必要的序列帧。Lottie 的次要动画思维是绘制某一个图层一直的扭转 CSS 属性,如果设计师偷懒用了一些插件实现的动画成果,可能会造成每一帧都是一张图,如下图所示,那就会造成这个 JSON 文件十分大,留神和设计师提前进行沟通。
- 局部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!