掰掰 Lottie
前言
之前做运营活动的时候,写了一个比较有趣的打开盲盒的交互动画,流程如下:一个挣扎的未打开的盲盒,点击出现确认弹窗,确认后集齐的5张卡片会飞入盲盒,盲盒开启弹出礼品。怎么实现的呢?其中挣扎的效果是使用的CSS3做的一个呼吸加抖动的 Animation,飞入的效果是使用JS获取到盲盒的中心点坐标并设置弹窗只展示5张卡片和要改变后样式,盲盒开启的过程则是一个 Lottie 动画。
如果想要在页面中实现类似的动效,那大概有以下几个方案可以选用:
- CSS3动画:使用CSS3新增的属性和选择器来实现动画,也是最常见的前端动画方案
- 优点是实现简单,不需要太多的代码也可以实现较为复杂的动画效果
- 缺点是不可控。比较适用于对于元素做一些形变、位移、透明度变化等,比如:按钮呼吸,文本淡出等
- JavaScript动画:通过JavaScript脚本来实现动画效果
- 优点是非常灵活,可以交互,渲染动态数据等,可以配合CSS3做一些动画流程,比如开红包等
- 缺点是实现较为复杂,需要编写大量的代码,动画质量受限于开发水平
- GIF/APNG图片:通过引入动图来实现动画效果,APNG 相较于 GIF 支持更多的色彩,性能稍好
- 优点是没什么开发成本
- 缺点是循环播放动画不可控,存在较大的性能问题。适合做小图标,比如:loading 加载
- Lottie:本文的主角
- 优点是开发成本较低,可以实现复杂的动画效果,动画可控,性能较好
- 缺点在需要一个很会的UI设计,动画中动态渲染数据较为麻烦
前端做动画还存在许多方案例如:Canvas、 SVG、 SVGA 等,这里不做过多介绍大家可以自行了解
什么是Lottie
Lottie 是 Airbnb 开发的一款开源的动画库,它可以把 Adobe After Effects 制作的动画导出为交互式的矢量动画,可以在 iOS、Android 和 React Native 等移动平台上使用。它通过解析用Bodymovin导出为json的AE动画,在移动端和 Web 上进行了原生渲染。能够帮助开发者快速制作出精美的动画效果,而且还可以节省大量的开发时间。
Lottie如何使用
第一步
由动画设计同学使用AE实现动画效果后通过 Bodymovin 插件导出 json 文件 或者在Lottie动画资源网站如:Lottiefiles 找到现成的动画进行编辑后导出。
对于Bodymovin插件的安装与使用可以参考该文章AE 插件 Bodymovin介绍
在实现动画时要考虑各端对于AE效果的支持,具体可参考官方文档
第二步
开发同学安装 Lottie ,根据设计同学提供的 json 文件在项目中使用
安装 lottie-web
依赖,在项目中进行引用
npm install lottie-web
# 或使用pnpm
pnpm add lottie-web
import bodymovin from 'lottie-web'
通过以下几种方式初始化Lottie并挂载到页面中
使用Lottie的 loadAnimation
方法进行配置,其中挂载节点 container
和 json 文件路径 path
这两个是必须配置的
<template>
<div style="width:700px;height:390px;" id="bm01" />
</template>
<script>
const bm01 = document.getElementById('bm01')
bodymovin.loadAnimation({
container: bm01, // 挂载DOM
animType: 'svg', // 渲染类型
loop: true, // 是否循环播放
autoplay: true, // 自动播放
path: 'data1.json' // JSON文件路径
});
</script>
使用Lottie的 registerAnimation
方法进行注册,挂载节点以入参形式传入,json 文件路径需在节点中配置data-animation-path
属性
<template>
<div style="width:100%;height:100%;" id="bm02" data-animation-path="data2.json" />
</template>
<script>
var bm02 = document.getElementById('bm02')
bodymovin.registerAnimation(bm02)
</script>
使用 searchAnimations
方法进行注册,Lottie自行通过 document.querySelector
选择类名为 “lottie” 的节点进行挂载,json 文件路径需在节点中添加 data-animation-path
属性
<template>
<div style="width:100%;height:100%;" class="lottie" data-animation-path="data3.json" />
</template>
<script>
bodymovin.searchAnimations()
</script>
另外Lottie实例也提供了许多的 Method
和 Event
,可以控制动画播放、暂停、卸载等。更多API请移步官方文档
看懂JSON文件
其实 json 文件就是对于导出动画的具象描述
基本信息
{
"v": "5.7.1", // Bodymovin 插件版本号
"fr": 25, // 帧率
"ip": 0, // 开始帧
"op": 20, // 结束帧
"w": 700, // 宽
"h": 600, // 高
"nm": "测试文件", // 名称
"ddd": 0, // 是否为3D
"assets": [], // 静态资源
"layers": [], // 图层信息
"markers": [] // 标记
}
其中包括了一些基本信息:插件版本、画布的宽高、帧率、起始关键帧,结束帧等。从中我们不难看出这个测试动画 1000 / 25 * 20 = 800
播放一次时长为 800
毫秒,宽高比为 7 :6
assets静态资源
{
"assets": [
{
"id": "image_0", // 唯一标识
"w": 169, // 宽
"h": 172, // 高
"u": "images/", // 静态资源导出文件夹
"p": "img_0.png", // 文件路径
"e": 0 // 是否直接使用p作为路径
}
]
}
这里是制作动画时引用到的静态资源,使用时通过唯一标识“id”进行指定
关于获取静态资源的路径,遵从以下逻辑:
e
不为0
时直接以p
作为路径e
为0
时,如果初始化时配置了assetsPath
则使用其与p
进行拼接,若没配置则使用u
进行拼接作为路径
function getAssetsPath(assetData, assetsPath, originalPath) {
var path = '';
if (assetData.e) {
path = assetData.p;
} else if (assetsPath) {
path = assetsPath + assetData.p;
} else {
path = originalPath;
path += assetData.u ? assetData.u : '';
path += assetData.p;
}
return path;
}
layers图层
{
"layers": [
{
"ddd": 0, // 是否使用了3d
"ind": 1, // 索引
"ty": 2, // 图片图层
"nm": "图层2.png", // 名称
"cl": "png", // 图片后缀
"refId": "image_0", // 在assets中的id
"sr": 1,
"ks": { // 需要做的变化
"o": {}, // 不透明度
"r": {}, // 旋转
"p": {}, // 位置
"a": {}, // 锚点
"s": {} // 缩放
},
"ao": 0, // 自动
"ip": 4, // 开始帧
"op": 18, // 持续帧
"st": 0, // 开始时间
"bm": 0 // 混合模式
}
]
}
各个图层相关的信息,其中包括了对应元素的ORPAS变换,即为透明,旋转,位置,锚点、缩放 每一个变化的对象内部又包括了:起止时间,贝塞尔曲线的入参等描述
源码解析
以 registerAnimation
方法为例,看看Lottie的工作流程吧
function registerAnimation(element, animationData) {
if (!element) { // 没传入挂载DOM直接结束
return null;
}
var i = 0;
// 缓存注册的动画
while (i < len) {
if (registeredAnimations[i].elem === element && registeredAnimations[i].elem !== null) {
return registeredAnimations[i].animation;
}
i += 1;
}
var animItem = new AnimationItem();
setupAnimation(animItem, element); // 绑定自定义事件
animItem.setData(element, animationData); // 与 loadAnimation 的区别
return animItem;
}
- 首先对注册的动画进行缓存,由于一个页面可能存在多个Lottie动画,因此定义了全局的变量
len
用于记录。然后实例化AnimationItem
类 setupAnimation
方法绑定 destroy、 _active、 _idle 事件,当你调用Lottie实例上的 play、 pause、 destroy 等方法时会触发- 调用
setData
方法从 DOM 的属性中获取配置参数再调用setParams
,如果使用 loadAnimation 注册的话则直接调用setParams
方法 - 在
setParams
方法中会确定渲染方式并创建对应的渲染器,将配置参数挂载到实例上。支持的渲染器有 CanvasRenderer、 HybridRenderer、 SVGRenderer,官方推荐选用svg
AnimationItem.prototype.setParams = function (params) {
// 确定渲染方式并创建对应的渲染器
var animType = params.animType || 'svg';
var RendererClass = getRenderer(animType); // SVGRenderer
this.renderer = new RendererClass(this, params.rendererSettings);
this.imagePreloader.setCacheType(animType, this.renderer.globalData.defs);
this.renderer.setProjectInterface(this.projectInterface);
// 配置信息挂载到实例上
this.animType = animType;
this.autoplay = 'autoplay' in params ? params.autoplay : true;
this.name = params.name ? params.name : '';
// ...
dataManager.loadAnimation(params.path, this.configAnimation, this.onSetupError);
};
- 通过
createWorker
方法生成一个worker。后续执行assetLoader
方法通过Ajax获取 json 文件,请求完成后对 json 文件中进行检查和处理,由于 json 文件往往比较大这个过程放在worker中进行。完成后开始执行configAnimation
方法
function loadAnimation(path, onComplete, onError) {
setupWorker(); // 生成一个worker
var processId = createProcess(onComplete, onError);
workerInstance.postMessage({
type: 'loadAnimation',
path: path,
fullPath: window.location.origin + window.location.pathname,
id: processId
});
}
// 如果当前浏览器环境支持Worker,则创建Worker
function createWorker(fn) {
if (window.Worker && window.Blob && getWebWorker()) {
var blob = new Blob(['var _workerSelf = self; self.onmessage = ', fn.toString()], {
type: 'text/javascript'
});
var url = URL.createObjectURL(blob);
return new Worker(url);
}
}
- 在
configAnimation
方法中主要是初始化渲染器参数并等待所需资源加载完毕后,调用renderer上的initItems
方法初始化所有元素再调用renderFrame
方法进行绘制,若配置了 autoplay 则会自动调用play
方法播放动画
AnimationItem.prototype.configAnimation = function (animData) {
this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip);
this.firstFrame = Math.round(this.animationData.ip);
// 初始化渲染器参数
this.renderer.configAnimation(animData);
this.assets = this.animationData.assets;
this.frameRate = this.animationData.fr;
this.frameMult = this.animationData.fr / 1000;
this.renderer.searchExtraCompositions(animData.assets);
this.markers = markerParser(animData.markers || []);
this.trigger('config_ready');
// 加载静态资源
this.preloadImages();
this.loadSegments();
this.updaFrameModifier();
// 每20ms检查一次等待所有资源加载完毕
this.waitForFontsLoaded();
};
其他
Matrix矩阵
<g clip-path="url(#__lottie_element_737)" style="display: none;" transform="matrix(0.44115760922431946,-0.679323136806488,0.679323136806488,0.44115760922431946,52.165771484375,997.0018920898438)" opacity="1"><rect width="1920" height="1080" fill="#ffffff"></rect></g>
当使用 SVGRender
进行渲染时,观察 DOM 结构发现对于元素的变化都是使用CSS函数matrix进行描述的
总的来说使用matrix函数可以代替 transform
的以下属性:斜拉(skew)、缩放(scale)、旋转(rotate)、位移(translate),这些方法都是基于matrix函数的再封装,为了易于理解
更为详细的了解可以参考这篇博客
requestAnimationFrame
function resume(nowTime) {
var elapsedTime = nowTime - initTime; // 时间戳差值
var i;
for (i = 0; i < len; i += 1) {
registeredAnimations[i].animation.advanceTime(elapsedTime);
}
initTime = nowTime;
if (playingAnimationsNum && !_isFrozen) {
window.requestAnimationFrame(resume);
} else {
_stopped = true;
}
}
在 lottie-web
中动画的播放是通过不断的调用requestAnimationFrame进行每一次的绘制的,我们知道requestAnimationFrame的回调函数执行次数通常是每秒60次,可以理解为最终呈现的动画是60帧的。
那么如果设计师导出的动画不是60帧的怎么办,Lottie要怎么渲染呢?
通过上边源码我们可以发现,每次渲染都会去执行 advanceTime
方法,该方法会根据每次更新的时间差值进行计算,最终得到要渲染哪一帧;
举个例子:假如设计师导出的动画为 30 帧,一秒内让一个元素位移 1000px
。那么当第一次调用requestAnimationFrame后当前动画 elapsedTime
值为16.67ms,就应该渲染动画对应 16.67 / ( 1000 / 30 ) = 0.5
帧时的状态,也就是位移 1000 / 30 * 0.5 = 16.67px
,以此类推。
总结
Lottie虽好,但是想要在项目落地使用并达到预期效果,还是要和设计同学有比较充分的沟通。尽量避免三种情况:动画并不复杂(使用CSS3即可达到效果)、帧动画(考虑一下SVGA)、assets中图片素材太多(json文件太大),大家有什么问题也可以多多交流,比如:json文件的压缩处理、在 Lottie 动画中插入动态数据等
参考资料
转载自:https://juejin.cn/post/7189445206054273083