揭秘!如何將動(dòng)效描述自動(dòng)轉(zhuǎn)化為動(dòng)效代碼
導(dǎo)讀:在上一篇文章中,我們?cè)敿?xì)介紹了Vision動(dòng)效平臺(tái)的渲染引擎——Crab,并分享在復(fù)雜動(dòng)效渲染場(chǎng)景下積累的實(shí)踐經(jīng)驗(yàn)和精彩案例。今天,我們將揭秘如何將「動(dòng)效描述翻譯為動(dòng)效代碼」——從Lottie導(dǎo)出CSS/Animated代碼。
一、項(xiàng)目背景
在進(jìn)行前端頁(yè)面開(kāi)發(fā)中,經(jīng)常需要涉及到元素動(dòng)效的開(kāi)發(fā),比如按鈕的呼吸狀態(tài)動(dòng)效,彈窗的出現(xiàn)和消失動(dòng)效等等,這些動(dòng)效為用戶在頁(yè)面交互過(guò)程中獲得良好的體驗(yàn)起到重要的作用。
要開(kāi)發(fā)這些動(dòng)效,一般的工作流程是由設(shè)計(jì)同學(xué)提供動(dòng)效描述,然后研發(fā)同學(xué)按照參數(shù)實(shí)現(xiàn)對(duì)應(yīng)平臺(tái)的動(dòng)效代碼(如Web平臺(tái)的CSS或React Native的Animated),從而進(jìn)行動(dòng)效的還原。
1.1 元素動(dòng)效開(kāi)發(fā)的痛點(diǎn)
對(duì)于一些獨(dú)立性較強(qiáng)或比較復(fù)雜的動(dòng)效,可以直接使用Lottie來(lái)進(jìn)行播放,但是一方面對(duì)于一些比較簡(jiǎn)單的動(dòng)效需求,如果引入Lottie來(lái)進(jìn)行播放,則Lottie帶來(lái)的額外運(yùn)行時(shí)包體積的成本相比于動(dòng)效本身過(guò)高,另一方面,對(duì)于元素動(dòng)效中常見(jiàn)的和業(yè)務(wù)邏輯或用戶操作綁定的情況,直接使用Lottie有時(shí)反而會(huì)引入額外的開(kāi)發(fā)成本。
在動(dòng)效還原的過(guò)程中,研發(fā)需要面對(duì)設(shè)計(jì)師交付的各種不同格式的動(dòng)效描述,可能是一句自然語(yǔ)言的描述,一個(gè)時(shí)間軸或者使用AE插件導(dǎo)出的文本描述等等,然后人肉將設(shè)計(jì)同學(xué)提供的這些動(dòng)效描述翻譯為動(dòng)效代碼,這個(gè)過(guò)程常常是一個(gè)重復(fù)性很強(qiáng)的工作,且耗時(shí)耗力,會(huì)帶來(lái)不小的心智負(fù)擔(dān)。
文本動(dòng)效參數(shù)交付示例:
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)
- 旋轉(zhuǎn) -
Delay: 267ms
Dur: 73ms
Val: 0° ??? -3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 340ms
Dur: 73ms
Val: -3° ??? 3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 413ms
Dur: 73ms
Val: 3° ??? -3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 487ms
Dur: 73ms
Val: -3° ??? 3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 560ms
Dur: 73ms
Val: 3° ??? -3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 633ms
Dur: 67ms
Val: -3° ??? 3°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 700ms
Dur: 67ms
Val: 3° ??? 0°
(0.33, 0, 0.67, 1)
Total Dur: 500ms
≡ 蓋子_關(guān).png ≡
- 位置 -
Delay: 0ms
Dur: 500ms
Val: [74,13]??[74,13]
No Change
- 旋轉(zhuǎn) -
Delay: 0ms
Dur: 28ms
Val: 0.75° ??? 0°
(0.33, 0.54, 0.83, 1)
- 旋轉(zhuǎn) -
Delay: 28ms
Dur: 72ms
Val: 0° ??? -2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 100ms
Dur: 72ms
Val: -2° ??? 2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 172ms
Dur: 72ms
Val: 2° ??? -2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 244ms
Dur: 72ms
Val: -2° ??? 2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 317ms
Dur: 72ms
Val: 2° ??? -2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 389ms
Dur: 72ms
Val: -2° ??? 2°
(0.33, 0, 0.67, 1)
- 旋轉(zhuǎn) -
Delay: 461ms
Dur: 39ms
Val: 2° ??? 0.75°
(0.33, 0, 0.67, 0.55)
(盒子.png是蓋子_關(guān).png父級(jí))
Total Dur: 1633ms
≡ 蓋子_開(kāi).png ≡
- 位置 -
Delay: 0ms
Dur: 1633ms
Val: [113,5]??[113,5]
Linear
(在第1633ms,切換 蓋子_開(kāi).png和盒子.2.png)
Total Dur: 267ms
≡ 盒子.2.png ≡
- 縮放 -
Delay: 0ms
Dur: 267ms
Val: 189.6% ?? 0%(0.17, 0, 0.83, 1)
表格動(dòng)效參數(shù)交付示例:
圖片
要解決這個(gè)痛點(diǎn),我們可以考慮將「從動(dòng)效描述翻譯為動(dòng)效代碼」的工作通過(guò)自動(dòng)化的方式完成。而要實(shí)現(xiàn)這個(gè)自動(dòng)化的流程,首先要解決的就是設(shè)計(jì)師提供的動(dòng)效描述沒(méi)有統(tǒng)一格式的問(wèn)題。
最適合用作動(dòng)效描述統(tǒng)一格式的方案就是Lottie,Lottie是一個(gè)基于JSON的動(dòng)畫(huà)文件格式,它可以使用Bodymmovin解析導(dǎo)出Adobe After Effects動(dòng)畫(huà),并在移動(dòng)設(shè)備上渲染它們。通過(guò)它,設(shè)計(jì)師可以創(chuàng)造和發(fā)布酷炫的動(dòng)畫(huà),且無(wú)需工程師費(fèi)心的手工重建動(dòng)畫(huà)效果。
它具有以下優(yōu)點(diǎn):
- 標(biāo)準(zhǔn)化:Lottie的JSON格式中,每個(gè)屬性的含義和數(shù)據(jù)類(lèi)型都很明確,相比于自然語(yǔ)言的描述方式,更加清晰明確。
- 無(wú)感知:設(shè)計(jì)師在AE中完成動(dòng)效的編輯后,可以直接使用AE的BodyMovin插件導(dǎo)出我們期望Lottie格式動(dòng)效描述,導(dǎo)出過(guò)程不會(huì)為設(shè)計(jì)師引入額外的成本。
- 透明化:Lottie的運(yùn)行庫(kù)是開(kāi)源的,這意味著我們可以通過(guò)它的代碼和文檔完全弄清楚json中每一個(gè)字段的具體含義和處理方式。
二、Lottie格式簡(jiǎn)介
在進(jìn)行代碼轉(zhuǎn)換之前,我們首先來(lái)介紹下Lottie的JSON格式。
首先在Lottie格式的Root層,會(huì)存儲(chǔ)動(dòng)畫(huà)的全局信息,比如動(dòng)效的展示寬高,播放幀率,引用的圖片等資源描述以及動(dòng)畫(huà)細(xì)節(jié)描述等。
interface LottieSchema {
/**
* Adobe After Effects 插件 Bodymovin 的版本
* Bodymovin Version
*/
v: string;
/**
* Name: 動(dòng)畫(huà)名稱(chēng)
* Animation name
*/
nm: string; // name
/**
* Width: 動(dòng)畫(huà)容器寬度
* Animation Width
*/
w: number; // width
/**
* Height: 動(dòng)畫(huà)容器高度
* Animation Height
*/
h: number; // height
/**
* Frame Rate: 動(dòng)畫(huà)幀率
* Frame Rate
*/
fr: number; // fps
/**
* In Point: 動(dòng)畫(huà)起始幀
* In Point of the Time Ruler. Sets the initial Frame of the animation.
*/
ip: number; // startFrame
/**
* Out Point: 動(dòng)畫(huà)結(jié)束幀
* 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: 可被復(fù)用的資源
* source items that can be used in multiple places. Comps and Images for now.
*/
assets: RuntimeAsset[]; // assets
// ......
}
在這些屬性中,
最為關(guān)鍵的是描述可復(fù)用資源的assets和描述詳細(xì)動(dòng)畫(huà)信息的layers。
2.1 AE中動(dòng)畫(huà)的實(shí)現(xiàn)方式
為了更好的理解Lottie中的layers和assets的具體含義,我們首先從前端角度簡(jiǎn)單了解下設(shè)計(jì)師是如何在AE中實(shí)現(xiàn)動(dòng)畫(huà),并導(dǎo)出為L(zhǎng)ottie的。
AE中進(jìn)行動(dòng)畫(huà)展示的基礎(chǔ)模塊是圖層(layer),設(shè)計(jì)師通過(guò)在AE中創(chuàng)建圖層的方式來(lái)創(chuàng)建動(dòng)畫(huà)元素,而要讓動(dòng)畫(huà)元素動(dòng)起來(lái),則可以通過(guò)在圖層上的不同屬性進(jìn)行關(guān)鍵幀的設(shè)置來(lái)實(shí)現(xiàn)。這樣,通過(guò)多個(gè)圖層的疊加,以及在每個(gè)圖層的不同屬性上設(shè)置不同的關(guān)鍵幀就可以實(shí)現(xiàn)最終的效果。
示例
如下所示的引導(dǎo)小手動(dòng)效,就可以通過(guò)創(chuàng)建四個(gè)圖層以及設(shè)置每個(gè)圖層的位移、旋轉(zhuǎn)、縮放或透明度的關(guān)鍵幀來(lái)實(shí)現(xiàn)。
詳細(xì)動(dòng)畫(huà)信息layers
layers是一個(gè)數(shù)組,其中的每一項(xiàng)會(huì)描述來(lái)自AE的一個(gè)圖層的具體動(dòng)畫(huà)信息和展示信息。AE中有許多不同的圖層類(lèi)型,每種有不同的特性和用途,Lottie中最常用的圖層類(lèi)型有:文本圖層、圖像圖層、純色圖層、空?qǐng)D層以及合成圖層等,所有圖層有一些通用的屬性,其中比較重要的屬性如下:
type LottieBaseLayer {
/**
* Type: 圖層類(lèi)型
* Type of layer
*/
ty: LayerType;
/**
* Key Frames: Transform和透明度動(dòng)畫(huà)關(guān)鍵幀
* Transform properties
*/
ks: RuntimeTransform;
/**
* Index: AE 圖層的 Index,用于查找圖層(如圖層父級(jí)查找和表達(dá)式中圖層查找)
* Layer index in AE. Used for parenting and expressions.
*/
ind: number;
/**
* In Point: 圖層開(kāi)始展示幀
* In Point of layer. Sets the initial frame of the layer.
*/
ip: number;
/**
* Out Point: 圖層開(kāi)始隱藏幀
* 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 圖層名稱(chēng)
* After Effects Layer Name
*/
nm: string;
/**
* Stretch: 時(shí)間縮放系數(shù)
* Layer Time Stretching
*/
sr: number;
/**
* Parent: 父級(jí)圖層的 ind
* Layer Parent. Uses ind of parent.
*/
parent?: number;
/**
* Width: 圖層寬度
* Width
*/
w?: number;
/**
* Height: 圖層高度
* Height
*/
h?: number;
}
所有圖層中都含有描述Transform關(guān)鍵幀的ks屬性,這也是我們?cè)谧鰟?dòng)效代碼轉(zhuǎn)換時(shí)著重關(guān)注的屬性。ks屬性中會(huì)描述圖層的位移、旋轉(zhuǎn)、縮放這樣的Transform屬性以及展示透明度的動(dòng)畫(huà),其中每一幀(每一段)的描述格式大致如下:
// keyframe desc
type KeyFrameSchema<T extends Array<number> | number> {
// 起始數(shù)值 (p0)
s: T;
// 結(jié)束數(shù)值 (p3)
e?: T;
// 起始幀
t: number;
// 時(shí)間 cubic bezier 控制點(diǎn)(p1)
o?: T;
// 時(shí)間 cubic bezier 控制點(diǎn)(p2)
i?: T;
// 路徑 cubic bezier 控制點(diǎn)(p1)
to?: T;
// 路徑 cubic bezier 控制點(diǎn)(p2)
ti?: T;
}
圖層的關(guān)鍵幀信息中會(huì)包含每個(gè)關(guān)鍵點(diǎn)的屬性數(shù)值,所在幀,該點(diǎn)上的控制緩動(dòng)曲線的出射控制點(diǎn)和入射控制點(diǎn),另外,對(duì)于位移的動(dòng)畫(huà),AE還支持路徑運(yùn)動(dòng),在Lottie中的體現(xiàn)就是to和ti兩個(gè)參數(shù),它們是和當(dāng)前控制點(diǎn)相關(guān)的路徑貝塞爾曲線的控制點(diǎn)。
圖片
2.2 可復(fù)用資產(chǎn) assets
layers里面描述的圖層信息有時(shí)會(huì)包含對(duì)外部資源的引用,比如圖像圖層會(huì)引用一張外部圖片,預(yù)合成圖層會(huì)引用一份預(yù)合成。這些被引用的資源描述都會(huì)存放在assets里。
關(guān)于預(yù)合成
預(yù)合成圖層是Lottie中一個(gè)比較特殊的圖層類(lèi)型。一般情況下,Lottie是從設(shè)計(jì)師在AE中編輯的合成來(lái)導(dǎo)出的,但就像程序員寫(xiě)的函數(shù)中可以調(diào)用其他的函數(shù)一樣,合成中也可以使用其他的合成,合成中引用的其他合成,就是預(yù)合成圖層,它是該合成的外部資源,因此存放在Lottie的assets屬性里;它的內(nèi)容是另一個(gè)合成,因此Lottie里該圖層信息的描述方式和一個(gè)單獨(dú)的Lottie類(lèi)似;預(yù)合成作為一個(gè)單獨(dú)的合成,當(dāng)然也可以引用其他的合成,因此嵌套的預(yù)合成也是允許存在的。
在實(shí)現(xiàn)預(yù)合成圖層中的圖層動(dòng)畫(huà)時(shí),我們不單要關(guān)注這個(gè)圖層本身的Transform和透明度變化,還要關(guān)注它所在的合成被上層合成引用的預(yù)合成圖層的Transform和透明度變化。
三、從Lottie導(dǎo)出動(dòng)效代碼
從上一章的Lottie格式的介紹中,我們了解了Lottie中的動(dòng)畫(huà)描述方式,以及每個(gè)動(dòng)畫(huà)元素(圖層)中的關(guān)鍵動(dòng)畫(huà)信息,比如開(kāi)始幀,結(jié)束幀,緩動(dòng)函數(shù)控制點(diǎn)以及屬性關(guān)鍵幀的數(shù)值等等。
現(xiàn)在我們已經(jīng)從Lottie中獲得了動(dòng)效代碼所需的完備信息,可以開(kāi)始進(jìn)行動(dòng)效代碼的生成了。
3.1 CSS代碼生成
逐幀方案
最符合直覺(jué)最簡(jiǎn)單的從Lottie導(dǎo)出CSS動(dòng)效代碼的方式可能就是逐幀記錄CSS關(guān)鍵幀的方式了。我們可以計(jì)算一個(gè)圖層從出現(xiàn)到消失每一幀transform和opacity的值,然后記錄在CSS keyframes里。
如下圖所示的就是使用逐幀記錄CSS關(guān)鍵幀方式還原的Lottie動(dòng)畫(huà)效果:
- 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
雖然是逐幀的方案,但是每秒對(duì)應(yīng)30個(gè)甚至更多 CSS keyframes 中的關(guān)鍵幀的話,一方面在效果上沒(méi)有明顯提升,另一方面,也會(huì)導(dǎo)致生成的CSS 代碼片段更大,因此是沒(méi)有必要的,更好的方式是每秒采樣5-10個(gè)關(guān)鍵幀,然后通過(guò)設(shè)置easing function來(lái)將關(guān)鍵幀之間的插值方式設(shè)置為線性插值,這樣在擬合效果的同時(shí),生成的CSS代碼量更少。
優(yōu)點(diǎn)
- 實(shí)現(xiàn)簡(jiǎn)單:只需要按照固定間隔采樣并計(jì)算圖層的transform和透明度信息并組織為CSS keyframes的形式就可以擬合效果,原理簡(jiǎn)單,易于實(shí)現(xiàn)。
缺點(diǎn)
- 生成代碼量大:因?yàn)槭敲棵牍潭ㄩg隔采樣關(guān)鍵幀,當(dāng)動(dòng)畫(huà)的總時(shí)長(zhǎng)較長(zhǎng)的時(shí)候,采樣的關(guān)鍵幀會(huì)比較多,導(dǎo)致生成的代碼量也比較大。
- 可讀性差,不易修改:逐幀方案采樣的是每幀的最終transform和透明度,相比原始的Lottie描述,會(huì)增加一些冗余信息,不利于人類(lèi)理解,并且因?yàn)椴蓸拥年P(guān)鍵幀密度比較大且距離近的關(guān)鍵幀相關(guān)性高,因此導(dǎo)出的CSS代碼很難手動(dòng)修改,比如一個(gè)只包含起點(diǎn)和終點(diǎn)關(guān)鍵幀的路徑移動(dòng)的動(dòng)畫(huà),在Lottie的json中,只需要修改兩個(gè)數(shù)值就可以自然的改變動(dòng)畫(huà)的終點(diǎn),而要在導(dǎo)出的逐幀CSS中實(shí)現(xiàn)同樣的修改則需要修改者修改多個(gè)關(guān)鍵幀的數(shù)值,且數(shù)值的內(nèi)容需要自行計(jì)算才能得到。
逐幀方案雖然可以擬合Lottie中的動(dòng)畫(huà)效果,但有著生成代碼量大和可讀性差,不易修改的缺點(diǎn),因此只適合時(shí)長(zhǎng)較短且比較簡(jiǎn)單的動(dòng)效。
關(guān)鍵幀方案
那么有沒(méi)有什么方式,在保留對(duì)Lottie的擬合效果的同時(shí),生成的代碼量更小,且可讀性更好呢?
一種可行的想法是,忠實(shí)的還原Lottie中的動(dòng)畫(huà)描述,使生成的CSS keyframes中的關(guān)鍵幀以及幀間的緩動(dòng)函數(shù)等和Lottie中描述的關(guān)鍵幀和緩動(dòng)方式等完全對(duì)應(yīng)。但遺憾的是,CSS的動(dòng)畫(huà)描述方式和Lottie的動(dòng)畫(huà)描述方式并不能直接對(duì)應(yīng),要將Lottie的關(guān)鍵幀動(dòng)畫(huà)描述方式映射為CSS的關(guān)鍵幀動(dòng)畫(huà)描述方式,我們需要做一些中間操作抹平它們的差別。
「Lottie和CSS關(guān)鍵幀動(dòng)畫(huà)描述方式的差別」
從每一個(gè)幀動(dòng)畫(huà)信息的描述方式來(lái)說(shuō),Lottie中的動(dòng)畫(huà)描述基本都在關(guān)鍵幀信息中進(jìn)行描述,包括關(guān)鍵幀對(duì)應(yīng)的時(shí)間(幀數(shù)),屬性數(shù)值,時(shí)間樣條曲線(三次貝塞爾控制點(diǎn))和路徑樣條曲線(應(yīng)用在位移的三次貝塞爾控制點(diǎn))。
而在CSS的動(dòng)畫(huà)描述中,關(guān)鍵幀只描述對(duì)應(yīng)的時(shí)間(百分比)和屬性數(shù)值,時(shí)間樣條曲線在在關(guān)鍵幀外的animation-easing-func里描述,路徑樣條曲線要直觀實(shí)現(xiàn)更是需要通過(guò)支持性不高的offset-path 和 offset-distance / motion-path 和 motion-offset 來(lái)實(shí)現(xiàn),這樣的差別導(dǎo)致CSS的動(dòng)畫(huà)描述方式不如Lottie中的描述方式靈活。
從不同屬性的動(dòng)畫(huà)信息的描述方式來(lái)說(shuō),Lottie中的位移、旋轉(zhuǎn)、縮放和透明度變化分別使用不同的屬性來(lái)進(jìn)行描述,如果某個(gè)屬性的不同維度需要不同的關(guān)鍵幀分布或時(shí)間插值方式來(lái)進(jìn)行描述,還可以更進(jìn)一步細(xì)分。比如,縮放的動(dòng)畫(huà)可以s屬性來(lái)進(jìn)行描述,如果2維情況下的x軸和y軸需要不同關(guān)鍵幀和插值方式,則s屬性可以被拆分為sx 和 sy 兩個(gè)獨(dú)立屬性,各自不相關(guān)的描述x軸和y軸的縮放動(dòng)畫(huà)。
而在CSS中,位移、旋轉(zhuǎn)和縮放的描述都由transform屬性承接,位移、旋轉(zhuǎn)和縮放的順序和數(shù)量也不像常見(jiàn)的AE、Unity等軟件那樣進(jìn)行約束,這讓單個(gè)Dom上的transform屬性在描述特定靜態(tài)狀態(tài)或由js進(jìn)行修改達(dá)成動(dòng)態(tài)效果時(shí)的描述能力上限很高,但對(duì)于使用CSS @keyframes制作動(dòng)畫(huà)時(shí)則會(huì)帶來(lái)災(zāi)難性的屬性耦合問(wèn)題。
「示例」
考慮這樣的一個(gè)情況,一張圖片有一個(gè)總長(zhǎng)為100幀的元素動(dòng)畫(huà),元素的2D旋轉(zhuǎn)角度在第0幀(0%)處為0deg、第5幀(5%)處為-18deg, 第10幀處為18deg, 第15幀(100%)之后為0deg,幀間使用線性插值;元素的縮放系數(shù)在第0幀(0%)處為0,第50幀(50%)處為2,第100幀(100%)處為1,幀間分別使用ease-in和ease-out插值,這樣的動(dòng)畫(huà)用AE可以簡(jiǎn)單的實(shí)現(xiàn),也可以自然的導(dǎo)出為L(zhǎng)ottie格式描述,但如果要用CSS動(dòng)畫(huà)來(lái)描述的話,因?yàn)槭褂昧巳N時(shí)間插值函數(shù)超出了單個(gè)@keyframes的描述能力,無(wú)法使用一個(gè)動(dòng)畫(huà)來(lái)進(jìn)行描述;又因?yàn)閯?dòng)畫(huà)同時(shí)作用于縮放和旋轉(zhuǎn)這些在CSS中使用同一個(gè)屬性描述的變換,在一個(gè)Dom上使用多個(gè)動(dòng)畫(huà)描述又會(huì)引入屬性值互相覆蓋的問(wèn)題。
「實(shí)現(xiàn)方案」
總之,在將Lottie動(dòng)畫(huà)轉(zhuǎn)換成CSS關(guān)鍵幀動(dòng)畫(huà)時(shí),主要有兩個(gè)需要解決的問(wèn)題,第一個(gè)是不同的transform屬性在CSS動(dòng)畫(huà)中內(nèi)容互相耦合的問(wèn)題,另一個(gè)是同一個(gè)屬性的動(dòng)畫(huà)不能通過(guò)一組@keyframes應(yīng)用多種時(shí)間插值曲線的問(wèn)題。
對(duì)于第一個(gè)問(wèn)題,我們可以通過(guò)多個(gè)嵌套的Dom來(lái)進(jìn)行規(guī)避,將不應(yīng)耦合的屬性動(dòng)畫(huà)放在不同Dom的CSS動(dòng)畫(huà)中進(jìn)行實(shí)現(xiàn)。
對(duì)于第二個(gè)問(wèn)題,我們可以將應(yīng)用了不同時(shí)間插值曲線的部分放在不同的@keyframes里進(jìn)行描述,然后應(yīng)用在同一個(gè)Dom的CSS動(dòng)畫(huà)中。
如下圖所示的就是使用關(guān)鍵幀CSS還原的Lottie動(dòng)畫(huà)效果:
- 變量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月開(kāi)始,主流瀏覽器和設(shè)備開(kāi)始支持CSS的animation-composition屬性,該屬性的開(kāi)放讓多個(gè)動(dòng)畫(huà)上對(duì)同一個(gè)屬性的賦值除了選擇覆蓋邏輯,還可選擇相加或累加邏輯,大大降低了CSS動(dòng)畫(huà)的耦合問(wèn)題。
在可以使用animation-composition屬性的前提下,關(guān)鍵幀方案導(dǎo)出的動(dòng)畫(huà)可共同作用在同一個(gè)元素上:keyframe css with composition snippet 。不過(guò)考慮到該屬性的覆蓋率,很遺憾還不推薦在現(xiàn)階段應(yīng)用在實(shí)際業(yè)務(wù)中。
「路徑的實(shí)現(xiàn)方式」
在上面展示的demo效果還原中涉及到了路徑動(dòng)畫(huà)的還原,從起點(diǎn)到終點(diǎn)的位移并不是沿著直線移動(dòng),而是沿著特定的曲線移動(dòng)。在還原這個(gè)效果前,我們首先觀察下路徑動(dòng)畫(huà)在Lottie中的原始描述方式:
{
{
// 時(shí)間插值曲線控制點(diǎn)
"i": {
"x": 0.874,
"y": 1
},
"o": {
"x": 0.869,
"y": 0
},
"t": 8,
"s": [
414.8,
907.857,
0
],
// 路徑曲線控制點(diǎn)
"to": [
-251.534,
-388.714,
0
],
"ti": [
16,
-336.878,
0
]
},
{
"t": 20,
"s": [
90,
1514.769,
0
]
}
}
Lottie中的路徑曲線也是由三次貝塞爾曲線來(lái)進(jìn)行描述的,而三次貝塞爾曲線則通過(guò)它的兩個(gè)控制點(diǎn)進(jìn)行描述。而根據(jù)貝塞爾曲線的定義,我們可以發(fā)現(xiàn),N維貝塞爾曲線的維度之間是互相獨(dú)立的,這意味著2D平面上的曲線路徑可以通過(guò)拆分的x軸和y軸位移來(lái)進(jìn)行重現(xiàn),如上面demo中.hash5edafe06_1 和 .hash5edafe06_2 中的內(nèi)容可以重現(xiàn)原Lottie的曲線路徑。
不過(guò)需要注意的是,路徑曲線上的時(shí)間曲線并不是簡(jiǎn)單對(duì)應(yīng)于路徑貝塞爾曲線的變量t , 而是對(duì)應(yīng)于路徑曲線長(zhǎng)度的百分比位置,因此路徑曲線上的時(shí)間插值曲線并不能完全重現(xiàn),只能盡量擬合。
優(yōu)點(diǎn)
- 關(guān)鍵幀的數(shù)量相比逐幀CSS更少,語(yǔ)義更加清晰,方便修改
- 生成的代碼體積更小,可以降低使用者的使用負(fù)擔(dān)
缺點(diǎn):對(duì)較為復(fù)雜的動(dòng)畫(huà)效果,可能會(huì)生成需要應(yīng)用在多個(gè)Dom上的動(dòng)畫(huà)代碼,會(huì)引入一定的使用成本
Tip:關(guān)于貝塞爾曲線
貝塞爾曲線是樣條曲線的一種,它的優(yōu)點(diǎn)是使用靈活和實(shí)現(xiàn)直觀,貝塞爾曲線的使用非常廣泛,它被用作一些其他樣條曲線的一部分(如B樣條, 優(yōu)化了高階貝塞爾曲線的耦合問(wèn)題),也是動(dòng)效領(lǐng)域的一種通用曲線實(shí)現(xiàn)方案,在動(dòng)畫(huà)插值(如CSS關(guān)鍵幀插值, 模型動(dòng)作關(guān)鍵幀插值),矢量繪制(如路徑移動(dòng), 字體字形描述)等方面均有重要應(yīng)用。
貝塞爾曲線的一般描述方程如下:
圖片
該描述方程中不存在矩陣計(jì)算部分,因此貝塞爾曲線在N維空間中的描述方式都是統(tǒng)一的,且各個(gè)維度坐標(biāo)(e.g. x/y/z)的數(shù)值計(jì)算互相獨(dú)立。
變量方案
關(guān)鍵幀方案生產(chǎn)的代碼可能存在需要多個(gè)Dom共同作用來(lái)實(shí)現(xiàn)一個(gè)元素動(dòng)畫(huà)的情況,這是它的最大缺點(diǎn),而這個(gè)問(wèn)題的根本原因就在于前面提到過(guò)的「不同的transform屬性在CSS動(dòng)畫(huà)中內(nèi)容互相耦合」,如果可以將它們解藕,則我們就不會(huì)不得不使用多個(gè)Dom來(lái)避免屬性覆寫(xiě),可以簡(jiǎn)單的通過(guò)一個(gè)Dom上使用多個(gè)@keyframes來(lái)實(shí)現(xiàn)目標(biāo),避免對(duì)應(yīng)用動(dòng)畫(huà)的元素UI結(jié)構(gòu)的影響。
CSS Houdini API提供的@property 為解藕提供了一種方式:我們可以用它定義諸如 --scaleX , --translateX 之類(lèi)的CSS屬性,需要?jiǎng)赢?huà)的元素并不直接在動(dòng)畫(huà)的關(guān)鍵幀中設(shè)置transform 或 opacity的值,而是這些屬性的值,然后在CSS動(dòng)畫(huà)的外部將transform 或 opacity 的值用這些屬性的值來(lái)進(jìn)行設(shè)置,這樣,就可以在避免耦合的情況下,在同一個(gè)Dom中實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果了。
如下圖所示的就是使用變量CSS還原的Lottie動(dòng)畫(huà)效果:
- 關(guān)鍵幀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;
}
}
優(yōu)點(diǎn)
- 可讀性進(jìn)一步提高
- 不會(huì)發(fā)生需要多個(gè)Dom來(lái)解一個(gè)元素上的復(fù)雜動(dòng)畫(huà)耦合問(wèn)題的情況
缺點(diǎn):CSS的@property 仍屬于實(shí)驗(yàn)性能力,兼容性不好。
3.2 總結(jié)
總的來(lái)說(shuō),最理想的解決方案是變量方案,但因?yàn)槭褂昧吮容^新的CSS功能,所以兼容性不佳。關(guān)鍵幀方案適合動(dòng)畫(huà)拆解比較簡(jiǎn)單不會(huì)引入輔助嵌套Dom的場(chǎng)景或不介意引入輔助Dom的場(chǎng)景。逐幀方案適合動(dòng)畫(huà)持續(xù)時(shí)間不長(zhǎng)且不需要關(guān)鍵幀數(shù)值修改的場(chǎng)景,也可作為兜底的解決方案。
圖片
React Native Animated代碼生成
React Native Animated的描述能力比CSS更強(qiáng),可以自然映射Lottie中互相獨(dú)立的位移、旋轉(zhuǎn)等非耦合Transform動(dòng)畫(huà),也可以自然映射Lottie中每一個(gè)相鄰關(guān)鍵幀之間的段上應(yīng)用不同時(shí)間插值曲線的情況,唯一遜于Lottie動(dòng)畫(huà)描述能力的地方在于路徑動(dòng)畫(huà)的描述上,不過(guò)要實(shí)現(xiàn)我們上面提到的CSS程度的路徑動(dòng)畫(huà)還原的話,仍是非常簡(jiǎn)單的,其實(shí)現(xiàn)方式和上面提到的方式并無(wú)不同。
如下圖所示的就是使用React Native Animated還原的Lottie動(dòng)畫(huà)效果:
圖片
- 代碼片段:
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,
}
};
四、平臺(tái)集成
目前從Lottie導(dǎo)出CSS/Animated代碼的能力已經(jīng)集成到公司內(nèi)部的Vision動(dòng)效平臺(tái)中,作為公司內(nèi)動(dòng)效整體解決方案的一部分。
平臺(tái)中的出碼能力詳細(xì)使用方式見(jiàn):??快手前端動(dòng)效大揭秘:告別低效,vision平臺(tái)來(lái)襲!??
圖片
在下期內(nèi)容中,我們將重點(diǎn)介紹Vision 動(dòng)效平臺(tái)在序列幀動(dòng)效格式轉(zhuǎn)換方面的能力和流程:動(dòng)效平臺(tái)通過(guò)提供多種序列幀格式自動(dòng)轉(zhuǎn)換功能,優(yōu)化動(dòng)效交付流程,提高動(dòng)效的兼容性和性能。敬請(qǐng)期待!
- END -
