自訂捲軸條非常少見,主要是因為捲軸條是網站上幾乎無法設定樣式的元素之一 (我指的是日期挑選器)。您可以使用 JavaScript 自行建構,但這麼做成本高昂、精確度低,而且可能會造成延遲。在本文中,我們將利用一些非傳統的 CSS 矩陣來建構自訂捲軸,在捲動時不需要任何 JavaScript,只需要一些設定程式碼即可。
TL;DR
你不關心小事嗎?您只想查看 Nyan cat 示範並取得程式庫嗎?您可以在我們的 GitHub 存放區中找到示範程式碼。
LAM;WRA (長且數學性質;但還是會讀)
我們先前曾建立了視差捲動畫面 (您有閱讀這篇文章嗎?它真的很棒,值得你花時間瞭解!使用 CSS 3D 轉換功能將元素推回,元素移動速度會比實際捲動速度較慢。
重點回顧
首先,讓我們回顧一下視差捲軸的運作方式。
如動畫所示,我們在 3D 空間中沿著 Z 軸將元素推向「後方」,藉此產生視差效果。捲動文件實際上是沿著 Y 軸進行轉譯。因此,如果我們向下捲動 100 像素,每個元素都會向上移動 100 像素。這項設定適用於所有元素,即使是「較遠」的元素也一樣。但由於這些元素離相機較遠,因此觀察到的螢幕上移動距離會小於 100 像素,進而產生所需的視差效果。
當然,將元素移回空間也會使其顯示尺寸變小,我們會透過將元素縮放回來來修正。我們在建構視差捲軸時,已找出確切的數學運算,因此我不會重複說明所有細節。
步驟 0:我們想做什麼?
捲軸。我們將建構的就是這類應用程式。但您是否曾經認真思考過他們的職責?我絕對沒有。捲軸列可用來顯示目前可見的內容數量,以及讀者瀏覽進度。如果您向下捲動,捲軸也會跟著捲動,表示您正在往下捲動。如果所有內容都符合檢視區範圍,捲軸通常會隱藏。如果內容的高度是可視區域的 2 倍,捲軸就會填滿可視區域的 ½ 高度。內容高度是可視區域的 3 倍,會將捲軸縮放至可視區域的 ⅓ 等等。您會發現這類模式。除了捲動畫面,您也可以點選並拖曳捲軸,以便更快速地瀏覽網站。對於這種不起眼的元素,這實在是相當多的行為。我們一場一場打吧。
步驟 1:倒車
好,我們可以使用 CSS 3D 轉換,讓元素的移動速度低於捲動速度,如同視差捲動文章所述。我們也可以反轉方向嗎?事實上,我們可以做到,這也是我們建構完美無瑕的自訂捲軸的入門方式。為了瞭解這項功能的運作方式,我們需要先介紹幾個 CSS 3D 的基本概念。
如要以數學方式取得任何類型的透視投影,您很可能會使用同質座標。我不會詳細說明這些是什麼,以及為何有效,但您可以將它們視為 3D 座標,再加上一個稱為 w 的第四個座標。除非您想要讓透視圖失真,否則這個座標應為 1。我們不需要擔心 w 的詳細資料,因為我們不會使用 1 以外的任何值。因此,從現在開始,所有點都是 4 維向量 [x, y, z, w=1],因此矩陣也必須是 4x4。
您可以透過以下方式,瞭解 CSS 在幕後使用同構座標:使用 matrix3d()
函式在 transform 屬性中定義自己的 4x4 矩陣。matrix3d
會接收 16 個引數 (因為矩陣為 4x4),依序指定每個資料欄。因此,我們可以使用這個函式手動指定旋轉、平移等作業,但這也讓我們可以處理 w 座標!
我們必須先取得 3D 內容,才能使用 matrix3d()
,因為沒有 3D 內容,就不會有任何透視扭曲,也不需要使用同質座標。如要建立 3D 內容,我們需要一個容器,其中包含 perspective
和一些元素,我們可以在新建立的 3D 空間中轉換這些元素。例如:
透視容器內的元素會由 CSS 引擎以以下方式處理:
- 將元素的每個角落 (頂點) 轉換為相對於透視容器的均勻座標
[x,y,z,w]
。 - 將所有元素的轉換作業做為矩陣,從右到左套用。
- 如果透視圖元素可捲動,請套用捲動矩陣。
- 套用透視矩陣。
捲動矩陣是沿著 y 軸平移。如果我們向下捲動 400 像素,所有元素都需要向上移動 400 像素。透視矩陣是一種矩陣,會將點「拉近」消失點,並在 3D 空間中往後退。這會同時產生兩種效果:讓物體在遠處時看起來較小,並且在經過轉換時「移動速度較慢」。因此,如果元素被推回,轉譯 400 像素會導致元素在畫面上只移動 300 像素。
如要瞭解所有詳細資訊,請參閱 CSS 轉換算繪模型的規格,但為了方便說明,我簡化了上述演算法。
我們的方塊位於透視容器內,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]
。
我列出了中間步驟,以顯示元素轉換矩陣的效果。如果您不熟悉矩陣運算,也沒關係。值得一提的是,在最後一行中,我們會將捲動偏移量 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
進行轉譯。對於捲軸的每個像素,我們希望拇指移動的像素數量為整數的一部分:
這就是我們的縮放比例係數。接下來,我們需要將縮放比例轉換為沿著 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 貓示範中的那個一樣。
如果您看不到 Nyan 貓,表示您遇到了我們在建構此示範時發現並記錄的錯誤 (點選大拇指即可顯示 Nyan 貓)。Chrome 非常擅長避免不必要的工作,例如繪製或製作畫面外物件的動畫。壞消息是,我們的矩陣操作會讓 Chrome 認為 Nyan 貓 GIF 實際上是在畫面外。希望這個問題能盡快解決。
就是這樣。這項工作相當繁重。我很高興你能讀完整篇文章。這項技巧確實很難實作,而且除非自訂捲軸是體驗中不可或缺的部分,否則這項技巧的效益可能不大。但很高興知道這麼做是可行的,對吧?自訂捲軸的難度如此高,顯示出 CSS 方面有待改進之處。不過請放心! 日後,Houdini 的 AnimationWorklet 將可讓這類完美無縫的捲動連結效果變得更簡單。