揭秘!如何將動效描述自動轉化為動效代碼
導讀:在上一篇文章中,我們詳細介紹了Vision動效平臺的渲染引擎——Crab,并分享在復雜動效渲染場景下積累的實踐經(jīng)驗和精彩案例。今天,我們將揭秘如何將「動效描述翻譯為動效代碼」——從Lottie導出CSS/Animated代碼。
一、項目背景
在進行前端頁面開發(fā)中,經(jīng)常需要涉及到元素動效的開發(fā),比如按鈕的呼吸狀態(tài)動效,彈窗的出現(xiàn)和消失動效等等,這些動效為用戶在頁面交互過程中獲得良好的體驗起到重要的作用。
要開發(fā)這些動效,一般的工作流程是由設計同學提供動效描述,然后研發(fā)同學按照參數(shù)實現(xiàn)對應平臺的動效代碼(如Web平臺的CSS或React Native的Animated),從而進行動效的還原。
1.1 元素動效開發(fā)的痛點
對于一些獨立性較強或比較復雜的動效,可以直接使用Lottie來進行播放,但是一方面對于一些比較簡單的動效需求,如果引入Lottie來進行播放,則Lottie帶來的額外運行時包體積的成本相比于動效本身過高,另一方面,對于元素動效中常見的和業(yè)務邏輯或用戶操作綁定的情況,直接使用Lottie有時反而會引入額外的開發(fā)成本。
在動效還原的過程中,研發(fā)需要面對設計師交付的各種不同格式的動效描述,可能是一句自然語言的描述,一個時間軸或者使用AE插件導出的文本描述等等,然后人肉將設計同學提供的這些動效描述翻譯為動效代碼,這個過程常常是一個重復性很強的工作,且耗時耗力,會帶來不小的心智負擔。
文本動效參數(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)
- 旋轉 -
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)
表格動效參數(shù)交付示例:
圖片
要解決這個痛點,我們可以考慮將「從動效描述翻譯為動效代碼」的工作通過自動化的方式完成。而要實現(xiàn)這個自動化的流程,首先要解決的就是設計師提供的動效描述沒有統(tǒng)一格式的問題。
最適合用作動效描述統(tǒng)一格式的方案就是Lottie,Lottie是一個基于JSON的動畫文件格式,它可以使用Bodymmovin解析導出Adobe After Effects動畫,并在移動設備上渲染它們。通過它,設計師可以創(chuàng)造和發(fā)布酷炫的動畫,且無需工程師費心的手工重建動畫效果。
它具有以下優(yōu)點:
- 標準化:Lottie的JSON格式中,每個屬性的含義和數(shù)據(jù)類型都很明確,相比于自然語言的描述方式,更加清晰明確。
- 無感知:設計師在AE中完成動效的編輯后,可以直接使用AE的BodyMovin插件導出我們期望Lottie格式動效描述,導出過程不會為設計師引入額外的成本。
- 透明化:Lottie的運行庫是開源的,這意味著我們可以通過它的代碼和文檔完全弄清楚json中每一個字段的具體含義和處理方式。
二、Lottie格式簡介
在進行代碼轉換之前,我們首先來介紹下Lottie的JSON格式。
首先在Lottie格式的Root層,會存儲動畫的全局信息,比如動效的展示寬高,播放幀率,引用的圖片等資源描述以及動畫細節(jié)描述等。
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中動畫的實現(xiàn)方式
為了更好的理解Lottie中的layers和assets的具體含義,我們首先從前端角度簡單了解下設計師是如何在AE中實現(xiàn)動畫,并導出為Lottie的。
AE中進行動畫展示的基礎模塊是圖層(layer),設計師通過在AE中創(chuàng)建圖層的方式來創(chuàng)建動畫元素,而要讓動畫元素動起來,則可以通過在圖層上的不同屬性進行關鍵幀的設置來實現(xiàn)。這樣,通過多個圖層的疊加,以及在每個圖層的不同屬性上設置不同的關鍵幀就可以實現(xiàn)最終的效果。
示例
如下所示的引導小手動效,就可以通過創(chuàng)建四個圖層以及設置每個圖層的位移、旋轉、縮放或透明度的關鍵幀來實現(xiàn)。
詳細動畫信息layers
layers是一個數(shù)組,其中的每一項會描述來自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: 時間縮放系數(shù)
* 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> {
// 起始數(shù)值 (p0)
s: T;
// 結束數(shù)值 (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;
}
圖層的關鍵幀信息中會包含每個關鍵點的屬性數(shù)值,所在幀,該點上的控制緩動曲線的出射控制點和入射控制點,另外,對于位移的動畫,AE還支持路徑運動,在Lottie中的體現(xiàn)就是to和ti兩個參數(shù),它們是和當前控制點相關的路徑貝塞爾曲線的控制點。
圖片
2.2 可復用資產(chǎn) assets
layers里面描述的圖層信息有時會包含對外部資源的引用,比如圖像圖層會引用一張外部圖片,預合成圖層會引用一份預合成。這些被引用的資源描述都會存放在assets里。
關于預合成
預合成圖層是Lottie中一個比較特殊的圖層類型。一般情況下,Lottie是從設計師在AE中編輯的合成來導出的,但就像程序員寫的函數(shù)中可以調(diào)用其他的函數(shù)一樣,合成中也可以使用其他的合成,合成中引用的其他合成,就是預合成圖層,它是該合成的外部資源,因此存放在Lottie的assets屬性里;它的內(nèi)容是另一個合成,因此Lottie里該圖層信息的描述方式和一個單獨的Lottie類似;預合成作為一個單獨的合成,當然也可以引用其他的合成,因此嵌套的預合成也是允許存在的。
在實現(xiàn)預合成圖層中的圖層動畫時,我們不單要關注這個圖層本身的Transform和透明度變化,還要關注它所在的合成被上層合成引用的預合成圖層的Transform和透明度變化。
三、從Lottie導出動效代碼
從上一章的Lottie格式的介紹中,我們了解了Lottie中的動畫描述方式,以及每個動畫元素(圖層)中的關鍵動畫信息,比如開始幀,結束幀,緩動函數(shù)控制點以及屬性關鍵幀的數(shù)值等等。
現(xiàn)在我們已經(jīng)從Lottie中獲得了動效代碼所需的完備信息,可以開始進行動效代碼的生成了。
3.1 CSS代碼生成
逐幀方案
最符合直覺最簡單的從Lottie導出CSS動效代碼的方式可能就是逐幀記錄CSS關鍵幀的方式了。我們可以計算一個圖層從出現(xiàn)到消失每一幀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代碼量更少。
優(yōu)點
- 實現(xiàn)簡單:只需要按照固定間隔采樣并計算圖層的transform和透明度信息并組織為CSS keyframes的形式就可以擬合效果,原理簡單,易于實現(xiàn)。
缺點
- 生成代碼量大:因為是每秒固定間隔采樣關鍵幀,當動畫的總時長較長的時候,采樣的關鍵幀會比較多,導致生成的代碼量也比較大。
- 可讀性差,不易修改:逐幀方案采樣的是每幀的最終transform和透明度,相比原始的Lottie描述,會增加一些冗余信息,不利于人類理解,并且因為采樣的關鍵幀密度比較大且距離近的關鍵幀相關性高,因此導出的CSS代碼很難手動修改,比如一個只包含起點和終點關鍵幀的路徑移動的動畫,在Lottie的json中,只需要修改兩個數(shù)值就可以自然的改變動畫的終點,而要在導出的逐幀CSS中實現(xiàn)同樣的修改則需要修改者修改多個關鍵幀的數(shù)值,且數(shù)值的內(nèi)容需要自行計算才能得到。
逐幀方案雖然可以擬合Lottie中的動畫效果,但有著生成代碼量大和可讀性差,不易修改的缺點,因此只適合時長較短且比較簡單的動效。
關鍵幀方案
那么有沒有什么方式,在保留對Lottie的擬合效果的同時,生成的代碼量更小,且可讀性更好呢?
一種可行的想法是,忠實的還原Lottie中的動畫描述,使生成的CSS keyframes中的關鍵幀以及幀間的緩動函數(shù)等和Lottie中描述的關鍵幀和緩動方式等完全對應。但遺憾的是,CSS的動畫描述方式和Lottie的動畫描述方式并不能直接對應,要將Lottie的關鍵幀動畫描述方式映射為CSS的關鍵幀動畫描述方式,我們需要做一些中間操作抹平它們的差別。
「Lottie和CSS關鍵幀動畫描述方式的差別」
從每一個幀動畫信息的描述方式來說,Lottie中的動畫描述基本都在關鍵幀信息中進行描述,包括關鍵幀對應的時間(幀數(shù)),屬性數(shù)值,時間樣條曲線(三次貝塞爾控制點)和路徑樣條曲線(應用在位移的三次貝塞爾控制點)。
而在CSS的動畫描述中,關鍵幀只描述對應的時間(百分比)和屬性數(shù)值,時間樣條曲線在在關鍵幀外的animation-easing-func里描述,路徑樣條曲線要直觀實現(xiàn)更是需要通過支持性不高的offset-path 和 offset-distance / motion-path 和 motion-offset 來實現(xiàn),這樣的差別導致CSS的動畫描述方式不如Lottie中的描述方式靈活。
從不同屬性的動畫信息的描述方式來說,Lottie中的位移、旋轉、縮放和透明度變化分別使用不同的屬性來進行描述,如果某個屬性的不同維度需要不同的關鍵幀分布或時間插值方式來進行描述,還可以更進一步細分。比如,縮放的動畫可以s屬性來進行描述,如果2維情況下的x軸和y軸需要不同關鍵幀和插值方式,則s屬性可以被拆分為sx 和 sy 兩個獨立屬性,各自不相關的描述x軸和y軸的縮放動畫。
而在CSS中,位移、旋轉和縮放的描述都由transform屬性承接,位移、旋轉和縮放的順序和數(shù)量也不像常見的AE、Unity等軟件那樣進行約束,這讓單個Dom上的transform屬性在描述特定靜態(tài)狀態(tài)或由js進行修改達成動態(tài)效果時的描述能力上限很高,但對于使用CSS @keyframes制作動畫時則會帶來災難性的屬性耦合問題。
「示例」
考慮這樣的一個情況,一張圖片有一個總長為100幀的元素動畫,元素的2D旋轉角度在第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插值,這樣的動畫用AE可以簡單的實現(xiàn),也可以自然的導出為Lottie格式描述,但如果要用CSS動畫來描述的話,因為使用了三種時間插值函數(shù)超出了單個@keyframes的描述能力,無法使用一個動畫來進行描述;又因為動畫同時作用于縮放和旋轉這些在CSS中使用同一個屬性描述的變換,在一個Dom上使用多個動畫描述又會引入屬性值互相覆蓋的問題。
「實現(xiàn)方案」
總之,在將Lottie動畫轉換成CSS關鍵幀動畫時,主要有兩個需要解決的問題,第一個是不同的transform屬性在CSS動畫中內(nèi)容互相耦合的問題,另一個是同一個屬性的動畫不能通過一組@keyframes應用多種時間插值曲線的問題。
對于第一個問題,我們可以通過多個嵌套的Dom來進行規(guī)避,將不應耦合的屬性動畫放在不同Dom的CSS動畫中進行實現(xiàn)。
對于第二個問題,我們可以將應用了不同時間插值曲線的部分放在不同的@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 。不過考慮到該屬性的覆蓋率,很遺憾還不推薦在現(xiàn)階段應用在實際業(yè)務中。
「路徑的實現(xiàn)方式」
在上面展示的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中的路徑曲線也是由三次貝塞爾曲線來進行描述的,而三次貝塞爾曲線則通過它的兩個控制點進行描述。而根據(jù)貝塞爾曲線的定義,我們可以發(fā)現(xiàn),N維貝塞爾曲線的維度之間是互相獨立的,這意味著2D平面上的曲線路徑可以通過拆分的x軸和y軸位移來進行重現(xiàn),如上面demo中.hash5edafe06_1 和 .hash5edafe06_2 中的內(nèi)容可以重現(xiàn)原Lottie的曲線路徑。
不過需要注意的是,路徑曲線上的時間曲線并不是簡單對應于路徑貝塞爾曲線的變量t , 而是對應于路徑曲線長度的百分比位置,因此路徑曲線上的時間插值曲線并不能完全重現(xiàn),只能盡量擬合。
優(yōu)點
- 關鍵幀的數(shù)量相比逐幀CSS更少,語義更加清晰,方便修改
- 生成的代碼體積更小,可以降低使用者的使用負擔
缺點:對較為復雜的動畫效果,可能會生成需要應用在多個Dom上的動畫代碼,會引入一定的使用成本
Tip:關于貝塞爾曲線
貝塞爾曲線是樣條曲線的一種,它的優(yōu)點是使用靈活和實現(xiàn)直觀,貝塞爾曲線的使用非常廣泛,它被用作一些其他樣條曲線的一部分(如B樣條, 優(yōu)化了高階貝塞爾曲線的耦合問題),也是動效領域的一種通用曲線實現(xiàn)方案,在動畫插值(如CSS關鍵幀插值, 模型動作關鍵幀插值),矢量繪制(如路徑移動, 字體字形描述)等方面均有重要應用。
貝塞爾曲線的一般描述方程如下:
圖片
該描述方程中不存在矩陣計算部分,因此貝塞爾曲線在N維空間中的描述方式都是統(tǒng)一的,且各個維度坐標(e.g. x/y/z)的數(shù)值計算互相獨立。
變量方案
關鍵幀方案生產(chǎn)的代碼可能存在需要多個Dom共同作用來實現(xiàn)一個元素動畫的情況,這是它的最大缺點,而這個問題的根本原因就在于前面提到過的「不同的transform屬性在CSS動畫中內(nèi)容互相耦合」,如果可以將它們解藕,則我們就不會不得不使用多個Dom來避免屬性覆寫,可以簡單的通過一個Dom上使用多個@keyframes來實現(xiàn)目標,避免對應用動畫的元素UI結構的影響。
CSS Houdini API提供的@property 為解藕提供了一種方式:我們可以用它定義諸如 --scaleX , --translateX 之類的CSS屬性,需要動畫的元素并不直接在動畫的關鍵幀中設置transform 或 opacity的值,而是這些屬性的值,然后在CSS動畫的外部將transform 或 opacity 的值用這些屬性的值來進行設置,這樣,就可以在避免耦合的情況下,在同一個Dom中實現(xiàn)復雜的動畫效果了。
如下圖所示的就是使用變量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;
}
}
優(yōu)點
- 可讀性進一步提高
- 不會發(fā)生需要多個Dom來解一個元素上的復雜動畫耦合問題的情況
缺點:CSS的@property 仍屬于實驗性能力,兼容性不好。
3.2 總結
總的來說,最理想的解決方案是變量方案,但因為使用了比較新的CSS功能,所以兼容性不佳。關鍵幀方案適合動畫拆解比較簡單不會引入輔助嵌套Dom的場景或不介意引入輔助Dom的場景。逐幀方案適合動畫持續(xù)時間不長且不需要關鍵幀數(shù)值修改的場景,也可作為兜底的解決方案。
圖片
React Native Animated代碼生成
React Native Animated的描述能力比CSS更強,可以自然映射Lottie中互相獨立的位移、旋轉等非耦合Transform動畫,也可以自然映射Lottie中每一個相鄰關鍵幀之間的段上應用不同時間插值曲線的情況,唯一遜于Lottie動畫描述能力的地方在于路徑動畫的描述上,不過要實現(xiàn)我們上面提到的CSS程度的路徑動畫還原的話,仍是非常簡單的,其實現(xiàn)方式和上面提到的方式并無不同。
如下圖所示的就是使用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代碼的能力已經(jīng)集成到公司內(nèi)部的Vision動效平臺中,作為公司內(nèi)動效整體解決方案的一部分。
平臺中的出碼能力詳細使用方式見:??快手前端動效大揭秘:告別低效,vision平臺來襲!??
圖片
在下期內(nèi)容中,我們將重點介紹Vision 動效平臺在序列幀動效格式轉換方面的能力和流程:動效平臺通過提供多種序列幀格式自動轉換功能,優(yōu)化動效交付流程,提高動效的兼容性和性能。敬請期待!
- END -
