Web 帧动画解决方案 |
您所在的位置:网站首页 › lottie资源 › Web 帧动画解决方案 |
Banner 图片来源:aescripts.com/bodymovin/ 本文首发 剖析 lottie-web 动画实现原理,如果看过的同学可以直接看我的其他动画文章: Web 帧动画解决方案 - APNG原理与实现 Web 帧动画解决方案 - lottie-web源码剖析 Web 帧动画解决方案 - WebGl 实现透明视频绘制 前言Lottie 是一个复杂帧动画的解决方案,它提供了一套从设计师使用 AE(Adobe After Effects)到各端开发者实现动画的工具流。在设计师通过 AE 完成动画后,可以使用 AE 的扩展程序 Bodymovin 导出一份 JSON 格式的动画数据,然后开发同学可以通过 Lottie 将生成的 JSON 数据渲染成动画。 1、如何实现一个 Lottie 动画更多动画 JSON 模板可以查看 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、高 200 v:Bodymovin 插件版本号 4.5.4 fr:帧率 30fps ip 和 op:开始帧 0、结束帧 180 assets:静态资源信息(如图片) layers:图层信息(动画中的每一个图层以及动作信息) ddd:是否为 3d comps:合成图层其中 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 了。如下图所示: 接下来再看 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 方法来初始化动画。渲染器初始化流程如下: 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 帧,对应如下: 那么为什么在初始化渲染图层时,需要初始化 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 fpsrequestAnimationFrame 在正常情况下能达到 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 文件非常大,注意和设计师提前进行沟通。 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |