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

好まれも悪くも、パララックスは定着しつつあります。適切に使用することで、ウェブアプリに奥行きと巧妙さがもたらされます。しかし問題は、パララックスを効率的に実装するのが難しいことです。この記事では、パフォーマンスが高く、かつ重要な点として、ブラウザをまたいで機能するソリューションについて説明します。

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

要約

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

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

問題のパララクサー

まず 視差効果を得る一般的な方法を 2 つ見てみましょう 特に 目的に適さない理由を説明します

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

視差の重要な要件は、スクロールと結合していることです。ページのスクロール位置が変化するたびに、視差要素の位置を更新する必要があります。簡単そうに聞こえますが、最新のブラウザの重要なメカニズムは、非同期に動作できることです。これは、このケースではスクロール イベントに適用されます。ほとんどのブラウザでは、スクロール イベントは「ベスト エフォート」として配信され、スクロール アニメーションのすべてのフレームに配信される保証はありません。

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

悪い例: background-position を更新しています

避けたいもう一つの状況は、すべてのフレームでペイントすることです。多くのソリューションでは、視差効果を確保するために 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>

視点のスケールの調整

子要素をプッシュバックすると、視点値に対する比例が小さくなります。どれくらいスケールアップする必要があるかは、(視点 - 距離)÷ 視点の式で計算できます。視差要素を視差効果のあるものにしたいが、作成したサイズで表示されるため、そのままにするのではなく、この方法でスケールアップする必要があります。

上記のコードの場合、視点は 1pxparallax-child の Z 距離は -2px です。つまり、要素を 3 倍にスケールアップする必要があります。これは、コード scale(3) に接続された値です。

translateZ 値が適用されていないコンテンツについては、値を 0 に置き換えることができます。つまり、スケールは (perspective - 0) / Perspective で、値は 1 になります。つまり、スケールアップもスケールダウンもされていないことを意味します。とても便利です。

このアプローチの仕組み

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

ただし、スクロール要素に視点値を適用すると、この処理が煩雑になり、スクロール変換の基盤となる行列が変化します。これで、300 ピクセルのスクロールで、選択した perspectivetranslateZ の値に応じて、子が 150 ピクセルしか移動しないようになりました。要素の translateZ 値が 0 の場合、これまでと同様に 1:1 でスクロールされますが、視点原点から Z だけ離れた子は異なる速度でスクロールされます。最終的な結果: パララックス モーション。そして何よりも、これはブラウザの内部スクロール機能の一部として自動的に処理されるため、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 効果(視点値など)が伝播されます。

.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 リポジトリをご覧ください。

ぜひプレイして、ご意見をお寄せください。