2024 年 1 月 20 日

CSS 動畫

CSS 動畫讓你可以完全不使用 JavaScript 就製作簡單的動畫。

JavaScript 可用於控制 CSS 動畫,並使用少量程式碼讓它們變得更好。

CSS 轉場

CSS 轉場的概念很簡單。我們描述一個屬性,以及它的變更應如何產生動畫。當屬性變更時,瀏覽器會繪製動畫。

也就是說,我們只需要變更屬性,而流暢的轉場會由瀏覽器完成。

例如,以下 CSS 會為 background-color 的變更產生 3 秒鐘的動畫

.animated {
  transition-property: background-color;
  transition-duration: 3s;
}

現在,如果一個元素有 .animated 類別,background-color 的任何變更都會在 3 秒鐘內產生動畫。

按一下下面的按鈕,為背景產生動畫

<button id="color">Click me</button>

<style>
  #color {
    transition-property: background-color;
    transition-duration: 3s;
  }
</style>

<script>
  color.onclick = function() {
    this.style.backgroundColor = 'red';
  };
</script>

描述 CSS 轉場有 4 個屬性

  • transition-property
  • transition-duration
  • transition-timing-function
  • transition-delay

我們稍後會介紹它們,現在請注意常見的 transition 屬性允許按順序同時宣告它們:property duration timing-function delay,以及一次動畫化多個屬性。

例如,這個按鈕同時動畫化 colorfont-size

<button id="growing">Click me</button>

<style>
#growing {
  transition: font-size 3s, color 2s;
}
</style>

<script>
growing.onclick = function() {
  this.style.fontSize = '36px';
  this.style.color = 'red';
};
</script>

現在,讓我們逐一介紹動畫屬性。

transition-property

transition-property 中,我們寫入要動畫化的屬性清單,例如:leftmargin-leftheightcolor。或者我們可以寫入 all,表示「動畫化所有屬性」。

請注意,有些屬性無法動畫化。然而,大多數一般使用的屬性都可以動畫化

transition-duration

transition-duration 中,我們可以指定動畫應持續多久。時間應採用 CSS 時間格式:秒數 s 或毫秒 ms

transition-delay

transition-delay 中,我們可以在動畫 之前 指定延遲。例如,如果 transition-delay1s,而 transition-duration2s,則動畫在屬性變更後 1 秒開始,總持續時間為 2 秒。

負值也是可能的。然後動畫會立即顯示,但動畫的起點會在給定的值(時間)之後。例如,如果 transition-delay-1s,而 transition-duration2s,則動畫從中點開始,總持續時間為 1 秒。

這裡動畫使用 CSS translate 屬性將數字從 0 轉移到 9

結果
script.js
style.css
index.html
stripe.onclick = function() {
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Click below to animate:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>

</html>

transform 屬性以這種方式動畫化

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
}

在上面的範例中,JavaScript 會將類別 .animate 新增到元素,然後動畫開始

stripe.classList.add('animate');

我們也可以從轉場的某個中間點開始,從一個確切的數字開始,例如對應於目前的秒數,使用負的 transition-delay

這裡如果您按數字,它會從目前的秒數開始動畫

結果
script.js
style.css
index.html
stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Click below to animate:
  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>
</html>

JavaScript 使用額外一行來執行此操作

stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  // for instance, -3s here starts the animation from the 3rd second
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};

transition-timing-function

計時函數描述動畫流程如何沿著其時間軸分佈。它會從慢開始然後變快,還是相反。

它一開始看起來是最複雜的屬性。但如果我們花點時間研究它,它就會變得非常簡單。

該屬性接受兩種類型的值:貝茲曲線或步驟。讓我們從曲線開始,因為它使用得更頻繁。

貝茲曲線

計時函數可以設定為具有 4 個控制點的貝茲曲線,這些控制點滿足條件

  1. 第一個控制點:(0,0)
  2. 最後一個控制點:(1,1)
  3. 對於中間點,x 的值必須在區間 0..1 中,y 可以是任何值。

CSS 中貝茲曲線的語法:cubic-bezier(x2, y2, x3, y3)。這裡我們只需要指定第 2 個和第 3 個控制點,因為第 1 個固定為 (0,0),第 4 個為 (1,1)

計時函數描述動畫過程進行的速度。

  • x 軸是時間:0 – 開始,1transition-duration 的結束。
  • y 軸指定過程的完成度:0 – 屬性的起始值,1 – 最終值。

最簡單的變體是動畫以相同的線性速度均勻進行。這可以用曲線 cubic-bezier(0, 0, 1, 1) 指定。

以下是該曲線的樣子

…正如我們所見,它只是一條直線。隨著時間(x)的流逝,動畫的完成度(y)穩步從 01

以下範例中的火車以恆定的速度從左到右行駛(按一下它)

結果
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

CSS transition 基於該曲線

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
  /* click on a train sets left to 450px, thus triggering the animation */
}

…我們如何顯示火車減速?

我們可以使用另一個貝茲曲線:cubic-bezier(0.0, 0.5, 0.5 ,1.0)

圖表

正如我們所見,這個過程開始得很快:曲線高高飆升,然後越來越慢。

以下是計時函數的實際運作(按一下火車)

結果
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0px;
  transition: left 5s cubic-bezier(0.0, 0.5, 0.5, 1.0);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

CSS

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, .5, .5, 1);
  /* click on a train sets left to 450px, thus triggering the animation */
}

有幾個內建曲線:lineareaseease-inease-outease-in-out

linearcubic-bezier(0, 0, 1, 1) 的簡寫,也就是我們上面描述的直線。

其他名稱是以下 cubic-bezier 的簡寫

ease* ease-in ease-out ease-in-out
(0.25, 0.1, 0.25, 1.0) (0.42, 0, 1.0, 1.0) (0, 0, 0.58, 1.0) (0.42, 0, 0.58, 1.0)

* – 預設情況下,如果沒有計時函數,則使用 ease

因此,我們可以使用 ease-out 來減慢火車的速度

.train {
  left: 0;
  transition: left 5s ease-out;
  /* same as transition: left 5s cubic-bezier(0, .5, .5, 1); */
}

但它看起來有點不同。

貝茲曲線會讓動畫超出其範圍。

曲線上的控制點可以有任意 y 座標:甚至是負值或巨大值。然後貝茲曲線也會延伸得很低或很高,使動畫超出其正常範圍。

在以下範例中,動畫程式碼為

.train {
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
  /* click on a train sets left to 450px */
}

屬性 left 應從 100px 動畫到 400px

但如果你按一下火車,你會看到

  • 首先,火車往移動:left 變小於 100px
  • 然後往前移動,比 400px 再遠一點。
  • 然後再往後移動 – 到 400px
結果
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='400px'">

</body>

</html>

如果我們查看給定貝茲曲線的圖形,就會很明顯為什麼會發生這種情況

我們將第 2 個點的 y 座標移到零以下,而第 3 個點則移到 1 以上,因此曲線超出「常規」象限。y 超出「標準」範圍 0..1

如我們所知,y 測量「動畫過程的完成度」。值 y = 0 對應於起始屬性值,而 y = 1 對應於結束值。因此,值 y<0 將屬性移到起始 left 之外,而 y>1 則移到最終 left 之外。

這肯定是一個「軟」變體。如果我們將 y 值設為 -9999,那麼火車將跳出範圍更多。

但是我們如何為特定任務建立貝茲曲線?有很多工具。

  • 例如,我們可以在網站 https://cubic-bezier.com 上進行。
  • 瀏覽器開發人員工具也特別支援 CSS 中的貝茲曲線
    1. 使用 F12(Mac:Cmd+Opt+I)開啟開發人員工具。
    2. 選取 Elements 標籤,然後注意右側的 Styles 子面板。
    3. 包含字詞 cubic-bezier 的 CSS 屬性會在這個字詞之前顯示一個圖示。
    4. 按一下這個圖示以編輯曲線。

步驟

計時函數 steps(步驟數目[, 起始/結束]) 允許將轉場拆分為多個步驟。

讓我們在數字範例中看看這個功能。

以下是數字清單,沒有任何動畫,僅作為來源

結果
style.css
index.html
#digit {
  border: 1px solid red;
  width: 1.2em;
}

#stripe {
  display: inline-block;
  font: 32px monospace;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="digit"><div id="stripe">0123456789</div></div>

</body>
</html>

在 HTML 中,數字條紋被封閉在固定長度的 <div id="digits">

<div id="digit">
  <div id="stripe">0123456789</div>
</div>

#digit div 有固定寬度和邊框,因此它看起來像一個紅色的視窗。

我們將製作一個計時器:數字將一個接一個地以離散的方式出現。

為實現此目的,我們將使用 overflow: hidden#stripe 隱藏在 #digit 外部,然後逐步將 #stripe 向左移動。

將有 9 個步驟,每個數字一個步驟移動

#stripe.animate  {
  transform: translate(-90%);
  transition: transform 9s steps(9, start);
}

steps(9, start) 的第一個參數是步驟數。變換將分為 9 個部分(每個 10%)。時間間隔也自動分為 9 個部分,因此 transition: 9s 為我們提供了整個動畫 9 秒的時間——每個數字 1 秒。

第二個參數是兩個單詞之一:startend

start 表示在動畫開始時,我們需要立即執行第一步。

在行動中

結果
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, start);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Click below to animate:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

點擊數字會立即將其更改為 1(第一步),然後在下一秒開始時更改。

過程如下進行

  • 0s-10%(在第 1 秒開始時立即進行第一次更改)
  • 1s-20%
  • 8s-90%
  • (最後一秒顯示最終值)。

這裡,第一次更改是立即進行的,因為 steps 中的 start

備用值 end 表示更改不應在開始時應用,而應在每秒結束時應用。

因此,steps(9, end) 的過程將如下所示

  • 0s0(在第一秒內沒有任何變化)
  • 1s-10%(在第 1 秒結束時進行第一次更改)
  • 2s-20%
  • 9s-90%

以下是 steps(9, end) 的實際操作(注意第一次數字更改前的暫停)

結果
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, end);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Click below to animate:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

steps(...) 還有一些預定義的簡寫

  • step-start – 與 steps(1, start) 相同。也就是說,動畫立即開始並執行 1 步。因此,它立即開始和結束,就好像沒有動畫一樣。
  • step-end – 與 steps(1, end) 相同:在 transition-duration 結束時以單步執行動畫。

這些值很少使用,因為它們表示的不是真正的動畫,而是一個單步更改。我們在這裡提到它們是為了完整性。

事件:“transitionend”

當 CSS 動畫結束時,transitionend 事件觸發。

它廣泛用於在動畫完成後執行操作。我們還可以加入動畫。

例如,以下範例中的船隻在被點擊時開始在那裡和回來航行,每次都向右航行得越來越遠

動畫由函數 go 初始化,該函數在每次變換結束時重新執行,並翻轉方向

boat.onclick = function() {
  //...
  let times = 1;

  function go() {
    if (times % 2) {
      // sail to the right
      boat.classList.remove('back');
      boat.style.marginLeft = 100 * times + 200 + 'px';
    } else {
      // sail to the left
      boat.classList.add('back');
      boat.style.marginLeft = 100 * times - 200 + 'px';
    }

  }

  go();

  boat.addEventListener('transitionend', function() {
    times++;
    go();
  });
};

transitionend 的事件物件有幾個特定屬性

event.propertyName
完成動畫屬性。如果我們同時動畫多個屬性,這會很有用。
event.elapsedTime
動畫花費的時間(以秒為單位),不包含 transition-delay

關鍵影格

我們可以使用 @keyframes CSS 規則將多個簡單動畫結合在一起。

它指定動畫的「名稱」和規則,包括動畫的內容、時間和位置。然後使用 animation 屬性,我們可以將動畫附加到元素並為其指定其他參數。

以下是一個帶有說明的範例

<div class="progress"></div>

<style>
  @keyframes go-left-right {        /* give it a name: "go-left-right" */
    from { left: 0px; }             /* animate from left: 0px */
    to { left: calc(100% - 50px); } /* animate to left: 100%-50px */
  }

  .progress {
    animation: go-left-right 3s infinite alternate;
    /* apply the animation "go-left-right" to the element
       duration 3 seconds
       number of times: infinite
       alternate direction every time
    */

    position: relative;
    border: 2px solid green;
    width: 50px;
    height: 20px;
    background: lime;
  }
</style>

有許多關於 @keyframes 的文章和 詳細規格

除非網站上的所有內容都在不斷移動,否則您可能不需要經常使用 @keyframes

效能

大多數 CSS 屬性都可以動畫化,因為它們大多數都是數值。例如,widthcolorfont-size 都是數字。當您對它們進行動畫化時,瀏覽器會逐幀逐漸更改這些數字,創造出平滑的效果。

然而,並非所有動畫都能像您希望的那麼平滑,因為不同的 CSS 屬性更改成本不同。

在更技術性的細節中,當有樣式變更時,瀏覽器會執行 3 個步驟來呈現新的外觀

  1. 配置:重新計算每個元素的幾何形狀和位置,然後
  2. 繪製:重新計算所有內容在它們的位置上應如何顯示,包括背景、顏色,
  3. 合成:將最終結果渲染成螢幕上的像素,如果存在,套用 CSS 轉換。

在 CSS 動畫期間,此程序會在每個影格中重複。但是,從不影響幾何形狀或位置的 CSS 屬性,例如 color,可能會略過配置步驟。如果 color 發生變更,瀏覽器不會計算任何新的幾何形狀,它會轉到繪製 → 合成。而且有少數屬性會直接轉到合成。您可以在 https://csstriggers.com 找到 CSS 屬性及其觸發階段的較長清單。

計算可能需要時間,特別是在具有許多元素和複雜配置的頁面上。而且延遲實際上會在大多數裝置上可見,導致動畫「不穩定」、流暢度較低。

略過配置步驟的屬性動畫速度較快。如果繪製也略過,那就更好了。

transform 屬性是一個很好的選擇,因為

  • CSS 轉換影響目標元素框整體(旋轉、翻轉、伸展、移動它)。
  • CSS 轉換從不影響鄰近元素。

…因此,瀏覽器在合成階段,在現有的配置和繪製計算「之上」套用 transform

換句話說,瀏覽器在配置階段計算配置(大小、位置),在繪製階段使用顏色、背景等繪製它,然後將 transform 套用在需要的元素框上。

transform 屬性的變更(動畫)永遠不會觸發 Layout 和 Paint 步驟。除此之外,瀏覽器會利用圖形加速器(CPU 或顯示卡上的特殊晶片)來執行 CSS 變形,因此效率非常高。

幸運的是,transform 屬性非常強大。透過對元素使用 transform,你可以旋轉和翻轉它、伸展和縮小它、移動它,以及 更多。因此,我們可以使用 transform: translateX(…) 來取代 left/margin-left 屬性,使用 transform: scale 來增加元素大小,等等。

opacity 屬性也不會觸發 Layout(在 Mozilla Gecko 中也會略過 Paint)。我們可以使用它來顯示/隱藏或淡入/淡出效果。

transformopacity 配對通常可以滿足我們的大部分需求,提供流暢、美觀的動畫。

例如,在這裡,按一下 #boat 元素會新增具有 transform: translateX(300px)opacity: 0 的類別,因此會讓它向右移動 300px 並消失

<img src="https://js.cx/clipart/boat.png" id="boat">

<style>
#boat {
  cursor: pointer;
  transition: transform 2s ease-in-out, opacity 2s ease-in-out;
}

.move {
  transform: translateX(300px);
  opacity: 0;
}
</style>
<script>
  boat.onclick = () => boat.classList.add('move');
</script>

以下是更複雜的範例,包含 @keyframes

<h2 onclick="this.classList.toggle('animated')">click me to start / stop</h2>
<style>
  .animated {
    animation: hello-goodbye 1.8s infinite;
    width: fit-content;
  }
  @keyframes hello-goodbye {
    0% {
      transform: translateY(-60px) rotateX(0.7turn);
      opacity: 0;
    }
    50% {
      transform: none;
      opacity: 1;
    }
    100% {
      transform: translateX(230px) rotateZ(90deg) scale(0.5);
      opacity: 0;
    }
  }
</style>

摘要

CSS 動畫允許平滑(或逐步)對一個或多個 CSS 屬性進行動畫變更。

它們適用於大多數動畫任務。我們也可以使用 JavaScript 進行動畫,下一章將專門探討這個主題。

與 JavaScript 動畫相比,CSS 動畫的限制

優點
  • 簡單的事情簡單做。
  • 快速且對 CPU 負擔輕。
缺點
  • JavaScript 動畫很靈活。它們可以實作任何動畫邏輯,例如元素的「爆炸」。
  • 不只是屬性變更。我們可以在 JavaScript 中建立新元素作為動畫的一部分。

在本章節的早期範例中,我們會對 font-sizeleftwidthheight 等進行動畫處理。在實際專案中,我們應該使用 transform: scale()transform: translate() 來獲得更好的效能。

大多數動畫都可以使用 CSS 來實作,如本章所述。而且 transitionend 事件允許在動畫之後執行 JavaScript,因此它可以與程式碼良好整合。

但在下一章,我們將執行一些 JavaScript 動畫來涵蓋更複雜的情況。

任務

重要性:5

顯示動畫,如下圖所示(按一下飛機)

  • 圖片按一下後從 40x24px 放大到 400x240px(放大 10 倍)。
  • 動畫需要 3 秒。
  • 最後輸出:「完成!」。
  • 在動畫過程中,可能會按一下飛機多次。這些按一下不應「中斷」任何動作。

開啟沙盒執行任務。

CSS 動畫 widthheight

/* original class */

#flyjet {
  transition: all 3s;
}

/* JS adds .growing */
#flyjet.growing {
  width: 400px;
  height: 240px;
}

請注意,transitionend 會觸發兩次,每個屬性一次。因此,如果我們不執行額外的檢查,訊息就會顯示 2 次。

在沙盒中開啟解決方案。

重要性:5

修改前一個任務 動畫飛機(CSS) 的解決方案,讓飛機放大超過其原始大小 400x240px(跳出),然後再縮回該大小。

以下是應有的外觀(按一下飛機)

以先前任務的解決方案為來源。

我們需要為該動畫選擇正確的貝茲曲線。它應在某處具有 y>1,才能讓飛機「跳出」。

例如,我們可以使用 y>1 的兩個控制點,例如:cubic-bezier(0.25, 1.5, 0.75, 1.5)

圖表

在沙盒中開啟解決方案。

重要性:5

建立一個函式 showCircle(cx, cy, radius),顯示一個動畫的放大圓形。

  • cx,cy 是圓形中心相對於視窗的座標,
  • radius 是圓形的半徑。

按一下下面的按鈕,看看應有的外觀

原始文件有一個具有正確樣式的圓形範例,因此任務就是要正確執行動畫。

開啟沙盒執行任務。

在任務 動畫圓形 中,顯示了一個動畫的放大圓形。

現在假設我們不只要一個圓形,還要在其中顯示訊息。訊息應在動畫完成(圓形完全放大)之後出現,否則看起來會很醜。

在任務的解決方案中,函式 showCircle(cx, cy, radius) 會繪製圓形,但沒有提供追蹤其準備就緒時間的方法。

新增一個 callback 參數:showCircle(cx, cy, radius, callback),當動畫完成時會被呼叫。callback 應該接收圓形 <div> 作為參數。

以下是範例

showCircle(150, 150, 100, div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

示範

動畫圓形 任務的解答為基礎。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 議題 或發起 pull request,而不是留言。
  • 如果您看不懂文章中的某些內容,請說明。
  • 若要插入少數幾個字的程式碼,請使用 <code> 標籤;若要插入多行程式碼,請用 <pre> 標籤將其包起來;若要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)