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

自訂捲軸極為罕見,主要原因是捲軸是網路上的其餘位元之一,不太容易辨識 (我正在看著你,日期挑選器)。您可以使用 JavaScript 自行建立版本,但這樣不但費用高昂、精確度較低,還可能覺得延遲。在本文中,我們會運用一些特殊的 CSS 矩陣建構自訂捲動工具,其捲動時不需要任何 JavaScript,且僅有部分設定程式碼。

重點摘要

一點不在乎?您只要觀看 Nyan cat 示範並取得程式庫嗎?您可以在我們的 GitHub 存放區中找到示範的程式碼。

LAM;WRA (長版和數學題,仍舊會閱讀)

我們在前一陣子我們建構了視差捲動器 (您讀過該文章了嗎?非常值得你花時間!)使用 CSS 3D 轉換將元素往回推,元素移動的幅度會比實際捲動速度慢

重點回顧

我們先回顧一下視差捲動工具的運作方式。

如動畫所示,我們透過 Z 軸在 3D 空間中將元素「向後」推送,藉此達到視差效果。捲動文件實際上是沿著 Y 軸進行翻譯。因此,如果向下捲動「向下」(例如 100px),每個元素會「向上」平移 100 個像素。這適用於「所有」元素,甚至是「後面」的元素。但「因為」距離相機較遠,「觀察」在畫面上的動作會小於 100 像素,進而產生想要的視差效果。

當然,在空間中將元素移回空間也會使元素變小,而我們要將元素重新放大,即可進行修正。我們在建構視差捲軸時已經能確定確切的計算結果,因此我並未重複說明所有細節。

步驟 0:我們該怎麼做?

捲軸。這就是我們要建構的內容。但你是否曾真的思考過這些品牌的營運方式?我其實沒有。捲軸代表目前可看到內容的進度,以及讀者完成的進度。向下捲動時,捲軸會指示您正在最後進展。如果所有內容都符合可視區域,捲軸通常會隱藏。如果內容的高度為可視區域高度的 2 倍,捲軸會填滿可視區域高度的 1⁄2。內容的高度為可視區域高度的 3 倍,系統會將捲軸縮放至可視區域的 1⁄3 等。您可以看到該模式。除了捲動之外,您也可以點選並拖曳捲軸,加快瀏覽網站的速度。對這樣的不明顯的元素而言,這種情況是出乎意料的行為。讓我們一次戰鬥。

步驟 1:反過來

好,我們可以使用 CSS 3D 轉換,讓元素的移動速度低於捲動速度,如視差捲動文章所述。我們是否可以反轉方向?我們可以藉此建構完美影格的自訂捲軸我們必須先介紹一些 CSS 3D 基本概念 才能理解運作方式

如要取得任何類型的視角投影,在數學方面,一般會使用同質座標。我不會詳細說明它們的定義和運作原理,但您可以把它們想成是 3D 座標以及另一個名為 w 的第四個座標。這個座標應為 1,但要呈現視角變形的情況除外。我們不用擔心 w 的細節,因為我們不會使用 1 以外的值。因此,所有點均來自現在的 4D 向量 [x, y, z, w=1],因此矩陣也必須是 4x4。

可以發現,CSS 實際上會使用同質座標,就是在使用 matrix3d() 函式在轉換屬性中定義自己的 4x4 矩陣。matrix3d 會使用 16 個引數 (因為矩陣是 4x4),請依序指定一欄。因此我們可以使用這個函式手動指定旋轉、翻譯等。但是,這樣做也讓我們無法避免對該 w 座標造成雜亂!

使用 matrix3d() 之前,我們需要 3D 結構定義,因為如果沒有 3D 結構定義,就不會有觀點扭曲情形,也不必使用同質的座標。如要建立 3D 結構定義,需要含有 perspective 的容器,以及可在新建立的 3D 空間中轉換的部分元素。範例

一段 CSS 程式碼,會使用 CSS 的角度屬性來扭曲 div。

CSS 引擎會處理觀點容器中的元素,如下所示:

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

捲動矩陣是沿著 Y 軸呈現的。如果我們向下捲動 400 像素,則所有元素都必須上移 400 像素。透視矩陣是一種矩陣,在 3D 空間中,「提取」指向較接近消失點的位置。這麼做可讓兩個位置在較靠近時變小的效果,也能在翻譯時「速度變慢」。因此,如果將元素退回,400px 的平移會導致元素只在畫面上移動 300 像素。

如要查看「所有」詳細資料,請參閱 CSS 轉換算繪模型中的spec,但為方便您瞭解本文,我們簡化了上述演算法。

我們的方塊位於視角容器內,其值為 p,且 perspective 屬性為 p,假設容器可捲動且以 n 像素向下捲動。

將透視矩陣乘以元素轉換矩陣,其轉換矩陣等於四等於四

第一個矩陣是透視矩陣,第二個矩陣是捲動矩陣。複習一下:捲動矩陣的工作,是在我們向下捲動時,將元素「上移」,因此負號表示負數。

但對於捲軸,我們希望在捲動向下時,讓元素向下移動這時我們可以使用技巧:反轉方塊角落的 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]

四個身分矩陣在第四列第三欄減去一,P 乘以四個身分矩陣,第二列的第四欄乘以 4 個身分矩陣,第四列的第 4 欄減去一,再乘以四、Y、z、1 等於四減去第四欄、第四列減去四、分之四相減。

我列出了一個中繼步驟,用來顯示元素轉換矩陣的效果。如果您不想使用矩陣數學,沒關係。在最後一行,「Eureka」時刻就是在 y 座標中加入捲動偏移 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 是可見內容的部分。縮圖覆蓋的垂直空間比例應等於可見內容的比例:

在 ScrollerHeight 上,thumb 點樣式的點高度會等於捲動工具點捲動高度,但前提是 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 軸平移需要多少資源不過請注意,由於 Google 的 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,您在建構此示範時遇到系統發現並回報錯誤的錯誤 (按一下拇指讓貓出現)。Chrome 非常擅長避免不必要的工作 例如繪圖或繪製動畫等元素但遺憾的是,我們的矩陣調查動作讓 Chrome 將 Nyan cat GIF 視為不在畫面中。希望這個問題很快就會得到解決。

就是這樣實在很費力。我以你閱讀整篇報導為榮這是完成上述作業的一大挑戰,而且通常不值得費力,除非自訂捲軸是體驗不可或缺的一環。不過,知道這都可行,不行嗎?很困難的是自訂捲軸,表示 CSS 端有尚待處理的部分。但請放心!日後,HoudiniAnimationWorklet 會讓影格完美的捲動連結效果變得簡單許多。