Web Animations APIのcompositeが凄過ぎてすごいからみんな見てくれ

この記事はようやくSafariでもフルサポートされそうな`Web Animations API`の`composite`（効果の組成）って機能がすごいよ！！って、ただそれだけを伝えたい記事です。平たくいうと複数のアニメーションを簡単キレイに合成できる機能なのですが、通常のWebのコーディングでもよく出てくる辛さを解決してくれる結構すごいヤツなのです。

▼ こういうアニメーション作るのもだいぶん楽になります

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Web Animations APIで星空パーティクル<br><br>単にCSSのアニメーションをJSで描けるよってだけではあるんだけど、ライブラリなしでそこそこ簡単にインタラクティブなもの作れるって意味ではうれしい。主要ブラウザ全部で使える。<a href="https://t.co/8H8zXfc5NL">https://t.co/8H8zXfc5NL</a> <a href="https://t.co/bfTERJPxIX">pic.twitter.com/bfTERJPxIX</a></p>&mdash; ゆき＠ティアF47a (@yuneco) <a href="https://twitter.com/yuneco/status/1315319022322290688?ref_src=twsrc%5Etfw">October 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

▼ 全国から寄せられた喜びの声

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">SafariのTechnology Preview 142でついにアニメーションのcompositeがサポートされたよ！！このサンプルが！うごくよ！！ <a href="https://twitter.com/hashtag/WebAnimationsAPI?src=hash&amp;ref_src=twsrc%5Etfw">#WebAnimationsAPI</a> <a href="https://t.co/vip74klL3Q">https://t.co/vip74klL3Q</a></p>&mdash; ゆき＠ティアF47a (@yuneco) <a href="https://twitter.com/yuneco/status/1510571897817239560?ref_src=twsrc%5Etfw">April 3, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

一応このツイートは100いいねくらい貰ったのだけど、この喜びが果たしてWebエンジニア界隈に広く伝わっているのか不安なのでQiitaにちゃんと書きます。この感動をみんなと共有したいんだ:dancers:

**:warning:重要:warning:** 上のツイートにもある通り、この機能はChromeとFireFoxでは実装済みですがSafariはまだTP（Technology Preview）です。次の正式リリースに入るかどうかもたぶん確定していません。[^tp]

[^tp]: より正確にはcomposite機能自体は少し前から実装済みで、TP142で入ったのは一部の未対応だった機能の追加実装です。感触的にはぼちぼち正式リリースに入るのでは...と（希望的観測で）見ています

## そもそもの問題： アニメーションの合成がないと何が辛いのか

最初に紹介したパーティクルの作例はちょっと極端なので、もうちょっと普通にある例で説明します。

この例ではキャラクター（たまさん）が常時CSS Animationで回転しています。「この状態でたまさんをクリックすると一瞬ズームしたい」というのが今回のお題です。ポイントは**回転もズームもCSSでは`transform`プロパティで指定する**という点です。わかりやすく両方アニメーションにしていますが、ベースの回転はアニメーションでなくても構いません。**とにかく`transform`プロパティを重ねて使っている**のが問題です。

![今回のお題](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/7d7a3845-b32b-8f84-4ead-63bd922d3ead.png)


これは別に特殊なことではなく、ごく普通のWebページでも頻出するものです。`transform: translate()`で位置指定をしつつ、ホバーした時だけ`transform: scale()`でちょっと大きくする、みたいなの、よくありますよね？

で、何も考えずに作るとどうなるか:rolling_eyes:

```html
<div class="stage" id="stage1">
  <!-- 「たまさん」のキャラ要素 -->
  <div class="tama"></div>
</div>

<script>
  const tama1 = document.querySelector('#stage1 .tama');
  // キャラをズームする
  // animateメソッド（Web Animations API）を使っていますが、CSS Animation等を使っても結果は同じです
  const zoomTama1 = () => {
    tama1.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.5)' }, // 1.5倍にズームして
      { transform: 'scale(1)' }    // 元のサイズに戻す
    ], { duration: 1500 })
  };
  tama1.addEventListener('pointerdown', zoomTama1); // クリックで発動
</script>
```
![「何も考慮しない場合」の実行例](https://yuneco.github.io/webanimation-star/static/demoimgs/demo1.gif)

→ [「何も考慮しない場合」を試す](https://yuneco.github.io/webanimation-star/composite-vs-wrapper.html#sample1)

:sob:ズームした瞬間回転が解除されてます:sob:

理屈は簡単ですね。回転もズームもどっちも`transform`プロパティなので、上書きされて片方しか効かなくなってるだけです。アニメーションを適用する方法はいくつかありますが、古典的なCSSのクラス名を追加する方法でも、CSS TransitionやCSS Animationを使う方法でも基本全部同じです。[^priority]

[^priority]: 方法の組み合わせによってどっちが優先されるかは変わってきます。でも「どっちか一方しか効かない」のは一緒

## 今までの解決法： divで全部入れ子にしようの術

結局のところ、ひとつの要素に`transform`プロパティは1つしか設定できないのが問題だとわかりました。つまり**タイミングやイージングの異なる複数のアニメーションを適用したければ、要素を2つ作らないと**いけません。[^othermethods] 今回の例では、元の回転するキャラクターを`div`で括って、「ズーム用のラッパー」を作ります。

[^othermethods]: 他に真っ当で簡単な方法ってないですよね...？もしあったら教えてください。[`transform`じゃない`rotate`](https://developer.mozilla.org/en-US/docs/Web/CSS/rotate)を使う方法も考えたのですが、こっちはChromeが対応してないんですよね...

```html
<div class="stage" id="stage2">
  <!-- ズーム用のラッパーdivを作る -->
  <div class="zoomWrapper">
    <div class="tama"></div>
  </div>
</div>

<script>
  // 追加したラッパー要素に対してズームを実行
  const tama2wrapper = document.querySelector('#stage2 .zoomWrapper');
  const zoomTama2 = () => {
    tama2wrapper.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.5)' },
      { transform: 'scale(1)' }
    ], { duration: 1500 })
  };
  tama2wrapper.addEventListener('pointerdown', zoomTama2);
</script>
```

![「ラッパーを使う場合（従来の方法）」の実行例](https://yuneco.github.io/webanimation-star/static/demoimgs/demo2.gif)


→ [「ラッパーを使う場合（従来の方法）」を試す](https://yuneco.github.io/webanimation-star/composite-vs-wrapper.html#sample2)


:stuck_out_tongue_closed_eyes:**コングラチュレーション**！！！:stuck_out_tongue_closed_eyes:おめでとう！！これで無事回転を維持しながらズームもできるようになりました。

でもさ、これ、めんどくないですか？面倒というか、クリーンじゃない。アニメーションの内容に合わせてDOM構造を変えないといけないこと自体が綺麗じゃないし、**何よりも「アニメーションのモジュール化」がすごく困難になるのです**。

任意の要素をズームする`zoomElement(el, scale, duration)`みたいな汎用的な関数を作ることを考えてみてください。動的にラッパーの`div`を差し込んでアニメーションして、終わったら元に戻す？...怖いですね:cold_sweat:。ReactとかVueとか使ってたら絶対ロクなことにならないです。

## 新しい解決法[^new]： compositeならアニメーションの重ね掛けができるよ！

[^new]: 仕様としては別に新しくないのですが、Safariの対応目処が立たなかったのでずっと使えなかったのです。。

やっとここで本題の`composite`です。`composite`自体は`Web Animations API`が提供するオプションのひとつに過ぎません。なので使い方も簡単です。`composite`を使って解決するコードは↓こんな感じ。

```html
<div class="stage" id="stage3">
  <div class="tama"></div>
</div>

<script>
  const tama3 = document.querySelector('#stage3 .tama');
  const zoomTama3 = () => {
    tama3.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.5)' },
      { transform: 'scale(1)' }
    ], { duration: 1500, composite: 'accumulate' })
    // composite: 'accumulate'を指定👆
  };
  tama3.addEventListener('pointerdown', zoomTama3);
</script>
```

動きは大体同じなのでキャプチャは省略

→ [「compositeを使う（新しい方法）」を試す](https://yuneco.github.io/webanimation-star/composite-vs-wrapper.html#sample3)

`composite: 'accumulate'`を指定してるだけ。これだけで元の`transform: rotate()`を維持したまま`transform: scale()`を重ねて適用することができます。

`composite`には`replace`・`add`・`accumulate`の3種類の値を指定できます。このうち、`replace`は従来と同じ上書きの動作で`add`と`accumulate`が「重ね掛け」するための設定です。この2つは大体同じですが、`transform`や`filter`みたいな複数の値リストを指定できるプロパティではちょっと結果が変わってくることがあります。詳しくは仕様読んでね。

> 注記： 実数や長さなど，多くの型のアニメーション用の累積は、 加算と一致するように定義される。 この 2 つの定義が相違するよくある事例として，リストに基づく型がある — そこでは、 加算は リストに付加するものとして, 累積は 成分ごとの加算として定義されることもある。 例えば，フィルタリスト値【 \<filter-value-list\> 】［ blur(2), blur(3) ］は、 加算されるときは blur(2) blur(3) を生産する一方で， 累積されるときは blur(5) を生産することになる。

引用元： [CSS におけるカスケード法と継承 — CSS Cascading and Inheritance Level 5 § 3. 値の結合法： 補間, 加算, 累積](https://triple-underscore.github.io/css-values-ja.html#addition)


ちなみに、`composite`による合成は3つ以上でもOKなので、この例ではマウスを連打するとたまさんがどんどん大きくなります。入れ子にする方法だとクリックするたびにズームがリセットされるので、ちょっと違う挙動ですね。

![composite使用例で連打した場合の実行例](https://yuneco.github.io/webanimation-star/static/demoimgs/demo3.gif)

どっちが良いかはケースバイケースですが、無限にズームされちゃうのが困る場合、↓のように`async`/`await`を使えば連打防止の制御も簡単です。Web Animations API便利ですね

```html
<div class="stage" id="stage4">
  <div class="tama"></div>
</div>

<script>
  const tama4 = document.querySelector('#stage4 .tama');
  let isRunning = false;
  const zoomTama4 = async () => {
    if (isRunning) return;
    isRunning = true;
    await tama4.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.5)' },
      { transform: 'scale(1)' }
    ], { duration: 1500, composite: 'accumulate' }).finished
    isRunning = false;
  };
  tama4.addEventListener('pointerdown', zoomTama4);
</script>
```

![「composite使用例で連打帽子を入れた実行例](https://yuneco.github.io/webanimation-star/static/demoimgs/demo4.gif)

→ [「compositeを使う（新しい方法） + 連打できないように制御」を試す](https://yuneco.github.io/webanimation-star/composite-vs-wrapper.html#sample4)

いい感じですね:ok_woman:

`composite`を正式に使えるようになるのはもうちょっと先かもしれませんが、Web Animations APIの基本機能は現状でもほぼ問題なく利用できるので、使えるものから少しずつ取り入れていくのが良さそうです:v:

## おまけのFAQ

### Q: `composite`はいつから使えますか？今使うと何か問題になりますか？
:a: ChromeとFirefoxのみでよければ使えます。SafariはTP版に入ったところなので正式リリースは未定です。現状Safariで`composite: 'accumulate'`または`composite: 'add'`を指定すると、エラーにはなりませんが謎の挙動を楽しめます

### Q: ポリフィルはありますか？
:a: Web Animations API自体のポリフィルはありますが、私の知る限り`composite`が動くものはないように思います（あったら教えてください）。仕組み的にもポリフィルできちんと実現するのはかなり難しいはずです

### Q: [Can I Use...](https://caniuse.com/web-animation)で見るとMac Safari（iOSではなくmac OSの）は対応しているように見えるのですが...
:a: 大体対応しているらしいのですが、少なくとも`transform`の`composite`は動作しません。他にも色々あるので[TP142のリリースノート](https://webkit.org/blog/12522/release-notes-for-safari-technology-preview-142/)のWeb Animationsの項も参照してください

### Q: パフォーマンスは変わりますか？
:a: ラッパー要素を作る必要がなくなるので、CPU/メモリとも幾らかは向上する可能性があります。Mac版Chromeで軽く測定した感じでは僅かな改善は見られました。ただ、よほど特殊なケースでなければ誤差程度の違いかと思います