Houdini のアニメーション ワークレット

ウェブアプリのアニメーションを強化する

要約: アニメーション ワークレットを使用すると、デバイスのネイティブ フレームレートで実行される命令型アニメーションを作成して、ジャンクのないスムーズな動作を実現できます。また、アニメーションがメインスレッドのジャンクに対する耐性を高め、時間ではなくスクロールするようにリンクできます。アニメーション ワークレットは Chrome Canary 版(「試験運用版 Web Platform の機能」フラグの下)にあるため、Chrome 71 のオリジン トライアルを計画しています。段階的な機能強化として、今すぐご利用を開始できます。

別のアニメーション API を使うべきですか?

実はそうではありません。これはすでに Google が進めてきた方法を拡張したもので、もっともな理由があります。最初から始めましょう。現在、ウェブ上で DOM 要素をアニメーション化する場合は、2 つ 1/2 の選択肢があります。単純な A から B への遷移の場合は CSS 遷移、周期的で複雑な時間ベースのアニメーションの場合は CSS アニメーション、ほぼ任意の複雑なアニメーションの場合は Web Animations API(WAAPI)です。WAAPI のサポート マトリックスはかなり厳しそうに見えますが、すでに増加しています。それまでは、polyfillを使用します。

これらのメソッドに共通するのは、ステートレスで時間ドリブンであるということです。しかしデベロッパーが行おうとしている効果には 時間ドリブンでもステートレスでもないものもありますたとえば、悪名高いパララックス スクローラーは、その名前が示すようにスクロールドリブンです。現在、高性能な視差スクローラーをウェブに実装することは、驚くほど困難です。

ステートレスについてはどうでしょうかたとえば、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 thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 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!
}

ワークレットを読み込む

ワークレットは、新しい API の多くの構築とスケーリングを容易にするために Houdini タスクフォースによって導入された新しいコンセプトです。ワークレットの詳細は後ほど説明しますが、簡単にするために、ここではワークレットを安価で軽量のスレッド(ワーカーなど)と考えることができます。

アニメーションを宣言する前に、「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)
      );
    }
  }
);

currentTimeMath.sin() を取得し、その値を範囲 [0; 2000] に再マッピングします。これは効果が定義されている時間範囲です。これで、キーフレームやアニメーションのオプションを変更せずに、アニメーションの外観が大きく変わりました。ワークレット コードは任意の複雑性にすることが可能で、どのエフェクトをどの順序で、どの程度再生するかをプログラムで定義できます。

[Options] ではなく [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();

このでは、両方のアニメーションが同じコードで動作しますが、オプションが異なります。

お住まいの州を教えて。

前にも少し触れましたが、アニメーション ワークレットが解決しようとしている主な問題の一つは、ステートフル アニメーションです。アニメーション ワークレットは状態を保持できます。ただし、ワークレットのコア機能の一つは、リソースを節約するために別のスレッドに移行できることです。また、ワークレットを破棄することによっても、ワークレットの状態も破棄されます。状態の喪失を防ぐため、アニメーション ワークレットには、ワークレットが破棄される前に呼び出されるフックが用意されています。このフックを使用すると、状態オブジェクトを返すことができます。ワークレットが再作成されると、このオブジェクトはコンストラクタに渡されます。初回作成時には、このパラメータは 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/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 になり、下(または右)までスクロールされると currentTimetimeRange に設定されます。このデモのボックスをスクロールすると、赤いボックスの位置を制御できます。

スクロールしない要素で ScrollTimeline を作成すると、タイムラインの currentTimeNaN になります。そのため、特にレスポンシブ デザインを念頭に置いている場合は、常に currentTime として NaN を用意する必要があります。多くの場合、デフォルトの値を 0 にすることをおすすめします。

アニメーションとスクロール位置のリンクは、長らく望まれていましたが、このレベルの忠実度では実現されていませんでした(CSS3D の巧妙な回避策を除く)。アニメーション ワークレットを使用すると、これらの効果を簡単に実装しながら高パフォーマンスを実現できます。たとえば、こちらのデモのような視差スクロール効果では、わずか数行でスクロールドリブン アニメーションを定義できるようになりました。

仕組み

ワークレット

ワークレットは、分離されたスコープと非常に小さな API サーフェスを持つ JavaScript コンテキストです。特にローエンド デバイスでは、API サーフェスが小さいため、ブラウザからより積極的な最適化を行うことができます。また、ワークレットは特定のイベントループにバインドされませんが、必要に応じてスレッド間で移動できます。これは、AnimationWorklet では特に重要です。

コンポジターの NSync

CSS プロパティには、アニメーションが速いものとそうでないものがあります。プロパティの中には、アニメーション化するために GPU になんらかの作業が必要なものもありますが、ブラウザに対してドキュメント全体の再レイアウトを強制するものもあります。

Chrome には(他の多くのブラウザと同様に)コンポジタというプロセスがあります。コンポジタはレイヤとテクスチャを配置し、GPU を使用して可能な限り定期的に画面を更新(通常は 60Hz が理想的)します。アニメーション化する CSS プロパティによっては、ブラウザはコンポジタに動作を行わせるだけでよく、他のプロパティはメインスレッドのみが実行できるレイアウトを実行する必要があります。アニメーション ワークレットは、メインスレッドにバインドされるか、コンポジタと同期して別のスレッドで実行されます。

手首をたたく

GPU は競合が激しいリソースであるため、通常、複数のタブで共有される可能性があるコンポジター プロセスは 1 つだけです。コンポジタがなんらかの形でブロックされると、ブラウザ全体が停止し、ユーザー入力に応答しなくなります。絶対に回避する必要があります。それでは、フレームのレンダリングまでにコンポジタが必要とするデータをワークレットが提供できない場合はどうなるでしょうか。

この場合、仕様上、ワークレットは「スリップ」する可能性があります。コンポジタより後に配置され、コンポジタは最後のフレームのデータを再利用してフレームレートを維持できます。視覚的にはジャンクのように見えますが、大きな違いは、ブラウザがユーザー入力にまだ反応していることです。

おわりに

AnimationWorklet には多くの側面があり、ウェブにもたらす利点があります。明白なメリットは、アニメーションをより細かく制御できることと、アニメーションを駆動する新しい方法によってウェブに新たなレベルの視覚的な忠実性をもたらすことです。また、API の設計により、ジャンクに対するアプリの復元性を高めながら、新しい優れた機能すべてを同時に利用できるようになります。

アニメーション ワークレットは Canary 版であり、Chrome 71 でオリジン トライアルを提供する予定です。新しいウェブ エクスペリエンスの皆様のご意見、今後の改善点をお聞かせください。同じ API を提供しますが、パフォーマンスの分離は行われないpolyfillもあります。

CSS 遷移と CSS アニメーションは有効なオプションであり、基本的なアニメーションの場合ははるかにシンプルです。ただし、さらに工夫が必要な場合は、AnimationWorklet が役立ちます。