CSS Deep-Dive - フレーム完璧なカスタム スクロールバーのための matrix3d()

カスタム スクロールバーは非常にまれです。その主な原因は、スクロールバーがウェブ上にほとんどスタイリングされていない部分の 1 つであるからです(日付選択ツール、日付選択ツール)。JavaScript を使用して独自のコードを作成することもできますが、これはコストが高く、再現性が低く、動作が遅く感じられます。この記事では、通常とは異なる CSS 行列を利用して、スクロール中に JavaScript を必要とせず、設定コードのみでカスタム スクロールを作成する方法を説明します。

要約

小さなことは気にしない?Nyan cat のデモを見てライブラリを入手したいだけですか?デモのコードは GitHub リポジトリにあります。

LAM;WRA(長くて数学的な内容だが、読む)

以前、パララックス スクロールを作成しました(こちらの記事を読みましたか?ぜひご覧ください。CSS 3D 変換を使用して要素を押し戻すことで、要素が実際のスクロール速度よりも遅く移動しました。

内容のまとめ

まず、パララックス スクローラーの仕組みを復習しましょう。

アニメーションに示すように、3D 空間の Z 軸に沿って要素を「後方」に押し出すことで、視差効果を実現しました。ドキュメントのスクロールは実質的には Y 軸に沿った変換ですたとえば 100 ピクセルを下にスクロールすると、すべての要素が 100 ピクセルを上に移動します。これは「後ろにある」要素にもすべて適用されます。ただし、カメラから遠いため、画面上で観測される動きは 100 ピクセル未満になり、望ましい視差効果が得られます。

もちろん、要素を空間内で後方に移動すると、要素は小さく見えます。この場合は、要素をスケールアップして修正します。パララックス スクロールを作成する際に正確な計算を行ったため、詳細については省略します。

ステップ 0: 何をしたいのか?

スクロールバー。私たちが構築するのは、これだけです。その仕組みについてじっくり考えたことはありますか?私もそうは思いません。スクロールバーは、現在表示されている利用可能なコンテンツのと、読者として進んでいる進捗状況を示すインジケーターです。下方向にスクロールすると、スクロールバーも下方向に移動し、最後までスクロールしていることを示します。すべてのコンテンツがビューポートに収まる場合、通常はスクロールバーは非表示になります。コンテンツの高さがビューポートの高さの 2 倍の場合、スクロールバーはビューポートの高さの 1/2 を占有します。ビューポートの高さの 3 倍のコンテンツは、スクロールバーをビューポートの 1/3 にスケーリングします。パターンは明らかです。スクロールする代わりに、スクロールバーをクリックしてドラッグし、サイト内をすばやく移動することもできます。このような目立たない要素に対して 意外な挙動ですね1 つずつ戦いましょう。

ステップ 1: リバースにする

パララックス スクロールの記事で説明したように、CSS 3D 変換を使用して、要素をスクロール速度よりも遅く動かすことができます。方向を逆にすることもできますか?実は、フレーム パーフェクトなカスタム スクロールバーを作成するための方法があります。この仕組みを理解するには、まず CSS 3D の基本をいくつか説明する必要があります。

数学的な意味でのあらゆる種類の遠近投影を取得するには、ほとんどの場合、均質座標を使用する必要があります。関数の機能と仕組みについては詳しく説明しませんが、4 番目の座標 w がある 3D 座標と考えることができます。遠近感の歪みを設定する場合を除き、この座標は 1 にする必要があります。1 以外の値は使用しないため、w の詳細については心配する必要はありません。したがって、すべてのポイントは今後 4 次元ベクトル [x, y, z, w=1] になり、その結果、行列も 4x4 にする必要があります。

CSS が内部でホモジニアス座標を使用していることを確認できる例として、matrix3d() 関数を使用して transform プロパティで独自の 4x4 行列を定義する場合が挙げられます。matrix3d は 16 個の引数を取ります(行列が 4x4 であるため)。列を 1 つずつ指定します。この関数を使用すると、回転や移動などを手動で指定できますが、w 座標を操作することもできます。

matrix3d() を使用するには、3D コンテキストが必要です。3D コンテキストがないと、遠近感の歪みが発生せず、均質座標が必要ないためです。3D コンテキストを作成するには、perspective を含むコンテナと、新しく作成された 3D 空間で変換できる要素が必要です。:

CSS の perspective 属性を使用して div を歪ませる CSS コード。

パースペクティブ コンテナ内の要素は、CSS エンジンによって次のように処理されます。

  • 要素の各コーナー(頂点)を、パースペクティブ コンテナを基準とした均質座標 [x,y,z,w] に変換します。
  • 要素のすべての変換を右から左にマトリックスとして適用します。
  • パースペクティブ要素がスクロール可能である場合は、スクロール マトリックスを適用します。
  • 遠近感マトリクスを適用します。

スクロール マトリックスは y 軸に沿った変換です。400 ピクセルにスクロールダウンすると、すべての要素を 400 ピクセルに移動する必要があります。パースペクティブ マトリックスは、3D 空間の奥にあるポイントを消失点に近づけるマトリックスです。これにより、遠くにあると小さく見えるだけでなく、翻訳時に「動きが遅くなる」という効果も得られます。そのため、要素が押し戻されると、400 ピクセルの移動で要素は画面上で 300 ピクセルしか移動しません。

詳細をすべて知りたい場合は、CSS の変換レンダリング モデルの仕様をお読みください。この記事では、上記のアルゴリズムを簡素化しています。

ボックスは、perspective 属性の値が p のパースペクティブ コンテナ内にあります。コンテナはスクロール可能で、n ピクセル下にスクロールされているとします。

遠近行列 × スクロール行列 × 要素変換行列は、4 × 4 の単位行列(4 行目 3 列目に -1 ÷ p がある)× 4 × 4 の単位行列(2 行目 4 列目に -n がある)× 要素変換行列に等しい。

最初の行列はパースペクティブ行列、2 番目の行列はスクロール行列です。要約すると、スクロール マトリックスの役割は、下にスクロールするときに要素を上に移動させることです。そのため、負の符号が付いています。

ただし、スクロールバーの場合はにする必要があります。つまり、下にスクロールすると要素が下に移動するようにします。ここでトリックを使用できます。箱の角の w 座標を反転します。w 座標が -1 の場合、すべての変換が反対方向に適用されます。では、どうすればよいでしょうか。CSS エンジンは、ボックスの角を均質座標に変換し、w を 1 に設定します。matrix3d() の登場です。

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

この行列は、w を負にする以外に何もしません。したがって、CSS エンジンが各コーナーを [x,y,z,1] 形式のベクトルに変換すると、行列はそれを [x,y,z,-1] に変換します。

4 行 4 列の単位行列(4 行目 3 列目が -1 ÷ p)× 4 行 4 列の単位行列(2 行目 4 列目が -n)× 4 行 4 列の単位行列(4 行目 4 列目が -1)× 4 次元ベクトル(x, y, z, 1)は、4 行 4 列の単位行列(4 行目 3 列目が -1 ÷ p、2 行目 4 列目が -n、4 行目 4 列目が -1)× 4 次元ベクトル(x, y + n, z, -z ÷ p - 1)に等しい。

要素変換行列の効果を示すために、中間ステップをリストしました。行列演算に慣れていない場合でも問題ありません。最後の行では、スクロール オフセット n を y 座標から減算するのではなく、加算していることに気付くはずです。下にスクロールすると、要素は下に移動します。

ただし、このマトリックスをに配置しただけの場合、要素は表示されません。これは、CSS 仕様で、w < 0 の頂点は要素のレンダリングをブロックすることが義務付けられているためです。現在、z 座標は 0、p は 1 であるため、w は -1 になります。

幸い、z の値は選択できます。w=1 になるようにするには、z = -2 に設定する必要があります。

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

ご覧のとおり、ボックスが復活しました

ステップ 2: 移動する

箱が配置され、変換なしの場合と同じように表示されます。現在、パースペクティブ コンテナはスクロールできないため、表示されませんが、スクロールすると要素が反対方向に移動することはわかります。コンテナをスクロールできるようにしましょう。スペースを占有するスペーサー要素を追加するだけです。

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

ボックスをスクロールします。赤いボックスが下に移動します。

ステップ 3: サイズを指定する

ページを下にスクロールすると下に移動する要素があります。これは難しい作業です次に、スクロールバーのように見えるようにスタイルを設定し、もう少しインタラクティブにする必要があります。

通常、スクロールバーは「つまみ」と「トラック」で構成されますが、トラックは常に表示されるとは限りません。サムネイルの高さは、表示されるコンテンツの量に正比例します。

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight はスクロール可能な要素の高さで、scroller.scrollHeight はスクロール可能なコンテンツの合計高さです。scrollerHeight/scroller.scrollHeight は、表示されるコンテンツの割合です。サムネイルが占有する垂直方向のスペースの比率は、表示されるコンテンツの比率と同じにする必要があります。

スクロールバーの高さに対するサムドット スタイルのドット高さが、スクロールバーのドット スクロールの高さに対するスクロールバーの高さに等しい場合、かつその場合に限って、スクロールバーの高さに対するサムドット スタイルのドット高さが、スクロールバーの高さ × スクロールバーのドット スクロールの高さに対するスクロールバーの高さに等しくなります。
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

サムネイルのサイズは問題ないものの、動きが速すぎます。ここで、パララックス スクローラーからテクニックを利用できます。要素をさらに戻ると、スクロール中の移動が遅くなります。サイズを修正するには、スケールアップします。では、どれくらい遅らせればよいのでしょうか。では、計算をしましょう。これが最後です。

重要な情報は、一番下までスクロールしたときに、サムネイルの下端がスクロール可能な要素の下端と揃っていることです。つまり、scroller.scrollHeight - scroller.height ピクセルをスクロールした場合、親指が scroller.height - thumb.height ピクセル分移動します。スクロールの 1 ピクセルごとに、親指が 1 ピクセル未満動くようにします。

係数は、スクロール ドットの高さ - サムネイル ドットの高さ ÷ スクロール ドットのスクロール高さ - スクロール ドットの高さです。

これがスケーリング ファクタです。次に、スケーリング ファクタを z 軸に沿った変換に変換する必要があります。これは、パララックス スクロールに関する記事ですでに行いました。仕様の関連セクションによると、スケーリング ファクタは p/(p - z) と等しくなります。この方程式を解いて z を求めることで、親指を z 軸に沿って移動させる必要がある距離を計算できます。ただし、w 座標の不正行為により、z に沿って追加の -2px を変換する必要があります。また、要素の変換は右から左に適用されます。つまり、特殊な行列の前のすべての変換は反転されませんが、特殊な行列の後のすべての変換は反転されます。これをコード化しましょう。

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

スクロールバーがあります。自由にスタイル設定できる DOM 要素です。ユーザー補助の観点から重要なことの一つは、多くのユーザーがスクロールバーをそのように操作することに慣れているため、親指がクリックとドラッグに応答するようにすることです。このブログ投稿を長くしないため、この部分の詳細については説明しません。作成方法については、ライブラリ コードをご覧ください。

iOS の場合はどうなりますか?

ああ、おなじみの iOS Safari です。ここでも パララックス スクロールと同様に問題が発生します。要素をスクロールするため、-webkit-overflow-scrolling: touch を指定する必要がありますが、これにより 3D フラット化が発生し、スクロール エフェクト全体が機能しなくなります。パララックス スクロールでは、iOS Safari を検出して position: sticky を回避策として使用することでこの問題を解決しました。ここでもまったく同じことを行います。パララックスに関する記事で、パララックスについて確認してください。

ブラウザのスクロールバーについてはどうですか?

一部のシステムでは、永続的なネイティブ スクロールバーを処理する必要があります。これまで、スクロールバーを非表示にすることはできませんでした(標準以外の疑似セレクタを使用した場合を除く)。そのため、それを隠すには、(数学を使わない)ハッキングに頼る必要があります。スクロール要素を overflow-x: hidden でコンテナにラップし、スクロール要素をコンテナよりも広くします。ブラウザのネイティブ スクロールバーは表示されなくなりました。

フィン

ここまでの説明をすべてまとめると、Nyan cat デモのような、フレーム パーフェクトなカスタム スクロールバーを作成できます。

ニャンキャットが表示されない場合は、このデモの作成中にGoogle が検出して報告したバグが発生しています(再生ボタンをクリックするとニャンキャットが表示されます)。Chrome は、画面外のペイントやアニメーション化などの不要な作業を回避するのに非常に優れています。残念ながら、このマトリックスのトリックで、Chrome はニャンコの GIF が実際には画面外にあると判断します。この問題が早急に解決されるよう願っております。

これで準備は完了です。そのため、作業が煩雑になる場合があります。最後までお読みいただきありがとうございます。これを機能させるには非常に厄介な作業であり、カスタマイズされたスクロールバーがエクスペリエンスに不可欠な要素である場合を除き、労力を費やす価値はほとんどありません。可能であることは知っておいて損はないですね。カスタム スクロールバーを作成するのがこれほど難しいということは、CSS 側で作業が必要なことを意味します。ご安心ください。 今後、HoudiniAnimationWorklet により、このようなフレーム パーフェクトのスクロール連動エフェクトが大幅に容易になります。