ウェブアプリのアニメーションを強化
要約: アニメーション ワークレットを使用すると、デバイスのネイティブ フレームレートで実行される命令型アニメーションを記述して、ジャギーのない滑らかなアニメーションを実現できます。また、メインスレッドのジャンクに対してアニメーションの耐障害性を高め、時間ではなくスクロールにリンクできます。アニメーション ワークレットは Chrome Canary 版(「試験運用版ウェブ プラットフォーム機能」フラグの背後)にあり、Chrome 71 のオリジン トライアルを計画しています。今すぐ、漸進的な機能強化として使い始めることができます。
別のアニメーション API はどうでしょうか。
実際はそうではありません。既存の機能を拡張したものであり、もっともな理由があります。初めから始めましょう。ウェブ上の任意の DOM 要素をアニメーション化したい場合は、2 1⁄2 の選択肢があります。シンプルな A から B への遷移用の CSS 遷移、周期的で複雑な時間ベースのアニメーション用の CSS アニメーション、そしてほぼ任意の複雑なアニメーション用の Web Animations API(WAAPI)です。WAAPI のサポート マトリックスは非常に厳しい状況ですが、改善の方向に向かっています。それまでは、ポリフィルを使用できます。
これらのメソッドに共通するのは、ステートレスで時間駆動型である点です。ただし、デベロッパーが試しているエフェクトの中には、時間駆動型でもステートレスでもないものもあります。たとえば、悪名高いパララックス スクロールは、名前が示すようにスクロール駆動です。現在、ウェブでパフォーマンスの高いパララックス スクロールを実装するのは驚くほど難しいです。
ステートレスについてはどうでしょうか。たとえば、Android 版 Chrome のアドレスバーについて考えてみましょう。下にスクロールすると、ビューの外にスクロールします。ただし、ページの途中までスクロールしても、上にスクロールすると、その広告が再び表示されます。アニメーションは、スクロール位置だけでなく、以前のスクロール方向にも依存します。ステートフルです。
もう 1 つの問題は、スクロールバーのスタイル設定です。スタイル設定が難しい(少なくとも十分にスタイル設定できない)ことで有名です。スクロールバーにニャンコを表示したい場合はどうすればよいですか?どの手法を使用する場合でも、カスタム スクロールバーの作成は効率的ではなく、簡単でもありません。
要するに、これらのことはすべて面倒で、効率的に実装するのは困難です。ほとんどのイベントや requestAnimationFrame
は、画面が 90 fps、120 fps 以上で動作できる場合でも 60 fps のままになる可能性があり、貴重なメインスレッドのフレーム バジェットのごく一部しか使用しません。
アニメーション ワークレットは、ウェブのアニメーション スタックの機能を拡張して、このような効果を簡単に実現できるようにします。本題に入る前に アニメーションの基本を 確認しましょう
アニメーションとタイムラインの基礎
WAAPI とアニメーション ワークレットでは、タイムラインを広範に使用して、アニメーションとエフェクトを思い通りにオーケストレートできます。このセクションでは、タイムラインとアニメーションとの連携について簡単に説明します。
各ドキュメントには document.timeline
があります。ドキュメントの作成時には 0 から始まり、ドキュメントが存在し始めてからのミリ秒がカウントされます。ドキュメントのすべてのアニメーションは、このタイムラインを基準に動作します。
もう少し具体的なために、この WAAPI スニペットを見てみましょう。
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
animation.play()
を呼び出すと、アニメーションはタイムラインの currentTime
を開始時間として使用します。このアニメーションには 3, 000 ミリ秒の遅延があります。つまり、タイムラインが `startTime に達すると、アニメーションが開始(または「アクティブ」)になります。
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000` です。つまり、タイムラインはアニメーションの進行状況を制御します。
アニメーションが最後のキーフレームに達すると、最初のキーフレームに戻り、アニメーションの次の反復処理を開始します。このプロセスは、iterations: 3
を設定してから合計 3 回繰り返されます。アニメーションを停止させないようにするには、iterations: Number.POSITIVE_INFINITY
と記述します。上記のコードの結果は次のとおりです。
WAAPI は非常に強力であり、イージング、開始オフセット、キーフレームの重み付け、塗りつぶし動作など、この記事の範囲を大幅に超える機能もあります。詳しくは、CSS のトリックでの CSS アニメーションに関する記事をご覧ください。
アニメーション ワークレットの作成
タイムラインの概念を理解できたところで、アニメーション ワークレットと、それを使ってタイムラインを調整する方法を説明します。Animation Worklet API は WAAPI に基づいているだけでなく、拡張可能なウェブの観点から、WAAPI の機能について説明する低レベルのプリミティブです。構文の点では、非常によく似ています。
アニメーション ワークレット | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
違いは最初のパラメータです。これは、このアニメーションを駆動するワークレットの名前です。
特徴検出
この機能を搭載した最初のブラウザは Chrome であるため、コードだけで AnimationWorklet
が存在することを想定していないことを確認する必要があります。そのため、ワークレットを読み込む前に、ユーザーのブラウザが AnimationWorklet
をサポートしているかどうかを簡単なチェックで検出する必要があります。
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
ワークレットの読み込み
ワークレットは、Houdini タスクフォースによって導入された新しいコンセプトで、多くの新しい API の構築とスケーリングを容易にします。ワークレットの詳細については後で説明しますが、ここでは簡単に、ワークレットを低コストで軽量なスレッド(ワーカーなど)と考えてください。
アニメーションを宣言する前に、名前が「passthrough」のワークレットが読み込まれていることを確認する必要があります。
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
何が起きているのでしょうか?AnimationWorklet の registerAnimator()
呼び出しを使用して、クラスをアニメーターとして登録し、「passthrough」という名前にします。これは、上記の WorkletAnimation()
コンストラクタで使用した名前と同じです。登録が完了すると、addModule()
から返されたプロミスが解決し、そのワークレットを使用してアニメーションの作成を開始できます。
ブラウザがレンダリングするすべてのフレームで、インスタンスの animate()
メソッドが呼び出され、アニメーションのタイムラインの currentTime
と、現在処理中のエフェクトが渡されます。効果は KeyframeEffect
の 1 つしかなく、currentTime
を使用して効果の localTime
を設定しています。そのため、このアニメーターは「パススルー」と呼ばれています。ワークレットのこのコードでは、デモでわかるように、上記の WAAPI と AnimationWorklet はまったく同じように動作します。
時間
animate()
メソッドの currentTime
パラメータは、WorkletAnimation()
コンストラクタに渡したタイムラインの currentTime
です。前の例では、その時間を作用関数に渡しました。ただし、これは JavaScript コードであるため、時間を歪ませることができます。
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
currentTime
の Math.sin()
を取り出し、その値を [0; 2000] の範囲に再マッピングします。これは、エフェクトが定義されている時間範囲です。キーフレームやアニメーションのオプションを変更していませんが、アニメーションの見た目が大きく変わりました。ワークレット コードは任意の複雑さにすることができ、どのエフェクトをどの順序でどの程度再生するかをプログラムで定義できます。
Options over Options
ワークレットを再利用して数値を変更する必要がある場合があります。このため、WorkletAnimation コンストラクタでは、オプション オブジェクトをワークレットに渡すことができます。
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
この例では、両方のアニメーションは同じコードで駆動されていますが、オプションが異なります。
地元の州を見せましょう!
前述のとおり、アニメーション ワークレットが解決を目指す主な問題の 1 つは、ステートフル アニメーションです。アニメーション ワークレットは状態を保持できます。ただし、ワークレットのコア機能の 1 つは、ワークレットを別のスレッドに移行したり、リソースを節約するために破棄したりできることです。この場合、ワークレットの状態も破棄されます。状態の損失を防ぐため、アニメーション ワークレットには、ワークレットが破棄される前に呼び出されるフックが用意されています。このフックを使用して、状態オブジェクトを返すことができます。このオブジェクトは、ワークレットが再作成されるときにコンストラクタに渡されます。最初に作成された場合、このパラメータは undefined
になります。
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
このデモを更新するたびに、正方形が回転する方向は 50% の確率で決まります。ブラウザがワークレットを破棄して別のスレッドに移行すると、作成時に別の Math.random()
呼び出しが発生し、方向が突然変わる可能性があります。このようなことが起こらないように、アニメーションでランダムに選択された方向を state として返します。この値は、コンストラクタで使用されます(指定されている場合)。
時空連続体へのフック: ScrollTimeline
前のセクションで示したように、AnimationWorklet を使用すると、タイムラインの進行がアニメーションの効果にどう影響するかをプログラムで定義できます。これまでのところ、タイムラインは常に document.timeline
で、時間を追跡しています。
ScrollTimeline
を使用すると、時間ではなくスクロールでアニメーションを駆動できるため、新しい可能性が開かれます。このデモでは、最初の「パススルー」ワークレットを再利用します。
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
document.timeline
を渡す代わりに、新しい ScrollTimeline
を作成しています。お気づきかもしれませんが、ScrollTimeline
は時間を使用せず、scrollSource
のスクロール位置を使用してワークレットに currentTime
を設定します。上端(または左端)までスクロールされている場合は currentTime = 0
、下端(または右端)までスクロールされている場合は currentTime
が timeRange
に設定されます。このデモでボックスをスクロールすると、赤いボックスの位置を制御できます。
スクロールしない要素で ScrollTimeline
を作成すると、タイムラインの currentTime
は NaN
になります。特にレスポンシブ デザインを念頭に置いて、currentTime
として NaN
を常に準備しておく必要があります。多くの場合、デフォルトで値を 0 に設定することをおすすめします。
アニメーションをスクロール位置にリンクすることは長い間求められていましたが、(CSS3D を使用したハッキング的な回避策を除き)このレベルの忠実度で実現されたことはありませんでした。アニメーション ワークレットを使用すると、これらのエフェクトを簡単に実装しながら、高いパフォーマンスを実現できます。たとえば、このデモのようなパララックス スクロール エフェクトでは、スクロール駆動アニメーションを定義するのに数行しか必要ありません。
詳細
ワークレット
ワークレットは、分離されたスコープと非常に小さな API サーフェスを持つ JavaScript コンテキストです。小さな API サーフェスにより、特にローエンド デバイスでブラウザからより積極的な最適化が可能になります。また、ワークレットは特定のイベントループにバインドされず、必要に応じてスレッド間で移動できます。これは、AnimationWorklet では特に重要です。
コンポジタ NSync
特定の CSS プロパティはアニメーション化が速く、他の CSS プロパティは速くないことはご存じでしょう。一部のプロパティは、GPU でアニメーション化するための作業のみが必要ですが、他のプロパティは、ブラウザにドキュメント全体の再レイアウトを強制します。
Chrome には(他の多くのブラウザと同様に)コンポジタと呼ばれるプロセスがあります。このプロセスの役割は、レイヤとテクスチャを配置し、GPU を使用して画面をできるだけ定期的に更新することです(理想的には画面の更新速度と同じくらい速く(通常は 60 Hz)更新します)。アニメーション化する CSS プロパティによっては、ブラウザがコンポーザに処理を任せればよい場合もあれば、他のプロパティではレイアウトを実行する必要がある場合もあります。レイアウトはメイン スレッドでのみ実行できるオペレーションです。アニメーション化するプロパティによっては、アニメーション ワークレットがメイン スレッドにバインドされる場合もあれば、コンポーザと同期して別のスレッドで実行される場合もあります。
軽い罰
GPU は競合率の高いリソースであるため、通常、コンポジタ プロセスは 1 つだけ存在し、複数のタブで共有される可能性があります。コンポジタが何らかの理由でブロックされると、ブラウザ全体が停止し、ユーザー入力に応答しなくなります。このような状況は絶対に避ける必要があります。ワークレットがフレームのレンダリングに間に合うようにコンポーザに必要なデータを提供できない場合はどうなりますか?
この場合、仕様により、ワークレットは「スリップ」できます。コンポジタより遅れ、コンポジタはフレームレートを維持するために最後のフレームのデータの再利用が許可されます。視覚的にはジャンクのように見えますが、大きな違いは、ブラウザがユーザー入力に引き続き応答することです。
まとめ
AnimationWorklet には多くの側面があり、ウェブにも多くのメリットをもたらします。アニメーションをより細かく制御し、新しい方法でアニメーションを駆動して、ウェブに新しいレベルの視覚的忠実度をもたらすという明らかなメリットがあります。しかし、この API 設計により、ジャンクに対するアプリの耐性を高めると同時に、新しいメリットをすべて利用できるようになります。
アニメーション ワークレットは Canary で提供されており、Chrome 71 でオリジン トライアルを実施する予定です。新しいウェブ エクスペリエンスについて、ぜひご意見をお寄せください。また、同じ API を提供する ポリフィルもありますが、パフォーマンスの分離は提供されません。
CSS 遷移と CSS アニメーションは引き続き有効なオプションであり、基本的なアニメーションでははるかにシンプルにできます。ただし、高度なアニメーションを必要とする場合は、AnimationWorklet が役に立ちます。