畫物語——CSS動畫之美 | 掘金技術徵文-雙節特別篇

語言: CN / TW / HK

前言

大家好,這裏是CSS魔法使——alphardex。

我對動畫創作有着巨大的興趣,平時你們能夠經常在掘金的沸點上看到我創作的各種各樣的動畫作品。於是乎,我就決定將我所使用的各種技巧總結起來,讓大家也能愉快地玩耍CSS動畫。

好了,話不多説。讓我們一起進入CSS動畫這個領域吧!

小彩蛋:本文的標題neta了“化物語”,是筆者最喜歡的動畫系列之一,裏面的動畫非常具有表現力,強烈推薦大家去看。

CSS動畫簡介

平時,我們習慣了用CSS來實現各種的靜態頁面佈局。但是呢,設計得再好看的頁面如果缺少動畫,就會像一個缺少靈魂的空殼一般毫無生機。那麼如何賦予頁面以靈魂呢?答案就是動畫。

在CSS世界中,實現動畫主要有2種方式:transition和animation,在本文我們重點探討的是後者,也就是動畫。

首先簡要説説transition吧:transition即過渡,意思是從一種狀態變化到另一種狀態。但是,如果我們需要元素能在多個狀態之間切換,甚至還能循環播放,那麼transition就顯得無能為力了。這時我們就要藉助強大的animation屬性了

首先,讓我們來看以下的一個動畫

感覺如何?有點酷對吧,但當我們看它的源代碼時,才發現居然要整整12個關鍵幀,還沒算上時間軸的編排

由此可見,一個複雜的動畫背後是由許多小動畫編排而成的,就如同樂隊的演奏一般,只有經過指揮家精心的指揮才能演奏出美好的樂章。而你,就是CSS動畫的指揮家。

如何學習動畫

一言以蔽之:化動為靜。

動畫的本質其實就是一張張圖片快速地切換播放而已,利用了人腦的視覺暫留效應才形成了動畫效果。如果想學習動畫,那麼必然要把動畫裏的每一張靜態幀都單獨抽離出來。或許,有的人相信自己有動態視力可以不這麼做,但這樣必然會錯失很多細節,而細節恰恰決定了動畫的成敗。

可能很多前端都用過騰訊智圖這個軟件吧?平時用它來批量壓縮圖片還是蠻爽的,但是裏面內置的ImageMagick卻有個鮮為人知的實用功能——將gif的幀批量轉化為png。命令行上使用很簡單:

convert -coalesce target.gif target_%d.png
複製代碼

實操一下吧,將上一節的動畫gif保存到本地,運行如上的命令,你就會得到該動畫所有的靜態幀

有點多對不對?別慌,先把關鍵幀給提取出來。關鍵幀是什麼呢?就是動畫從A狀態切換到B狀態時的2個狀態的幀,省去了中間的所有過渡幀,以下是該動畫的3張關鍵幀

通過這3張關鍵幀,我們就能完成一項很重要的任務——佈局。

就算是動畫也離不開佈局,根據畫面上的所有元素,我們可以寫出如下的HTML結構

<div class="relative flex items-center justify-center">
  <div class="bar-1">
    <div class="arrow left"></div>
    <div class="flex flex-col items-center self-stretch">
      <div class="lines top">
        <div class="line"></div>
        <div class="line"></div>
      </div>
      <div class="block">
        <span class="staggered-scale-in">High stakes table</span>
      </div>
      <div class="lines bottom">
        <div class="line"></div>
        <div class="line"></div>
      </div>
    </div>
    <div class="arrow right"></div>
  </div>
  <div class="bar-2">
    <div class="clips">
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
      <div class="clip"></div>
    </div>
    <div class="arrows">
      <div class="arrow top"></div>
      <div class="arrow right"></div>
      <div class="arrow bottom"></div>
      <div class="arrow left"></div>
    </div>
    <div class="block">
      <span class="staggered-scale-in">$100-$200</span>
    </div>
    <div class="lines">
      <div class="line left-top"></div>
      <div class="line right-top"></div>
      <div class="line left-bottom"></div>
      <div class="line right-bottom"></div>
    </div>
  </div>
</div>
複製代碼

接下來就是寫佈局了,佈局可謂是仁者見仁智者見智,各有各的寫法,這裏就提幾個要點吧:

  1. 諸如箭頭、三角形、平行四邊形等形狀可以用clip-path來繪製,用好這個在線的工具網站就行
  2. 佈局儘量用flex實現,因為它是當代的CSS佈局之王
  3. CSS最重要的就是微調,好好利用devtools裏面的CSS面板吧,它會祝你一臂之力的

當你成功地完成了佈局後,就要開始想辦法讓它動起來了。

首先通過上面的3個關鍵幀結合原動畫可以知道該動畫能分為3個部分:

第一部分:2個箭頭arrow從左右出現並向各自的方向劃去;中間的紅條block伸展出來;紅條上下方的線line伸展出來;文字span交錯出現;整體bar-1旋轉消失

第二部分:另一個整體bar-2旋轉進入;上下方交錯排列的平行四邊形clip交錯地向中間斜向移動;4個箭頭arrow從上下左右出現並向各自的方向劃去

第三部分:中間的灰條block伸展出來;文字span交錯出現;四條線line同時伸展出來

接下來,讓我們來逐句分析吧

(注:由於本文並非從零開始的動畫教程,因此animation的詳細用法本文並不會提及,請讀者先自行做好功課再閲讀下一部分)

準備工作

筆者之前寫了一個自己用的CSS框架——aqua.css,裏面內置了許多有用的工具類(包括許多動畫的緩動函數),將其引入我們的html

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@alphardex/[email protected]/dist/aqua.min.css" />
複製代碼

第一部分

part-1.gif

2個箭頭arrow從左右出現並向各自的方向劃去,此句對應的關鍵幀和動畫如下

.arrow {
  &.left {
    animation: slide-left-in 1s var(--ease-out-quart) both;
  }

  &.right {
    animation: slide-right-in 1s var(--ease-out-quart) both;
  }
}

@keyframes slide-left-in {
  from {
    transform: translateX(1000%);
    opacity: 0;
  }

  to {
    transform: translateX(0);
  }
}

@keyframes slide-right-in {
  from {
    transform: translateX(-1000%);
    opacity: 0;
  }

  to {
    transform: translateX(0);
  }
}
複製代碼

中間的紅條block伸展出來

.block {
  animation: scale-x-in 1.2s 0.15s var(--ease-out-quart) both;
}

@keyframes scale-x-in {
  from {
    transform: scaleX(0);
  }

  to {
    transform: scaleX(1);
  }
}
複製代碼

紅條上下方的線line伸展出來

.line {
  animation: scale-x-in 0.8s var(--ease-out-quart) both;
}
複製代碼

文字span交錯出現(注意這裏要先用JS分割一下文字並應用交錯動畫,用到了gsap的SplitText插件,它還有個免費的替代品Splitting

.scale-in-bounce {
  opacity: 0;
  animation: scale-in-bounce 0.2s both;
  animation-delay: calc(var(--basic-delay) + 0.05s * var(--i));
}

@keyframes scale-in-bounce {
  0% {
    opacity: 0;
    transform: scale(2.5);
  }

  40% {
    opacity: 1;
    transform: scale(0.8);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}
複製代碼
const split = new SplitText(".staggered-scale-in", {
  type: "chars",
  charsClass: "scale-in-bounce",
});
split.chars.forEach((item, i) => {
  item.style.setProperty("--basic-delay", "0.7s");
  item.style.setProperty("--i", `${i}`);
});
複製代碼

整體bar-1旋轉消失

.bar-1 {
  animation: rotate-right-out 0.3s var(--bar-1-duration) both;
}

@keyframes rotate-right-out {
  to {
    transform: rotate(90deg);
    opacity: 0;
  }
}
複製代碼

第二部分

part-2.gif

另一個整體bar-2旋轉進入

.bar-2 {
  animation: rotate-left-in 0.3s var(--bar-1-duration) both;
}

@keyframes rotate-left-in {
  from {
    transform: rotate(-45deg);
    opacity: 0;
  }
}
複製代碼

上下方交錯排列的平行四邊形clip交錯地向中間斜向移動(這裏用到了nth-child偽類來選擇奇數和偶數項)

.clip {
  &:nth-child(odd) {
    animation: slide-right-top-in 0.8s var(--ease-out-quart) both;
  }

  &:nth-child(even) {
    animation: slide-left-bottom-in 0.8s var(--ease-out-quart) both;
  }
}

@keyframes slide-right-top-in {
  from {
    transform: translate(50%, -100%);
    opacity: 0.5;
  }

  to {
    transform: translate(0, 0);
    opacity: 1;
  }
}

@keyframes slide-left-bottom-in {
  from {
    transform: translate(-50%, 100%);
    opacity: 0.5;
  }

  to {
    transform: translate(0, 0);
    opacity: 1;
  }
}
複製代碼

4個箭頭arrow從上下左右出現並向各自的方向劃去

.arrow {
  &.top {
    animation: slide-bottom-in 0.8s var(--bar-2-delay) var(--ease-out-quart) both;
  }

  &.right {
    animation: slide-left-in-2 0.8s var(--bar-2-delay) var(--ease-out-quart) both;
  }

  &.bottom {
    animation: slide-top-in 0.8s var(--bar-2-delay) var(--ease-out-quart) both;
  }

  &.left {
    animation: slide-right-in-2 0.8s var(--bar-2-delay) var(--ease-out-quart) both;
  }
}

@keyframes slide-bottom-in {
  from {
    transform: translateY(300%);
    opacity: 0.5;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@keyframes slide-top-in {
  from {
    transform: translateY(-300%);
    opacity: 0.5;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@keyframes slide-left-in-2 {
  from {
    transform: translateX(-1150%);
    opacity: 0.5;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes slide-right-in-2 {
  from {
    transform: translateX(1150%);
    opacity: 0.5;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}
複製代碼

第三部分

part-3.gif

中間的灰條block伸展出來(跟之前的紅條几乎一模一樣)

.bar {
  animation: scale-x-in 1.2s calc(var(--bar-2-delay) + 0.6s) var(--ease-out-quart) both;
}
複製代碼

文字交錯出現同上

最後,四條線line同時伸展出來(注意它們的朝向不同,因此要更改它們的運動中心點)

.line {
  &.left-top {
    transform-origin: right;
    animation: scale-x-in 0.8s calc(var(--bar-2-delay) + 0.8s) var(--ease-out-quart) both;
  }

  &.right-top {
    transform-origin: left;
    animation: scale-x-in 0.8s calc(var(--bar-2-delay) + 0.8s) var(--ease-out-quart) both;
  }

  &.left-bottom {
    transform-origin: right;
    animation: scale-x-in 0.8s calc(var(--bar-2-delay) + 0.8s) var(--ease-out-quart) both;
  }

  &.right-bottom {
    transform-origin: left;
    animation: scale-x-in 0.8s calc(var(--bar-2-delay) + 0.8s) var(--ease-out-quart) both;
  }
}
複製代碼

最後的成品:猛戳這裏

完成一個作品固然值得慶賀,但完成的過程也很重要,因為通過這個過程,你也在學習動畫相關的方法論,如果掌握了門道,以後再複雜炫酷的動畫相信你也能手到擒來

CSS動畫技巧

通過上面的學習相信你已經感受到CSS動畫的魅力了吧。接下來筆者會講一些常用的動畫技巧,用好這些技巧能寫出更具有多樣性的動畫來

交錯

以上是一個縮放相關的動畫,注意到放大的方塊有4個,且都是放大到自身的大小,如果給它們加上不同的延時,就能達成交錯的動畫效果

<div class="blocks">
  <div class="block block-1"></div>
  <div class="block block-2"></div>
  <div class="block block-3"></div>
  <div class="block block-4"></div>
</div>
複製代碼
.block {
  animation: scale-in-center 0.8s var(--ease-out-cubic) both;

  &-1 {
    animation-delay: 0;
  }

  &-2 {
    animation-delay: 0.3s;
  }

  &-3 {
    animation-delay: 0.45s;
  }

  &-4 {
    animation-delay: 0.6s;
  }
}

@keyframes scale-in-center {
  from {
    transform: scale(0);
  }

  to {
    transform: scale(1);
  }
}
複製代碼

本demo地址:Motion Table - Symmetric Scale

描邊

六邊形可以通過矢量繪圖軟件繪製而成(筆者用的是InkScape

描邊效果要控制這兩個屬性:stroke-dasharraystroke-offset,前者控制svg的點劃線長度,後者控制sv**劃線的偏移量,當前者足夠大時,控制後者的值便能達成描邊效果

<div class="hexagon" style="--i: 1">
  <svg width="6rem" viewBox="0 0 68.982 79.653" xmlns="http://www.w3.org/2000/svg" class="half left">
    <path d="M34.492 78.5L1.001 59.164V20.492L34.492 1.156l33.491 19.336v38.672z" fill="none" stroke-width="2" />
  </svg>
  <svg width="6rem" viewBox="0 0 68.982 79.653" xmlns="http://www.w3.org/2000/svg" class="half right">
    <path d="M34.492 78.5L1.001 59.164V20.492L34.492 1.156l33.491 19.336v38.672z" fill="none" stroke-width="2" />
  </svg>
</div>
複製代碼
.hexagon {
  .half {
    stroke-dasharray: 233;
    animation: stroke-in 1s both;

    &.right {
      transform: scaleX(-1);
    }
  }
}

@keyframes stroke-in {
  from {
    stroke-dashoffset: 236;
  }

  to {
    stroke-dashoffset: 117;
  }
}
複製代碼

本demo地址:Motion Table - Repeat Scale

環形運動

首先父元素毫無疑問是繞中心360旋轉,關鍵就是子元素的位移距離要等於環的半徑,這樣就能達成環形旋轉的效果了

<div class="orbit">
  <div class="point"></div>
</div>
複製代碼
.orbit {
  .point {
    animation: spin var(--spin-duration) var(--spin-delay) linear infinite;

    &::before {
      transform: translateX(calc((var(--orbit-width)) / 2));
    }
  }
}

@keyframes spin {
  from {
    transform: rotate(0);
  }

  to {
    transform: rotate(1turn);
  }
}
複製代碼

本demo地址:Motion Table - Orbit

3d視角

父元素設置transform-style: preserve-3dperspective,子元素進行3D變換即可

<div class="camera">
  <div class="cards">
    <div class="card"></div>
    ...
  </div>
</div>
複製代碼
.camera {
  transform-style: preserve-3d;
  perspective: 200px;
  transform: rotateX(60deg) rotateZ(35deg) scale(1.1);
}
複製代碼

本demo地址:Motion Table - Depth Of Field

其實ios通知的摺疊也是典型的3d變換,以下是它的CSS動畫版實現

看到彩蛋了嗎?沒錯,是春物

本demo地址:IOS Notification Fold Toggle

隨機性

主要用到以下2個SCSS的Mixin來生成隨機的數據

@function random_range($min, $max) {
  $rand: random();
  $random_range: $min + floor($rand * (($max - $min) + 1));
  @return $random_range;
}

@function sample($list) {
  @return nth($list, random(length($list)));
}
複製代碼

本demo地址:Bubble Ring

截斷法

主要利用了overflow: hidden這個屬性來把多餘的部分截掉,可以用來模擬各種效果(比如描邊)

本demo地址:Frame Text Reveal

最後

依舊是一個彩蛋

該動畫的靈感來源:憑物語第 3 集開頭

本 demo 地址:3D Puzzle Animation

🏆 掘金技術徵文|雙節特別篇