图片来源: https://aescripts.com/bodymovin/本文做者:青舟javascript
Lottie 是一个复杂帧动画的解决方案,它提供了一套从设计师使用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师经过 AE 完成动画后,可使用 AE 的扩展程序 Bodymovin 导出一份 JSON 格式的动画数据,而后开发同窗能够经过 Lottie 将生成的 JSON 数据渲染成动画。css
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/html
笔者本身制做了 Lottie Demo -> 点我预览前端
scale
属性值从 100% 变到 50%。scale
属性值从 50% 变到 100%,完成动画。经过 Bodymovin 插件导出 JSON 数据结构以下图所示:java
详细 JSON 信息能够经过 Demo 查看,JSON 信息命名比较简洁,第一次看可能难以理解。接下来结合笔者本身制做的 Demo 进行解读。css3
左侧为使用 AE 新建动画合成须要填入的信息,和右面第一层 JSON 信息对应以下:git
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 帧。github
理解 JSON 外层信息后,再来展开看下 JSON 中 layers
的具体信息,首先 demo 制做动画细节以下:web
主要是 3 个区域:json
对应上图动画制做信息,即可以对应到 JSON 中的 layers
了。以下图所示:
接下来再看 ks
(变化属性) 中的 s
展开,也就是缩放信息。
其中:
t
表明关键帧数s
表明变化前(图层为二维,因此第 3 个值 固定为 100)。e
表明变化后(图层为二维,因此第 3 个值 固定为 100)。前面简单理解了 JSON 的数据意义,那么 Lottie 是如何把 JSON 数据动起来的呢?接下来结合 Demo 的 Lottie 源码阅读,只会展现部分源码,重点是理清思路便可,不要执着源代码。
如下源码介绍主要分为 2 大部分:
如 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 三种渲染模式,通常使用第一种或第二种。
每一个渲染器均有各自的实现,复杂度也各有不一样,可是动画越复杂,其对性能的消耗也就越高,这些要看实际的情况再去判断。渲染器源码在 player/js/renderers/ 文件夹下,本文 Demo 只分析 SVG 渲染动画的实现。因为 3 种 Renderer 都是基于 BaseRenderer
类,因此下文中除了 SVGRenderer
也会出现 BaseRenderer
类的方法。
确认使用 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
实现细节。
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 帧,对应以下:
transform: scale(1); opacity: 1;
那么为何在初始化渲染图层时,须要初始化 transform
和 opacity
呢?这个问题会在 3.4 小节中进行回答。
在分析 Lottie 源码动画播放前,先来回忆下。笔者 Demo 的动画设置:
scale
属性值从 100% 变到 50%。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
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); }
前文提到的动画参数:
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
时间。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
采用系统时间间隔,保持最佳绘制效率,让动画可以有一个统一的刷新机制,从而节省系统资源,提升系统性能,改善视觉效果。
虽然咱们了解了 Lottie 的实现原理,可是在实际应用中也有一些优点和不足,要按照实际状况进行取舍。
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!