揭秘!如何将动效描述自动转化为动效代码
导读:在上一篇文章中,我们详细介绍了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动画效果:
- 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
}
- 逐帧CSS效果:
图片
- 代码片段:
.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和透明度信息并组织为CSS keyframes的形式就可以拟合效果,原理简单,易于实现。
缺点
- 生成代码量大:因为是每秒固定间隔采样关键帧,当动画的总时长较长的时候,采样的关键帧会比较多,导致生成的代码量也比较大。
- 可读性差,不易修改:逐帧方案采样的是每帧的最终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动画效果:
- 变量CSS效果:
图片
- 代码片段:
<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 , 而是对应于路径曲线长度的百分比位置,因此路径曲线上的时间插值曲线并不能完全重现,只能尽量拟合。
优点
- 关键帧的数量相比逐帧CSS更少,语义更加清晰,方便修改
- 生成的代码体积更小,可以降低使用者的使用负担
缺点:对较为复杂的动画效果,可能会生成需要应用在多个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动画效果:
- 关键帧CSS:
图片
- 代码片段:
@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;
}
}
优点
- 可读性进一步提高
- 不会发生需要多个Dom来解一个元素上的复杂动画耦合问题的情况
缺点: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 -