ウェブアプリのアニメーションを強化する
要約: アニメーション ワークレットを使用すると、デバイスのネイティブ フレームレートで実行される命令型アニメーションを記述して、ジャギーのない滑らかなアニメーションを実現できます。また、メインスレッドのジャンクに対してアニメーションの耐障害性を高め、時間ではなくスクロールにリンクできます。アニメーション ワークレットは Chrome Canary で利用可能(「試験運用版のウェブ プラットフォームの機能」フラグの背後)で、Chrome 71 のオリジン トライアルを予定しています。今すぐ、段階的な拡張機能として使用を開始できます。
別のアニメーション API はありますか?
実は、そうではありません。すでにあるものの拡張版です。その理由は、最初から始めましょう。現在、ウェブ上の DOM 要素をアニメーション化するには、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 は非常に強力で、この API には、この記事の範囲を超える多くの機能(減衰、開始オフセット、キーフレーム重み付け、塗りつぶし動作など)があります。詳しくは、CSS Tricks の 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()
から返された Promise が解決し、そのワークレットを使用してアニメーションの作成を開始できます。
ブラウザがレンダリングするすべてのフレームで、インスタンスの animate()
メソッドが呼び出され、アニメーションのタイムラインの currentTime
と、現在処理中のエフェクトが渡されます。エフェクトは KeyframeEffect
のみで、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 プロパティはアニメーション化が速く、他のプロパティは速くないことはご存じでしょう。一部のプロパティは、GPU でアニメーション化するための作業のみが必要ですが、他のプロパティは、ブラウザにドキュメント全体の再レイアウトを強制します。
Chrome には(他の多くのブラウザと同様に)コンポジタと呼ばれるプロセスがあります。このプロセスの役割は、レイヤとテクスチャを配置し、GPU を使用して画面をできるだけ定期的に更新することです(理想的には画面の更新速度と同じくらい速く(通常は 60 Hz)更新します)。アニメーション化する CSS プロパティによっては、ブラウザがコンポーザに処理を任せれば済む場合もあれば、他のプロパティではレイアウトを実行する必要がある場合もあります。レイアウトはメイン スレッドでのみ実行できるオペレーションです。アニメーション化するプロパティによっては、アニメーション ワークレットがメイン スレッドにバインドされる場合もあれば、コンポーザと同期して別のスレッドで実行される場合もあります。
軽くたたく
GPU は競合率の高いリソースであるため、通常、コンポジタ プロセスは 1 つだけ存在し、複数のタブで共有される可能性があります。コンポジタが何らかの理由でブロックされると、ブラウザ全体が停止し、ユーザー入力に応答しなくなります。これを回避するために、フレームのレンダリングに間に合うように、コンポジタに必要なデータをワークレットが提供できない場合はどうなりますか?
この場合、仕様により、ワークレットは「スリップ」できます。コンポジタより遅れ、コンポジタは最後のフレームのデータの再利用を許可され、フレームレートを維持できます。視覚的にはジャンクのように見えますが、大きな違いは、ブラウザがユーザー入力に引き続き応答することです。
まとめ
AnimationWorklet には多くの側面があり、ウェブにも多くのメリットをもたらします。アニメーションをより細かく制御し、新しい方法でアニメーションを駆動して、ウェブに新しいレベルの視覚的忠実度をもたらすという明らかなメリットがあります。また、API の設計により、新しい機能のすべてにアクセスしながら、アプリのジャンクに対する耐障害性を高めることもできます。
アニメーション ワークレットは Canary で提供されており、Chrome 71 でオリジン トライアルを実施する予定です。新しいウェブ エクスペリエンスについて、ぜひご意見をお寄せください。また、同じ API を提供する ポリフィルもありますが、パフォーマンスの分離は行われません。
CSS 遷移と CSS アニメーションは引き続き有効なオプションであり、基本的なアニメーションでははるかにシンプルにできます。ただし、高度なアニメーションを必要とする場合は、AnimationWorklet が役に立ちます。