圖片來源: https://aescripts.com/bodymovin/本文做者:青舟javascript
Lottie 是一個複雜幀動畫的解決方案,它提供了一套從設計師使用 AE(Adobe After Effects)到各端開發者實現動畫的工具流。在設計師經過 AE 完成動畫後,可使用 AE 的擴展程序 Bodymovin 導出一份 JSON 格式的動畫數據,而後開發同窗能夠經過 Lottie 將生成的 JSON 數據渲染成動畫。css
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 模板能夠查看 https://lottiefiles.com/html
筆者本身製做了 Lottie Demo -> 點我預覽前端
scale
屬性值從 100% 變到 50%。scale
屬性值從 50% 變到 100%,完成動畫。
經過 Bodymovin 插件導出 JSON 數據結構以下圖所示:java
詳細 JSON 信息能夠經過 Demo 查看,JSON 信息命名比較簡潔,第一次看可能難以理解。接下來結合筆者本身製做的 Demo 進行解讀。css3
左側爲使用 AE 新建動畫合成須要填入的信息,和右面第一層 JSON 信息對應以下:git
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 幀。github
理解 JSON 外層信息後,再來展開看下 JSON 中 layers
的具體信息,首先 demo 製做動畫細節以下:web
主要是 3 個區域:json
對應上圖動畫製做信息,即可以對應到 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 三種渲染模式,通常使用第一種或第二種。
每一個渲染器均有各自的實現,複雜度也各有不一樣,可是動畫越複雜,其對性能的消耗也就越高,這些要看實際的情況再去判斷。渲染器源碼在 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
時間。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!