パフォーマンスに優れたパララックス

Robert Flack 氏
Robert Flack

好きでも嫌いでも、パララックスは定着します。適切に使用すれば、ウェブアプリに深みと繊細さが加わりますが、問題は、効率の良い方法で視差を実装することが困難な場合があることです。この記事では、パフォーマンスが高く、かつ重要な点として、ブラウザ間で動作するソリューションについて説明します。

パララックスのイラスト。

概要

  • 視差アニメーションの作成にスクロール イベントや background-position を使用しないでください。
  • CSS 3D 変換を使用して、より正確な視差効果を作成します。
  • Mobile Safari の場合は、position: sticky を使用して視差効果が反映されるようにします。

ドロップイン ソリューションが必要な場合は、UI Element Samples GitHub リポジトリにアクセスして、Parallax ヘルパー JS を入手してください。視差スクローラーのライブデモは GitHub リポジトリをご覧ください。

問題の視差効果

まずは視差効果を実現するための一般的な 2 つの方法を取り上げ、特に 2 つが本来の目的に適していない理由を見ていきます。

悪い例: スクロール イベントを使用する

視差に関する重要な要件は、スクロールと結合させることです。ページのスクロール位置が 1 つ変更されるたびに、視差要素の位置も更新されます。単純に聞こえますが、最新のブラウザにとって重要なメカニズムは、非同期的に動作できることです。この場合は、スクロール イベントに当てはまります。ほとんどのブラウザでは、スクロール イベントは「ベスト エフォート」として配信されるため、スクロール アニメーションのすべてのフレームで配信されるとは限りません。

この重要な情報を見ると、スクロール イベントに基づいて要素を移動する JavaScript ベースのソリューションを避ける必要がある理由がわかります。JavaScript では、ページのスクロール位置に視差が維持されるとは限りません。古いバージョンの Mobile Safari では、実際にはスクロールの最後にスクロール イベントが配信されていたため、JavaScript ベースのスクロール効果を実現することができませんでした。新しいバージョンではアニメーション中にスクロール イベントが配信されますが、Chrome と同様に「ベスト エフォート」方式です。メインスレッドが他の処理でビジー状態となると、スクロール イベントはすぐに配信されません。つまり、視差効果が失われます。

悪い例: background-position の更新

避けたいもう 1 つの状況は、すべてのフレームをペイントすることです。多くのソリューションでは、視差効果を提供するために background-position を変更しようとします。これにより、ブラウザはスクロール時にページの影響を受ける部分を再描画します。また、アニメーションが大幅にジャンクするほどコストがかかります。

視差運動を実現するためには、加速プロパティ(現在は変換と不透明度を維持すること)として適用でき、スクロール イベントに依存しないものが必要です。

3D の CSS

Scott KellumKeith Clark はどちらも、CSS 3D を使用して視差運動を実現する分野で多大な成果を上げており、実質的に以下の手法を使用しています。

  • overflow-y: scroll(場合によっては overflow-x: hidden)でスクロールする要素を設定します。
  • 同じ要素に perspective 値と、top left または 0 0 に設定された perspective-origin を適用します。
  • その要素の子に対して Z 単位で移動を適用し、拡大縮小して、画面上でのサイズに影響を与えることなく視差運動を提供します。

この方法の CSS は次のようになります。

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

これは、次のような HTML のスニペットであると想定しています。

<div class="container">
    <div class="parallax-child"></div>
</div>

遠近に合わせて拡大縮小する

子要素を押し戻すと、パースペクティブ値に比例して小さくなります。どれだけの拡大が必要かは、(Perspective - 距離) / 視点の式で計算できます。視差要素を視差にしたいが、作成したサイズで表示されるため、そのままにするのではなく、この方法でスケールアップする必要があります。

上記のコードでは、Perspective は 1px で、parallax-child の Z 距離は -2px です。つまり、要素を 3 倍にスケールアップする必要があります。これは、コードに挿入した値 scale(3) です。

translateZ 値が適用されていないコンテンツの場合は、値を 0 に置き換えることができます。つまり、尺度は (perspective - 0) / perspective で、値が 1 になります。これは、どちらの値も縮小されていないことを意味します。非常に便利です。

この方法の仕組み

この知識はすぐに使用するので、なぜこれが機能するのかを明確にすることが重要です。スクロールは実質的に変換であるため、高速化が可能です。ほとんどの場合、GPU とともにレイヤをシフトします。遠近の概念のない一般的なスクロールでは、スクロール要素とその子を比較すると、スクロールは 1 対 1 で行われます。要素を 300px だけ下にスクロールすると、その子は同じ量(300px)で上に変換されます。

ただし、スクロール要素にパースペクティブ値を適用すると、このプロセスが妨げられ、スクロール変換を支える行列が変更されます。これで、選択した perspectivetranslateZ の値に応じて、300 ピクセルのスクロールで子が 150 ピクセルしか移動しないようになりました。要素の translateZ 値が 0 の場合、これまでのように 1:1 でスクロールされますが、視点の起点から Z に push された子は別の速度でスクロールされます。最終的に得られる結果は視差モーションです。そして非常に重要な点は、この処理がブラウザの内部スクロール機構の一部として自動的に処理されることです。つまり、scroll イベントのリッスンや background-position の変更が不要になります。

塗り絵のハエ: モバイル サファリ

すべての効果に注意点があります。変換で重要な点の一つは、子要素に対する 3D 効果を保持することです。視点を持つ要素とその視差のある子要素の間の階層に要素がある場合、3D パースペクティブは「フラット化」され、効果が失われます。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

上記の HTML では .parallax-container は新しく、perspective 値が実質的にフラット化され、視差効果が失われます。ほとんどの場合、解決策は非常に簡単です。transform-style: preserve-3d を要素に追加すると、ツリーのさらに上に適用された 3D 効果(Google の視点の値など)が伝播されます。

.parallax-container {
  transform-style: preserve-3d;
}

Mobile Safari の場合、やや複雑になります。 コンテナ要素に overflow-y: scroll を適用することは技術的には機能しますが、スクロール要素をフリングできるという代償が伴います。この問題を解決するには、-webkit-overflow-scrolling: touch を追加しますが、perspective もフラット化され、視差が生じることはありません。

段階的な機能強化の観点から、これはおそらくそれほど問題ではないでしょう。あらゆる状況でパララックス化できない場合でもアプリは引き続き機能しますが、回避策を考案した方がよいでしょう。

position: sticky が助けを手にしました!

実際には、スクロール中に要素をビューポートまたは特定の親要素に「固定」させるために役立つ position: sticky という形式があります。他の多くの機能と同様、かなり大きな仕様がありますが、次のような便利な小さな宝石が含まれています。

一見するとあまり意味がないに思えるかもしれませんが、この文の重要なポイントは、要素のスティッキネスをどのように計算するか、正確に言うと「オフセットは、スクロール ボックスで最も近い祖先を参照して計算されます」というところです。つまり、追尾要素を移動する距離は、(別の要素またはビューポートに取り付けられているように見えるように)他の変換が適用される前ではなく、適用される前に計算されます。つまり、前のスクロールの例と同様に、オフセットが 300 ピクセルで計算された場合、スティッキー要素に適用する前に、パースペクティブ(またはその他の変換)を使用して 300 ピクセルのオフセット値を操作できるようになります。

position: -webkit-sticky を視差要素に適用することで、-webkit-overflow-scrolling: touch のフラット化効果を効果的に「逆転」できます。これにより、視差要素がスクロール ボックスのある最も近い祖先(この場合は .container)を参照するようになります。次に、前と同様に、.parallax-containerperspective 値を適用します。この値は、計算されたスクロール オフセットを変更し、視差効果を作成します。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

これにより、Mobile Safari の視差効果が復元されます。これは非常に良いニュースです。

固定ポジショニングの注意点

ただし、ここには違いがあります。position: sticky は視差の仕組みを変更します。スティッキー ポジショニングでは、要素をスクロール コンテナに固定しようとしますが、スティッキー以外のバージョンではそうしません。つまり、固定された視差は、次のような視差のない視差の逆になります。

  • position: sticky ありの場合、要素が z=0 に近づくほど移動量が少なくなります
  • position: sticky がない場合、要素は z=0 に近づくほど移動します。

全体的に抽象的に思える場合は、Robert Flack 氏によるこちらのデモをご覧ください。スティッキー位置指定の有無によって要素の動作がどのように変化するかを示しています。違いを確認するには、Chrome Canary(執筆時点でバージョン 56)または Safari が必要です。

パララックス視点のスクリーンショット

position: sticky が視差スクロールに与える影響を示す Robert Flack のデモ

さまざまなバグと回避策

ただし、他の問題と同様に、凹凸や凹凸もまだ修正しておく必要があります。

  • 固定サポートに一貫性がない。Chrome ではサポートが実装中ですが、Edge では完全にサポートされていません。また、Firefox ではスティッキー変換とパースペクティブ変換を組み合わせた場合にバグが描画されることがあります。そのような場合は、必要なときのみ position: sticky-webkit- プレフィックス付きバージョン)を追加するだけで済みます。これは Mobile Safari のみに該当します。
  • この効果は、Edge では「ただ機能する」わけではありません。Edge は OS レベルでスクロールを処理しようとします。これは通常、望ましいことですが、この場合、スクロール中に視点の変更が検出されなくなります。これを修正するには、固定位置要素を追加します。これにより、Edge を OS 以外のスクロール方法に切り替え、視点の変化に対応するようになります。
  • 「ページのコンテンツが大きくなりました」多くのブラウザでは、ページのコンテンツの大きさを決定する際に考慮しますが、Chrome と Safari では遠近表現は考慮されません。たとえば、要素に 3 倍のスケールが適用されると、perspective の適用後に要素が 1 倍になっても、スクロールバーなどが表示される場合があります。この問題を回避するには、右下から要素を(transform-origin: bottom right で)スケーリングします。

まとめ

パララックスはよく注意して使うと楽しい効果があります。ご覧のとおり、パフォーマンスを高め、スクロール結合を行い、クロスブラウザに対応する方法で実装できます。目的の効果を得るには、少量の数学的なリグリングと少量のボイラープレートが必要になるため、小さなヘルパー ライブラリとサンプルを作成しました。UI 要素のサンプル GitHub リポジトリから入手できます。

ご視聴のうえ、ご感想をお聞かせください。