使用 Web 動畫 API

Web Animations API 允許我們使用 JavaScript 構建動畫並控制其播放。本文將透過有趣的演示和愛麗絲夢遊仙境主題的教程,引導您走上正確的學習道路。

瞭解 Web Animations API

Web Animations API 向開發者和 JavaScript 操作開放了瀏覽器的動畫引擎。此 API 旨在成為 CSS AnimationsCSS Transitions 兩種實現的底層基礎,併為未來的動畫效果打開了大門。它是 Web 上執行動畫最高效的方式之一,允許瀏覽器進行自己的內部最佳化,無需任何技巧、強制或 Window.requestAnimationFrame()

藉助 Web Animations API,我們可以將互動式動畫從樣式表移動到 JavaScript,將表現與行為分離。我們不再需要依賴 DOM 大量操作的技術,例如編寫 CSS 屬性和將作用域類新增到元素來控制播放方向。與純宣告式 CSS 不同,JavaScript 還允許我們動態設定從屬性到持續時間的值。對於構建自定義動畫庫和建立互動式動畫,Web Animations API 可能是完成這項工作的完美工具。讓我們看看它能做什麼!

此頁面包含一套利用 Web Animations API 的示例,靈感來自《愛麗絲夢遊仙境》。這些示例由 Rachel Nabors 建立並無償分享。完整的示例套件可在 CodePen 上獲取;這裡我們展示了與我們文件相關的示例。

使用 Web Animations API 編寫 CSS 動畫

學習 Web Animations API 的一種更熟悉的方法是,從大多數 Web 開發者以前都玩過的東西開始:CSS 動畫。CSS 動畫具有熟悉的語法,非常適合演示。

CSS 版本

這是一個用 CSS 編寫的翻滾動畫,展示了愛麗絲掉進通往仙境的兔子洞

請注意,背景移動,愛麗絲旋轉,並且她的顏色在與旋轉偏移的位置發生變化。本教程中我們將只關注愛麗絲。您可以透過單擊程式碼塊上的“播放”來檢視完整的原始碼。這是控制愛麗絲動畫的簡化 CSS

css
#alice {
  animation: alice-tumbling infinite 3s linear;
}

@keyframes alice-tumbling {
  0% {
    color: black;
    transform: rotate(0) translate3d(-50%, -50%, 0);
  }
  30% {
    color: #431236;
  }
  100% {
    color: black;
    transform: rotate(360deg) translate3d(-50%, -50%, 0);
  }
}

這會在 3 秒內以恆定(線性)速度改變愛麗絲的顏色和她的變換旋轉,並無限迴圈。在 @keyframes 塊中,我們可以看到在每次迴圈的 30%(大約 0.9 秒時),愛麗絲的顏色從黑色變為深勃艮第,然後在迴圈結束時再次變回。

將其移動到 JavaScript

現在讓我們嘗試使用 Web Animations API 建立相同的動畫。

表示關鍵幀

我們需要做的第一件事是建立一個與我們的 CSS @keyframes 塊對應的關鍵幀物件

js
const aliceTumbling = [
  { transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
  { color: "#431236", offset: 0.3 },
  { transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
];

這裡我們使用了一個包含多個物件的陣列。每個物件代表原始 CSS 中的一個關鍵幀。然而,與 CSS 不同,Web Animations API 不需要明確告知每個關鍵幀在動畫中出現的百分比。它將根據您提供的關鍵幀數量自動將動畫分成相等的部分。這意味著,除非另有說明,具有三個關鍵幀的關鍵幀物件將在動畫的每次迴圈中播放中間關鍵幀的 50%。

當我們想要明確設定一個關鍵幀相對於其他關鍵幀的偏移量時,我們可以在物件中直接指定偏移量,用逗號將其與宣告分開。在上面的示例中,為了確保愛麗絲的顏色變化在 30%(而不是 50%)處發生,我們給它設定了 offset: 0.3

目前,應至少指定兩個關鍵幀(代表動畫序列的開始和結束狀態)。如果您的關鍵幀列表只有一個條目,在某些瀏覽器更新之前,Element.animate() 可能會丟擲 NotSupportedError DOMException

因此,總結一下,關鍵幀預設等距分佈,除非您在某個關鍵幀上指定了偏移量。很方便,不是嗎?

表示時間屬性

我們還需要建立一個時間屬性物件,對應於愛麗絲動畫中的值

js
const aliceTiming = {
  duration: 3000,
  iterations: Infinity,
};

你會注意到這裡與 CSS 中等效值的一些差異

  • 首先,持續時間以毫秒為單位,而不是秒——3000 而不是 3 秒。像 setTimeout()Window.requestAnimationFrame() 一樣,Web Animations API 只接受毫秒。
  • 你會注意到的另一件事是它是 iterations,而不是 iteration-count

注意:CSS 動畫和 Web 動畫中使用的術語之間存在許多細微的差異。例如,Web 動畫不使用字串 "infinite",而是使用 JavaScript 關鍵字 Infinity。並且不使用 timing-function,而是使用 easing。我們這裡沒有列出 easing 值,因為與 CSS 動畫中預設的 animation-timing-functionease 不同,在 Web 動畫 API 中,預設的緩動是 linear — 這正是我們想要的。

將這些片段組合起來

現在是時候使用 Element.animate() 方法將它們組合在一起了

js
document.getElementById("alice").animate(aliceTumbling, aliceTiming);

然後砰的一聲:動畫開始播放

animate() 方法可以在任何可以用 CSS 動畫化的 DOM 元素上呼叫。它可以用幾種方式編寫。與其為關鍵幀和計時屬性建立物件,我們可以直接傳入它們的值,如下所示

js
document.getElementById("alice").animate(
  [
    { transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
    { color: "#431236", offset: 0.3 },
    { transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
  ],
  {
    duration: 3000,
    iterations: Infinity,
  },
);

更重要的是,如果只想指定動畫的持續時間而不指定其迭代次數(預設情況下,動畫迭代一次),我們可以只傳入毫秒數

js
document.getElementById("alice").animate(
  [
    { transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
    { color: "#431236", offset: 0.3 },
    { transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
  ],
  3000,
);

使用 play()、pause()、reverse() 和 updatePlaybackRate() 控制播放

雖然我們可以用 Web Animations API 編寫 CSS 動畫,但該 API 真正派上用場的地方是操縱動畫的播放。Web Animations API 提供了幾種有用的方法來控制播放。讓我們看看在“追逐白兔”示例中暫停和播放動畫

在這個例子中,白兔有一個動畫,導致它掉進兔子洞。它只有在使用者點選它時才會觸發。

暫停和播放動畫

我們可以像往常一樣使用 animate() 方法來動畫化兔子

js

Element.animate() 方法在被呼叫後會立即執行。為了防止蛋糕在使用者有機會點選它之前就自己吃掉,我們立即在其定義後呼叫 Animation.pause(),如下所示

js

注意: 另外,您也可以使用 Animation() 建構函式定義 rabbitDownAnimation,該建構函式在您呼叫 play() 之前不會開始播放。

現在我們可以隨時使用 Animation.play() 方法來執行它。具體來說,我們希望將其與點選操作關聯起來。我們可以透過以下方式實現這一點

js

當用戶點選或觸控兔子時,我們現在可以呼叫 downHeGoes 來播放所有動畫。

其他有用的方法

除了暫停和播放,我們還可以使用以下動畫方法

讓我們首先看一下 playbackRate——負的 playbackRate 將導致動畫反向執行。在 《愛麗絲鏡中奇遇記》中,愛麗絲來到一個她必須奔跑才能保持原地的世界——並且必須跑得快兩倍才能前進!在紅皇后賽跑的例子中,愛麗絲和紅皇后正在奔跑以保持原地

因為小孩子不像自動棋子那樣容易疲勞,愛麗絲不斷減速。我們可以透過設定她動畫的 playbackRate 的衰減來實現這一點。我們使用 updatePlaybackRate() 而不是直接設定 playbackRate,因為這會產生平滑的更新

js
setInterval(() => {
  // Make sure the playback rate never falls below .4
  if (redQueenAlice.playbackRate > 0.4) {
    redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 0.9);
  }
  adjustBackgroundPlayback();
}, 1000);

但是透過點選或輕觸來催促它們會透過將其 playbackRate 相乘來加速它們

js
function goFaster() {
  // But you can speed them up by giving the screen a click or a tap.
  redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 1.1);
  adjustBackgroundPlayback();
}

document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);

背景元素的 playbackRate 也會在您點選或輕觸時受到影響。它們的播放速率源自愛麗絲的,如下所示。當您讓愛麗絲和紅皇后跑得快兩倍時會發生什麼?當您讓它們減速時會發生什麼?

js
/* Alice tires so easily! 
  Every so many seconds, reduce their playback rate so they slow a little. 
*/
const sceneries = [
  foreground1Movement,
  foreground2Movement,
  background1Movement,
  background2Movement,
];

function adjustBackgroundPlayback() {
  // If Alice and the Red Queen are running at a speed of 0.8–1.2,
  // the background doesn't move.
  // But if they fall under 0.8, the background slides backwards
  if (redQueenAlice.playbackRate < 0.8) {
    sceneries.forEach((anim) => {
      anim.updatePlaybackRate(-redQueenAlice.playbackRate / 2);
    });
  } else if (redQueenAlice.playbackRate > 1.2) {
    sceneries.forEach((anim) => {
      anim.updatePlaybackRate(redQueenAlice.playbackRate / 2);
    });
  } else {
    sceneries.forEach((anim) => {
      anim.updatePlaybackRate(0);
    });
  }
}
adjustBackgroundPlayback();

保留動畫樣式

在對元素進行動畫處理時,常見的用例是在動畫完成後保留動畫的最終狀態。有時用於此的一種方法是將動畫的填充模式設定為 forwards。然而,不建議無限期地使用填充模式來保留動畫效果,原因有二

  • 瀏覽器必須在動畫仍處於活動狀態時維護動畫狀態,因此即使動畫不再播放,它也會繼續消耗資源。請注意,這在一定程度上透過瀏覽器自動移除填充動畫而得到緩解。
  • 動畫應用的樣式在級聯中具有更高的優先順序,因此在需要時可能難以覆蓋它們。

更好的方法是使用 Animation.commitStyles() 方法。這會將動畫當前樣式的計算值寫入其目標元素的 style 屬性,之後該元素可以正常重新設定樣式。

自動移除填充動畫

可以在同一元素上觸發大量動畫。如果它們是無限的(即,向前填充),這可能導致巨大的動畫列表,從而造成記憶體洩漏。因此,瀏覽器會在較新的動畫替換舊的動畫後自動移除填充動畫,除非開發者明確指定保留它們。

當滿足以下所有條件時,動畫將被移除

  • 動畫正在填充(如果它向前播放,其 fillforwards;如果它向後播放,則為 backwards;或為 both)。
  • 動畫已完成。(請注意,由於 fill,它仍然有效。)
  • 動畫的時間線單調遞增。(對於 DocumentTimeline 來說這總是真的;其他時間線例如 scroll-timeline 可以反向執行。)
  • 動畫不受宣告性標記(如 CSS)控制。
  • 動畫的 AnimationEffect 的每個樣式效果都被另一個也滿足上述所有條件的動畫所覆蓋。(通常,當兩個動畫設定同一元素的同一樣式屬性時,後建立的動畫會覆蓋另一個。)

前四個條件確保,在沒有 JavaScript 程式碼干預的情況下,動畫的效果永遠不會改變或結束。最後一個條件確保動畫永遠不會真正影響任何元素的樣式:它已被完全替換。

當動畫自動移除時,動畫的 remove 事件將被觸發。

為了防止瀏覽器自動移除動畫,請呼叫動畫的 persist() 方法。

如果動畫已被移除,則動畫的 replaceState 屬性將為 removed;如果您已對動畫呼叫 persist(),則為 persisted;否則為 active

從動畫中獲取資訊

想象一下我們還可以如何使用 playbackRate,例如透過讓患有前庭疾病的使用者減慢整個網站的動畫來改善可訪問性。這在沒有重新計算每個 CSS 規則中的持續時間的情況下是無法用 CSS 實現的,但使用 Web Animations API,我們可以使用 Document.getAnimations 方法遍歷頁面上的每個動畫,並將其 playbackRate 減半,如下所示

js
document.getAnimations().forEach((animation) => {
  animation.updatePlaybackRate(animation.playbackRate * 0.5);
});

使用 Web Animations API,您只需要更改一個小屬性!

單獨使用 CSS 動畫很難做到的另一件事是建立對其他動畫提供的值的依賴。例如,在“愛麗絲變大變小遊戲”示例中,您可能注意到蛋糕的持續時間有些奇怪

js
document.getElementById("eat-me-sprite").animate([], {
  duration: aliceChange.effect.getComputedTiming().duration / 2,
});

要了解這裡發生了什麼,讓我們看一下愛麗絲的動畫

js
const aliceChange = document
  .getElementById("alice")
  .animate(
    [
      { transform: "translate(-50%, -50%) scale(.5)" },
      { transform: "translate(-50%, -50%) scale(2)" },
    ],
    {
      duration: 8000,
      easing: "ease-in-out",
      fill: "both",
    },
  );

愛麗絲的動畫讓她在 8 秒內從她的一半大小變為她的兩倍大小。然後我們暫停她

js
aliceChange.pause();

如果我們在動畫開始時就暫停了她,她就會從她的一半大小開始,就好像她已經喝光了整瓶藥水一樣!我們希望將她的動畫“播放頭”設定在中間,這樣她就已經完成了一半。我們可以透過將她的 Animation.currentTime 設定為 4 秒來做到這一點,如下所示

js
aliceChange.currentTime = 4000;

但是,在製作此動畫時,我們可能會經常改變愛麗絲的持續時間。如果我們動態設定她的 currentTime,這樣我們就不用一次進行兩次更新,那不是更好嗎?實際上,我們可以透過引用 aliceChange 的 Animation.effect 屬性來做到這一點,該屬性返回一個包含愛麗絲身上所有活動效果詳細資訊的物件

js
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;

effect 允許我們訪問動畫的關鍵幀和時間屬性——aliceChange.effect.getComputedTiming() 指向愛麗絲的計時物件——這包含她的 duration。我們可以將她的持續時間除以一半來獲得她動畫時間線的中點,將她設定為正常高度。現在我們可以反向播放她的動畫,讓她變小或變大!

我們在設定蛋糕和瓶子持續時間時也可以做同樣的事情

js
const drinking = document
  .getElementById("liquid")
  .animate([{ height: "100%" }, { height: "0" }], {
    fill: "forwards",
    duration: aliceChange.effect.getComputedTiming().duration / 2,
  });
drinking.pause();

現在,所有三個動畫都只與一個持續時間相關聯,我們可以輕鬆地從一個地方更改它。

我們還可以使用 Web Animations API 來計算動畫的當前時間。遊戲在您吃完蛋糕或喝光瓶子時結束。玩家看到的插曲取決於愛麗絲在動畫中進展到何處,是她變得太大了無法進入小門,還是變得太小了無法拿到開門的鑰匙。我們可以透過獲取她動畫的 currentTime 並將其除以她的 activeDuration 來確定她在動畫中是處於大尺寸還是小尺寸

js
const endGame = () => {
  // get Alice's timeline's playhead location
  const alicePlayhead = aliceChange.currentTime;
  const aliceTimeline = aliceChange.effect.getComputedTiming().activeDuration;

  // stops Alice's and other animations
  stopPlayingAlice();

  // depending on which third it falls into
  const aliceHeight = alicePlayhead / aliceTimeline;

  if (aliceHeight <= 0.333) {
    // Alice got smaller!
    // …
  } else if (aliceHeight >= 0.666) {
    // Alice got bigger!
    // …
  } else {
    // Alice didn't change significantly
    // …
  }
};

回撥和 Promise

CSS 動畫和過渡有自己的事件監聽器,Web Animations API 也支援這些監聽器

  • onfinishfinish 事件的事件處理程式,可以使用 finish() 手動觸發。
  • oncancelcancel 事件的事件處理程式,可以使用 cancel() 觸發。

這裡我們將蛋糕、瓶子和愛麗絲的回撥設定為觸發 endGame 函式

js
// When the cake or bottle runs out
nommingCake.onfinish = endGame;
drinking.onfinish = endGame;

// Alice reaches the end of her animation
aliceChange.onfinish = endGame;

更好的是,Web Animations API 還提供了一個 finished Promise,當動畫完成時它會解析,如果動畫被取消則會拒絕。

總結

這些是 Web Animations API 的基本功能。現在您應該已經準備好“跳進兔子洞”,在瀏覽器中進行動畫製作,並準備好編寫自己的動畫實驗了!

另見