圖片來源:aescripts.com/bodymovin/javascript
本文做者:青舟css
Lottie 是一個複雜幀動畫的解決方案,它提供了一套從設計師使用 AE(Adobe After Effects)到各端開發者實現動畫的工具流。在設計師經過 AE 完成動畫後,可使用 AE 的擴展程序 Bodymovin 導出一份 JSON 格式的動畫數據,而後開發同窗能夠經過 Lottie 將生成的 JSON 數據渲染成動畫。html
import lottie from 'lottie-web';
import animationJsonData from 'xxx-demo.json'; // json 文件
const lot = lottie.loadAnimation({
container: document.getElementById('lottie'),
renderer: 'svg',
loop: true,
autoplay: false,
animationData: animationJsonData,
});
// 開始播放動畫
lot.play();
複製代碼
更多動畫 JSON 模板能夠查看 lottiefiles.com/前端
筆者本身製做了 Lottie Demo -> 點我預覽java
scale
屬性值從 100% 變到 50%。scale
屬性值從 50% 變到 100%,完成動畫。經過 Bodymovin 插件導出 JSON 數據結構以下圖所示:css3
詳細 JSON 信息能夠經過 Demo 查看,JSON 信息命名比較簡潔,第一次看可能難以理解。接下來結合筆者本身製做的 Demo 進行解讀。git
左側爲使用 AE 新建動畫合成須要填入的信息,和右面第一層 JSON 信息對應以下:github
w
和 h
: 寬 200、高 200v
:Bodymovin 插件版本號 4.5.4fr
:幀率 30fpsip
和 op
:開始幀 0、結束幀 180assets
:靜態資源信息(如圖片)layers
:圖層信息(動畫中的每個圖層以及動做信息)ddd
:是否爲 3dcomps
:合成圖層其中 fr
、ip
、op
在 Lottie 動畫過程當中尤其重要,前面提到咱們的動畫 Demo 是 0 - 6s,可是 Lottie 是以幀率計算動畫時間的。Demo 中設置的幀率爲 30fps,那麼 0 - 6s 也就等同於 0 - 180 幀。web
理解 JSON 外層信息後,再來展開看下 JSON 中 layers
的具體信息,首先 demo 製做動畫細節以下:json
主要是 3 個區域:
對應上圖動畫製做信息,即可以對應到 JSON 中的 layers
了。以下圖所示:
接下來再看 ks
(變化屬性) 中的 s
展開,也就是縮放信息。
其中:
t
表明關鍵幀數s
表明變化前(圖層爲二維,因此第 3 個值 固定爲 100)。e
表明變化後(圖層爲二維,因此第 3 個值 固定爲 100)。前面簡單理解了 JSON 的數據意義,那麼 Lottie 是如何把 JSON 數據動起來的呢?接下來結合 Demo 的 Lottie 源碼閱讀,只會展現部分源碼,重點是理清思路便可,不要執着源代碼。
如下源碼介紹主要分爲 2 大部分:
如 Demo 所示,Lottie 經過 loadAnimation
方法來初始化動畫。渲染器初始化流程以下:
function loadAnimation(params){
// 生成當前動畫實例
var animItem = new AnimationItem();
// 註冊動畫
setupAnimation(animItem, null);
// 初始化動畫實例參數
animItem.setParams(params);
return animItem;
}
function setupAnimation(animItem, element) {
// 監聽事件
animItem.addEventListener('destroy', removeElement);
animItem.addEventListener('_active', addPlayingCount);
animItem.addEventListener('_idle', subtractPlayingCount);
// 註冊動畫
registeredAnimations.push({elem: element, animation:animItem});
len += 1;
}
複製代碼
AnimationItem
這個類是 Lottie 動畫的基類,loadAnimation
方法會先生成一個 AnimationItem
實例並返回,開發者使用的 配置參數和方法 都是來自於這個類。
生成 animItem
實例後,調用 setupAnimation
方法。這個方法首先監聽了 destroy
、_active
、_idle
三個事件等待被觸發。因爲能夠多個動畫並行,所以定義了全局的變量 len
、registeredAnimations
等,用於判斷和緩存已註冊的動畫實例。
接下來調用 animItem
實例的 setParams
方法初始化動畫參數,除了初始化 loop
、 autoplay
等參數外,最重要的是選擇渲染器。以下:
AnimationItem.prototype.setParams = function(params) {
// 根據開發者配置選擇渲染器
switch(animType) {
case 'canvas':
this.renderer = new CanvasRenderer(this, params.rendererSettings);
break;
case 'svg':
this.renderer = new SVGRenderer(this, params.rendererSettings);
break;
default:
// html 類型
this.renderer = new HybridRenderer(this, params.rendererSettings);
break;
}
// 渲染器初始化參數
if (params.animationData) {
this.configAnimation(params.animationData);
}
}
複製代碼
Lottie 提供了 SVG、Canvas 和 HTML 三種渲染模式,通常使用第一種或第二種。
SVG 渲染器支持的特性最多,也是使用最多的渲染方式。而且 SVG 是可伸縮的,任何分辨率下不會失真。
Canvas 渲染器就是根據動畫的數據將每一幀的對象不斷重繪出來。
HTML 渲染器受限於其功能,支持的特性最少,只能作一些很簡單的圖形或者文字,也不支持濾鏡效果。
每一個渲染器均有各自的實現,複雜度也各有不一樣,可是動畫越複雜,其對性能的消耗也就越高,這些要看實際的情況再去判斷。渲染器源碼在 player/js/renderers/ 文件夾下,本文 Demo 只分析 SVG 渲染動畫的實現。因爲 3 種 Renderer 都是基於 BaseRenderer
類,因此下文中除了 SVGRenderer
也會出現 BaseRenderer
類的方法。
確認使用 SVG 渲染器後,調用 configAnimation
方法初始化渲染器。
AnimationItem.prototype.configAnimation = function (animData) {
if(!this.renderer) {
return;
}
// 總幀數
this.totalFrames = Math.floor(this.animationData.op - this.animationData.ip);
this.firstFrame = Math.round(this.animationData.ip);
// 渲染器初始化參數
this.renderer.configAnimation(animData);
// 幀率
this.frameRate = this.animationData.fr;
this.frameMult = this.animationData.fr / 1000;
this.trigger('config_ready');
// 加載靜態資源
this.preloadImages();
this.loadSegments();
this.updaFrameModifier();
// 等待靜態資源加載完畢
this.waitForFontsLoaded();
};
複製代碼
在這個方法中將會初始化更多動畫對象的屬性,好比總幀數 totalFrames
、幀率 frameMult
等。而後加載一些其餘資源,好比圖像、字體等。以下圖所示:
同時在 waitForFontsLoaded
方法中等待靜態資源加載完畢,加載完畢後便會調用 SVG 渲染器的 initItems
方法繪製動畫圖層,也就是將動畫繪製出來。
AnimationItem.prototype.waitForFontsLoaded = function(){
if(!this.renderer) {
return;
}
// 檢查加載完畢
this.checkLoaded();
}
AnimationItem.prototype.checkLoaded = function () {
this.isLoaded = true;
// 初始化全部元素
this.renderer.initItems();
setTimeout(function() {
this.trigger('DOMLoaded');
}.bind(this), 0);
// 渲染第一幀
this.gotoFrame();
// 自動播放
if(this.autoplay){
this.play();
}
};
複製代碼
在 checkLoaded
方法中能夠看到,經過 initItems
初始化全部元素後,便經過 gotoFrame
渲染第一幀,若是開發者配置了 autoplay
爲 true
,則會直接調用 play
方法播放。這裏有個印象就好,會在後面詳細講。接下來仍是先看 initItems
實現細節。
initItems
方法主要是調用 buildAllItems
建立全部圖層。buildItem
方法又會調用 createItem
肯定具體圖層類型,這裏的方法源碼中拆分較細,本文只保留了 createItem
方法,其餘感興趣能夠查看源碼細節。
在製做動畫時,設計師操做的圖層元素有不少種,好比圖片、形狀、文字等等。因此 layers
中每一個圖層會有一個字段 ty
來區分。結合 createItem
方法來看,一共有如下 8 中類型。
BaseRenderer.prototype.createItem = function(layer) {
// 根據圖層類型,建立相應的 svg 元素類的實例
switch(layer.ty){
case 0:
// 合成
return this.createComp(layer);
case 1:
// 固態
return this.createSolid(layer);
case 2:
// 圖片
return this.createImage(layer);
case 3:
// 兜底空元素
return this.createNull(layer);
case 4:
// 形狀
return this.createShape(layer);
case 5:
// 文字
return this.createText(layer);
case 6:
// 音頻
return this.createAudio(layer);
case 13:
// 攝像機
return this.createCamera(layer);
}
return this.createNull(layer);
};
複製代碼
因爲筆者以及大多數開發者,都不是專業的 AE 玩家,所以沒必要不過糾結每種類型是什麼,理清主要思路便可。結合筆者的 Demo ,只有一個圖層,而且圖層的 ty
爲 4 。是一個 Shape
形狀圖層,所以在初始化圖層過程當中只會執行 createShape
方法。
其餘圖層類型的渲染邏輯,如 Image
、Text
、Audio
等等,每一種元素的渲染邏輯都實如今源碼 player/js/elements/ 文件夾下,具體實現邏輯這裏就不進行展開了,感興趣的同窗自行查看。
接下來即是執行 createShape
方法,初始化元素相關屬性。
除了一些細節的初始化方法,其中值得注意的是 initTransform
方法。
initTransform: function() {
this.finalTransform = {
mProp: this.data.ks
? TransformPropertyFactory.getTransformProperty(this, this.data.ks, this)
: {o:0},
_matMdf: false,
_opMdf: false,
mat: new Matrix()
};
},
複製代碼
利用 TransformPropertyFactory
對 transform
初始化,結合 Demo 第 0 幀,對應以下:
transform: scale(1);
opacity: 1;
複製代碼
那麼爲何在初始化渲染圖層時,須要初始化 transform
和 opacity
呢?這個問題會在 3.4 小節中進行回答。
在分析 Lottie 源碼動畫播放前,先來回憶下。筆者 Demo 的動畫設置:
scale
屬性值從 100% 變到 50%。scale
屬性值從 50% 變到 100%。若是按照這個設置,3s 進行一次改變的話,那動畫就過於生硬了。所以設計師設置了幀率爲 30fps ,意味着每隔 33.3ms 進行一次變化,使得動畫不會過於僵硬。那麼如何實現這個變化,即是 3.3 小節提到的 transform
和 opacity
。
在 2.2 小節中提到的 5 個變化屬性(錨點、位置、縮放、旋轉、不透明度)。其中不透明度經過 CSS 的 opacity
來控制,其餘 4 個(錨點、位置、縮放、旋轉)則經過 transform
的 matrix
來控制。筆者的 Demo 中實際上初始值以下:
transform: matrix(1, 0, 0, 1, 100, 100);
/* 上文的 transform: scale(1); 只是爲了方便理解*/
opacity: 1;
複製代碼
這是由於不管是旋轉仍是縮放等屬性,本質上都是應用 transform
的 matrix()
方法實現的,所以 Lottie 統一使用 matrix
處理。平時開發者使用的相似於 transform: scale
這種表現形式,只是由於更容易理解,記憶與上手。 matrix
相關知識點能夠學習張鑫旭老師的 理解CSS3 transform中的Matrix。
因此 Lottie 動畫播放流程可暫時小結爲:
transform
和 opacity
transform
和 opacity
並修改 DOM然而 Lottie 如何控制 30fps 的時間間隔呢?若是設計師設置 20fps or 40fps 怎麼處理?能夠經過 setTimeout
、setInterval
實現嗎?帶着這個問題看看源碼是如何處理的,如何實現一個通用的解決方案。
Lottie 動畫播放主要是使用 AnimationItem
實例的 play
方法。若是開發者配置了 autoplay
爲 true
,則會在全部初始化工做準備完畢後(3.2 小節有說起),直接調用 play
方法播放。不然由開發者主動調用 play
方法播放。
接下來從 play
方法瞭解一下整個播放流程的細節:
AnimationItem.prototype.play = function (name) {
this.trigger('_active');
};
複製代碼
去掉多餘代碼, play
方法主要是觸發了 _active
事件,這個 _active
事件即是在 3.1 小節初始化時註冊的。
animItem.addEventListener('_active', addPlayingCount);
function addPlayingCount(){
activate();
}
function activate(){
// 觸發第一幀渲染
window.requestAnimationFrame(first);
}
複製代碼
觸發後經過調用 requestAnimationFrame 方法,不斷的調用 resume
方法來控制動畫。
function first(nowTime){
initTime = nowTime;
// requestAnimationFrame 每次都進行計算修改 DOM
window.requestAnimationFrame(resume);
}
複製代碼
前文提到的動畫參數:
requestAnimationFrame
在正常狀況下能達到 60 fps(每隔 16.7ms 左右)。那麼 Lottie 如何保證動畫按照 30 fps (每隔 33.3ms)流暢運行呢。這個時候咱們要轉化下思惟,設計師但願按照每隔 33.3ms 去計算變化,那也能夠經過 requestAnimationFrame
方法,每隔 16.7ms 去計算,也能夠計算動畫的變化。只不過計算的更細緻而已,並且還會使得動畫更流暢,這樣不管是 20fps or 40fps 均可以處理了,來看下源碼是如何處理的。
在不斷調用的 resume
方法中,主要邏輯以下:
function resume(nowTime) {
// 兩次 requestAnimationFrame 間隔時間
var elapsedTime = nowTime - initTime;
// 下一次計算幀數 = 上一次執行的幀數 + 本次間隔的幀數
// frameModifier 爲幀率( fr / 1000 = 0.03)
var nextValue = this.currentRawFrame + value * this.frameModifier;
this.setCurrentRawFrameValue(nextValue);
initTime = nowTime;
if(playingAnimationsNum && !_isFrozen) {
window.requestAnimationFrame(resume);
} else {
_stopped = true;
}
}
AnimationItem.prototype.setCurrentRawFrameValue = function(value){
this.currentRawFrame = value;
// 渲染當前幀
this.renderFrame();
};
複製代碼
resume
方法:
首先會計算當前時間和上次時間的 diff
時間。
以後計算動畫開始到如今的時間的當前幀數。注意這裏的幀數只是相對 AE 設置的一個計算單位,能夠有小數。
最後經過 renderFrame()
方法更新當前幀對應的 DOM 變化。
舉例說明:
假設上一幀爲 70.25 幀,本次 requestAnimationFrame
間隔時間爲 16.78 ms,那麼:
當前幀數:70.25 + 16.78 * 0.03 = 70.7534幀
複製代碼
因爲 70.7534 幀在 Demo 中的 0 - 90 幀動畫範圍內,所以幀比例(表明動畫運行時間百分比)的計算以下:
幀比例:70.7534 / 90 = 0.786148889
複製代碼
0 - 90 幀的動畫爲圖層從 100% 縮放至 50% ,由於僅計算 50% 的變化,因此縮放到以下:
縮放比例: 100 - (50 * 0.781666)= 60.69255555%
複製代碼
對應計算代碼在 TransformPropertyFactory
類中:
// 計算百分比
perc = fnc((frameNum - keyTime) / (nextKeyTime - keyTime ));
endValue = nextKeyData.s || keyData.e;
// 計算值
keyValue = keyData.s[i] + (endValue[i] - keyData.s[i]) * perc;
複製代碼
其中 fnc
爲計算函數,若是設置了貝塞爾運動曲線函數,那麼 fnc
也會相應修改計算規則。當前 Demo 爲了方便理解,採用的是線性變化。具體源碼感興趣的同窗能夠自行查看。
計算好當前 scale
的值後,再利用 TransformPropertyFactory
計算好當前對應的 transform
的 matrix
值,而後修改對應 DOM 元素上的 CSS 屬性。這樣經過 requestAnimationFrame
不停的計算幀數,再計算對應的 CSS 變化,在必定的時間內,便實現了動畫。播放流程以下:
幀數計算這裏須要時刻記住,在 Lottie 中,把 AE 設置的幀數做爲一個計算單位,Lottie 並非根據設計師設置的 30fps(每隔 33.3ms) 進行每一次變化,而是根據 requestAnimationFrame
的間隔(每隔 16.7ms 左右)計算了更細緻的變化,保證動畫的流暢運行。
沒有經過 setTimeout
、setInterval
實現,是由於它們都有各自的缺點,這裏就不展開了,你們自行查閱資料。requestAnimationFrame
採用系統時間間隔,保持最佳繪製效率,讓動畫可以有一個統一的刷新機制,從而節省系統資源,提升系統性能,改善視覺效果。
雖然咱們瞭解了 Lottie 的實現原理,可是在實際應用中也有一些優點和不足,要按照實際狀況進行取捨。
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!