Web Animations APIでシマウマを回したくなったときに見るやつ

Web Animations APIの出番が少なすぎて使い方を忘れそうなので覚えてるうちに。シマウマを回したくなったときの参考にしてください。

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="VwVZryp" data-user="nishinoshake" style="height: 00px; 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/nishinoshake/pen/VwVZryp">
  Web Animations API Zebra Rotation</a> by nishinoshake (<a href="https://codepen.io/nishinoshake">@nishinoshake</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## Web Animations API
* CSSアニメーションのJS版
* CSSで良くない？
* GSAPで良くない？

仕様を見ていて便利だなぁと思ったのが、再生速度をコントロールできるところ。現時点での印象は、JSでキーフレームを使いたい、かつアニメーションライブラリが使えない状況で役に立つAPIという感じ。ニッチだな！

[Web Animations APIのcompositeが凄過ぎてすごいからみんな見てくれ](https://qiita.com/yuneco/items/8be607b9bfe8e609a658)を見てみたら、composite プロパティはたしかに使えそう。あの罪深い `<div>` を減らせるならそれだけでも価値がある。たまさんかわいい。

些細なところですが、デフォルトではアニメーション時にインラインスタイルを書き換えないので、なんでこの要素動いてるの！っていう驚きを同業者に与えられる。結局ニッチだな！

:::note info
[commitStyles()](https://developer.mozilla.org/en-US/docs/Web/API/Animation/commitStyles) でインラインスタイルにぶち込めます
:::

## シマウマを回す

<img width="250" height="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89103/60233230-68b2-219a-6005-1a6ce501f6f2.gif">


```css
/* CSSアニメーション */
@keyframes rotation {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.zebra {
  /* animation-timing-function の初期値は ease*/
  animation: rotation 2s linear infinite;
}
```
```js
/* Web Animations API */
document.querySelector('.zebra').animate(
  [
    { transform: 'rotate(0deg)' },
    { transform: 'rotate(360deg)' }
  ], {
    duration: 2000,
    iterations: Infinity,
    easing: 'linear' // こっちは linear が初期値
  }
)
```

CSSアニメーションとの比較。パッと見は似ていますが、プロパティがちょこちょこ違います。 無限ループが `"infinite"` ではなく [Infinity](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Infinity) だったり、イージングの初期値も地味に違います。他にも違いがありそうなので、使う前に仕様のご確認を。



## シマウマと遊ぶ

<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="VwVZryp" data-user="nishinoshake" style="height: 00px; 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/nishinoshake/pen/VwVZryp">
  Web Animations API Zebra Rotation</a> by nishinoshake (<a href="https://codepen.io/nishinoshake">@nishinoshake</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

回すだけだとCSSで良くない？ってなるので、再生速度を変更できるようにしました。CSSで animation-duration を変えるとアニメーションがバキっとなりますが、こっちだとそれがないのが優秀。

あと、finished プロパティが Promise を返してくれるので、終わったら要素を消したい、みたいなときに便利。animationend イベントよりも扱いやすいので、単発で消すアニメーションには良いかも。（このサンプルだとボタンを押したときの Faster! の文字とか）

再生速度の playbackRate は 0 で動きが止まり、負の値は逆再生になります。

playbackRate が負の状態で currentTime が 0 まで巻き戻ると　アニメーションが止まってしまうので、currentTime を `duration * 10000` からスタートさせる心温まる実装をしています。狂気の1万イテレーション！

![1万イテレーションで静止するシマウマ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89103/61730616-f820-2d66-bd02-ccf65338f161.png)

何かを回したい人のために関数を置いておくので、よかったら使ってください。

<details><summary>TypeScript</summary>

```ts
type RotationOption = {
  el: HTMLElement
  duration?: number
  step?: number
}

const createRotation = (option: RotationOption) => {
  const OFFSET_ITERATIONS = 10000
  const duration = option.duration === undefined ? 1000 : option.duration
  const step = option.step === undefined ? 0.5 : option.step
  const animation = option.el.animate(
    [
      { transform: 'rotate(0deg)' },
      { transform: 'rotate(360deg)' }
    ], {
      duration,
      iterations: Infinity
    }
  )

  animation.currentTime = duration * OFFSET_ITERATIONS

  return {
    faster: () => {
      animation.playbackRate += step
    },
    slower: () => {
      animation.playbackRate -= step
    }
  }
}

const target = document.querySelector<HTMLElement>('.target')

if (target) {
  const rotation = createRotation({
    el: target,
    duration: 1000,
    step: 0.5
  })
  
  // Faster!
  document.querySelector<HTMLElement>('.faster')?.addEventListener('click', () => rotation.faster())
  
  // Slower!
  document.querySelector<HTMLElement>('.slower')?.addEventListener('click', () => rotation.slower())
}
```
</details>

<details><summary>JavaScript</summary>

```js
const createRotation = (option) => {
  const OFFSET_ITERATIONS = 10000
  const duration = option.duration === undefined ? 1000 : option.duration
  const step = option.step === undefined ? 0.5 : option.step
  const animation = option.el.animate(
    [
      { transform: 'rotate(0deg)' },
      { transform: 'rotate(360deg)' }
    ], {
      duration,
      iterations: Infinity
    }
  )

  animation.currentTime = duration * OFFSET_ITERATIONS

  return {
    faster: () => {
      animation.playbackRate += step
    },
    slower: () => {
      animation.playbackRate -= step
    }
  }
}

const target = document.querySelector('.target')

if (target) {
  const rotation = createRotation({
    el: target,
    duration: 1000,
    step: 0.5
  })
  
  // Faster!
  document.querySelector('.faster')?.addEventListener('click', () => rotation.faster())
  
  // Slower!
  document.querySelector('.slower')?.addEventListener('click', () => rotation.slower())
}
```
</details>

## 参考
* [Web Animations API を利用する](https://developer.mozilla.org/ja/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API)
* [Web Animations APIのcompositeが凄過ぎてすごいからみんな見てくれ](https://qiita.com/yuneco/items/8be607b9bfe8e609a658)