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

カスタム スクロールバーは非常にまれです。その主な原因は、スクロールバーがウェブ上にほとんどスタイリングされていない部分の 1 つであるからです(日付選択ツール、日付選択ツール)。JavaScript を使用して独自のコードを作成することもできますが、これはコストが高く、再現性が低く、動作が遅く感じられます。この記事では、型にはまらない CSS マトリックスを活用して、スクロール中に JavaScript を使わず、セットアップ コードだけを必要とするカスタム スクローラーを作成します。

要約

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

LAM;WRA(Long と数学的、いずれにせよ読み上げられる)

少し前に、パララックス スクローラーを開発しました(その記事を読んだことはありますか?時間をかける価値は十分にあります)。CSS 3D 変換を使用して要素をプッシュバックすることで、要素が実際のスクロール速度よりも遅く移動しました。

内容のまとめ

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

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

もちろん、要素を空間内に戻すと小さく見える場合もあります。これは、要素を縮小して元に戻して修正します。正確な計算については、パララックス スクローラーを作成した際に解明したので、詳しい説明は省きます。

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

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

ステップ 1: 逆に並べる

パララックス スクロールに関する記事で説明しているように、CSS 3D 変換を使用すると、要素のスクロール速度より遅くすることができます。逆ものでしょうか?それが、フレームに完璧なカスタム スクロールバーを作るための手がかりとなることがわかりました。この仕組みを理解するには、まず CSS の 3D の基本をいくつか確認しましょう。

数学的な観点であらゆる視点投影を行うには、同次座標を使用することになる可能性が高くなります。関数の機能と仕組みについては詳しく説明しませんが、4 番目の座標 w がある 3D 座標と考えることができます。視点の歪みを除き、この座標は 1 にする必要があります。1 以外の値は使用しないため、w の詳細を気にする必要はありません。したがって、すべてのポイントは今後 4 次元ベクトル [x, y, z, w=1] になり、行列も 4x4 である必要があります。

CSS が内部で同種座標を使用していることがわかるのは、matrix3d() 関数を使用して変換プロパティで独自の 4x4 行列を定義した場合です。matrix3d は 16 個の引数を取り(行列が 4x4 であるため)、列を連続して指定します。この関数を使用して、回転や翻訳などを手動で指定できます。ただし、この w 座標をいろいろ変えることもできます。

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

CSS の視点属性を使って div を変形させる CSS コードです。

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

  • 要素の各隅(頂点)を、視点コンテナに対して同次座標 [x,y,z,w] に変換します。
  • 要素のすべての変換を右から左に行列として適用します。
  • 視点要素がスクロール可能な場合は、スクロール マトリックスを適用します。
  • 遠近感マトリックスを適用します。

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

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

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

視点行列 × スクロール行列 × 要素の変換行列は、4 行 4 の単位行列で、4 行 3 列目にマイナス 1 を乗算し、2 行目、4 列目にマイナス n を乗じ、要素の変換行列を 4 行 4 分の p で割ったものです。

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 行 4 の単位行列、4 行 3 列にマイナス 1 を掛けた 4 行 x 4 行列、2 行 4 列にマイナス n、4 行 4 列を 4 次元ベクトル x、y、z、1 をマイナス n 列とする

要素変換行列の効果を示す中間ステップをリストしました。行列の計算に慣れていない場合でも問題ありません。エウレカの瞬間は、最後の行でスクロール オフセット 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 ピクセルずつ動かします。

係数は、スクローラーのドットの高さ - サムドットの高さをスクローラーのドット スクロールの高さからスクローラーのドットの高さを引いた値に等しくなります。

これがスケーリング ファクタです。次に、スケーリング ファクタを 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 フラット化が発生し、スクロール効果全体が機能しなくなります。Google は、iOS Safari を検出し、回避策として position: sticky を利用することで、パララックス スクローラーでこの問題を解決しました。ここでもまったく同じことを行います。パララックスの記事を参照して記憶を更新してください。

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

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

フィン

以上をまとめると、Nyan cat デモのように、フレームに最適なカスタム スクロールバーを構築できるようになりました。

ニャン猫が表示されない場合は、このデモの作成中に発見して報告したバグが発生していることになります(サムをクリックすると、ニャン猫が表示されます)。Chrome は画面外での描画やアニメーションの 不要な作業を避けるのが得意です悪いニュースとしては、マトリックスの不正操作により、Chrome で「ニャン猫の GIF 画像」が画面に表示されないようにしてしまうことです。 この問題が早急に解決されることを願っております。

これで準備は完了です。これは大変な作業でした。最後まで読んでもらえると助かります。これを機能させるには非常に厄介な作業であり、カスタマイズしたスクロールバーがエクスペリエンスに不可欠な要素である場合を除き、労力に見合う価値はほとんどありません。でも、 それが可能だとわかっていればよろしいでしょうか。カスタム スクロールバーを作成するのが難しいという事実は、CSS 側で行うべき作業があることを示しています。でも大丈夫!将来的には、HoudiniAnimationWorklet により、このようなフレームパーフェクトのスクロール リンク効果がはるかに簡単になる予定です。