讓網頁應用程式的動畫更上層樓
TL;DR:動畫 Worklet 可讓您編寫在裝置原生影格速率執行的動畫動畫,處理這類額外但不含卡頓的流暢效能 TM,讓動畫更靈活地對抗主執行緒卡頓,並且與主要執行緒資源浪費(而非時間) 連結。「動畫小程式」位於 Chrome Canary 中 (位於「實驗性 Web Platform 功能」旗標下方),且我們預計在 Chrome 71 推出來源試用。可以立即開始使用這項產品,逐步增強功能。
要採用其他 Animation API 嗎?
其實不,這是我們目前已有的延伸,有充分理由! 先從頭說起如果您想為網路上的任何 DOM 元素加上動畫效果,有 2 1⁄2 選擇:適用於簡單的 A 轉 B 轉場效果的 CSS 轉場效果;針對極有循環、較複雜的時間動畫,以及 Web Animation API (WAAPI) 製作幾乎不致複雜的動畫。WAAPI 的支援矩陣看起來很不容易,但進展在路上。在此之前,還會有 polyfill。
這些方法的共通點都是無狀態和時間導向。不過,開發人員嘗試的一些效果並非處於即時導向或無狀態。舉例來說,惡名昭彰的視差捲動器就稱為「捲動式」。想在網路上導入高效能的視差捲動器真的很困難。
無狀態呢?舉例來說,假設 Android 裝置上的 Chrome 網址列如果向下捲動,捲動畫面就會超出檢視畫面。但是,向上捲動的秒數會恢復下來,即使您已經向下捲動頁面也沒問題。動畫不只取決於捲動位置,也取決於先前的捲動方向。為「有狀態」。
另一個問題是設定捲軸樣式。它們極具代表性,且風格不大。如果我想將尼阿貓當做捲軸,該怎麼做? 無論您選擇哪種技術,建立自訂捲軸都欠佳,也容易。
重點是這些事情都很尷尬,而且無法有效實作。大多數情況下,會仰賴事件和/或 requestAnimationFrame
,即使螢幕能以每秒 90 FPS、120 FPS 或更高級別執行,且只將寶貴的主執行緒影格預算投入下來,仍可能讓應用程式達到 60 FPS。
Animation Worklet 可以擴充網頁動畫堆疊的功能,簡化這類效果。開始深入探討前,先讓我們瞭解 動畫的基本基本概念。
動畫和時間軸的入門介紹
WAAPI 和 Animation Worklet 可廣泛運用時間軸,讓您按照所需方式協調動畫和效果。本節將帶您快速複習、認識時間軸,以及時間軸動畫的使用方式。
每份文件都有 document.timeline
。從建立文件時的 0 開始,並計算文件從現有開始後的毫秒數。文件的所有動畫都按照這個時間軸運作。
為了更具體地說明,我們來看看這個 WAAPI 程式碼片段
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
當我們呼叫 animation.play()
時,動畫會使用時間軸的 currentTime
做為開始時間。我們的動畫會延遲 3000 毫秒,這表示當時間軸達到「startTime」時,動畫就會開始 (或變成「有效」)
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
時間長度options. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000`。時間點可控制動畫所處的時間軸!
動畫到達最後一個主要畫面格後,就會跳回第一個主要畫面格,並開始動畫的下一個疊代作業。自設定 iterations: 3
以來,這項程序總共會重複 3 次。如果我們希望動畫永不停止,就會寫入 iterations: Number.POSITIVE_INFINITY
。以下是上述程式碼的結果。
WAAPI 功能極其強大,這個 API 中還有許多其他功能,例如加/減速、起始偏移、主要畫面格權重和填滿行為,都比本文涵蓋的範圍更勝一籌。如要瞭解詳情,建議您參閱這篇文章,瞭解 CSS 秘訣的 CSS 動畫。
撰寫動畫小程式
瞭解時間軸的概念後,我們可以開始瞭解 Animation Worklet,以及它如何讓您使用時間軸打亂!Animation Worklet API 不僅以 WAAPI 為基礎,同時也以可擴充網路為基礎,這個基本基本功能說明瞭 WAAPI 的運作方式。就語法而言,兩者十分相似:
動畫小程式 | 網路應用程式防火牆 |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
差異在於第一個參數,也就是驅動此動畫的 worklet 名稱。
功能偵測
Chrome 是第一個提供這項功能的瀏覽器,因此請確保您的程式碼不會只是 AnimationWorklet
出現在其中。因此,在載入 Worklet 之前,應透過簡單檢查的方式,偵測使用者的瀏覽器是否支援 AnimationWorklet
:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
正在載入 Worklet
小程式是 Houdini 工作小組引進的新概念,可讓許多新的 API 更輕鬆地建構及擴充。我們稍後會詳細介紹工作小程式的細節,但為求簡單起見,您可以暫時將其視為便宜且輕量的執行緒 (例如工作站)。
我們必須確保在宣告動畫之前,已載入名為「passthrough」的工作小程式:
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
這裡是怎樣的地點?我們要使用 AnimationWorklet 的 registerAnimator()
呼叫,將類別註冊為動畫器,並將其命名為「passthrough」。與上述 WorkletAnimation()
建構函式中使用的名稱相同。註冊完成後,addModule()
傳回的承諾會解析,而我們就能開始使用該 Worklet 建立動畫。
針對瀏覽器要轉譯的每個影格,系統會呼叫執行個體的 animate()
方法,傳遞動畫時間軸的 currentTime
,以及目前正在處理的效果。我們只有一個效果:KeyframeEffect
,而我們使用 currentTime
設定效果的 localTime
,因此該動畫器稱為「直通」。使用這段程式碼的程式碼時,上述 WAAPI 和 AnimationWorklet 的行為完全相同,如示範所示。
時間
animate()
方法的 currentTime
參數是我們傳遞至 WorkletAnimation()
建構函式的時間軸 currentTime
。在上一個範例中,我們剛剛將這段時間傳遞至「作用」。不過,這是 JavaScript 程式碼,因此我們可以扭轉時間 💫?
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
我們採用 currentTime
的 Math.sin()
,將該值重新對應至 [0; 2000] 範圍,也就是系統定義效果的時間範圍。現在,動畫看起來會不一樣,而且並未變更主要畫面格或動畫選項。工作小程式程式碼可以任意複雜,可讓您透過程式輔助方式定義效果,以特定順序以及程度播放。
選項之外
您可以重複使用 Worklet 並變更其編號。因此,WorkletAnimation 建構函式會將選項物件傳遞至 Worklet:
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
在此範例中,兩個動畫都是透過相同的程式碼驅動,但選項不同。
讚揚您當地的州!
如我之前所見,動畫的練習題之一,就是要解決的一大問題是有狀態的動畫。允許保留狀態的動畫小工具。但 Worklet 的其中一項核心功能是可遷移至其他執行緒,甚至可以刪除以節省資源,這會一併刪除其狀態。為防止狀態遺失,動畫工作程式會提供掛鉤,此掛鉤可在運動程式刪除「之前」,讓您用來傳回狀態物件。重新建立 Worklet 時,該物件會傳遞至建構函式。在初始建立時,該參數將是 undefined
。
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
每次重新整理這個示範時,都會有 50/50 的機率會旋轉正方形的方向。如果瀏覽器要拆除工作小程式,並將其遷移至其他執行緒,則會在建立時又發出 Math.random()
呼叫,這可能會導致方向突然改變。為確保不會發生這種情形,我們會傳回隨機選擇的動畫方向做為「狀態」state,並在建構函式中使用該動畫 (如有提供)。
引領時空探索:ScrollTimeline
如上一節所示,AnimationWorklet 可讓我們透過程式輔助方式定義時間軸上移對動畫效果的影響。但到目前為止,時間軸始終為 document.timeline
,可追蹤時間。
ScrollTimeline
會開啟新可能性,讓您能夠透過捲動 (而非時間) 播放動畫。我們會針對這個示範重複使用第一個「直通」小程式:
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
我們要建立新的 ScrollTimeline
,而不是傳遞 document.timeline
。您可能猜到,ScrollTimeline
不會使用時間,而 scrollSource
的捲動位置可在工作小程式中設定 currentTime
。捲動至頂端 (或左側) 表示 currentTime = 0
,而一路捲動至底部 (或右側) 則會將 currentTime
設為 timeRange
。在這個示範中捲動方塊時,可以控制紅色方塊的位置。
如果您要使用不會捲動的元素建立 ScrollTimeline
,時間軸的 currentTime
將為 NaN
。因此,在考量回應式設計時,建議您一律為 currentTime
做好準備,以便使用 NaN
。通常預設為 0 值。
連結動畫與捲動位置是很久以來的種種之處,但從來無法像 CSS3D 的精巧做法達成此目標。動畫 Worklet 能以直接簡單的方式實作這些效果,同時展現卓越效能。舉例來說,如這個示範所示的視差捲動效果,現在只要幾行程式碼就能定義捲動式動畫。
深入解析
小程式
小程式是具有獨立範圍和非常小 API 介面的 JavaScript 結構定義。小型 API 介面可讓瀏覽器更積極最佳化,特別是在低階裝置上。此外,工作小程式並未繫結至特定事件迴圈,但可視需要在執行緒之間移動。這對 AnimationWorklet 來說尤其重要。
合成器 NSync
您可能已經知道,某些 CSS 屬性可快速建立動畫,有些則無法。有些屬性只需要在 GPU 上處理一些動畫工作,有些屬性則強制瀏覽器重新調整整份文件的版面配置。
在 Chrome (與其他許多瀏覽器相同) 中,我們有一個稱為「合成器」的程序,它是相當簡化的,我在這裡可以大幅簡化這些結構,然後安排層和紋理,然後盡可能使用 GPU 定期更新畫面,盡可能加快螢幕更新速度 (通常為 60 Hz)。根據要建立動畫效果的 CSS 屬性而定,瀏覽器可能只需要該合成器就能運作,而其他屬性則需要執行版面配置,而版面配置是只有主執行緒可執行的作業。視您想製作動畫效果的屬性而定,動畫工作程式可能會繫結至主執行緒,或是與合成器同步在另一個執行緒中執行。
戴在手腕上
GPU 是高度競爭的資源,因此通常只會有多個分頁共用一個合成器程序。如果合成器遭到阻斷,整個瀏覽器都會停滯,無法回應使用者輸入內容。必須完全不用付費。那麼,如果 Worklet 無法及時傳送合成器需要的資料以轉譯影格,會發生什麼事呢?
如果發生這種情況,請依規格允許將 Worklet 轉換為「slip」。它位於合成器後方,且合成器可重複使用最後一個影格的資料,保持影格速率。視覺上看起來像是卡頓,但最大的差別在於瀏覽器仍在回應使用者輸入內容。
結論
AnimationWorklet 有許多面向,可為網路帶來好處。 顯而易見的好處是,您可以進一步掌控動畫,並透過新的方式啟動動畫,讓網頁的視覺擬真度更上層樓。不過,API 設計也可讓您的應用程式更能抵禦卡頓,同時獲得所有新功能。
動畫 Worklet 目前為 Canary 版,我們的目標是讓 Chrome 71 的來源試用。我們非常期待您提供絕佳的全新網頁體驗,也希望瞭解該如何改進。另外還有 polyfill:當中隨附相同的 API,但並未提供效能隔離。
請注意,CSS 轉場效果和 CSS 動畫仍是有效的選項,對於基本動畫來說,較簡單。但如果您想繼續精進,AnimationWorklet 可做為您的後盾!