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

カスタム スクロールバーは非常にまれです。その主な原因は、スクロールバーがウェブでほとんどスタイルに欠けている部分の 1 つであるためです(私が見ています、日付選択ツールです)。JavaScript を使って独自にビルドする場合でも、それは高コストで忠実度が低く、遅延が感じられる可能性があります。この記事では、型にはまらない CSS マトリックスを利用して、スクロール時に JavaScript を必要とせず、セットアップ コードだけを提供するカスタム スクローラーを作成します。

要約

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

LAM;WRA(長文および数学的。このまま読み取れる)

少し前に、Google は視差スクローラーを構築しました(こちらの記事を本当に効果が上がるので、時間を設ける価値があります)。CSS 3D 変換を使用して要素を押し戻すことで、実際のスクロール速度よりも要素の移動が遅くなります。

まとめ

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

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

もちろん、要素を空間内に戻すと要素の縮小も行われますが、これを修正するには、要素を元の場所に拡大縮小します。正確な計算方法は、視差スクローラーを構築したときに解明したので、説明は省略します。

ステップ 0: 目的

スクロールバー。これから作るものは、これです。でも、どんなアプリなのかよく考えたことはありますか?やっぱり、できなかった。スクロールバーは、現在表示されている利用可能なコンテンツの量と、読者の進行状況を示すインジケーターです。下にスクロールすると、最後までスクロールしていることを示すスクロールバーが表示されます。すべてのコンテンツがビューポートに収まっている場合、通常、スクロールバーは非表示になります。コンテンツの高さがビューポートの 2 倍である場合、スクロールバーはビューポートの高さの 2 分の 1 の高さになります。ビューポートの高さの 3 倍のコンテンツの場合、スクロールバーがビューポートの 1/3 にスケーリングされ、パターンが表示されます。スクロールする代わりにスクロールバーをクリック&ドラッグすると、サイト内をすばやく移動できます。このような目立たない要素でも、それは驚くべき行動です。1 つずつ戦っていきましょう。

ステップ 1: 裏返す

視差スクロールに関する記事で説明されているように、CSS 3D 変換を使用して、スクロール速度よりも遅く要素を移動させることができます。逆方向に逆転もできる?これが可能なことが判明し、フレームパーフェクトなカスタム スクロールバーを構築する道となります。この仕組みを理解するには まず CSS 3D の基本を いくつか確認しておく必要があります

数学的にあらゆる視点図法を取得するには、同次座標を使用することになります。これらが何なのか、またなぜ機能するのかについては詳しく説明しませんが、これらは 3D 座標に w という 4 番目の座標が追加されたものと考えることができます。視点の歪みを適用する場合を除き、この座標は 1 にする必要があります。w の詳細は 1 以外の値を使用しないため、これについては特に気にする必要はありません。したがって、すべてのポイントはここから 4 次元のベクトル [x, y, z, w=1] 上にあるため、行列も 4x4 にする必要があります。

CSS が内部で同種座標を使用するケースの一つは、matrix3d() 関数を使用して変換プロパティで独自の 4x4 行列を定義する場合です。matrix3d は 16 個の引数を取り(この行列は 4×4 であるため)、1 つの列を順番に指定します。この関数を使用して、回転や変換などを手動で指定できます。ただし、w 座標を変更することもできます。

matrix3d() を使用するには、3D コンテキストが必要です。3D コンテキストがなければ、視点の歪みはなく、同種座標は必要ありません。3D コンテキストを作成するには、perspective を含むコンテナと、新しく作成された 3D 空間で変換できる要素(内部の要素)が必要です。:

CSS の Perspective 属性を使用して div を変形させる CSS コードです。

Perspective コンテナ内の要素は、CSS エンジンによって次のように処理されます。

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

スクロール マトリックスは y 軸に沿った変換です。400 ピクセル分下スクロールした場合は、すべての要素を 400 ピクセル分上方に移動する必要があります。視点行列は、3D 空間で消失点に近づくほどポイントを「pull」する行列です。これにより、遠くにあるものが小さく見えるという効果と、翻訳時には「動きが遅く」なるという両方の効果があります。したがって、要素がプッシュバックされた場合、400 ピクセルの変換により、要素は画面上で 300 ピクセルだけ移動します。

すべての詳細を把握したい場合は、CSS の変換レンダリング モデルのspecをご覧ください。この記事では、上記のアルゴリズムを簡略化しています。

このボックスは、perspective 属性の値が p である視点コンテナ内にあります。コンテナがスクロール可能で、n ピクセル分スクロールすると仮定します。

視点行列 × スクロール行列 × 要素変換行列は 4 × 4 の単位行列で、4 行 3 列の p から 1 を引いた 4 × 4 の単位行列 × 4 × 4 の単位行列で、2 行目に n を掛けた値に要素変換行列を掛けます。

1 つ目のマトリックスは視点マトリックス、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 行 3 列の p を引く 4 x 4 の単位行列と、2 行目の 4 列目がマイナス n である 4 × 4 の単位行列と、4 行 4 列目にマイナス 1 を掛けた値、4 次元のベクトル x, y, z, 1 は、4 行目のベクトル x 1 にプラス 1 と n 列の 4 行から 1 を引いた 4 x 4 の単位行列です。

要素変換行列の効果を示す中間ステップを記載しました。行列計算に不慣れな方でも大丈夫です。最後の行で、スクロール オフセット 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: 移行する

これでボックスが表示され、変換なしの場合と同じように表示されます。現時点では、Perspective コンテナはスクロールできないため表示できませんが、スクロールすると要素が逆方向に移動することがわかっています。では、コンテナをスクロールしてみましょう。スペースを占有するスペーサー要素を追加するだけです。

<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 で翻訳されるようにします。スクローラーのピクセルごとに、親指でピクセルの一部を動かすようにします。

係数は、スクローラー ドットの高さから親指のドットの高さを引いた値から、スクローラー ドットのスクロールの高さからスクローラー ドットの高さを引いた値になります。

これがスケーリング ファクタです。次に、スケーリング ファクタを z 軸に沿った変換に変換する必要があります。これは、視差スクロールに関する記事ですでに実施しました。仕様の関連セクションによると、スケーリング ファクタは p/(p - z) に等しくなります。この z の方程式を解くと、親指を z 軸に沿って移動させる量がわかります。ただし、w 座標の不正の関係により、追加の -2px を z に沿って変換する必要があることに留意してください。また、要素の変換は右から左に適用されます。つまり、特別なマトリックスの前のすべての翻訳は反転しませんが、特別なマトリックス後のすべての変換は反転します。これを体系化しましょう。

<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 のフラット化が発生し、スクロール効果全体が機能しなくなります。Google では、iOS Safari を検出し、回避策として position: sticky を使用することにより、視差スクローラーのこの問題を解決しました。ここでは、まったく同じことを行います。パララックスに関する記事を確認して、内容を復習してください。

ブラウザのスクロールバーはどうでしょうか?

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

フィン

これで、Nyan cat のデモで紹介したようなフレーム完璧なカスタム スクロールバーを作成できるようになりました。

Nyan cat が表示されない場合は、このデモの構築中に発見され、報告したバグが発生しています(親指をクリックすると Nyan 猫が表示されます)。Chrome では画面外を描画したりアニメーション化したりといった 不要な処理を省くことができますただ、これはマトリックスの不正により、Chrome に「ニャン猫の GIF」が実際には画面に表示されないようにしてしまうということです。 この問題が早急に修正されることを願っております。

これで準備は完了です。かなりの経験ですね。全部読んでくれてありがとう ありがとうこれは、この機能を機能させるうえで非常に厄介なことであり、カスタマイズされたスクロールバーがエクスペリエンスに不可欠な場合を除き、労力をかける価値はほとんどないでしょう。しかし 可能であることは知っておいてほしいことです。カスタム スクロールバーの実行がこれほど難しいという事実は、CSS 側で行うべき作業があることを示しています。でも、心配はいりません。将来的には、HoudiniAnimationWorklet によって、このようなフレーム パーフェクトのスクロールリンクされたエフェクトがかなり簡単になるでしょう。