CSSanimationの@keyframesを表現ごとに分割し、汎用性を持たせる

この記事は[un-T factory! XA Advent Calendar 2021](https://qiita.com/advent-calendar/2021/unt-xa)８日目の記事です・


## 概要
こんにちは、最近オフィスの一画にカフェ＆バーが完成し、営業許可証と食品衛生責任者の資格を取りに行った、フロントエンドエンジニアの[茂吉](https://qiita.com/s-kato)です。

今回はCSSのanimationとkeyframesについて話したいと思います。

CSSアニメーションにはtransitionとanimationの２つがあり、CSSで複雑なアニメーションを実装する場合、animationプロパティを駆使して表現することが多いのではないでしょうか。

キーフレームを使うことで自由に表現することができる反面、一つのkeyframesに詰め込みすぎてしまうと、どのキーフレームで何のアニメーションをしているかがわかりづらくなります。

本記事は、CSSのkeyframesを表現ごとに分割することで可読性を高め、汎用性を持たせつつメンテンナンスしやすくする方法を説明していきたいと思います。

## この記事で伝えたいこと

- キーフレームを整理することで可読性を高められる
- animationの値を分割することで汎用性を持たせることができる
- キーフレームごとにイージングやdurationを柔軟に調整することができる

## 分かりづらいkeyframesの例

一つのkeyframesに詰め込んだ場合の記述を例に説明していきます。
例はリンゴが実った後に地面に向かって落下し、弾むというものです。

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="NWaNKmx" data-user="mokichi" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mokichi/pen/NWaNKmx">
  animation keyframes sample3</a> by seiya kato (<a href="https://codepen.io/mokichi">@mokichi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

下記のようにkeyframesが一つにまとまっていることで、どのキーフレームが何の表現をしているのかがひと目ではわかりづらく、部分的な調整がとても大変です。
（keyframsの中身はサラッと読み飛ばしてください。）

```scss:
.dropBounce {
  animation: dropBounce 1.8s $easeInOutSine forwards;
}

// 【Keyframes】 落下して弾む
@keyframes dropBounce {
  0% {
    transform: scale(0);
  }
  13% {
    transform: scale(1.5);
  }
  23% {
    transform: scale(0.8);
  }
  30% {
    transform: scale(1.1);
  }
  33% {
    transform: scale(1);
  }
  33.001% {
    top: 0;
    animation-timing-function: $easeInQuart;
  }
  56%,56.001% {
    top: 200px;
  }
  62% {
    top: 190px;
  }
  66% {
    top: 200px;
  }
  71% {
    top: 195px;
  }
  74% {
    top: 200px;
  }
  77% {
    top: 198px;
  }
  80%, 100% {
    top: 200px;
  }
}
```

## キーフレーム名だけで表現がわかるように整理する

まずは**落下**と**弾む**という2つの動きに絞って説明していきます。
**drop**と**bounce**というキーフレーム名にして、animationの値を設定します。
animationの値をカンマ区切りにすることで、複数のkeyframesを繋げて指定することができます。

- 落下（drop）
- 弾む（bounce）

```scss:落下して弾む
.dropBounce {
  animation:
    drop 0.6s $easeInQuart forwards,
    bounce 0.6s $easeOutSine forwards 0.6s
  ;
}

// 【Keyframes】 落下
@keyframes drop {
  0% {
    top: 0;
  }
  100% {
    top: 200px;
  }
}

// 【Keyframes】 弾む
@keyframes bounce {
  0% { top: 200px; }
  10% { top: 190px; }
  20% { top: 200px; }
  30% { top: 195px; }
  40% { top: 200px; }
  50% { top: 198px; }
  55% { top: 200px; }
}
```

`$easeInQuart`や`$easeOutSine`はイージングの変数で、ベジェ曲線というものを利用して動きの緩急を付けるのですが、イージングについては以前記事で書いた
[【CSSアニメーション】transitionのイージングにease-inやease-outを適当に設定するのはやめましょう](https://qiita.com/s-kato/items/d5d1ceee9e2a28e02e6e)を読んでいただければと思います。

`forwards`は`animation-fill-mode`プロパティの値で、最後のキーフレームで停止する指定です。
keyframes名の後ろの`0.6s`は`animation-duration`で0.6秒かけてアニメーションをするという指定です。forwardsの後ろの`0.6s`は`animation-delay`で0.6秒後にbounceが始まります。

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="abLdrop" data-user="mokichi" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mokichi/pen/abLdrop">
  animation keyframes sample1</a> by seiya kato (<a href="https://codepen.io/mokichi">@mokichi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## 汎用性を持たせるキーフレームを作成する

ここからは**落下（drop）して弾む（bounce）**をベースに、`実る`や`横移動`、`転がる`、`潰れる`などを追加して3パターン作成してみます。
dropやbounce、途中で登場するkeyframesは割愛して説明します。

### 実って落ちる
リンゴが実るようなアニメーションを先頭に追加します。
keyframes名はgrowとします。

- 実る（grow）

実る演出のanimation-durationは`0.6s`なので、dropやbounceにはanimation-delayを`0.6s`ずつ追加します。

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="NWaNKmx" data-user="mokichi" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mokichi/pen/NWaNKmx">
  animation keyframes sample2</a> by seiya kato (<a href="https://codepen.io/mokichi">@mokichi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

冒頭で挙げた「分かりづらいkeyframesの例」と見比べてみると、表現ごとにキーフレームを分割したことで**可読性が高まり**、キーフレームも0%〜100%で指定することができるため、**追加調整時の余計な計算が不要になります**。

```scss:
.grow {
  animation:
    grow 0.6s $easeInSine forwards,
    drop 0.6s $easeInQuart forwards 0.6s,
    bounce 0.6s $easeOutSine forwards 1.2s
  ;
}

// 【Keyframes】 実る
@keyframes grow {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  70% {
    transform: scale(0.8);
  }
  90% {
    transform: scale(1.1);
  }
  100% {
    transform: scale(1);
  }
}
```

### 転がる

横移動しながら転がる表現を追加してみます。

- 横移動（moveOn）
- 転がる（rollOver）

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="LYzGKwV" data-user="mokichi" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mokichi/pen/LYzGKwV">
  animation keyframes sample1</a> by seiya kato (<a href="https://codepen.io/mokichi">@mokichi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

今回は転がる+横移動+回転が同時進行のため、`animation-delay`はbounceにしかかけません。
補足として、弾んだタイミングで回転を強くかけたいため、rollOverのイージングは変数ではなく、cubic-bezierを独自で作成しています。→ [https://cubic-bezier.com/#.45,0,.42,1](https://cubic-bezier.com/#.45,0,.42,1)

```scss:落下+横移動+転がる+弾む
.dropRollOver {
  animation: 
    drop 0.6s $easeInCubic forwards,
    moveOn 1.2s $easeInOutSine forwards,
    rollOver 1.2s cubic-bezier(.46,.01,.43,1) forwards,
    bounce 0.6s $easeOutSine forwards 0.6s
  ;
}

// 【Keyframes】 横に移動
@keyframes moveOn {
  0% {
    left: 0px;
  }
  100% {
    left: 200px;
  }
}

// 【Keyframes】 回転
@keyframes rollOver {
  0% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(50deg);
  }
  90% {
    transform: rotate(315deg);
  }
  100% {
    transform: rotate(320deg);
  }
}
```

### 潰れる
最後に実って落下したら潰れるという演出を加えてみます。

- 潰れる（break）

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="eYGZYmy" data-user="mokichi" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mokichi/pen/eYGZYmy">
  animation keyframes sample3</a> by seiya kato (<a href="https://codepen.io/mokichi">@mokichi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```scss:
.dropBreak {
  animation:
    grow 0.6s $easeInSine forwards,
    drop 0.6s $easeInQuart forwards 0.6s,
    break 0.1s $easeOutExpo forwards 1.2s  
  ;
}

// 【Keyframes】 落下後のバウンド
@keyframes break {
  0% {
    transform-origin: center center;
    transform: scale(1, 1);
  }
  50% {
    transform-origin: center bottom;
  }
  100% {
    transform-origin: center bottom;
    transform: scale(2, 0.04);
  }
}
```

## まとめ

- keyframesに複数のアニメーションを詰め込みすぎない
- キーフレーム名だけで表現がわかるようにまずは整理する
- animationの値はカンマで区切り、複数のkeyframeを繋げる
- キーフレームを分割することで可読性が高まり、メンテナンス性も向上する