使用 CSS 動畫

CSS 動畫使得從一種 CSS 樣式配置到另一種的過渡動畫成為可能。動畫由兩個元件組成:描述 CSS 動畫的樣式和一組關鍵幀,這些關鍵幀指示動畫樣式的開始和結束狀態,以及可能的中間路徑點。

與傳統的指令碼驅動動畫技術相比,CSS 動畫有三個主要優勢

  1. 它們對於基本動畫來說易於使用;你甚至無需瞭解 JavaScript 即可建立它們。
  2. 即使在適度的系統負載下,動畫也能很好地執行。簡單的動畫在 JavaScript 中通常表現不佳。渲染引擎可以使用跳幀和其他技術來儘可能保持效能流暢。
  3. 讓瀏覽器控制動畫序列可以使瀏覽器最佳化效能和效率,例如,透過降低當前不可見標籤頁中執行的動畫的更新頻率。

配置動畫

要建立 CSS 動畫序列,你可以使用 animation 屬性或其子屬性來為要動畫化的元素設定樣式。這使你可以配置動畫序列應如何進行的計時、持續時間和其他詳細資訊。這不會配置動畫的實際外觀,動畫的實際外觀是使用 @keyframes at-rule 完成的,如下面使用關鍵幀定義動畫序列部分所述。

animation 屬性的子屬性是

animation-composition

指定當多個動畫同時影響同一屬性時要使用的合成操作。此屬性不是 animation 簡寫屬性的一部分。

animation-delay

指定元素載入與動畫序列開始之間的延遲,以及動畫是應該從頭開始立即開始還是從動畫的中間開始。

animation-direction

指定動畫的第一次迭代是向前還是向後,以及後續迭代是應該在每次執行序列時交替方向還是重置到起點並重復。

animation-duration

指定動畫完成一個週期的時長。

animation-fill-mode

指定動畫在執行前後如何將其樣式應用於目標。

注意:對於動畫 forwards 填充模式,動畫屬性的行為如同包含在一組 will-change 屬性值中。如果在動畫期間建立了新的堆疊上下文,則目標元素在動畫結束後會保留堆疊上下文。

animation-iteration-count

指定動畫應重複的次數。

animation-name

指定描述動畫關鍵幀的 @keyframes at-rule 的名稱。

animation-play-state

指定是暫停還是播放動畫序列。

animation-timeline

指定用於控制 CSS 動畫進度的時間線。

animation-timing-function

透過建立加速度曲線來指定動畫如何在關鍵幀之間過渡。

使用關鍵幀定義動畫序列

配置好動畫的計時後,你需要定義動畫的外觀。這是透過使用 @keyframes at-rule 建立一個或多個關鍵幀來完成的。每個關鍵幀描述了動畫元素在動畫序列的給定時間應如何渲染。

由於動畫的計時是在配置動畫的 CSS 樣式中定義的,因此關鍵幀使用 <percentage> 來指示它們在動畫序列中發生的時間。0% 表示動畫序列的第一個時刻,而 100% 表示動畫的最終狀態。由於這兩個時間非常重要,它們有特殊的別名:fromto。兩者都是可選的。如果未指定 from/0%to/100%,瀏覽器會使用所有屬性的計算值來開始或結束動畫。

你可以選擇包含其他關鍵幀,這些關鍵幀描述了動畫開始和結束之間的中間步驟。

使用動畫簡寫

animation 簡寫對於節省空間很有用。例如,本文中我們使用的一些規則

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

...可以使用 animation 簡寫代替。

css
p {
  animation: 3s infinite alternate slide-in;
}

要了解有關在使用 animation 簡寫時可以指定不同動畫屬性值的順序的更多資訊,請參閱 animation 參考頁面。

設定多個動畫屬性值

CSS 動畫的非簡寫屬性可以接受多個值,用逗號分隔。當你想在一個規則中應用多個動畫併為每個動畫設定不同的持續時間、迭代次數等時,可以使用此功能。讓我們看一些快速示例來解釋不同的排列。

在第一個示例中,有三個持續時間和三個迭代次數值。因此,每個動畫都分配了一個持續時間和迭代次數值,其位置與動畫名稱相同。fadeInOut 動畫的持續時間為 2.5s,迭代次數為 2,而 bounce 動畫的持續時間為 1s,迭代次數為 5

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 2.5s, 5s, 1s;
animation-iteration-count: 2, 1, 5;

在第二個示例中,設定了三個動畫名稱,但只有一個持續時間和迭代次數。在這種情況下,所有三個動畫都具有相同的持續時間和迭代次數。

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 3s;
animation-iteration-count: 1;

在第三個示例中,指定了三個動畫,但只有兩個持續時間和迭代次數。在這種情況下,如果列表中沒有足夠的值來為每個動畫分配一個單獨的值,則值分配會從可用列表中的第一個專案迴圈到最後一個專案,然後迴圈回第一個專案。因此,fadeInOut 的持續時間為 2.5smoveLeft300px 的持續時間為 5s,這是持續時間值列表中的最後一個值。現在持續時間值分配重置為第一個值;因此,bounce 的持續時間為 2.5s。迭代次數值(以及你指定的任何其他屬性值)將以相同的方式分配。

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 2.5s, 5s;
animation-iteration-count: 2, 1;

如果動畫數量和動畫屬性值之間的不匹配是顛倒的,例如三個 animation-name 值有五個 animation-duration 值,那麼在這種情況下,額外或未使用的動畫屬性值(例如兩個 animation-duration 值)不適用於任何動畫並且會被忽略。

示例

使文字在瀏覽器視窗中滑動

這個基本示例使用 translatescale 過渡屬性為 <p> 元素設定樣式,使文字從瀏覽器視窗右邊緣滑入。

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
}

@keyframes slide-in {
  from {
    translate: 150vw 0;
    scale: 200% 1;
  }

  to {
    translate: 0 0;
    scale: 100% 1;
  }
}

在此示例中,<p> 元素的樣式指定動畫應使用 animation-duration 屬性從開始到結束執行 3 秒,並且定義動畫序列關鍵幀的 @keyframes at-rule 的名稱為 slide-in

在這種情況下,我們只有兩個關鍵幀。第一個發生在 0%(使用別名 from)。在這裡,我們將元素的 translate 屬性配置為 150vw(即超出包含元素的極右邊緣),並將元素的 scale 配置為 200%(或其預設內聯大小的兩倍),導致段落的寬度是其 <body> 包含塊的兩倍。這導致動畫的第一幀將標題繪製在瀏覽器視窗的右邊緣之外。

第二個關鍵幀發生在 100%(使用別名 to)。translate 屬性設定為 0%,元素的 scale 設定為 1,即 100%。這導致標題在其預設狀態下完成動畫,與內容區域的左邊緣齊平。

html
<p>
  The Caterpillar and Alice looked at each other for some time in silence: at
  last the Caterpillar took the hookah out of its mouth, and addressed her in a
  languid, sleepy voice.
</p>

注意:重新載入頁面以檢視動畫。

新增另一個關鍵幀動畫

讓我們為上一個示例的動畫新增另一個關鍵幀。假設我們想讓 Alice 的名字變成粉紅色並變大,然後隨著它從右向左移動,再縮小回原始大小和顏色。雖然我們可以更改 font-size,但更改任何影響盒模型的屬性都會對效能產生負面影響。相反,我們將她的名字包裝在 <span> 中,然後單獨縮放併為其分配顏色。這需要新增另一個隻影響 <span> 的動畫

css
@keyframes grow-shrink {
  25%,
  75% {
    scale: 100%;
  }

  50% {
    scale: 200%;
    color: magenta;
  }
}

完整的程式碼現在看起來像這樣

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
}
p span {
  display: inline-block;
  animation-duration: 3s;
  animation-name: grow-shrink;
}

@keyframes slide-in {
  from {
    translate: 150vw 0;
    scale: 200% 1;
  }

  to {
    translate: 0 0;
    scale: 100% 1;
  }
}

@keyframes grow-shrink {
  25%,
  75% {
    scale: 100%;
  }

  50% {
    scale: 200%;
    color: magenta;
  }
}

我們在“Alice”周圍添加了一個 <span>

html
<p>
  The Caterpillar and <span>Alice</span> looked at each other for some time in
  silence: at last the Caterpillar took the hookah out of its mouth, and
  addressed her in a languid, sleepy voice.
</p>

這告訴瀏覽器,在動畫的第一個和最後一個 25% 中,名字應該是正常的,但在中間放大和縮小並變為粉紅色。我們將 span 的 display 屬性設定為 inline-block,因為 transform 屬性不影響非替換的內聯級別內容

注意:重新載入頁面以檢視動畫。

重複動畫

要使動畫重複,請使用 animation-iteration-count 屬性指示動畫重複的次數。在這種情況下,讓我們使用 infinite 來使動畫無限重複

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
}

使動畫來回移動

這使其重複了,但每次開始動畫時都跳回開頭非常奇怪。我們真正想要的是讓它在螢幕上來回移動。透過將 animation-direction 設定為 alternate,這很容易實現

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

使用動畫事件

透過利用動畫事件,你可以獲得對動畫的額外控制以及有用的資訊。這些事件由 AnimationEvent 物件表示,可用於檢測動畫何時開始、結束和開始新的迭代。每個事件都包括它發生的時間以及觸發事件的動畫名稱。

我們將修改滑動文字示例,以在每個動畫事件發生時輸出一些關於它的資訊,以便我們可以瞭解它們是如何工作的。

我們包含了與上一個示例相同的關鍵幀動畫。此動畫將持續 3 秒,名為“slide-in”,重複 3 次,並且每次都以交替方向移動。在 @keyframes 中,縮放和轉換沿 x 軸操縱,使元素在螢幕上滑動。

css
.slide-in {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: 3;
  animation-direction: alternate;
}

新增動畫事件監聽器

我們將使用 JavaScript 程式碼監聽所有三種可能的動畫事件。此程式碼配置我們的事件監聽器;我們會在文件首次載入時呼叫它來設定一切。

js
const element = document.getElementById("watch-me");
element.addEventListener("animationstart", listener);
element.addEventListener("animationend", listener);
element.addEventListener("animationiteration", listener);

element.className = "slide-in";

這是相當標準的程式碼;你可以在 eventTarget.addEventListener() 的文件中獲取有關其工作原理的詳細資訊。此程式碼做的最後一件事是將我們動畫元素的 class 設定為“slide-in”;我們這樣做是為了啟動動畫。

為什麼?因為 animationstart 事件在動畫開始時立即觸發,在我們的例子中,這發生在我們的程式碼執行之前。所以我們將透過將元素的類設定為之後動畫的樣式來啟動動畫。

接收事件

事件將傳遞到下面顯示的 listener() 函式。

js
function listener(event) {
  const l = document.createElement("li");
  switch (event.type) {
    case "animationstart":
      l.textContent = `Started: elapsed time is ${event.elapsedTime}`;
      break;
    case "animationend":
      l.textContent = `Ended: elapsed time is ${event.elapsedTime}`;
      break;
    case "animationiteration":
      l.textContent = `New loop started at time ${event.elapsedTime}`;
      break;
  }
  document.getElementById("output").appendChild(l);
}

這段程式碼也很簡單。它檢視 event.type 以確定發生了哪種動畫事件,然後將適當的註釋新增到我們用於記錄這些事件的 <ul>(無序列表)中。

最終輸出結果看起來像這樣

  • 已啟動:經過時間為 0
  • 新迴圈在時間 3.01200008392334 開始
  • 新迴圈在時間 6.00600004196167 開始
  • 已結束:經過時間為 9.234000205993652

請注意,這些時間非常接近,但不完全是,動畫配置時建立的預期時間。另請注意,在動畫的最後一次迭代之後,不會發送 animationiteration 事件;相反,會發送 animationend 事件。

為了完整起見,這裡是顯示頁面內容的 HTML,包括指令碼插入有關接收到的事件資訊的列表

html
<h1 id="watch-me">Watch me move</h1>
<p>
  This example shows how to use CSS animations to make <code>H1</code>
  elements move across the page.
</p>
<p>
  In addition, we output some text each time an animation event fires, so you
  can see them in action.
</p>
<ul id="output"></ul>

這是即時輸出。

注意:重新載入頁面以檢視動畫。

動畫化 display 和 content-visibility

此示例演示瞭如何動畫化 displaycontent-visibility。此行為對於建立進入/退出動畫很有用,例如,當你希望使用 display: none 從 DOM 中刪除容器,但希望它使用 opacity 平滑淡出而不是立即消失時。

支援的瀏覽器會使用 離散動畫型別的變體來動畫化 displaycontent-visibility。這通常意味著屬性將在兩個值之間動畫的 50% 處在兩個值之間翻轉。

但是,有一個例外,那就是當動畫從 display: nonecontent-visibility: hidden 到可見值時。在這種情況下,瀏覽器將在兩個值之間翻轉,以便動畫內容在整個動畫持續時間內顯示。

所以例如:

  • displaynone 動畫到 block(或其他可見的 display 值)時,該值將在動畫持續時間的 0% 處切換到 block,使其在整個過程中可見。
  • displayblock(或其他可見的 display 值)動畫到 none 時,該值將在動畫持續時間的 100% 處切換到 none,使其在整個過程中可見。

HTML

HTML 包含兩個 <p> 元素,中間有一個 <div>,我們將從 display none 動畫到 block

html
<p>
  Click anywhere on the screen or press any key to toggle the
  <code>&lt;div&gt;</code> between hidden and showing.
</p>

<div>
  This is a <code>&lt;div&gt;</code> element that animates between
  <code>display: none; opacity: 0</code> and
  <code>display: block; opacity: 1</code>. Neat, huh?
</div>

<p>
  This is another paragraph to show that <code>display: none;</code> is being
  applied and removed on the above <code>&lt;div&gt; </code>. If only its
  <code>opacity</code> was being changed, it would always take up the space in
  the DOM.
</p>

CSS

css
html {
  height: 100vh;
}

div {
  font-size: 1.6rem;
  padding: 20px;
  border: 3px solid red;
  border-radius: 20px;
  width: 480px;
  opacity: 0;
  display: none;
}

/* Animation classes */

div.fade-in {
  display: block;
  animation: fade-in 0.7s ease-in forwards;
}

div.fade-out {
  animation: fade-out 0.7s ease-out forwards;
}

/* Animation keyframes */

@keyframes fade-in {
  0% {
    opacity: 0;
    display: none;
  }

  100% {
    opacity: 1;
    display: block;
  }
}

@keyframes fade-out {
  0% {
    opacity: 1;
    display: block;
  }

  100% {
    opacity: 0;
    display: none;
  }
}

請注意在關鍵幀動畫中包含了 display 屬性。

JavaScript

最後,我們包含一些 JavaScript 來設定事件監聽器以觸發動畫。具體來說,當我們需要 <div> 顯示時,我們新增 fade-in 類,當我們需要它消失時,我們新增 fade-out

js
const divElem = document.querySelector("div");
const htmlElem = document.querySelector(":root");

htmlElem.addEventListener("click", showHide);
document.addEventListener("keydown", showHide);

function showHide() {
  if (divElem.classList[0] === "fade-in") {
    divElem.classList.remove("fade-in");
    divElem.classList.add("fade-out");
  } else {
    divElem.classList.remove("fade-out");
    divElem.classList.add("fade-in");
  }
}

結果

程式碼渲染如下:

另見