「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言
大厂案例
今年 618 互动的动效可以大致划分为两类,星秀猫主形象和入口引导动效。
在项目开始之前,我们就开始沟通整体的技术选型和美术创作工具,也就是创作选型,我们沟通了几种动效选型。一种是帧动画,以逐帧的方式来播放动画;第二种是骨骼动画,基于一套基础的骨架,来播放动画;然后是 Lottie 动画,设计师可以通过 Adobe After Effects(以下简称 AE) 设计动画然后通过插件导出即可渲染出动效。
由于骨骼动画天然适合这类有一个主体形象的动画,我们选择了2D 骨骼动画 Spine 来制作星秀猫主形象,通过网格自由变形和蒙皮技术在视觉上呈现“3D轴”的偏转。正是在 Spine 强大动画创作的支持下,星秀猫才有了“3D化”的动画化表现力。
而入口引导动效,存在量大且需要频繁调整的诉求,而 AE 是设计师们比较熟悉的软件,使用 AE 软件制作 Lottie 动画设计师们都比较愿意,因此我们采用 Lottie 来制作入口动效,制作简单的同时,时间较长的动画场景下相对 apng 和 gif 所需资源也更小。
前置知识
在看动画方案之前,我们先来了解一下位图和矢量图的概念。
- 矢量图是使用直线和曲线来描述的图形,构成这些图形的元素是一些点、线、矩形、多边形、圆和弧线等,它们都是通过数学公式计算获得的,具有编辑后不失真的特点。
- 位图是由被称作像素(图片元素)的单个点组成的,放大会失真。
方案介绍
那了解完矢量图和位图,我们来看看前端要实现一个动画效果,有多少种方案可以选择呢? 常见的动画方案有Gif动画、png序列帧、SVG动画,我们来看看他们有什么区别。
Gif动画
图像互换格式(GIF,Graphics Interchange Format)是一种位图图形文件格式,以8位色(即256种颜色)重现真彩色的图像。它实际上是一种压缩文档,采用LZW压缩算法进行编码,有效地减少了图像文件在网络上传输的时间。适用于小动画如下拉loading、小动态logo、直播小礼物。
- 优点
- 使用简单,只需要引入一张图片
- 还原度高
- 缺点
- 图片容易过大,需要权衡动画帧数和文件大小
- Gif图是位图,放大会失真
png序列帧
帧动画是一种常见的动画形式,通过系列图片的连续播放来达到动画效果。具体实现是用css keyframes操作每一帧需要展示的图片,当然也可以将图片合并成精灵图(Sprites Map),可参考AlloyTeam的这个方案,使用 gka 一键生成帧动画。
- 优点
- 可插入多帧,从而实现动画效果
- 缺点
- 每一帧都是一张图片,占比较大的体积
- png是位图,放大会失真
SVG动画
SVG 意为可缩放矢量图形(Scalable Vector Graphics)。 在 SVG 中,要实现一个动画效果,可以使用 CSS,JS,或者直接使用 SVG 中自带的 animate 元素添加动画。 开发示例可参考SVG 动画标签,图形渐变,路径动画,线条动画。
- 优点
- 矢量图,不失真
- 缺点
- 对初学者不友好,上手成本高,需要先了解SVG的API
更高效方案
了解了上面的常用方案后,我们可以看到,这些普通方案要么适配会有失真问题,要么上手有难度。我们更希望找到简单、高效、性能好、还原度高的动画方案。
那有没有更高效的方案呢? 答案是肯定的,下面有请主角登场。
一号嘉宾:SVGA
SVGA 是一种跨平台的开源动画格式,同时兼容 iOS / Android / Web。 SVGA Converter 可以将 After Effects 动画导出成 .SVGA 文件,供 SVGA Player 在各平台播放。 SVGA Player 支持在 iOS / Android / Web / ReactNative / LayaBox 等平台、游戏引擎播放。
更多介绍:SVGA官网
预览svga文件的动画效果:SVGA工具
各端播放器集成指南:SVGA集成
SVGA 动画库源码思路
通过阅读源码,我们捋一下他实现的思路
- 一帧一帧
- 通过设置帧率,来生成一个配置文件,使得每一帧都有一个配置,每一帧都是关键帧,通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就得到很大提升。并且不用解析高阶插值(比如二次线性方程,贝塞尔曲线方程)
优缺点
- 优点
- 使用简单,性能较好
- 可动态添加或替换动画中的元素
- 解析、播放器的库比lottie的精简许多(gzip前57KB),导出的文件也较小
- 缺点
- 动画是压缩产物,不支持二次编辑
- 动画帧数低会造成视觉上的卡顿
- 支持AE特性少
- 内存占用比lottie稍高
- 不支持复杂的矢量形状图层、不支持AE自带的渐变、生成、描边、擦除等
二号嘉宾:Lottie
Lottie是一个用于Android,iOS,Web和Windows的库。 设计师制作好动画,并且利用Bodymovin插件导出JSON文件。 前端直接引用lottie-web库即可,原理就是用JS操作Svg API,当然也可以指定为canvas。 前端完全不需要关心动画的过程,JSON文件里有每一帧动画的信息,而库会帮我们执行每一帧。
更多介绍:lottie官方文档
设计师安装插件:AE安装Bodymovin教程
Lottie 动画库源码思路
通过阅读源码,我们捋一下他实现的思路
- 一层一层
- 完全按照设计工具的设计思路来进行还原,将动画脚本导出并解析。动画脚本非常的轻量。
- 将所有的动画拆成多个层级,每个层级layer都有一个动画配置,播放时解析多个layer的配置,并给每个layer做相应的动画。也达到了图片可以复用。当需要解析高阶插值(比如二次线性方程,贝塞尔曲线方程)时,性能相对而言差一点。
优缺点
- 优点
- 开发成本低,设计师导出JSON后,开发同学只需引用文件即可
- 支持服务端URL创建,服务端可以配置JSON文件,随时替换动画
- 性能提升,替换原使用帧图完成的动画,节省客户端空间和内存
- 动画比例不会被拉伸,播放较为流畅
- 缺点
- 对某些AE属性不支持,具体可对照表
- 对缓动曲线的解析会占用非常多的内存
- 各平台效果的支持都不是很稳定,容易出 Bug
动画组件封装
Talk is cheap. Show me the code.
我们团队在项目中是如何落地实践更高效的动画方案的呢?来看看组件封装的Demo吧! 小伙伴们可以从中学习组件的思路,并且尝试着在自己项目中落地,推动设计部门的UI小姐姐,使用高效动画方案,解放我们的生产力。
以下代码均采用【Umi+React Hook】实现,仅供参考
Lottie组件代码
<h2>--------- 循环播放 ---------</h2>
<XLottie url="https://assets9.lottiefiles.com/datafiles/gUENLc1262ccKIO/data.json" size={80}></XLottie>
<h2>--------- 播放片段 ---------</h2>
<XLottie url="./anime/lottie/big.json" width={300} height={150} loop={false} startFrame={300} endFrame={500}></XLottie>
<h2>----- 0.5倍速 和 2倍速 -----</h2>
<XLottie url="./anime/lottie/loading.json" size={80} autoplay={true} loop={true} speed={0.5}></XLottie>
<XLottie url="./anime/lottie/loading.json" size={80} autoplay={true} loop={true} speed={2}></XLottie>
import React, { useEffect } from 'react';
import lottie from 'lottie-web';
/**
* @param url lottie的json地址
* @param size 尺寸
* @param width 宽度
* @param height 高度
* @param autoplay 是否自动播放
* @param loop 是否循环
* @param startFrame 播放片段(开始帧)
* @param endFrame 播放片段(结束帧)
* @param speed 播放速度,默认为1倍
*/
export type XLottieProps = {
url: string;
size?: string | number | undefined;
width?: string | number | undefined;
height?: string | number | undefined;
autoplay?: boolean | undefined;
loop?: boolean | undefined;
startFrame?: number;
endFrame?: number;
speed?: number;
};
export default function XLottie(props: XLottieProps) {
const { size, width, height, url, loop, autoplay, startFrame, endFrame, speed = 1 } = props;
useEffect(() => {
const element: HTMLElement = document.getElementById(id) as HTMLElement;
const animation = lottie.loadAnimation({
container: element,
renderer: 'svg',
loop: loop ?? true,
autoplay: autoplay ?? true,
path: url,
});
animation.setSpeed(speed);
animation.addEventListener('data_ready', () => {
if (startFrame !== undefined && endFrame !== undefined) {
animation.playSegments([startFrame, endFrame], true);
} else if (startFrame) {
animation.goToAndPlay(startFrame, true);
}
});
return () => {
animation.destroy();
};
}, []);
const id = `canvas_${randomNum(0, 1000)}_${new Date().getMilliseconds()}`;
const style = {
width: `${width || size}px`,
height: `${height || size}px`,
};
return <div id={id} style={style}></div>;
}
function randomNum(min: number, max: number) {
const Range = max - min;
const Random = Math.random();
return Math.round(Random * Range) + min;
}
SVGA组件代码
import React, { useEffect } from 'react';
import SVGA from 'svga.lite';
// SVGA轻量版: https://github.com/svga/SVGAPlayer-Web-Lite
/**
* XSvga props
* @param url svga的资源地址
* @param size 尺寸
* @param width 宽度
* @param height 高度
* @param loop 循环次数,默认一直循环(0)
* @param startFrame 播放片段(开始帧)
* @param endFrame 播放片段(结束帧)
*/
export interface XSvgaProps {
url: string;
size?: string | number | undefined;
width?: string | number | undefined;
height?: string | number | undefined;
startFrame?: number;
endFrame?: number;
loop?: number;
}
export default function XSvga(props: XSvgaProps) {
const { url, size, width, height, startFrame, endFrame, loop } = props;
useEffect(() => {
const downloader = new SVGA.Downloader();
// 禁止WebWorker线程解析
const parser = new SVGA.Parser({ disableWorker: true });
// #canvas 是 HTMLCanvasElement
const player = new SVGA.Player(`#${id}`);
(async () => {
const fileData = await downloader.get(url);
const svgaData = await parser.do(fileData);
player.set({ loop: loop ?? 0, cacheFrames: true, startFrame, endFrame, intersectionObserverRender: false });
await player.mount(svgaData);
// 开始播放动画
player.start();
// 暂停播放动画
// player.pause()
// 停止播放动画
// player.stop()
// 清空动画
// player.clear()
})();
return () => {
downloader.destroy();
parser.destroy();
player.destroy();
};
}, []);
const id = `canvas_${randomNum(1, 1000)}${new Date().getMilliseconds()}`;
const style = {
width: `${size || width}px`,
height: `${size || height}px`,
};
return <canvas id={id} style={style} />;
}
function randomNum(min: number, max: number) {
const Range = max - min;
const Random = Math.random();
return Math.round(Random * Range) + min;
}
方案对比
了解到更高效的方案后,我们来看看哪个动画库更加优秀,根据他们的特性和团队项目实践效果,得到下面的表格~
| 动画库 | SVGA | Lottie | ||
|---|---|---|---|---|
| 社区生态 | 600+ Star | 24k+ Star | ✅ | |
| 开发成本 | 低 | ✅ | 低 | ✅ |
| 支持平台 | Android/iOS/H5 | ✅ | Android/iOS/H5 | ✅ |
| 资源产物 | 压缩文件(不支持二次编辑) | JSON(支持二次编辑) | ✅ | |
| 动态替换元素 | 支持 | ✅ | 不支持 | |
| 循环指定某帧 | 支持 | ✅ | 支持 | ✅ |
| 占用内存 | 少 | ✅ | 少 | ✅ |
| 支持AE动效 | 较少 | 较多 | ✅ | |
| 适合场景 | 直播、光效 | ✅ | 渐变、矢量、图标、微交互 | ✅ |
我们可以看到,Lottie整体上更胜一筹,当然还要看具体的使用场景。
- 在平面转换、移动、帧率较高、特效较多的场景,推荐使用Lottie。
- 在直播送礼物的时候展示一些浮夸的光效,3D转换的效果,推荐使用SVGA。
注意
下面引用字节跳动飞书团队的case,给大家分享下使用时需要注意的细节点
不需要动画时,及时卸载 Lottie 动画组件
飞书出现过偶现 CPU 升高的异常案例,经过排查定位到是 Lottie 动画没有卸载引起的 CPU 升高。页面中已经没有动画,但 Lottie 一直在调用 requestAnimationFrame,导致在没有任何操作的情况下,CPU 占用升高至 2%-5% 左右,一般情况在没有任何操作的情况下,cpu 占用 0.1% ~0.2%。
Lottie 动画调用 react-lottie 组件,组件在 componentWillUnmount 时,会销毁该动画实例。
飞书中 CPU 升高的时候,发现 Lottie 动画中有动画实例尚未销毁,导致会不停的调用 requestAnimationFrame,导致异常的动画是局部加载动画。
飞书中用到局部加载的动画的模块有主端(切换会话、联系人页面 、新添加的联系人、机器人、外部联系人、onCall、升级提示弹窗、添加联系搜索、发送云盘文件弹窗、Pin 列表、Docs Webview 加载动画、切换租户)、日历、应用中心等。
经过定位发现,应用中心里用到了局部加载动画, 在不需要动画的时候,没有卸载组件,只是通过 CSS 来隐藏了组件,导致没有销毁 Lottie 动画实例,requestAnimationFrame 会一直执行,代码如下:
// AppHome.js
<div className={!isLoaded ? 'app-home-loadingImg' : 'display-none'}><PartialLoading /></div>
解决方案:不需要 Lottie 动画的时候,卸载 Lottie 动画组件。
// AppHome.js
{
!!isLoading && (
<div className='app-home-loadingImg'>
<PartialLoading />
</div>
)
}
结尾
- 如果项目中需要一些动画,我们优先考虑高效的动画库,兼容性和效果都不错,可以提高团队开发效率。
- 作为工程师,跨部门沟通也是非常重要的职场技能之一。我们前端可以通过调研一些动画方案,然后去推动设计部门形成一个动画设计标准,以及对动画转换工具的使用,形成开发闭环。
- 作为工程师,我们要深刻意识到软件编程领域没有银弹,只有合适和不合适。我们可以根据每个团队的开发习惯和产品定位选择对应的技术栈,提高ROI,为业务产生更多价值!
- 📃创作不易,如果我的文章对你有帮助,辛苦大佬们点个赞👍🏻,支持我一下~
- 📌如果有错漏,欢迎大佬们指正~
- 👏欢迎转载分享,转载请注明出处,谢谢~