导读:在上一篇文章中,我们详细介绍了 Vision 动效平台的渲染引擎——Crab,并分享在复杂动效渲染场景下积累的实践经验和精彩案例。今天,我们将揭秘如何将「动效描述翻译为动效代码」——从 Lottie 导出 CSS/Animated 代码。
一、项目背景
在进行前端页面开发中,经常需要涉及到元素动效的开发,比如按钮的呼吸状态动效,弹窗的出现和消失动效等等,这些动效为用户在页面交互过程中获得良好的体验起到重要的作用。
要开发这些动效,一般的工作流程是由设计同学提供动效描述,然后研发同学按照参数实现对应平台的动效代码(如 Web 平台的 CSS 或 React Native 的 Animated),从而进行动效的还原。
1.1 元素动效开发的痛点
对于一些独立性较强或比较复杂的动效,可以直接使用 Lottie 来进行播放,但是一方面对于一些比较简单的动效需求,如果引入 Lottie 来进行播放,则 Lottie 带来的额外运行时包体积的成本相比于动效本身过高,另一方面,对于元素动效中常见的和业务逻辑或用户操作绑定的情况,直接使用 Lottie 有时反而会引入额外的开发成本。
在动效还原的过程中,研发需要面对设计师交付的各种不同格式的动效描述,可能是一句自然语言的描述,一个时间轴或者使用 AE 插件导出的文本描述等等,然后人肉将设计同学提供的这些动效描述翻译为动效代码,这个过程常常是一个重复性很强的工作,且耗时耗力,会带来不小的心智负担。
文本动效参数交付示例:
Total Dur: 1200ms
≡ 盒子.png ≡
- 缩放 -
Delay: 0ms
Dur: 267ms
Val: 0% ›› 189.6%
(0.33, 0, 0.67, 1)
- 缩放 -
Delay: 267ms
Dur: 500ms
Val: [189.6,189.6]%››[205.4,173.8]%
(0.33, 0, 0.83, 1)
- 缩放 -
Delay: 767ms
Dur: 67ms
Val: [205.4,173.8]%››[237,142.2]%
(0.17, 0, 0.83, 1)
- 缩放 -
Delay: 833ms
Dur: 100ms
Val: [237,142.2]%››[142.2,237]%
(0.17, 0, 0.83, 1)
- 缩放 -
Delay: 933ms
Dur: 167ms
Val: [142.2,237]%››[205.4,173.8]%
(0.17, 0, 0.83, 1)
- 缩放 -
Delay: 1100ms
Dur: 100ms
Val: [205.4,173.8]%››[189.6,189.6]%
(0.17, 0, 0.67, 1)
- 位置 -
Delay: 833ms
Dur: 100ms
Val: [380,957]››[380,848]
(0.33, 0, 0.67, 1)
- 位置 -
Delay: 933ms
Dur: 133ms
Val: [380,848]››[380,957]
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 267ms
Dur: 73ms
Val: 0° ››› -3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 340ms
Dur: 73ms
Val: -3° ››› 3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 413ms
Dur: 73ms
Val: 3° ››› -3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 487ms
Dur: 73ms
Val: -3° ››› 3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 560ms
Dur: 73ms
Val: 3° ››› -3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 633ms
Dur: 67ms
Val: -3° ››› 3°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 700ms
Dur: 67ms
Val: 3° ››› 0°
(0.33, 0, 0.67, 1)
Total Dur: 500ms
≡ 盖子_关.png ≡
- 位置 -
Delay: 0ms
Dur: 500ms
Val: [74,13]››[74,13]
No Change
- 旋转 -
Delay: 0ms
Dur: 28ms
Val: 0.75° ››› 0°
(0.33, 0.54, 0.83, 1)
- 旋转 -
Delay: 28ms
Dur: 72ms
Val: 0° ››› -2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 100ms
Dur: 72ms
Val: -2° ››› 2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 172ms
Dur: 72ms
Val: 2° ››› -2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 244ms
Dur: 72ms
Val: -2° ››› 2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 317ms
Dur: 72ms
Val: 2° ››› -2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 389ms
Dur: 72ms
Val: -2° ››› 2°
(0.33, 0, 0.67, 1)
- 旋转 -
Delay: 461ms
Dur: 39ms
Val: 2° ››› 0.75°
(0.33, 0, 0.67, 0.55)
(盒子.png是盖子_关.png父级)
Total Dur: 1633ms
≡ 盖子_开.png ≡
- 位置 -
Delay: 0ms
Dur: 1633ms
Val: [113,5]››[113,5]
Linear
(在第1633ms,切换 盖子_开.png和盒子.2.png)
Total Dur: 267ms
≡ 盒子.2.png ≡
- 缩放 -
Delay: 0ms
Dur: 267ms
Val: 189.6% ›› 0%(0.17, 0, 0.83, 1)
复制代码
表格动效参数交付示例:
要解决这个痛点,我们可以考虑将「从动效描述翻译为动效代码」的工作通过自动化的方式完成。而要实现这个自动化的流程,首先要解决的就是设计师提供的动效描述没有统一格式的问题。
最适合用作动效描述统一格式的方案就是 Lottie,Lottie 是一个基于 JSON 的动画文件格式,它可以使用 Bodymmovin 解析导出 Adobe After Effects 动画,并在移动设备上渲染它们。通过它,设计师可以创造和发布酷炫的动画,且无需工程师费心的手工重建动画效果。
它具有以下优点:
标准化:Lottie 的 JSON 格式中,每个属性的含义和数据类型都很明确,相比于自然语言的描述方式,更加清晰明确。
无感知:设计师在 AE 中完成动效的编辑后,可以直接使用 AE 的 BodyMovin 插件导出我们期望 Lottie 格式动效描述,导出过程不会为设计师引入额外的成本。
透明化:Lottie 的运行库是开源的,这意味着我们可以通过它的代码和文档完全弄清楚 json 中每一个字段的具体含义和处理方式。
二、Lottie 格式简介
在进行代码转换之前,我们首先来介绍下 Lottie 的 JSON 格式。
首先在 Lottie 格式的 Root 层,会存储动画的全局信息,比如动效的展示宽高,播放帧率,引用的图片等资源描述以及动画细节描述等。
interface LottieSchema {
/**
* Adobe After Effects 插件 Bodymovin 的版本
* Bodymovin Version
*/
v: string;
/**
* Name: 动画名称
* Animation name
*/
nm: string; // name
/**
* Width: 动画容器宽度
* Animation Width
*/
w: number; // width
/**
* Height: 动画容器高度
* Animation Height
*/
h: number; // height
/**
* Frame Rate: 动画帧率
* Frame Rate
*/
fr: number; // fps
/**
* In Point: 动画起始帧
* In Point of the Time Ruler. Sets the initial Frame of the animation.
*/
ip: number; // startFrame
/**
* Out Point: 动画结束帧
* Out Point of the Time Ruler. Sets the final Frame of the animation
*/
op: number; // endFrame
/**
* 3D: 是否含有3D特效
* Animation has 3-D layers
*/
ddd: BooleanType;
/**
* Layers: 特效图层
* List of Composition Layers
*/
layers: RuntimeLayer[]; // layers
/**
* Assets: 可被复用的资源
* source items that can be used in multiple places. Comps and Images for now.
*/
assets: RuntimeAsset[]; // assets
// ......
}
复制代码
在这些属性中,最为关键的是描述可复用资源的 assets 和描述详细动画信息的 layers。
2.1 AE 中动画的实现方式
为了更好的理解 Lottie 中的 layers 和 assets 的具体含义,我们首先从前端角度简单了解下设计师是如何在 AE 中实现动画,并导出为 Lottie 的。
AE 中进行动画展示的基础模块是图层(layer),设计师通过在 AE 中创建图层的方式来创建动画元素,而要让动画元素动起来,则可以通过在图层上的不同属性进行关键帧的设置来实现。这样,通过多个图层的叠加,以及在每个图层的不同属性上设置不同的关键帧就可以实现最终的效果。
示例
如下所示的引导小手动效,就可以通过创建四个图层以及设置每个图层的位移、旋转、缩放或透明度的关键帧来实现。
详细动画信息 layers
layers 是一个数组,其中的每一项会描述来自 AE 的一个图层的具体动画信息和展示信息。AE 中有许多不同的图层类型,每种有不同的特性和用途,Lottie 中最常用的图层类型有:文本图层、图像图层、纯色图层、空图层以及合成图层等,所有图层有一些通用的属性,其中比较重要的属性如下:
type LottieBaseLayer {
/**
* Type: 图层类型
* Type of layer
*/
ty: LayerType;
/**
* Key Frames: Transform和透明度动画关键帧
* Transform properties
*/
ks: RuntimeTransform;
/**
* Index: AE 图层的 Index,用于查找图层(如图层父级查找和表达式中图层查找)
* Layer index in AE. Used for parenting and expressions.
*/
ind: number;
/**
* In Point: 图层开始展示帧
* In Point of layer. Sets the initial frame of the layer.
*/
ip: number;
/**
* Out Point: 图层开始隐藏帧
* Out Point of layer. Sets the final frame of the layer.
*/
op: number;
/**
* Start Time: 图层起始帧偏移(合成维度)
* Start Time of layer. Sets the start time of the layer.
*/
st: number;
/**
* Name: AE 图层名称
* After Effects Layer Name
*/
nm: string;
/**
* Stretch: 时间缩放系数
* Layer Time Stretching
*/
sr: number;
/**
* Parent: 父级图层的 ind
* Layer Parent. Uses ind of parent.
*/
parent?: number;
/**
* Width: 图层宽度
* Width
*/
w?: number;
/**
* Height: 图层高度
* Height
*/
h?: number;
}
复制代码
所有图层中都含有描述 Transform 关键帧的 ks 属性,这也是我们在做动效代码转换时着重关注的属性。ks 属性中会描述图层的位移、旋转、缩放这样的 Transform 属性以及展示透明度的动画,其中每一帧(每一段)的描述格式大致如下:
// keyframe desc
type KeyFrameSchema<T extends Array<number> | number> {
// 起始数值 (p0)
s: T;
// 结束数值 (p3)
e?: T;
// 起始帧
t: number;
// 时间 cubic bezier 控制点(p1)
o?: T;
// 时间 cubic bezier 控制点(p2)
i?: T;
// 路径 cubic bezier 控制点(p1)
to?: T;
// 路径 cubic bezier 控制点(p2)
ti?: T;
}
复制代码
图层的关键帧信息中会包含每个关键点的属性数值,所在帧,该点上的控制缓动曲线的出射控制点和入射控制点,另外,对于位移的动画,AE 还支持路径运动,在 Lottie 中的体现就是 to 和 ti 两个参数,它们是和当前控制点相关的路径贝塞尔曲线的控制点。
2.2 可复用资产 assets
layers 里面描述的图层信息有时会包含对外部资源的引用,比如图像图层会引用一张外部图片,预合成图层会引用一份预合成。这些被引用的资源描述都会存放在 assets 里。
关于预合成
预合成图层是 Lottie 中一个比较特殊的图层类型。一般情况下,Lottie 是从设计师在 AE 中编辑的合成来导出的,但就像程序员写的函数中可以调用其他的函数一样,合成中也可以使用其他的合成,合成中引用的其他合成,就是预合成图层,它是该合成的外部资源,因此存放在 Lottie 的 assets 属性里;它的内容是另一个合成,因此 Lottie 里该图层信息的描述方式和一个单独的 Lottie 类似;预合成作为一个单独的合成,当然也可以引用其他的合成,因此嵌套的预合成也是允许存在的。
在实现预合成图层中的图层动画时,我们不单要关注这个图层本身的 Transform 和透明度变化,还要关注它所在的合成被上层合成引用的预合成图层的 Transform 和透明度变化。
三、从 Lottie 导出动效代码
从上一章的 Lottie 格式的介绍中,我们了解了 Lottie 中的动画描述方式,以及每个动画元素(图层)中的关键动画信息,比如开始帧,结束帧,缓动函数控制点以及属性关键帧的数值等等。
现在我们已经从 Lottie 中获得了动效代码所需的完备信息,可以开始进行动效代码的生成了。
3.1 CSS 代码生成
逐帧方案
最符合直觉最简单的从 Lottie 导出 CSS 动效代码的方式可能就是逐帧记录 CSS 关键帧的方式了。我们可以计算一个图层从出现到消失每一帧 transform 和 opacity 的值,然后记录在 CSS keyframes 里。
如下图所示的就是使用逐帧记录 CSS 关键帧方式还原的 Lottie 动画效果:
// in layers
{
"ddd": 0,
"ind": 2,
"ty": 2,
"nm": "截图103.png",
"cl": "png",
"refId": "image_0",
"sr": 1,
"ks": {
"o": {
"a": 1,
"k": [
{
"i": {
"x": [
0.667
],
"y": [
1
]
},
"o": {
"x": [
0.333
],
"y": [
0
]
},
"t": 16,
"s": [
100
]
},
{
"t": 20,
"s": [
1
]
}
],
"ix": 11
},
"r": {
"a": 0,
"k": 0,
"ix": 10
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.874,
"y": 1
},
"o": {
"x": 0.869,
"y": 0
},
"t": 8,
"s": [
414.8,
907.857,
0
],
"to": [
-251.534,
-388.714,
0
],
"ti": [
16,
-336.878,
0
]
},
{
"t": 20,
"s": [
90,
1514.769,
0
]
}
],
"ix": 2,
"l": 2
},
"a": {
"a": 0,
"k": [
414,
896,
0
],
"ix": 1,
"l": 2
},
"s": {
"a": 1,
"k": [
{
"i": {
"x": [
0.667,
0.667,
0.667
],
"y": [
1,
1,
1
]
},
"o": {
"x": [
0.333,
0.333,
0.333
],
"y": [
0,
0,
0
]
},
"t": 8,
"s": [
100,
100,
100
]
},
{
"t": 20,
"s": [
15,
15,
100
]
}
],
"ix": 6,
"l": 2
}
},
"ao": 0,
"ip": 0,
"op": 49,
"st": -95,
"bm": 0
}
复制代码
.hash5edafe06{
transform-origin: 50% 50%;
animation: hash5edafe06_kf 0.667s 0s linear /**forwards**/ /**infinite**/;
}
@keyframes hash5edafe06_kf {
0% {
opacity: 1;
transform: matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);
}
15% {
opacity: 1;
transform: matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);
}
30% {
opacity: 1;
transform: matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);
}
45% {
opacity: 1;
transform: matrix3d(0.983,0,0,0,0,0.983,0,0,0,0,1,0,-0.847,-1.301,0,1);
}
60% {
opacity: 1;
transform: matrix3d(0.78,0,0,0,0,0.78,0,0,0,0,1,0,-16.751,-23.566,0,1);
}
75% {
opacity: 1;
transform: matrix3d(0.47,0,0,0,0,0.47,0,0,0,0,1,0,-82.509,-56.177,0,1);
}
90% {
opacity: 0.3824875;
transform: matrix3d(0.213,0,0,0,0,0.213,0,0,0,0,1,0,-146.717,120.698,0,1);
}
100% {
opacity: 0.01;
transform: matrix3d(0.15,0,0,0,0,0.15,0,0,0,0,1,0,-162.4,303.456,0,1);
}
}
复制代码
Tips
虽然是逐帧的方案,但是每秒对应 30 个甚至更多 CSS keyframes 中的关键帧的话,一方面在效果上没有明显提升,另一方面,也会导致生成的 CSS 代码片段更大,因此是没有必要的,更好的方式是每秒采样 5-10 个关键帧,然后通过设置 easing function 来将关键帧之间的插值方式设置为线性插值,这样在拟合效果的同时,生成的 CSS 代码量更少。
优点
缺点
生成代码量大:因为是每秒固定间隔采样关键帧,当动画的总时长较长的时候,采样的关键帧会比较多,导致生成的代码量也比较大。
可读性差,不易修改:逐帧方案采样的是每帧的最终 transform 和透明度,相比原始的 Lottie 描述,会增加一些冗余信息,不利于人类理解,并且因为采样的关键帧密度比较大且距离近的关键帧相关性高,因此导出的 CSS 代码很难手动修改,比如一个只包含起点和终点关键帧的路径移动的动画,在 Lottie 的 json 中,只需要修改两个数值就可以自然的改变动画的终点,而要在导出的逐帧 CSS 中实现同样的修改则需要修改者修改多个关键帧的数值,且数值的内容需要自行计算才能得到。
逐帧方案虽然可以拟合 Lottie 中的动画效果,但有着生成代码量大和可读性差,不易修改的缺点,因此只适合时长较短且比较简单的动效。
关键帧方案
那么有没有什么方式,在保留对 Lottie 的拟合效果的同时,生成的代码量更小,且可读性更好呢?
一种可行的想法是,忠实的还原 Lottie 中的动画描述,使生成的 CSS keyframes 中的关键帧以及帧间的缓动函数等和 Lottie 中描述的关键帧和缓动方式等完全对应。但遗憾的是,CSS 的动画描述方式和 Lottie 的动画描述方式并不能直接对应,要将 Lottie 的关键帧动画描述方式映射为 CSS 的关键帧动画描述方式,我们需要做一些中间操作抹平它们的差别。
「Lottie 和 CSS 关键帧动画描述方式的差别」
从每一个帧动画信息的描述方式来说,Lottie 中的动画描述基本都在关键帧信息中进行描述,包括关键帧对应的时间(帧数),属性数值,时间样条曲线(三次贝塞尔控制点)和路径样条曲线(应用在位移的三次贝塞尔控制点)。
而在 CSS 的动画描述中,关键帧只描述对应的时间(百分比)和属性数值,时间样条曲线在在关键帧外的 animation-easing-func 里描述,路径样条曲线要直观实现更是需要通过支持性不高的 offset-path 和 offset-distance / motion-path 和 motion-offset 来实现,这样的差别导致 CSS 的动画描述方式不如 Lottie 中的描述方式灵活。
从不同属性的动画信息的描述方式来说,Lottie 中的位移、旋转、缩放和透明度变化分别使用不同的属性来进行描述,如果某个属性的不同维度需要不同的关键帧分布或时间插值方式来进行描述,还可以更进一步细分。比如,缩放的动画可以 s 属性来进行描述,如果 2 维情况下的 x 轴和 y 轴需要不同关键帧和插值方式,则 s 属性可以被拆分为 sx 和 sy 两个独立属性,各自不相关的描述 x 轴和 y 轴的缩放动画。
而在 CSS 中,位移、旋转和缩放的描述都由 transform 属性承接,位移、旋转和缩放的顺序和数量也不像常见的 AE、Unity 等软件那样进行约束,这让单个 Dom 上的 transform 属性在描述特定静态状态或由 js 进行修改达成动态效果时的描述能力上限很高,但对于使用 CSS @keyframes 制作动画时则会带来灾难性的属性耦合问题。
「示例」
考虑这样的一个情况,一张图片有一个总长为 100 帧的元素动画,元素的 2D 旋转角度在第 0 帧(0%)处为 0deg、第 5 帧(5%)处为-18deg, 第 10 帧处为 18deg, 第 15 帧(100%)之后为 0deg,帧间使用线性插值;元素的缩放系数在第 0 帧(0%)处为 0,第 50 帧(50%)处为 2,第 100 帧(100%)处为 1,帧间分别使用 ease-in 和 ease-out 插值,这样的动画用 AE 可以简单的实现,也可以自然的导出为 Lottie 格式描述,但如果要用 CSS 动画来描述的话,因为使用了三种时间插值函数超出了单个 @keyframes 的描述能力,无法使用一个动画来进行描述;又因为动画同时作用于缩放和旋转这些在 CSS 中使用同一个属性描述的变换,在一个 Dom 上使用多个动画描述又会引入属性值互相覆盖的问题。
「实现方案」
总之,在将 Lottie 动画转换成 CSS 关键帧动画时,主要有两个需要解决的问题,第一个是不同的 transform 属性在 CSS 动画中内容互相耦合的问题,另一个是同一个属性的动画不能通过一组 @keyframes 应用多种时间插值曲线的问题。
对于第一个问题,我们可以通过多个嵌套的 Dom 来进行规避,将不应耦合的属性动画放在不同 Dom 的 CSS 动画中进行实现。
对于第二个问题,我们可以将应用了不同时间插值曲线的部分放在不同的 @keyframes 里进行描述,然后应用在同一个 Dom 的 CSS 动画中。
如下图所示的就是使用关键帧 CSS 还原的 Lottie 动画效果:
<style>
.hash5edafe06_0{
transform-origin: 50% 50%;
animation: hash5edafe06_0_keyframe_0 0.4s 0.267s cubic-bezier(0.333, 0, 0.667, 1) /* forwards */;
}
@keyframes hash5edafe06_0_keyframe_0 {
0% {
transform: scale(1.000,1.000);
}
66.667% {
opacity: 1.000;
}
100% {
opacity: 0.010;
transform: scale(0.150,0.150);
}
}
.hash5edafe06_1{
transform-origin: 50% 50%;
animation: hash5edafe06_1_keyframe_0 0.4s 0.267s cubic-bezier(0.869, 0.774, 0.874, 0.951) /* forwards */;
}
@keyframes hash5edafe06_1_keyframe_0 {
0% {
transform: translateX(0.000px);
}
100% {
transform: translateX(-162.400px);
}
}
.hash5edafe06_2{
transform-origin: 50% 50%;
animation: hash5edafe06_2_keyframe_0 0.4s 0.267s cubic-bezier(0.869, -0.64, 0.874, 0.445) /* forwards */;
}
@keyframes hash5edafe06_2_keyframe_0 {
0% {
transform: translateY(0.000px);
}
100% {
transform: translateY(303.456px);
}
}
</style>
<!-- ....... -->
<!-- order matters -->
<div class='hash5edafe06_2'>
<div class='hash5edafe06_1'>
<div class='hash5edafe06_0'>
<!-- real content -->
</div>
</div>
</div>
复制代码
Tips
从 2023 年 7 月开始,主流浏览器和设备开始支持 CSS 的 animation-composition 属性,该属性的开放让多个动画上对同一个属性的赋值除了选择覆盖逻辑,还可选择相加或累加逻辑,大大降低了 CSS 动画的耦合问题。
在可以使用 animation-composition 属性的前提下,关键帧方案导出的动画可共同作用在同一个元素上:keyframe css with composition snippet 。不过考虑到该属性的覆盖率,很遗憾还不推荐在现阶段应用在实际业务中。
「路径的实现方式」
在上面展示的 demo 效果还原中涉及到了路径动画的还原,从起点到终点的位移并不是沿着直线移动,而是沿着特定的曲线移动。在还原这个效果前,我们首先观察下路径动画在 Lottie 中的原始描述方式:
{
{
// 时间插值曲线控制点
"i": {
"x": 0.874,
"y": 1
},
"o": {
"x": 0.869,
"y": 0
},
"t": 8,
"s": [
414.8,
907.857,
0
],
// 路径曲线控制点
"to": [
-251.534,
-388.714,
0
],
"ti": [
16,
-336.878,
0
]
},
{
"t": 20,
"s": [
90,
1514.769,
0
]
}
}
复制代码
Lottie 中的路径曲线也是由三次贝塞尔曲线来进行描述的,而三次贝塞尔曲线则通过它的两个控制点进行描述。而根据贝塞尔曲线的定义,我们可以发现,N 维贝塞尔曲线的维度之间是互相独立的,这意味着 2D 平面上的曲线路径可以通过拆分的 x 轴和 y 轴位移来进行重现,如上面 demo 中.hash5edafe06_1 和 .hash5edafe06_2 中的内容可以重现原 Lottie 的曲线路径。
不过需要注意的是,路径曲线上的时间曲线并不是简单对应于路径贝塞尔曲线的变量 t , 而是对应于路径曲线长度的百分比位置,因此路径曲线上的时间插值曲线并不能完全重现,只能尽量拟合。
优点
缺点:对较为复杂的动画效果,可能会生成需要应用在多个 Dom 上的动画代码,会引入一定的使用成本
Tip:关于贝塞尔曲线
贝塞尔曲线是样条曲线的一种,它的优点是使用灵活和实现直观,贝塞尔曲线的使用非常广泛,它被用作一些其他样条曲线的一部分(如 B 样条, 优化了高阶贝塞尔曲线的耦合问题),也是动效领域的一种通用曲线实现方案,在动画插值(如 CSS 关键帧插值, 模型动作关键帧插值),矢量绘制(如路径移动, 字体字形描述)等方面均有重要应用。
贝塞尔曲线的一般描述方程如下:
该描述方程中不存在矩阵计算部分,因此贝塞尔曲线在 N 维空间中的描述方式都是统一的,且各个维度坐标(e.g. x/y/z)的数值计算互相独立。
变量方案
关键帧方案生产的代码可能存在需要多个 Dom 共同作用来实现一个元素动画的情况,这是它的最大缺点,而这个问题的根本原因就在于前面提到过的「不同的 transform 属性在 CSS 动画中内容互相耦合」,如果可以将它们解藕,则我们就不会不得不使用多个 Dom 来避免属性覆写,可以简单的通过一个 Dom 上使用多个 @keyframes 来实现目标,避免对应用动画的元素 UI 结构的影响。
CSS Houdini API 提供的 @property 为解藕提供了一种方式:我们可以用它定义诸如 --scaleX , --translateX 之类的 CSS 属性,需要动画的元素并不直接在动画的关键帧中设置 transform 或 opacity 的值,而是这些属性的值,然后在 CSS 动画的外部将 transform 或 opacity 的值用这些属性的值来进行设置,这样,就可以在避免耦合的情况下,在同一个 Dom 中实现复杂的动画效果了。
如下图所示的就是使用变量 CSS 还原的 Lottie 动画效果:
@property --translateX {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
@property --translateY {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
@property --scaleX {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
@property --scaleY {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
@property --opacity {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
.ba522056 {
transform: translateX(calc(1px *var(--translateX))) translateY(calc(1px *var(--translateY))) scaleX(calc(var(--scaleX))) scaleY(calc(var(--scaleY)));
opacity: calc(var(--opacity));
animation: ba522056_opacity_0 0.13333333333333333s 0.5333333333333333s cubic-bezier(0.333, 0, 0.667, 1) forwards, ba522056_translateX_0 0.4s 0.26666666666666666s cubic-bezier(0.869, 0.774, 0.874, 0.951) forwards, ba522056_translateY_0 0.4s 0.26666666666666666s cubic-bezier(0.869, -0.64, 0.874, 0.445) forwards, ba522056_scaleX_0 0.4s 0.26666666666666666s cubic-bezier(0.333, 0, 0.667, 1) forwards, ba522056_scaleY_0 0.4s 0.26666666666666666s cubic-bezier(0.333, 0, 0.667, 1) forwards
}
@keyframes ba522056_opacity_0 {
0% {
--opacity: 1;
}
100% {
--opacity: 0.01;
}
}
@keyframes ba522056_translateX_0 {
0% {
--translateX: 0;
}
100% {
--translateX: -162.4;
}
}
@keyframes ba522056_translateY_0 {
0% {
--translateY: 0;
}
100% {
--translateY: 303.456;
}
}
@keyframes ba522056_scaleX_0 {
0% {
--scaleX: 1;
}
100% {
--scaleX: 0.15;
}
}
@keyframes ba522056_scaleY_0 {
0% {
--scaleY: 1;
}
100% {
--scaleY: 0.15;
}
}
复制代码
优点
缺点:CSS 的 @property 仍属于实验性能力,兼容性不好。
3.2 总结
总的来说,最理想的解决方案是变量方案,但因为使用了比较新的 CSS 功能,所以兼容性不佳。关键帧方案适合动画拆解比较简单不会引入辅助嵌套 Dom 的场景或不介意引入辅助 Dom 的场景。逐帧方案适合动画持续时间不长且不需要关键帧数值修改的场景,也可作为兜底的解决方案。
React Native Animated 代码生成
React Native Animated 的描述能力比 CSS 更强,可以自然映射 Lottie 中互相独立的位移、旋转等非耦合 Transform 动画,也可以自然映射 Lottie 中每一个相邻关键帧之间的段上应用不同时间插值曲线的情况,唯一逊于 Lottie 动画描述能力的地方在于路径动画的描述上,不过要实现我们上面提到的 CSS 程度的路径动画还原的话,仍是非常简单的,其实现方式和上面提到的方式并无不同。
如下图所示的就是使用 React Native Animated 还原的 Lottie 动画效果:
function useLayerAnimated() {
const opacityVal = useRef(new Animated.Value(1.00)).current;;
const translateXVal = useRef(new Animated.Value(0.00)).current;;
const translateYVal = useRef(new Animated.Value(0.00)).current;;
const scaleXVal = useRef(new Animated.Value(1.00)).current;;
const scaleYVal = useRef(new Animated.Value(1.00)).current;
const getCompositeAnimation = useCallback(() => {
const opacityAnim =
Animated.timing(opacityVal, {
toValue: 0.01,
duration: 133.333,
useNativeDriver: true,
delay: 533.333,
easing: Easing.bezier(0.333, 0, 0.667, 1),
})
;
const translateXAnim =
Animated.timing(translateXVal, {
toValue: -162.40,
duration: 400,
useNativeDriver: true,
delay: 266.667,
easing: Easing.bezier(0.869, 0.774, 0.874, 0.951),
})
;
const translateYAnim =
Animated.timing(translateYVal, {
toValue: 303.46,
duration: 400,
useNativeDriver: true,
delay: 266.667,
easing: Easing.bezier(0.869, -0.64, 0.874, 0.445),
})
;
const scaleXAnim =
Animated.timing(scaleXVal, {
toValue: 0.15,
duration: 400,
useNativeDriver: true,
delay: 266.667,
easing: Easing.bezier(0.333, 0, 0.667, 1),
})
;
const scaleYAnim =
Animated.timing(scaleYVal, {
toValue: 0.15,
duration: 400,
useNativeDriver: true,
delay: 266.667,
easing: Easing.bezier(0.333, 0, 0.667, 1),
})
return Animated.parallel([
opacityAnim, translateXAnim, translateYAnim, scaleXAnim, scaleYAnim
]);
}, []);
const style = useRef({
transform: [
{translateX: translateXVal}, {translateY: translateYVal}, {scaleX: scaleXVal}, {scaleY: scaleYVal},
],
opacity: opacityVal
}).current;
const resetAnimation = useCallback(() => {
opacityVal.setValue(1.00);
translateXVal.setValue(0.00);
translateYVal.setValue(0.00);
scaleXVal.setValue(1.00);
scaleYVal.setValue(1.00)
}), [];
return {
animatedStyle: style,
resetAnim: resetAnimation,
getAnim: getCompositeAnimation,
}
};
复制代码
四、平台集成
目前从 Lottie 导出 CSS/Animated 代码的能力已经集成到公司内部的 Vision 动效平台中,作为公司内动效整体解决方案的一部分。
平台中的出码能力详细使用方式见:快手前端动效大揭秘:告别低效,vision平台来袭!
在下期内容中,我们将重点介绍 Vision 动效平台在序列帧动效格式转换方面的能力和流程:动效平台通过提供多种序列帧格式自动转换功能,优化动效交付流程,提高动效的兼容性和性能。敬请期待!
- END -
评论