Houdini's 動畫小程式

大幅提升網頁應用程式的動畫效能

簡而言之:您可以使用 Animation Worklet 編寫命令式動畫,以裝置的原生影格速率執行,讓動畫更加流暢,不會出現卡頓情形。此外,這類動畫也更不容易受到主要執行緒卡頓的影響,而且可以連結至捲動,而非時間。動畫 Worklet 位於 Chrome Canary 版 (「Experimental Web Platform features」旗標後方),我們預計在 Chrome 71 版中進行來源試用。您今天就能開始使用,逐步改善體驗。

其他動畫 API?

其實不是,這是我們現有功能的延伸,而且有充分的理由! 讓我們從頭開始。如要在網頁上為任何 DOM 元素製作動畫,目前有 2 ½ 種選擇:CSS 轉場效果,適用於簡單的 A 到 B 轉場效果;CSS 動畫,適用於可能循環的複雜時間軸動畫;Web Animations API (WAAPI),適用於幾乎任意複雜的動畫。WAAPI 的支援矩陣看起來相當不樂觀,但情況正在好轉。在此之前,請使用 polyfill

這些方法的共通點是無狀態且以時間為準。但開發人員嘗試的部分效果既非時間驅動,也非無狀態。舉例來說,惡名昭彰的視差捲動器顧名思義就是由捲動驅動。令人意外的是,如今要在網路上導入高效能的視差捲動器相當困難。

無狀態呢?以 Android 上的 Chrome 網址列為例,如果向下捲動,橫幅就會從畫面上消失。但只要向上捲動,即使只捲動到頁面一半,工具列也會重新出現。動畫不僅取決於捲動位置,也取決於先前的捲動方向。這是有狀態的。

另一個問題是捲軸樣式。眾所皆知,這些元素無法設定樣式,或至少無法充分設定樣式。如果我想要彩虹貓做為捲軸,該怎麼辦?無論選擇哪種技術,建立自訂捲軸都不容易,而且效能也不佳。

重點是,這些做法都很不方便,而且很難有效率地實作,甚至不可能。其中大多數都依賴事件和/或 requestAnimationFrame,即使螢幕能夠以 90fps、120fps 以上的影格速率執行,且只佔用一小部分珍貴的主執行緒影格預算,仍可能將影格速率限制在 60fps。

動畫 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 thedurationoptions. 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 功能強大,還有許多其他功能,例如緩和效果、起始偏移、關鍵影格權重和填滿行為,這些都超出本文範圍。如要瞭解詳情,建議閱讀 這篇 CSS Tricks 上的 CSS 動畫文章

撰寫動畫 Worklet

現在我們已瞭解時間軸的概念,可以開始瞭解 Animation Worklet,以及如何使用這項功能調整時間軸!Animation Worklet API 不僅以 WAAPI 為基礎,就可擴充網頁而言,也是說明 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」的 worklet,再宣告動畫:

// 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() 傳回的 Promise 會解析,我們就能開始使用該工作單建立動畫。

系統會為瀏覽器要算繪的每個影格呼叫例項的 animate() 方法,並傳遞動畫時間軸的 currentTime,以及目前正在處理的特效。我們只有一個效果 (KeyframeEffect),並使用 currentTime 設定效果的 localTime,因此這個動畫師稱為「passthrough」。有了這個工作單的程式碼,WAAPI 和 AnimationWorklet 的行為完全相同,如示範所示。

時間

animate() 方法的 currentTime 參數是時間軸的 currentTime,我們已將時間軸傳遞至 WorkletAnimation() 建構函式。在前一個範例中,我們只是將時間傳遞至效果。但由於這是 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)
      );
    }
  }
);

我們會取得 Math.sin()currentTime,然後將該值重新對應至 [0; 2000] 範圍,這是我們定義效果的時間範圍。現在動畫看起來很不一樣,但我們沒有變更主要畫面格或動畫選項。小程式碼可任意複雜,並允許您以程式輔助方式定義要播放哪些效果、播放順序和程度。

選項優先於選項

您可能想重複使用工作站,並變更其號碼。因此,您可以使用 WorkletAnimation 建構函式,將選項物件傳遞至工作單:

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();

在這個範例中,兩個動畫都是以相同程式碼驅動,但選項不同。

請告訴我你所在的州/省!

如先前所述,動畫工作單要解決的主要問題之一,就是有狀態的動畫。動畫工作單元可保留狀態。不過,工作單的核心功能之一是可遷移至其他執行緒,甚至可銷毀以節省資源,這也會銷毀工作單的狀態。為避免狀態遺失,動畫工作單元提供一個在工作單元遭到刪除「之前」呼叫的 Hook,可用於傳回狀態物件。重新建立工作單時,系統會將該物件傳遞至建構函式。首次建立時,該參數會是 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% 的機率會朝某個方向旋轉。如果瀏覽器要拆除工作單元並遷移至其他執行緒,建立時會再次呼叫 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 的捲動位置,在 worklet 中設定 currentTime。捲動至最上方 (或最左側) 時,currentTime = 0 會設為 0,而捲動至最下方 (或最右側) 時,currentTime 則會設為 timeRange。如果您捲動這個示範中的方塊,即可控制紅色方塊的位置。

如果您使用不會捲動的元素建立 ScrollTimeline,時間軸的 currentTime 會是 NaN。因此,請務必做好準備,將 NaN 視為 currentTime,特別是考慮到回應式設計時。通常預設為 0 較為合理。

將動畫與捲動位置連結是長期以來的目標,但一直無法達到這個精確程度 (除了使用 CSS3D 的變通方法)。Animation Worklet 可讓您以簡單直接的方式實作這些效果,同時維持高效能。舉例來說,這個示範的視差捲動效果顯示,現在只要幾行程式碼,就能定義捲動驅動的動畫。

深入解析

Worklet

Worklet 是 JavaScript 環境,具有獨立範圍和極小的 API 介面。API 介面較小,因此瀏覽器可以進行更積極的效能最佳化,尤其是在低階裝置上。此外,工作單不會繫結至特定事件迴圈,但可視需要於執行緒之間移動。這對 AnimationWorklet 來說尤其重要。

Compositor NSync

您可能知道某些 CSS 屬性可以快速製作動畫,其他則不行。有些屬性只需要在 GPU 上進行一些作業即可產生動畫,有些則會強制瀏覽器重新排版整個文件。

在 Chrome 中 (以及許多其他瀏覽器),我們有一個稱為合成器的程序,其工作是排列圖層和紋理,然後盡可能規律地使用 GPU 更新畫面,最好是畫面更新速度越快越好 (通常為 60Hz)。視動畫的 CSS 屬性而定,瀏覽器可能只需要讓合成器執行作業,其他屬性則需要執行版面配置,而這項作業只能由主執行緒執行。視您打算製作動畫的屬性而定,動畫工作單會繫結至主執行緒,或在與合成器同步的獨立執行緒中執行。

輕微處罰

GPU 是競爭激烈的資源,因此通常只有一個合成器程序,可能會在多個分頁之間共用。如果合成器遭到封鎖,整個瀏覽器就會停止運作,無法回應使用者輸入內容。請務必避免這種情況。如果工作單元無法及時提供合成器所需的資料,導致影格無法順利算繪,會發生什麼情況?

如果發生這種情況,根據規格,工作單可以「滑動」。這會落後於合成器,而合成器可以重複使用上一個影格的資料,以維持影格速率。在視覺上,這看起來會像是卡頓,但最大的差異在於瀏覽器仍會回應使用者輸入。

結論

AnimationWorklet 有許多面向,可為網路帶來許多好處。 顯而易見的好處是,您可以進一步控制動畫,並以新方式驅動動畫,為網頁帶來全新層次的視覺保真度。但 API 設計也允許您讓應用程式更不容易發生延遲,同時存取所有新功能。

動畫 Worklet 目前位於 Canary 版,我們目標是在 Chrome 71 中進行初期試用。我們很期待你打造出優質的全新網站體驗,並分享需要改進的地方。此外,還有一個 polyfill 提供相同的 API,但不提供效能隔離功能。

請注意,CSS 轉場效果和 CSS 動畫仍是有效的選項,對於基本動畫來說,這些選項可能簡單許多。但如果需要更精緻的動畫,AnimationWorklet 就能派上用場!