CSS Deep-Dive - matrix3d() untuk scrollbar khusus bingkai yang sempurna

Scrollbar kustom sangat jarang digunakan dan hal ini sebagian besar disebabkan oleh fakta bahwa scrollbar adalah salah satu bagian yang tersisa di web yang hampir tidak dapat ditata gayanya (saya melihat Anda, pemilih tanggal). Anda dapat menggunakan JavaScript untuk membangun kode Anda sendiri, tetapi itu mahal, memiliki fidelitas rendah, dan terasa lambat. Dalam artikel ini, kita akan memanfaatkan beberapa matriks CSS yang tidak konvensional untuk membuat scroll kustom yang tidak memerlukan JavaScript saat men-scroll, hanya beberapa kode penyiapan.

TL;DR

Anda tidak peduli dengan hal-hal kecil? Anda hanya ingin melihat demo Nyanyan cat dan mendapatkan library? Anda dapat menemukan kode demo di repo GitHub kami.

LAM;WRA (Panjang dan matematis; tetap akan dibaca)

Beberapa waktu yang lalu, kami membuat scroller paralaks (Apakah Anda membaca artikel tersebut? Hasilnya sangat baik dan sepadan dengan waktu Anda.). Dengan mendorong elemen kembali menggunakan transformasi CSS 3D, elemen akan bergerak lebih lambat daripada kecepatan scroll sebenarnya.

Rekap

Mari kita mulai dengan rangkuman cara kerja scroller paralaks.

Seperti yang ditunjukkan dalam animasi, kita mendapatkan efek paralaks dengan mendorong elemen "mundur" dalam ruang 3D, di sepanjang sumbu Z. Men-scroll dokumen secara efektif adalah terjemahan di sepanjang sumbu Y. Jadi, jika kita men-scroll ke bawah, misalnya 100 px, setiap elemen akan diterjemahkan ke atas sebesar 100 px. Hal ini berlaku untuk semua elemen, bahkan yang “lebih jauh”. Namun, karena elemen tersebut lebih jauh dari kamera, gerakannya di layar yang diamati akan kurang dari 100 piksel, sehingga menghasilkan efek paralaks yang diinginkan.

Tentu saja, memindahkan elemen kembali ke ruang juga akan membuatnya tampak lebih kecil, yang kita perbaiki dengan menskalakan elemen kembali. Kita telah mengetahui matematika yang tepat saat membuat parallax scroller, jadi saya tidak akan mengulangi semua detailnya.

Langkah 0: Apa yang ingin kita lakukan?

Scrollbar. Itulah yang akan kita bangun. Tetapi pernahkah Anda benar-benar memikirkan apa yang mereka lakukan? Tentu saja tidak. Scrollbar adalah indikator jumlah konten yang tersedia saat ini dan progres yang telah Anda capai sebagai pembaca. Jika Anda men-scroll ke bawah, scrollbar juga akan men-scroll ke bawah untuk menunjukkan bahwa Anda sedang mencapai akhir. Jika semua konten sesuai dengan area pandang, scrollbar biasanya disembunyikan. Jika konten memiliki tinggi 2x dari area pandang, scrollbar akan mengisi ½ tinggi area pandang. Konten yang bernilai 3x tinggi area pandang akan menskalakan scrollbar ke ⅓ area pandang, dll. Anda akan melihat polanya. Sebagai ganti scroll, Anda juga dapat mengklik dan menarik scrollbar untuk berpindah di situs dengan lebih cepat. Jumlah perilaku yang mengejutkan untuk elemen yang tidak mencolok seperti itu. Mari kita hadapi satu per satu.

Langkah 1: Mundur

Baik, kita dapat membuat elemen bergerak lebih lambat dari kecepatan scroll dengan transformasi CSS 3D seperti yang diuraikan dalam artikel scroll paralaks. Dapatkah kita juga membalikkan arahnya? Ternyata kami bisa dan begitulah cara kami membuat scrollbar kustom yang sempurna untuk frame. Untuk memahami cara kerjanya, kita perlu membahas beberapa dasar CSS 3D terlebih dahulu.

Untuk mendapatkan jenis proyeksi perspektif apa pun dalam arti matematika, Anda kemungkinan besar akan menggunakan koordinat homogen. Saya tidak akan menjelaskan detail tentang apa itu dan mengapa berfungsi, tetapi Anda dapat menganggapnya seperti koordinat 3D dengan koordinat keempat tambahan yang disebut w. Koordinat ini harus 1 kecuali jika Anda ingin memiliki distorsi perspektif. Kita tidak perlu mengkhawatirkan detail w karena kita tidak akan menggunakan nilai lain selain 1. Oleh karena itu, semua titik mulai sekarang adalah vektor 4 dimensi [x, y, z, w=1] dan akibatnya matriks juga harus berukuran 4x4.

Salah satu kesempatan untuk melihat bahwa CSS menggunakan koordinat homogen di bawah adalah saat Anda menentukan matriks 4x4 Anda sendiri dalam properti transformasi menggunakan fungsi matrix3d(). matrix3d menggunakan 16 argumen (karena matriksnya 4x4), yang menentukan satu kolom setelah yang lain. Jadi, kita dapat menggunakan fungsi ini untuk menentukan rotasi, terjemahan, dll. secara manual. Namun, fungsi ini juga memungkinkan kita mengubah koordinat w tersebut.

Sebelum dapat menggunakan matrix3d(), kita memerlukan konteks 3D – karena tanpa konteks 3D, tidak akan ada distorsi perspektif dan tidak perlu koordinat homogen. Untuk membuat konteks 3D, kita memerlukan penampung dengan perspective dan beberapa elemen di dalamnya yang dapat kita ubah dalam ruang 3D yang baru dibuat. Contoh contoh:

Potongan kode CSS yang mendistorsi div menggunakan atribut perspektif CSS.

Elemen di dalam penampung perspektif diproses oleh mesin CSS sebagai berikut:

  • Ubah setiap sudut (vertikal) elemen menjadi koordinat homogen [x,y,z,w], relatif terhadap penampung perspektif.
  • Terapkan semua transformasi elemen sebagai matriks dari kanan ke kiri.
  • Jika elemen perspektif dapat di-scroll, terapkan matriks scroll.
  • Terapkan matriks perspektif.

Matriks scroll adalah terjemahan di sepanjang sumbu y. Jika kita men-scroll ke bawah sebesar 400 piksel, semua elemen harus dipindahkan ke atas sebesar 400 piksel. Matriks perspektif adalah matriks yang "menarik" titik lebih dekat ke titik hilang semakin jauh ke belakang dalam ruang 3D. Hal ini akan menghasilkan efek yang membuat objek tampak lebih kecil saat lebih jauh ke belakang dan juga membuatnya “bergerak lebih lambat” saat diterjemahkan. Jadi, jika elemen didorong kembali, terjemahan 400 piksel akan menyebabkan elemen hanya bergerak 300 piksel di layar.

Jika ingin mengetahui semua detailnya, Anda harus membaca spesifikasi tentang model rendering transformasi CSS, tetapi untuk artikel ini, saya menyederhanakan algoritma di atas.

Kotak kita berada di dalam penampung perspektif dengan nilai p untuk atribut perspective, dan mari kita asumsikan penampung dapat di-scroll dan di-scroll ke bawah dengan n piksel.

Matriks perspektif dikali scroll matriks kali matriks transformasi elemen sama dengan empat kali empat matriks identitas dengan minus satu di atas p di baris keempat kolom ketiga kali empat kali empat matriks identitas dengan minus n di baris kedua kolom keempat kali matriks transformasi elemen.

Matriks pertama adalah matriks perspektif, matriks kedua adalah matriks scroll. Untuk merangkum: Tugas matriks scroll adalah membuat elemen bergerak ke atas saat kita men-scroll ke bawah, sehingga tanda negatif.

Namun, untuk scrollbar, kita menginginkan hal yang berlawanan – kita ingin elemen kita bergerak ke bawah saat kita men-scroll ke bawah. Di sinilah kita dapat menggunakan trik: Menginversi koordinat w dari sudut kotak. Jika koordinat w adalah -1, semua terjemahan akan berlaku dalam arah yang berlawanan. Jadi, bagaimana cara melakukannya? Mesin CSS akan menangani konversi sudut kotak menjadi koordinat homogen, dan menetapkan w ke 1. Saatnya matrix3d() bersinar!

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Matriks ini tidak akan melakukan apa pun selain meniadakan w. Jadi, saat mesin CSS telah mengubah setiap sudut menjadi vektor dalam bentuk [x,y,z,1], matriks akan mengonversinya menjadi [x,y,z,-1].

Matriks identitas empat kali empat dengan minus satu per p di baris keempat
  kolom ketiga kali matriks identitas empat kali empat dengan minus n di
  baris kedua kolom keempat kali matriks identitas empat kali empat dengan minus satu di
  baris keempat kolom keempat kali vektor empat dimensi x, y, z, 1 sama dengan matriks
  identitas empat kali empat dengan minus satu per p di baris keempat kolom ketiga,
  minus n di baris kedua kolom keempat, dan minus satu di baris keempat
  kolom keempat sama dengan vektor empat dimensi x, y plus n, z, minus z per
  p minus 1.

Saya mencantumkan langkah perantara untuk menunjukkan efek matriks transformasi elemen kita. Jika Anda tidak nyaman dengan matematika matriks, tidak apa-apa. Momen Eureka adalah bahwa di baris terakhir, kita akhirnya menambahkan offset scroll n ke koordinat y, bukan menguranginya. Elemen akan diterjemahkan ke bawah jika kita men-scroll ke bawah.

Namun, jika kita hanya menempatkan matriks ini dalam contoh, elemen tidak akan ditampilkan. Hal ini karena spesifikasi CSS mewajibkan setiap vertikal dengan w < 0 memblokir elemen agar tidak dirender. Dan karena koordinat z kita saat ini adalah 0, dan p adalah 1, w akan menjadi -1.

Untungnya, kita dapat memilih nilai z. Untuk memastikan kita mendapatkan w=1, kita perlu menetapkan z = -2.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Lihat, kotak kami kembali!

Langkah 2: Bergeraklah

Sekarang kotak kita ada di sana dan terlihat sama seperti tanpa transformasi. Saat ini, container perspektif tidak dapat di-scroll, sehingga kita tidak dapat melihatnya, tetapi kita tahu bahwa elemen akan pergi ke arah lain saat di-scroll. Jadi, mari kita buat penampung di-scroll. Kita cukup menambahkan elemen {i>spacer <i}yang membutuhkan ruang:

<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>

Sekarang, scroll kotak. Kotak merah akan bergerak ke bawah.

Langkah 3: Berikan ukuran

Kita memiliki elemen yang bergerak ke bawah ketika halaman di-scroll ke bawah. Itu adalah bagian yang sulit. Sekarang kita perlu menata gayanya agar terlihat seperti scrollbar dan membuatnya sedikit lebih interaktif.

Scrollbar biasanya terdiri dari “thumb” dan “track”, sedangkan track tidak selalu terlihat. Tinggi thumb sebanding dengan jumlah konten yang terlihat.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight adalah tinggi elemen yang dapat di-scroll, sedangkan scroller.scrollHeight adalah tinggi total konten yang dapat di-scroll. scrollerHeight/scroller.scrollHeight adalah fraksi konten yang terlihat. Rasio ruang vertikal yang dicakup ibu jari harus sama dengan rasio konten yang terlihat:

tinggi titik gaya titik thumb di atas scrollerHeight sama dengan tinggi scroller di atas tinggi scroll titik scroller jika dan hanya jika tinggi titik gaya titik jempol sama dengan tinggi scroller dikali tinggi scroller di atas tinggi scroll titik scroller.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Ukuran ibu jari terlihat bagus, tetapi bergerak terlalu cepat. Di sinilah kita dapat mengambil teknik dari parallax scroller. Jika kita memindahkan elemen lebih jauh ke belakang, elemen akan bergerak lebih lambat saat men-scroll. Kita dapat memperbaiki ukurannya dengan menskalakannya. Namun, berapa banyak kita harus mendorongnya kembali? Ayo mulai matematika! Ini adalah yang terakhir, saya janji.

Informasi penting adalah kita ingin tepi bawah ibu jari sejajar dengan tepi bawah elemen yang dapat di-scroll saat di-scroll hingga ke bawah. Dengan kata lain: Jika kita telah men-scroll scroller.scrollHeight - scroller.height piksel, kita ingin ibu jari kita diterjemahkan oleh scroller.height - thumb.height. Untuk setiap piksel penggeser, kita ingin ibu jari kita memindahkan sebagian piksel:

Faktor sama dengan tinggi titik scroller dikurangi tinggi titik jempol di atas tinggi scroll titik titik scroller dikurangi tinggi titik scroller.

Itulah faktor penskalaan kami. Sekarang kita perlu mengonversi faktor penskalaan menjadi terjemahan di sepanjang sumbu z, yang telah kita lakukan dalam artikel scrolling paralaks. Menurut bagian yang relevan dalam spesifikasi: Faktor penskalaan sama dengan p/(p − z). Kita dapat menyelesaikan persamaan ini untuk z guna mengetahui seberapa banyak kita perlu menerjemahkan ibu jari kita di sepanjang sumbu z. Namun, perlu diingat bahwa karena trik koordinat w, kita perlu menerjemahkan -2px tambahan di sepanjang z. Perhatikan juga bahwa transformasi elemen diterapkan dari kanan ke kiri, yang berarti semua terjemahan sebelum matriks khusus kita tidak akan dibalik, tetapi semua terjemahan setelah matriks khusus kita akan dibalik. Mari kita kodifikasi.

<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>

Kita memiliki scrollbar. Dan itu hanya elemen DOM yang bisa kita tata gayanya sesuka kita. Satu hal yang penting untuk dilakukan dalam hal aksesibilitas adalah membuat ibu jari merespons klik-dan-tarik, karena banyak pengguna terbiasa berinteraksi dengan scrollbar dengan cara tersebut. Agar postingan blog ini tidak bertele-tele, saya tidak akan menjelaskan detailnya untuk bagian tersebut. Lihat kode library untuk mengetahui detailnya jika Anda ingin melihat cara melakukannya.

Bagaimana dengan iOS?

Ah, teman lama saya, Safari iOS. Seperti halnya scroll paralaks, kita mengalami masalah di sini. Karena men-scroll pada elemen, kita perlu menetapkan -webkit-overflow-scrolling: touch, tetapi tindakan ini akan menyebabkan perataan 3D dan seluruh efek scroll berhenti berfungsi. Kita telah menyelesaikan masalah ini di scroller paralaks dengan mendeteksi iOS Safari dan mengandalkan position: sticky sebagai solusinya, dan kita akan melakukan hal yang sama persis di sini. Lihat artikel paralaks untuk mengingat kembali.

Bagaimana dengan scrollbar browser?

Di beberapa sistem, kita harus menangani scrollbar native permanen. Secara historis, scrollbar tidak dapat disembunyikan (kecuali dengan pseudo-pemilih non-standar). Jadi untuk menyembunyikannya, kita harus melakukan beberapa peretas (bebas matematika). Kita menggabungkan elemen scroll dalam penampung dengan overflow-x: hidden dan membuat elemen scroll lebih lebar dari penampung. Scrollbar native browser kini tidak terlihat.

Sirip

Dengan menggabungkan semuanya, kini kita dapat membuat scrollbar kustom yang sempurna untuk frame – seperti yang ada di demo Nyan cat.

Jika tidak dapat melihat Nyan cat, Anda mengalami bug yang kami temukan dan laporkan saat mem-build demo ini (klik jempol untuk membuat Nyan cat muncul). Chrome sangat baik dalam menghindari pekerjaan yang tidak perlu seperti menggambar atau menganimasikan hal-hal yang berada di luar layar. Kabar buruknya adalah, kelakuan aneh matriks kami membuat Chrome mengira gif Nyan cat sebenarnya berada di luar layar. Semoga masalah ini segera teratasi.

Seperti itu. Itu adalah pekerjaan yang berat. Saya memuji Anda karena telah membaca semuanya. Ini adalah trik yang benar-benar rumit untuk membuatnya berfungsi dan mungkin jarang sepadan dengan upaya yang dilakukan, kecuali jika scrollbar yang disesuaikan adalah bagian penting dari pengalaman. Tapi senang rasanya mengetahui bahwa itu mungkin, bukan? Fakta bahwa scrollbar kustom begitu sulit dilakukan menunjukkan bahwa ada pekerjaan yang harus dilakukan di sisi CSS. Namun, jangan khawatir. Di masa mendatang, AnimationWorklet Houdini akan membuat efek scroll-linked frame-perfect seperti ini jauh lebih mudah.