CSS 深入探索 - 矩陣完美的自訂捲軸

自訂捲軸極為罕見,這主要是因為捲軸是網站上其他難以區分的位元之一 (我正在查看日期挑選器)。您可以使用 JavaScript 建構自己的程式碼,但這種做法會相當昂貴、低保真,且作業可能落後一些。在本文中,我們會利用一些不常見的 CSS 矩陣,建構不需任何 JavaScript 的自訂捲動器,只要一些設定程式碼即可。

重點摘要

你不是在乎小事嗎?只是想看看 Nyan cat 示範並取得程式庫嗎?您可以在 GitHub 存放區中找到示範的程式碼。

LAM;WRA (長篇和數學;無論如何都會讀)

我們之前建立了視差捲動器 (您讀過該文章?非常值得一試!)使用 CSS 3D 轉換將元素往回推送後,元素移動的速度會「較實際」捲動速度

重點回顧

我們先來回顧視差捲軸的運作方式。

如動畫所示,我們已在 3D 空間中沿著 Z 軸向後推送元素,藉此達到視差效果。捲動文件實際上是沿著 Y 軸的翻譯。因此,如果將「向下捲動」設為 100px,則每個元素都會「向上」轉譯 100 像素。這適用於「所有」元素,即使是「更遠」的元素也不例外。但因為這些元素距離相機距離太遠,其螢幕上「觀察到」的動作將小於 100 像素,進而產生所需的視差效果。

當然,如果將元素移回空間中,元素看起來也會變小,而我們可以將元素向上縮放,修正這個錯誤。我們在建構視差捲動器時已算出確切的數學運算,因此不會重複所有細節。

步驟 0:我們想要做什麼?

捲軸。這就是我們要建構的工具但你是否真的想過 他們會怎麼做?我當然不是。捲軸是當前可顯示內容的多少,以及讀者完成的進度。當您向下捲動時,捲軸表示您正在前進到尾聲。如果所有內容都符合可視區域,捲軸通常就會隱藏。如果內容寬度為可視區域的 2 倍,捲軸會填滿可視區域高度的 1⁄2。如果內容將可視區域高度為 3 倍,可將捲軸縮放至可視區域的 1⁄3 等。您可以看到模式。此外,您也可以透過點選並拖曳捲軸的方式,加快瀏覽網站的速度,而不需捲動畫面。對這樣的不明顯的元素來說,這有點出了驚人的行為。我們逐一戰鬥。

步驟 1:反向排序

好的,我們可以使用 CSS 3D 轉換,讓元素移動的速度比捲動速度慢,如視差捲動文章所述。我們也可以反轉這個方向嗎?結果發現,這可以建立完美影格的自訂捲軸。為瞭解運作方式 我們首先需要幾項 CSS 3D 基本概念

為獲得任何類型的數學投影,您通常會使用同質座標。我不會詳細介紹這些指標的意義和運作方式,但你可以把它們視為 3D 座標,再加上另一個第四個座標「w」。除非想讓視角扭曲,否則這個座標應為 1。我們不用使用 1 以外的任何值,因此不用擔心 w 的詳細資料。因此,所有點現在都是以 4D 向量 [x, y, z, w=1] 而之後,因此矩陣也都必須是 4x4。

您有機會看到 CSS 實際上會使用同質座標,就是您使用 matrix3d() 函式在轉換屬性中定義自己的 4x4 矩陣。matrix3d 使用 16 個引數 (因為矩陣是 4x4),依序指定一個資料欄。因此,我們可以使用這個函式手動指定旋轉、翻譯等,但它也讓我們能做的,就是難以理解該「w」座標

我們需要 3D 情境才能運用 matrix3d(),因為如果沒有 3D 情境,就不會出現任何扭曲效果,也不需要同質的座標。如要建立 3D 結構定義,我們需要一個包含 perspective 的容器,以及一些可在新建立的 3D 空間中轉換的部分元素。範例

一段 CSS 程式碼,運用 CSS 的視角屬性扭曲 div。

透視容器內的元素會由 CSS 引擎處理,如下所示:

  • 將元素的每個邊角 (頂點) 轉換為相對於透視容器的同質座標 [x,y,z,w]
  • 右到左,套用所有元素的轉換做為矩陣。
  • 如果透視元素可捲動,請套用捲動矩陣。
  • 運用透視矩陣。

捲動矩陣是指沿著 Y 軸的平移,如果我們將畫面向下 400 像素,則所有元素都必須向上移動 400 像素。透視矩陣是一種矩陣,它會更靠近後側的 3D 空間。如此一來,當畫面距離較遠時,就能縮小畫面大小,並在翻譯時「放慢移動速度」兩個效果。 因此,如果元素被推送,翻譯 400 像素會導致元素只在畫面上移動 300 像素。

如要瞭解「所有」詳細資料,請參閱 CSS 轉換轉譯模型的相關spec。但為本文內容,我們簡化了上述演算法。

我們的方塊位於透視容器內,perspective 屬性的值為 p,並假設容器可捲動並往下捲動 n 個像素。

透視矩陣時間捲動矩陣發生了元素轉換矩陣,元素轉換矩陣在第四列第三欄上,減去 1 乘以四個身分矩陣,後第二列是元素轉換矩陣,第 1 列是 4 x 4 的身分矩陣,第二列是元素轉換矩陣。

第一個矩陣是透視矩陣,第二個矩陣是捲動矩陣。重點回顧:捲動矩陣的工作是讓元素在「向下捲動」時「上移」,因此為負號。

但是,我們希望捲軸的相反元素,也就是當捲動畫面時,向下移動向下。在以下這個功能中,我們可以運用一些技巧: 反轉方塊邊角的 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 列的第 1 列,乘以四分之四,當中有負數矩陣,其中第四列的減號為四乘以四項身分矩陣,第四列是負一、y、z、1 等於四列第 1 列第 1 和第四欄第 1 列第 1 和第四欄。第四列

我列出了一個中繼步驟,展示元素轉換矩陣的效果。如果您不太熟悉矩陣數學,那也沒關係。Eureka 時刻就是在最後一行將捲動偏移 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 是可見內容的比例。縮圖所蓋的垂直空間比例應與可見內容比例相同:

如果在捲軸捲動高度,則 thumb dot style dot 高度會等於捲動器圓點捲動高度的捲軸高度,且只有在 thumb 點樣式高度等於捲動器點捲動高度時捲動器高度時捲動器高度相同時。
<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 協調作業的緣故,我們需要在 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 示範中的一樣。

如果您看不到 Nyan cat,那麼您在建構這個示範時,遇到我們發現並提出的錯誤 (按一下拇指可以讓 Nyan 貓) 出現。Chrome 非常擅長避免不必要的作業,例如繪製或以動畫形式呈現畫面外的內容可惜的是,我們的矩陣清除了,讓 Chrome 認為 Nyan cat GIF 實際上不在畫面上顯示。希望很快就能解決這個問題。

就是這樣工作量相當多。我鼓勵你閱讀完整內容這種做法可以達到這個目的,可能不值得這麼做,但除非自訂捲軸是體驗中不可或缺的一環。但好消息是 這種情況不會發生很難自訂捲軸,表示 CSS 端必須完成許多工作。但別擔心! 未來,HoudiniAnimationWorklet 預計會簡化這類影格與捲動連結的效果,讓影格看起來更加輕鬆。