Houdini's 動畫小程式

大幅提升 webapp 的動畫效果

重點摘要:Animation Worklet 可讓您編寫以裝置原生影格率執行的命令式動畫,以便提供更流暢的動畫效果,並讓動畫更能抵抗主執行緒的卡頓情形,且可連結至捲動動作,而非時間。動畫 Worklet 已在 Chrome Canary 中推出 (位於「Experimental Web Platform features」標記後方),我們也正規劃 Chrome 71 的來源試用。您可以立即開始使用漸進式強化功能。

其他 Animation API?

事實上,這只是我們現有的工具的延伸,而且有充分的理由! 讓我們從頭開始。如果您想在現今網站上為任何 DOM 元素製作動畫,有 2 1/2 種選擇:CSS 轉場可用於簡單的 A 到 B 轉場;CSS 動畫可用於可能會循環且更複雜的時間軸動畫;Web Animations API (WAAPI) 可用於幾乎任意複雜度的動畫。WAAPI 的支援矩陣看起來相當嚴重,但情況正在改善。在此之前,您可以使用polyfill

這些方法的共通點是,它們都是無狀態且以時間為驅動。不過,開發人員嘗試的某些效果既不是時間導向,也沒有狀態。舉例來說,眾所皆知的視差捲軸,顧名思義,就是捲動驅動。在現今的網站上實作高效的視差捲動效果,難度相當高。

那麼無狀態呢?舉例來說,想想 Android 版 Chrome 的網址列。如果向下捲動頁面,橫幅就會從畫面上消失,但只要您向上捲動,就會重新顯示,即使您已在該頁面中向下捲動一半也一樣。動畫不僅取決於捲動位置,也取決於您先前的捲動方向。屬性有狀態

另一個問題是捲軸樣式。它們是出了名的難以設定樣式,或至少不夠容易設定樣式。如果我想將 Nyan Cat 做為捲軸,該怎麼做?無論您選擇哪種方法,建構自訂捲軸的效能都不會太好,而且也不簡單

但重點是阻礙,難以有效實作。其中大多數都依賴事件和/或 requestAnimationFrame,因此即使螢幕可以 90fps、120fps 或更高的速度運作,並使用寶貴的主執行緒影格預算的一小部分,仍可能維持 60fps。

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 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 功能非常強大,而這個 API 還有許多其他功能,例如加/減速設定、主要畫面格粗細和填滿行為,都會影響本文的討論範圍。如要進一步瞭解,建議您參閱 CSS Animations on CSS Tricks 的這篇文章

編寫動畫工作區

瞭解時間軸的概念後,我們可以開始認識「動畫 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 會存在。因此,在載入工作區塊之前,我們應透過簡單的檢查,偵測使用者的瀏覽器是否支援 AnimationWorklet

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

載入工作單元

Worklet 是 Houdini 專案小組推出的新概念,可讓許多新的 API 更容易建構及擴充。我們稍後會進一步說明 worklet 的詳細資訊,但為了簡單起見,您可以先將 worklet 視為廉價且輕量級的執行緒 (例如 worker)。

我們需要先確認已載入名為「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() 傳回的承諾會解析,我們就可以開始使用該 worklet 建立動畫。

瀏覽器要算繪的每個影格都會呼叫例項的 animate() 方法,並傳遞動畫時間軸的 currentTime 以及目前正在處理的效果。我們只有一個效果,也就是 KeyframeEffect,並使用 currentTime 設定效果的 localTime,因此這個動畫稱為「快速導入」。使用這個 Worklet 的程式碼後,上述 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] 這個範圍,也就是效果所定義的時間範圍。這樣一來,動畫看起來就會很不一樣,而且不需要變更主要畫面格或動畫選項。工作區塊程式碼可任意複雜,可讓您以程式輔助方式定義要以何種順序和程度播放哪些特效。

選項選項

您可以重複使用練習題並變更數字。因此,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();

在這個示例中,兩個動畫都使用相同的程式碼,但有不同的選項。

請告訴我你的本機狀態!

如先前所述,動畫工作流程的主要問題之一,就是有狀態的動畫。動畫工作區塊可保留狀態。不過,工作項的主要功能之一,就是可以遷移至不同的執行緒,甚至可以銷毀以節省資源,這也會銷毀其狀態。為避免狀態遺失,動畫工作單元會提供鉤子,在工作單元遭到銷毀「之前」呼叫,您可以使用該鉤子傳回狀態物件。重新建立工作單元時,系統會將該物件傳遞至建構函式。在初始建立時,該參數會是 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 的機率。如果瀏覽器要拆解 worklet 並將其遷移至其他執行緒,則會在建立時發出另一個 Math.random() 呼叫,這可能會導致方向突然改變。為避免發生這種情況,我們會將動畫隨機選擇的方向做為「狀態」傳回,並在提供的建構函式中使用該狀態。

鉤掛時空連續體:ScrollTimeline

如上一節所示,AnimationWorklet 可讓我們透過程式輔助定義進階時間軸對動畫效果的影響。不過,目前我們的時間軸一律是 document.timeline,用於追蹤時間。

ScrollTimeline 開啟了新的可能性,讓您可以透過捲動而非時間來驅動動畫。我們將重複使用第一個「passthrough」工作區,用於這個示範

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

我們並未傳遞 document.timeline,而是建立新的 ScrollTimeline。您可能已經猜到,ScrollTimeline 並未使用時間,而是使用 scrollSource 的捲動位置,在工作區中設定 currentTime。捲動至頂端 (或左側) 代表 currentTime = 0,捲動至底部 (或右側) 則會將 currentTime 設為 timeRange。如果您在這個示範中捲動方塊,就能控制紅色方塊的位置。

如果您建立的 ScrollTimeline 包含無法捲動的元素,時間軸的 currentTime 會是 NaN。因此,特別是考量到回應式設計,您應隨時準備好將 NaN 設為 currentTime。通常預設值為 0。

長久以來,我們一直都致力於著連結動畫和捲動位置,但其實從未如此在這樣的保真度下達成 (除了利用 CSS3D 的駭客解決方法以外)。「動畫小程式」能以簡單明瞭的方式實作這些效果,同時兼顧效能。舉例來說,如這個示範所示,視差捲動效果現在只需幾行即可定義捲動導向的動畫。

深入解析

小程式

Worklet 是具有隔離範圍和極小 API 途徑的 JavaScript 情境。小型 API 途徑可讓瀏覽器進行更積極的最佳化,尤其是在低階裝置上。此外,工作項不會繫結至特定事件迴圈,但可視需要在執行緒之間移動。這點對於 AnimationWorklet 來說尤其重要。

合成器 NSync

您可能會發現某些 CSS 屬性能夠快速建立動畫效果,有些屬性則無法。有些屬性只需要在 GPU 上進行一些工作即可製作動畫,而其他屬性則會強制瀏覽器重新排版整份文件。

在 Chrome (以及許多其他瀏覽器) 中,我們有一個稱為合成器的程序,其工作 (我在此處簡化說明) 是安排圖層和紋理,然後利用 GPU 盡可能定期更新螢幕,理想情況下要盡可能以螢幕更新速度 (通常為 60Hz) 更新。視動畫所使用的 CSS 屬性而定,瀏覽器可能只需要讓轉譯器執行其工作,而其他屬性則需要執行版面配置,這是只有主執行緒才能執行的作業。視您要為哪些屬性製作動畫而定,動畫工作區塊會繫結至主執行緒,或是在與轉譯器同步的個別執行緒中執行。

戴在手腕上

由於 GPU 是高度競爭的資源,因此通常只有一個合成器程序可在多個分頁中共用。如果合成器遭到阻斷,整個瀏覽器都會停止執行,無法回應使用者輸入內容。您必須盡全力避免這種情況。如果工作區塊無法在算繪影格時及時提供合成器所需的資料,會發生什麼情況?

發生這種情況時,系統會根據規格允許工作單元「滑動」。這會落後於合成器,而合成器可重複使用上一個影格的資料,以維持影格速率。在視覺上,此程式碼看起來會像是卡頓,但差別在於瀏覽器會持續回應使用者輸入的內容。

結論

AnimationWorklet 有許多面向,可在網路上帶來好處。明顯的好處是,您可以進一步控制動畫,並透過新方法驅動動畫,為網頁帶來更高層級的視覺擬真度。不過,API 設計也讓您可以讓應用程式更能抵抗卡頓情形,同時還能同時存取所有新功能。

Animation Worklet 已在 Canary 中推出,我們預計在 Chrome 71 中進行初期試用。我們期待你分享全新網站的使用體驗,並告訴我們有哪些地方需要改善。另外還有一個polyfill,可提供相同的 API,但不會提供效能隔離功能。

請注意,CSS 轉場效果和 CSS 動畫仍是有效的選項,而且可用於簡單的動畫。但如果您需要更精緻的效果,AnimationWorklet 就是您的最佳選擇!