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

Scrollbar kustom sangat jarang terjadi dan itu sebagian besar disebabkan oleh fakta bahwa scrollbar adalah salah satu bit yang tersisa di web yang hampir tidak memiliki gaya (sepertinya Anda, alat pilih tanggal). Anda dapat menggunakan JavaScript untuk membuatnya sendiri, tetapi mahal, fidelitas rendahnya, dan dapat terasa lambat. Dalam artikel ini, kita akan memanfaatkan beberapa matriks CSS yang tidak konvensional untuk membuat scroller kustom yang tidak memerlukan JavaScript saat men-scroll, hanya beberapa kode penyiapan.

TL;DR (Ringkasan)

Kamu tidak peduli dengan hal-hal kecil? Anda hanya ingin melihat demo kucing Nyan dan mendapatkan library-nya? Anda dapat menemukan kode demo di repo GitHub kami.

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

Beberapa waktu yang lalu, kami membuat scroller paralaks (Apakah Anda sudah membaca artikel tersebut? Bagus sekali, sepadan dengan waktu Anda!). Dengan mendorong elemen kembali menggunakan transformasi 3D CSS, elemen bergerak lebih lambat dari kecepatan scroll kita yang sebenarnya.

Rangkuman

Mari kita mulai dengan rekap cara kerja scroller paralaks.

Seperti yang ditunjukkan dalam animasi, kami mencapai efek paralaks dengan mendorong elemen "mundur" dalam ruang 3D, di sepanjang sumbu Z. Menggulir dokumen pada dasarnya adalah terjemahan di sepanjang sumbu Y. Jadi, jika kita men-scroll ke bawah menurut, misalnya 100 piksel, setiap elemen akan diterjemahkan ke atas kali 100 piksel. Hal ini berlaku untuk semua elemen, bahkan elemen yang "lebih jauh". Namun karena elemen tersebut lebih jauh dari kamera, gerakan yang diamati di layarnya 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 kami perbaiki dengan menskalakan elemen kembali. Kami mengetahui matematika yang tepat saat membuat scroller paralaks, jadi saya tidak akan mengulangi semua detailnya.

Langkah 0: Apa yang ingin kita lakukan?

Scroll bar. Itulah yang akan kita bangun. Tetapi pernahkah Anda benar-benar berpikir tentang apa yang mereka lakukan? Tentu saja tidak. Scroll bar adalah indikator seberapa banyak konten yang tersedia yang saat ini terlihat dan seberapa banyak progres yang telah Anda buat sebagai pembaca. Jika Anda men-scroll ke bawah, scrollbar juga untuk menunjukkan bahwa Anda membuat progres menuju akhir. Jika semua konten sesuai dengan area pandang, scrollbar biasanya disembunyikan. Jika konten memiliki tinggi 2x area pandang, scrollbar akan mengisi 1⁄2 dari tinggi area pandang. Konten senilai 3x tinggi area pandang menskalakan scrollbar ke 1⁄3 area pandang, dll. Anda akan melihat polanya. Selain men-scroll, Anda juga dapat mengklik dan menarik scrollbar untuk menjelajahi situs dengan lebih cepat. Itu jumlah perilaku yang mengejutkan untuk elemen yang tidak mencolok seperti itu. Mari bertempur satu per satu.

Langkah 1: Membalikkan urutan

Oke, kita dapat membuat elemen bergerak lebih lambat dari kecepatan scroll dengan transformasi 3D CSS seperti yang diuraikan dalam artikel scroll paralaks. Bisakah kita juga membalik arahnya? Ternyata kita bisa dan itulah cara kita 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 matematis, kemungkinan besar Anda akan menggunakan koordinat homogen. Saya tidak akan menjelaskan secara mendetail 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 mengalami distorsi perspektif. Kita tidak perlu khawatir dengan detail w karena kita tidak akan menggunakan nilai selain 1. Oleh karena itu, dari sekarang semua titik berasal dari vektor 4 dimensi [x, y, z, w=1], sehingga matriks juga harus berukuran 4x4.

Salah satu contoh ketika Anda dapat melihat bahwa CSS menggunakan koordinat homogen di balik layar adalah saat Anda menentukan matriks 4x4 Anda sendiri di properti transformasi menggunakan fungsi matrix3d(). matrix3d menggunakan 16 argumen (karena matriksnya berukuran 4x4), menentukan satu kolom demi kolom lainnya. Jadi kita bisa menggunakan fungsi ini untuk menentukan rotasi, terjemahan, dll. secara manual. Tetapi hal yang juga memungkinkan kita lakukan adalah mengacaukan dengan koordinat w itu.

Sebelum dapat menggunakan matrix3d(), kita memerlukan konteks 3D – karena tanpa konteks 3D, tidak akan ada distorsi perspektif dan tidak memerlukan 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. Sebagai contoh:

Bagian kode CSS yang mendistorsi div menggunakan
    atribut perspektif CSS.

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

  • Ubah setiap sudut (verteks) 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 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-titik lebih dekat ke titik hilang ke belakang dalam ruang 3D. Hal ini menyebabkan kedua efek membuat objek tampak lebih kecil saat lebih jauh ke belakang dan juga membuatnya “bergerak lebih lambat” saat diterjemahkan. Jadi, jika elemen didorong kembali, penerjemahan 400 piksel akan menyebabkan elemen hanya bergerak 300 piksel di layar.

Jika ingin mengetahui semua detailnya, Anda harus membaca spec 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 anggaplah penampung dapat di-scroll dan di-scroll ke bawah sebesar n piksel.

Matriks Perspective dikali matriks scroll dikali matriks transformasi elemen sama dengan 4 kali empat matriks identitas dengan minus satu per p di baris keempat kolom ketiga dikalikan empat kali empat matriks identitas dengan minus n di baris kedua di kali kolom keempat dikali matriks elemen.

Matriks pertama adalah matriks perspektif, matriks kedua adalah matriks scroll. Singkatnya: Tugas matriks scroll adalah membuat elemen bergerak ke atas saat kita men-scroll ke bawah, oleh karena itu disebut sebagai tanda negatif.

Namun, untuk scrollbar, kita menginginkan yang berlawanan – kita ingin elemen bergerak ke bawah saat men-scroll ke bawah. Di sinilah kita bisa menggunakan trik: Membalikkan koordinat w dari sudut-sudut kotak. Jika koordinat w adalah -1, semua terjemahan akan diterapkan ke arah yang berlawanan. Jadi bagaimana kita melakukannya? Mesin CSS menangani konversi sudut kotak kita menjadi koordinat homogen, dan menyetel w ke 1. Saatnya matrix3d() tampil optimal!

.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 mengabaikan w. Jadi, saat mesin CSS telah mengubah setiap sudut menjadi vektor 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 dikali empat kali empat
 matriks identitas dengan minus n di baris kedua
 kolom keempat dikali empat kali empat matriks identitas dengan minus satu di
 kolom keempat baris keempat dikali empat vektor dimensi x, y, z, 1 sama dengan empat
 kali empat matriks identitas dikurangi 1 per x di kolom ketiga dikurangi n

Saya mencantumkan langkah perantara untuk menunjukkan efek dari matriks transformasi elemen. Jika Anda tidak nyaman dengan matematika matriks, tidak masalah. Momen Eureka adalah 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. Ini karena spesifikasi CSS mengharuskan setiap verteks dengan w < 0 memblokir elemen agar tidak dirender. Dan karena koordinat z saat ini adalah 0, dan p adalah 1, w akan menjadi -1.

Untungnya, kita dapat memilih nilai z! Untuk memastikan kita berakhir di w=1, kita perlu mengatur z = -2.

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

Lihatlah, kotak kembali!

Langkah 2: Buat bergerak

Sekarang kotak kita ada di sana dan terlihat sama seperti tanpa transformasi. Saat ini, penampung perspektif tidak dapat di-scroll sehingga kita tidak dapat melihatnya, tetapi kita tahu bahwa elemen akan bergerak ke arah lain saat di-scroll. Mari kita scroll container, oke? Kita cukup menambahkan elemen spacer yang menggunakan 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>

Dan sekarang, scroll kotaknya! Kotak merah bergerak ke bawah.

Langkah 3: Berikan ukuran

Kita memiliki elemen yang bergerak ke bawah saat halaman di-scroll ke bawah. Sebenarnya itulah bagian yang sulit untuk dilakukan. Sekarang kita perlu menata gayanya agar terlihat seperti scrollbar dan membuatnya sedikit lebih interaktif.

Scrollbar biasanya terdiri dari "thumb" dan "track", sedangkan trek tidak selalu terlihat. Tinggi thumb berbanding lurus dengan seberapa banyak 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 total tinggi konten yang dapat di-scroll. scrollerHeight/scroller.scrollHeight adalah bagian konten yang terlihat. Rasio ruang vertikal yang ditutupi thumb harus sama dengan rasio konten yang terlihat:

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

Ukuran jempol ini terlihat bagus, tetapi bergerak terlalu cepat. Di sinilah kita bisa mengambil teknik dari scroller paralaks. Jika kita memindahkan elemen lebih jauh ke belakang, elemen akan bergerak lebih lambat saat men-scroll. Kita dapat memperbaiki ukuran dengan meningkatkan skalanya. Namun, seberapa banyak kita harus mendorongnya kembali dengan tepat? Mari kita coba matematika – Anda dapat menebaknya! Ini adalah kesempatan terakhir, janji ini.

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

Faktorkan sama dengan tinggi titik scroller dikurangi tinggi titik thumb pada tinggi scroll titik scroller dikurangi tinggi titik scroller.

Itulah faktor penskalaan kami. Sekarang, kita perlu mengonversi faktor penskalaan menjadi terjemahan di sepanjang sumbu z, yang sudah kita lakukan di artikel scroll paralaks. Menurut bagian yang relevan dalam spesifikasi: Faktor penskalaan sama dengan p/(p − z). Kita bisa menyelesaikan persamaan untuk z ini untuk mengetahui seberapa banyak yang kita perlukan untuk menerjemahkan jari di sepanjang sumbu z. Namun perlu diingat bahwa karena gangguan 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 tidak akan dibalik, namun semua terjemahan setelah matriks khusus akan dibalik. Mari kita kodifikasikan ini!

<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 hanyalah elemen DOM yang bisa kita tata gayanya sesuka kita. Satu hal yang penting untuk dilakukan dalam hal aksesibilitas adalah membuat thumb merespons klik dan tarik, karena banyak pengguna yang terbiasa berinteraksi dengan scrollbar dengan cara itu. Agar tidak membuat postingan blog ini lebih lama, saya tidak akan menjelaskan detail untuk bagian tersebut. Lihat kode library untuk mengetahui detailnya jika Anda ingin melihat cara melakukannya.

Bagaimana dengan iOS?

Ah, teman lama saya iOS Safari. Seperti pada scroll paralaks, kita mengalami masalah di sini. Karena men-scroll pada suatu elemen, kita perlu menentukan -webkit-overflow-scrolling: touch, tetapi hal ini menyebabkan perataan 3D dan seluruh efek scroll berhenti berfungsi. Kita mengatasi masalah ini dalam scroller paralaks dengan mendeteksi iOS Safari dan mengandalkan position: sticky sebagai solusi, dan kita akan melakukan hal yang sama persis di sini. Lihat artikel paralaks untuk menyegarkan ingatan Anda.

Bagaimana dengan scrollbar browser?

Pada beberapa sistem, kita harus menangani scrollbar native yang permanen. Secara historis, scrollbar tidak dapat disembunyikan (kecuali dengan pemilih pseudo non-standar). Jadi untuk menyembunyikannya, kita harus menggunakan metode peretas (bebas matematika). Kita menggabungkan elemen scroll dalam penampung dengan overflow-x: hidden dan membuat elemen scroll lebih lebar dari container. 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 kucing Nyan.

Jika tidak dapat melihat kucing Nyan, Anda mengalami bug yang kami temukan dan laporkan saat membuat demo ini (klik ibu jari untuk membuat kucing Nyan muncul). Chrome sangat baik untuk menghindari pekerjaan yang tidak perlu seperti melukis atau menganimasikan hal-hal yang berada di luar layar. Kabar buruknya adalah kecurangan matriks kami membuat Chrome berpikir bahwa gif kucing Nyan sebenarnya berada di luar layar. Semoga masalah ini segera teratasi.

Seperti itu. Itu merepotkan. saya memuji Anda karena telah membaca keseluruhannya. Ini adalah tipu daya nyata agar fitur ini berfungsi dan mungkin hasilnya tidak sepadan dengan usaha Anda, kecuali jika scrollbar yang disesuaikan adalah bagian penting dari pengalaman. Tapi Senang mengetahui bahwa itu mungkin, bukan? Fakta bahwa sulit untuk melakukan scrollbar kustom menunjukkan bahwa ada pekerjaan yang harus dilakukan di pihak CSS. Tapi jangan takut! Di masa mendatang, AnimationWorklet Houdini akan jauh lebih mudah membuat efek scroll-link yang sempurna untuk frame seperti ini.