333
261

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-08

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

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

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

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

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

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

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

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

今回のお題

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

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

<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>

「何も考慮しない場合」の実行例

「何も考慮しない場合」を試す

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

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

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

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

<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>

「ラッパーを使う場合(従来の方法)」の実行例

「ラッパーを使う場合(従来の方法)」を試す

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

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

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

新しい解決法4: compositeならアニメーションの重ね掛けができるよ!

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

<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を使う(新しい方法)」を試す

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

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

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

引用元: CSS におけるカスケード法と継承 — CSS Cascading and Inheritance Level 5 § 3. 値の結合法: 補間, 加算, 累積

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

composite使用例で連打した場合の実行例

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

<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使用例で連打帽子を入れた実行例

「compositeを使う(新しい方法) + 連打できないように制御」を試す

いい感じですね: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...で見るとMac Safari(iOSではなくmac OSの)は対応しているように見えるのですが...

:a: 大体対応しているらしいのですが、少なくともtransformcompositeは動作しません。他にも色々あるのでTP142のリリースノートのWeb Animationsの項も参照してください

Q: パフォーマンスは変わりますか?

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

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

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

  3. 他に真っ当で簡単な方法ってないですよね...?もしあったら教えてください。transformじゃないrotateを使う方法も考えたのですが、こっちはChromeが対応してないんですよね...

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

333
261
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
333
261

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?