ウェブアニメーション API の使用
ウェブアニメーション API により、JavaScript でアニメーションを構築したり、再生を制御したりすることができます。この記事では「ふしぎの国のアリス」を利用した楽しいデモとチュートリアルで正しい利用方法を説明します。
ウェブアニメーション API との出会い
ウェブアニメーション API は、ブラウザー内のアニメーションエンジンを開発者に公開し、JavaScript で操作できるようにします。この API は CSS アニメーションと CSS トランジションの両方の実装基盤となるように設計されており、将来のアニメーション効果への扉を開いた状態にしています。これは、ハックや強制、Window.requestAnimationFrame() なしで、ブラウザーに自分自身で内部最適化をさせる、ウェブ上でアニメーションするためのほとんどのパフォーマンスの高い方法の 1 つです。
ウェブアニメーション API を使うと、インタラクティブなアニメーションをスタイルシートから JavaScript に移し、表示と動作を分離することができます。再生方向を制御するために、CSS プロパティを書いたり、要素にクラスをスコープしたりといった、DOM を酷使する技法に頼る必要はなくなりました。また、純粋な宣言型の CSS とは異なり、JavaScript ではプロパティから再生時間まで動的に値を設定することもできます。カスタムアニメーションライブラリーを作成したり、対話するアニメーションを作成したりするのに、ウェブアニメーション API は最適かもしれません。何ができるか見てみましょう!
このページには、ウェブアニメーション APIを活用した一連の例として、不思議の国のアリスに着想を得たデモが掲載されています。これらの例は、Rachel Nabors によって作成され、ご厚意により提供されています。一連の例は CodePen で利用できるのですが、ここでは当ドキュメントに関連するものを紹介します。
ウェブアニメーション API を利用して CSS アニメーションを構築する
ウェブアニメーション API を学習するためのより身近な手法は、多くのウェブ開発者が以前に使用したことのある CSS アニメーションから始めることです。CSS アニメーションには馴染みのある構文があり、そのためデモ用にうまく分解することができます。
CSS バージョン
アリスがうさぎの穴から落ちてワンダーランドに導かれる様子を示した CSS で書かれている転がり落ちるアニメーションです。
背景が移動しながら、アリスが回転し、回転のオフセットで色が変化することに注目してください。このチュートリアルでは、アリスのみに焦点を当てます。コードブロック内の "Play" をクリックすると、ソースコード全体を見ることができます。アリスのアニメーションを制御する簡単な CSS は次のとおりです。
#alice {
animation: alice-tumbling infinite 3s linear;
}
@keyframes alice-tumbling {
0% {
color: black;
transform: rotate(0) translate3d(-50%, -50%, 0);
}
30% {
color: #431236;
}
100% {
color: black;
transform: rotate(360deg) translate3d(-50%, -50%, 0);
}
}
これによって、アリスの色とトランスフォーム回転が 3 秒間にわたって一定に(線形で)変化し、それを無限にループします。@keyframes ブロックで、各ループの 30%(約 0.9 秒)でアリスの色が黒から濃いワイン色に変わり、ループが終わるときにもとに戻ることが分かります。
JavaScript への移行
次にウェブアニメーション API を利用して同じアニメーションを作成してみましょう。
キーフレームの表現
最初に必要なことは、Keyframe オブジェクト を作成することです。これは CSS の @keyframes ブロックに相当します。
const aliceTumbling = [
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
];
ここでは、複数のオブジェクトを含む配列を使用しています。それぞれのオブジェクトは元の CSS のキーを表します。しかし、CSS とは異なり、ウェブアニメーション API は各キーがアニメーションに沿って現れるパーセント値を明示的に指示する必要はありません。与えたキーの数に応じて、アニメーションが自動的に等分割されます。つまり、3 つのキーを持つ Keyframe オブジェクトは、特に指定がない限り、アニメーションの各ループの 50% で中央のキーを再生するということです。
キーのオフセットを他のキーから明示的に設定する場合は、カンマで CSS キーの宣言と区切ったオフセットを直接オブジェクトに指定できます。上記の例では、色の変化に対してアリスの色が 30%(50% ではない)で変化するために offset: 0.3 を指定しています。
少なくとも 2 つのキーフレーム(アニメーションシーケンスの開始状態・終了状態の 2 つを示す)を指定する必要があります。ブラウザーによっては、キーフレームリストの項目が 1 つしかない場合、更新されるまで Element.animate() で NotSupportedError DOMException が発生することがあります。
という具合に、キーにオフセットを指定しない限り、キーはデフォルトで等間隔に配置されます。便利でしょう?
タイミングプロパティの表現
アリスのアニメーションの値に対応するタイミングプロパティのオブジェクトも作成する必要があります。
const aliceTiming = {
duration: 3000,
iterations: Infinity,
};
ここで、CSS での数値の表現方法とはいくらかの違いがあることにお気づきでしょう。
- 1 つ目は間隔時間はミリ秒単位で表現されています。3 秒という指定ではなく、3000 ミリ秒です。
setTimeout()やWindow.requestAnimationFrame()と同じように、ウェブアニメーション API はミリ秒でしか値を取りません。 - もう 1 つは
iteration-countではなくiterationsということです。
メモ:
CSS アニメーションで使用される用語とウェブアニメーションで利用される用語とではいくつか小さな違いがあります。例えば、ウェブアニメーションは "infinite" という文字列を利用しない代わりに JavaScript の予約語である Infinity を利用します。そして、 timing-function の代わりに easing を利用します。ここでは easing の値を挙げていません。デフォルトの animation-timing-function が簡単な ease である CSS アニメーションとは異なり、ウェブアニメーション API ではデフォルトのイージングは linear (線形)であるためです。
パーツをまとめる
次は Element.animate() メソッドを利用して今までの 2 つの要素をまとめます。
document.getElementById("alice").animate(aliceTumbling, aliceTiming);
アニメーションが開始されます。
animate() メソッドは、CSS でアニメーションできる DOM 要素に対して呼び出すことができます。そして、それはいくつかの方法で書くことが出来ます。キーフレームとタイミングプロパティを作成する代わりに、次のように直接渡すことが出来ます。
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
{
duration: 3000,
iterations: Infinity,
},
);
さらに、アニメーションの間隔時間のみを指定し、繰り返さないアニメーションを指定する場合、ミリ秒だけを指定することができます(デフォルトではアニメーションを 1 回だけ実行します)。
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
3000,
);
play(), pause(), reverse(), updatePlaybackRate() を用いた再生制御
ウェブアニメーション API を使用して CSS アニメーションを作成することが可能ですが、API が本当に役立つ時はアニメーションの再生を制御するときです。ウェブアニメーション API は、再生を制御するための便利ないくつかのメソッドを提供しています。Follow the White Rabbit のサンプルで、アニメーションの一時停止と再生について見ていきましょう。
この例では、白いウサギには、ウサギの穴の中へ降りていくアニメーションが設定されています。このアニメーションは、ユーザーがウサギをクリックしたときにのみ発生します。
アニメーションの一時停止と再生
animate() メソッドを使用すれば、いつものようにウサギにアニメーションを適用できます。
const whiteRabbit = document.getElementById("rabbit");
const rabbitDownAnimation = whiteRabbit.animate(
[{ transform: "translateY(0%)" }, { transform: "translateY(100%)" }],
{ duration: 3000, fill: "forwards" },
);
Element.animate() メソッドは、呼び出されると直ちに実行されます。ユーザーがクリックする前にケーキが自分自身を食べてしまわないようにするため、定義後に同様に Animation.pause() を呼び出します。
rabbitDownAnimation.pause();
メモ:
あるいは、Animation() コンストラクターを使用して rabbitDownAnimation を定義することもできます。その場合、play() を呼び出すまで再生は開始されません。
これで、準備が整ったらいつでも Animation.play() メソッドを使用して実行できるようになりました。具体的には、これをクリック操作とリンクさせたいと思います。これを実現するには、次のようにします。
whiteRabbit.addEventListener("click", downHeGoes);
whiteRabbit.addEventListener("touchstart", downHeGoes);
function downHeGoes(event) {
whiteRabbit.removeEventListener("click", downHeGoes);
whiteRabbit.removeEventListener("touchstart", downHeGoes);
rabbitDownAnimation.play();
}
ユーザーがウサギをクリックするか、指でタップすると、downHeGoes を呼び出して、すべてのアニメーションを再生することが可能になりました。
その他の便利なメソッド
一時停止と再生に加えて、次のアニメーションメソッドを利用することが出来ます。
Animation.finish()はアニメーションの最後までスキップします。Animation.cancel()はアニメーションを中止し、その効果を削除します。Animation.reverse()を実行すると (Animation.playbackRateに負数を設定し、アニメーションの再生方向を逆方向にします。
まず playbackRate を見てみましょう。playbackRate を府にするとはアニメーションを逆に実行させます。
『鏡の国のアリス』の Through the Looking-Glass では、アリスは、その場所に留まるためには走り続けなければならない世界へと旅立ちます。「赤の女王のレース」の例では、アリスと赤の女王はその場所に留まるために走っています。
小さな子どもは疲れやすいので、おもちゃのチェスの駒と違って、アリスは常に減速しています。これはアニメーションの playbackRate を減少させることで実現します。updatePlaybackRate() を使用することと、playbackRate を直接設定することなく、スムーズに更新することができます。
setInterval(() => {
// 再生レートが 0.4 を下回らないようにする
if (redQueenAlice.playbackRate > 0.4) {
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 0.9);
}
adjustBackgroundPlayback();
}, 1000);
しかし、クリックまたはタップして彼女らを励ますと、playbackRate を乗算しているのでスピードアップします。
function goFaster() {
// ただし、画面をクリックまたはタップすれば、処理を早めることが可能
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 1.1);
adjustBackgroundPlayback();
}
document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);
また、背景要素にはクリックまたはタップしたときに影響を受ける playbackRate を設定しています。それらの再生レートは、下記に示すアリスの再生レートに基づいて算出されています。アリスとレッドクイーンを 2 倍早く走らせたらどうなると思いますか?速度を落とすとどうなると思いますか?
/* アリスはすぐに疲れてしまう!
数秒ごとに、再生速度を少し縮小して、動きを少し遅くするようにする
*/
const sceneries = [
foreground1Movement,
foreground2Movement,
background1Movement,
background2Movement,
];
function adjustBackgroundPlayback() {
// アリスと赤の女王が 0.8 ~ 1.2 の速度で走っている場合、
// 背景は移動しない。
// しかし、0.8 を下回ると、背景が後ろにずれていく。
if (redQueenAlice.playbackRate < 0.8) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(-redQueenAlice.playbackRate / 2);
});
} else if (redQueenAlice.playbackRate > 1.2) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(redQueenAlice.playbackRate / 2);
});
} else {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(0);
});
}
}
adjustBackgroundPlayback();
アニメーションのスタイル維持
要素をアニメーションさせるとき、アニメーションが完了した後に、最終的な状態を維持したいことがよくあります。このために、アニメーションの fill モードを forwards に設定するという方法がときどき使われます。しかし、2 つの理由から、アニメーションの効果を無期限に維持するために fill モードを使用することは推奨されません。
- アニメーションがアクティブなままであり、ブラウザーがその状態を維持しなければならないため、アニメーションが終了してもアニメーションがリソースを消費し続けます。なお、これは満了したアニメーションの自動削除をすることでいくらか緩和されます。
- アニメーションで適用されたスタイルは、指定されたスタイルよりカスケードにおいて高い優先度を持つため、必要に応じて上書きすることが困難になる場合があります。
より良い方法は、Animation.commitStyles() メソッドを使うことです。これはこのアニメーションの現在のスタイルを、対象要素の style 属性に書き込むので、その後は通常にスタイル設定しなおすことができます。
満了したアニメーションの自動削除
同じ要素に多数のアニメーションを発生させることが可能です。それらが不定である場合(つまり前方満了)、アニメーションリストが巨大になり、メモリーリークが発生する可能性があります。このためブラウザーは、新しいアニメーションに置き換わった後、開発者が明示的にアニメーションを残すように指定しない限り、満了したアニメーションを自動的に削除します。
アニメーションは、以下がすべて真の場合に削除されます。
- アニメーションが満了している(
fillは前方再生であればforwards、逆再生の場合はbackwards、両方の場合はbothです)。 - アニメーションが完了している。(
fillのため、まだ有効であることに注意してください。) - アニメーションのタイムラインが単調に増加している。(これは
DocumentTimelineでは常に真です。scroll-timelineのようなタイムラインは逆方向に動作することがあります。) - アニメーションが CSS のような宣言的なマークアップによって制御されていない。
- アニメーションの
AnimationEffectのスタイル設定がすべて、上記の条件をすべて満たす別のアニメーションによって上書きされている。(通常、2 種類のアニメーションが同じ要素の同じスタイルプロパティを設定した場合、最後に作成されたアニメーションが他のアニメーションを上書きします。)
最初の 4 つの条件は、JavaScript コードによる介入なしに、アニメーションの効果が変化したり終わったりしないことを保証します。最後の条件は、アニメーションが実際に何かの要素のスタイル設定に影響しないことを保証します。
アニメーションが自動的に解除されると、そのアニメーションに remove イベントが発行されます。
ブラウザーがアニメーションを自動的に削除することを防ぐには、そのアニメーションの persist() メソッドを呼び出してください。
アニメーションの replaceState プロパティは、アニメーションが削除された場合は removed、アニメーションに persist() が呼び出された場合は persisted、そうでない場合は active になります。
アニメーションからの情報の取得
前庭障害のあるユーザーのアクセシビリティを改善するなど、サイト全体のアニメーションを遅くすることを、playbackRate 以外の方法で行うことを想像してみてください。すべての CSS ルールで期間を再計算することなく CSS で全てを行うことは不可能ですが、ウェブアニメーション API では、次の Document.getAnimations メソッドを使用して、次のようにページ上の各アニメーションをループして playbackRate を半分にすることができます。
document.getAnimations().forEach((animation) => {
animation.updatePlaybackRate(animation.playbackRate * 0.5);
});
ウェブアニメーション API を利用すると、変更する必要があるのは 1 つの小さなプロパティだけです!
CSS アニメーションだけでは難しいのは、他のアニメーションが提供する値に依存するときです。例えばアリスの拡大/縮小ゲームの例では、ケーキの時間について奇妙なことに気づいたかもしれません。
document.getElementById("eat-me-sprite").animate([], {
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
ここで何がおこっているか理解するために、アリスのアニメーションを見てみましょう。
const aliceChange = document
.getElementById("alice")
.animate(
[
{ transform: "translate(-50%, -50%) scale(.5)" },
{ transform: "translate(-50%, -50%) scale(2)" },
],
{
duration: 8000,
easing: "ease-in-out",
fill: "both",
},
);
アリスのアニメーションでは、8 秒かけてサイズが半分から 2 倍になります。その後、彼女の効果を停止します。
aliceChange.pause();
もしアニメーションの始めに一時停止させたままにしておくと、ボトル全部を飲み干したかのように、半分の大きさから始まることになります。アニメーションの "playhead" を中央に設定し、アニメーションがすでに半分終わっている状態にしたいと思います。Animation.currentTime を 4 秒に設定したいので、次のようにします。
aliceChange.currentTime = 4000;
しかし、このアニメーションで作業している間、アリスの再生時間を何度も変更するかもしれません。彼女の currentTime を動的に設定すれば、時刻を 2 回更新する必要はないでしょう?実際、aliceChange の Animation.effect プロパティを参照することで、アリスに有効なエフェクトの詳細をすべて格納したオブジェクトを返すことができます。
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;
effect を利用すると、アニメーションのキーフレームとタイミングのプロパティにアクセスできます。aliceChange.effect.getComputedTiming() はアリスのタイミングオブジェクトを示します。これには duration が含まれています。
アニメーションのタイムラインの中間点を取得するには、アリスの再生時間を半分に分割し、通常の身長に設定します。これで、アリスのアニメーションをそれぞれの方向に巻き戻したり再生したりすると、小さくしたり、大きくしたりすることができます。
また、ボトルとケーキの再生時間を設定するときにも同じことが出来ます。
const drinking = document
.getElementById("liquid")
.animate([{ height: "100%" }, { height: "0" }], {
fill: "forwards",
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
drinking.pause();
これで、3 つのアニメーションがすべて 1 つの再生時間上に繋がり、1 つの場所から簡単に変更することが出来ます。
ウェブアニメーション API を利用して、アニメーションの現在の時刻を把握することも出来ます。ケーキを食べてなくなったり、ボトルが空になったりするとゲームが終了します。アリスが大きすぎてドアを通らなくなるか、小さすぎてドアノブに届かなくなるかはアリスのアニメーションによって表現されプレイヤーの操作に依存します。アリスのアニメーションの currentTime を取得して、彼女の activeDuration で割ることで彼女の大きさを把握できます。
const endGame = () => {
// アリスのタイムラインの再生位置を取得
const alicePlayhead = aliceChange.currentTime;
const aliceTimeline = aliceChange.effect.getComputedTiming().activeDuration;
// アリスやその他のアニメーションを停止
stopPlayingAlice();
// 3 つのどれに入るかを判断
const aliceHeight = alicePlayhead / aliceTimeline;
if (aliceHeight <= 0.333) {
// アリスが小さくなる
// …
} else if (aliceHeight >= 0.666) {
// アリスが大きくなる
// …
} else {
// アリスの大きさはあまり変わらない
// …
}
};
コールバックとプロミス
CSS アニメーションとトランジションには独自のイベントリスナーがあり、これらはウェブアニメーション API でも利用可能です。
onfinishはfinishイベントハンドラーであり、finish()メソッドを使用すると手動で起動することができます。oncancelはcancelイベントハンドラーであり、cancel()メソッドを使用すると起動することができます。
ここでは、ケーキ、ボトル、アリスのコールバックを設定して、endGame 関数を起動します。
// ケーキやボトルがなくなった時
nommingCake.onfinish = endGame;
drinking.onfinish = endGame;
// アリスのアニメーションが終わりに達した時
aliceChange.onfinish = endGame;
さらに良いことに、ウェブアニメーション API ではアニメーション終了時に解決されるか、またはキャンセル時に拒否される finished プロミスを提供しています。
まとめ
以上がウェブアニメーション API の基本的な機能です。これで、ブラウザーでのアニメーションという「ウサギの穴に飛び込む」準備ができ、自分自身でアニメーションの実験を書く準備ができたはずです。
関連情報
- 不思議の国のアリスの全体 (CodePen) を動作させたり、 フォークやシェアを行うことができます。
- Element.animate を気にせずにアニメーションする方法 (2016): ウェブアニメーション API の背景と、なぜ API が複数のウェブアニメーションメソッドよりもパフォーマンスが高いのかについて説明します。