Houdini's 動畫小程式

讓網頁應用程式的動畫更上層樓

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'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 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)
      );
    }
  }
);

我們採用 currentTimeMath.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 可做為您的後盾!