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

Scrollbar kustom sangat langka dan itu sebagian besar karena scrollbar adalah salah satu bit yang tersisa di web yang hampir tidak dapat diubah gayanya (saya lihat Anda, pemilih tanggal). Anda dapat menggunakan JavaScript untuk membangun kode Anda sendiri, tetapi itu mahal, memiliki fidelitas rendah, dan terasa lambat. Dalam artikel ini, kami akan memanfaatkan beberapa matriks CSS tidak konvensional untuk membuat scroller kustom yang tidak memerlukan JavaScript saat men-scroll, hanya beberapa kode penyiapan.

TL;DR (Ringkasan)

Kamu nggak peduli dengan hal-hal kecil? Anda hanya ingin melihat demo kucing Nyan dan mengakses koleksinya? Anda dapat menemukan kode demo di repositori 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 3D CSS, elemen bergerak lebih lambat daripada kecepatan scroll kita yang sebenarnya.

Rangkuman

Mari kita mulai dengan rangkuman cara kerja scroller paralaks.

Seperti yang ditunjukkan dalam animasi, kami 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 ke bawah, misalnya 100 px, setiap elemen akan diterjemahkan ke atas kali 100 px. Hal ini berlaku untuk semua elemen, bahkan elemen yang berada "paling belakang". Namun, karena elemen tersebut jauh dari kamera, gerakan yang diamati di layar akan kurang dari 100 px, sehingga menghasilkan efek paralaks yang diinginkan.

Tentu saja, memindahkan elemen kembali ke ruang juga akan membuatnya tampak lebih kecil, yang kita perbaiki dengan meningkatkan skala elemen kembali. Kami mendapatkan perhitungan pasti saat membuat scroller paralaks, 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 berpikir tentang apa yang mereka lakukan? Saya tentu tidak tahu. Scrollbar adalah indikator seberapa banyak konten tersedia yang saat ini terlihat dan seberapa banyak progres yang telah Anda lakukan sebagai pembaca. Jika Anda men-scroll ke bawah, scroll bar untuk menunjukkan bahwa Anda membuat progres menuju akhir. Jika semua konten sesuai dengan area pandang, scrollbar biasanya akan disembunyikan. Jika konten memiliki 2x tinggi area tampilan, scrollbar akan mengisi 1⁄2 tinggi area pandang. Konten senilai 3x tinggi area tampilan menskalakan scrollbar ke 1⁄3 area tampilan, dsb. 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. Ayo kita lawan satu per satu.

Langkah 1: Membalikkannya

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

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

Satu kesempatan ketika Anda dapat melihat bahwa CSS menggunakan koordinat homogen di balik layar adalah saat Anda menentukan matriks 4 x 4 Anda sendiri dalam properti transformasi menggunakan fungsi matrix3d(). matrix3d memerlukan 16 argumen (karena matriksnya 4x4), yang menentukan satu kolom demi kolom. Jadi kita dapat menggunakan fungsi ini untuk menentukan rotasi, terjemahan, dll. secara manual. Namun, fungsi ini juga memungkinkan kita melakukan masalah dengan koordinat w tersebut.

Sebelum dapat menggunakan matrix3d(), kita memerlukan konteks 3D – karena tanpa konteks 3D, tidak akan ada distorsi perspektif dan tidak perlu koordinat yang 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 (vertex) 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 px, semua elemen harus dipindahkan ke atas sebesar 400 px. Matriks perspektif adalah matriks yang “menarik” menunjuk lebih dekat ke titik hilang yang semakin jauh ke belakang dalam ruang 3D. Hal ini menghasilkan kedua efek yang membuat objek tampak lebih kecil saat berada lebih jauh ke belakang, dan juga membuat objek "bergerak lebih lambat" saat diterjemahkan. Jadi, jika elemen didorong ke belakang, terjemahan 400 px akan menyebabkan elemen hanya bergerak 300 px di layar.

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

Kotak kita berada di dalam penampung perspektif dengan nilai p untuk atribut perspective, dan anggaplah penampung tersebut dapat di-scroll dan di-scroll ke bawah sebesar 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. Kesimpulannya: Tugas matriks scroll adalah membuat elemen bergerak ke atas saat kita men-scroll ke bawah, sehingga menjadi tanda negatif.

Namun, untuk scrollbar, kita menginginkan yang kebalikannya – kita ingin elemen dipindahkan ke bawah saat men-scroll ke bawah. Di sinilah kita dapat menggunakan trik: Membalikkan koordinat w dari sudut kotak kita. Jika koordinat w adalah -1, semua terjemahan akan berlaku dalam arah yang berlawanan. Jadi bagaimana kita melakukannya? Mesin CSS menangani konversi sudut kotak menjadi koordinat homogen, dan menetapkan w ke 1. Inilah saatnya matrix3d() kembali 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 menegasikan w. Jadi, saat mesin CSS telah mengubah setiap sudut menjadi vektor bentuk [x,y,z,1], matriks akan mengonversinya menjadi [x,y,z,-1].

x z x z, 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 baris keempat kolom keempat dikali empat vektor dimensi x, y, z, 1 sama dengan empat kali empat matriks identitas baris keempat dikurangi satu di atas p di kolom keempat dikurangi satu dimensi kedua dikurangi n kolom ketiga dikurangi n dimensi keempat, dikurangi n x di kolom keempat dikurangi satu dimensi

Saya telah mencantumkan langkah perantara untuk menunjukkan efek matriks transformasi elemen. Tidak masalah jika Anda tidak terbiasa dengan matematika matriks. Momen Eureka adalah di baris terakhir kita 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 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 bisa memilih nilai z. Untuk memastikan kita mendapatkan nilai 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);
}

Lihat, kotak kita telah kembali!

Langkah 2: Bergeraklah

Sekarang kotak kita ada di sana dan terlihat sama seperti tanpa transformasi apa pun. Saat ini, penampung perspektif tidak dapat di-scroll, sehingga kita tidak dapat melihatnya, tetapi kita tahu bahwa elemen akan pergi ke arah lain saat di-scroll. Mari kita scroll container, bukan? 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 kotaknya! Kotak merah bergerak ke bawah.

Langkah 3: Beri ukuran

Kita memiliki elemen yang bergerak ke bawah ketika halaman di-scroll ke bawah. Itulah 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 trek tidak selalu terlihat. Tinggi jempol 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 tinggi total konten yang dapat di-scroll. scrollerHeight/scroller.scrollHeight adalah bagian dari konten yang terlihat. Rasio ruang vertikal yang dicakup jempol 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 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. Tapi, berapa banyak yang harus kita mundurkan dengan tepat? Ayo mulai matematika! Ini adalah yang terakhir, saya janji.

Informasi penting lainnya adalah kita ingin tepi bawah ibu jari sejajar dengan tepi bawah elemen yang dapat di-scroll saat di-scroll ke bawah. Dengan kata lain: Jika telah men-scroll scroller.scrollHeight - scroller.height piksel, kita ingin thumb diterjemahkan oleh scroller.height - thumb.height. Untuk setiap {i>pixel<i} di {i>scroller<i}, kita ingin jempol kita memindahkan sepersekian 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 scroll paralaks. Menurut bagian yang relevan dalam spesifikasi: Faktor penskalaan sama dengan p/(p - z). Kita dapat menyelesaikan persamaan z ini untuk mengetahui seberapa banyak kita perlu menerjemahkan ibu jari di sepanjang sumbu z. Namun, perlu diingat bahwa karena kesalahan 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, tetapi semua terjemahan setelah matriks khusus akan dibalik. Mari kita kodifikasikan hal 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 hanya elemen DOM yang bisa kita tata gayanya sesuka kita. Satu hal penting untuk dilakukan dalam hal aksesibilitas adalah membuat ibu jari merespons klik dan tarik, karena banyak pengguna yang sudah 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 iOS Safari. 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 menyegarkan ingatan Anda.

Bagaimana dengan scrollbar browser?

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

Sirip

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

Jika tidak dapat melihat kucing Nyan, Anda mengalami bug yang kami temukan dan laporkan saat membuat demo ini (klik jempol untuk memunculkan kucing Nyan). Chrome sangat hebat dalam menghindari pekerjaan yang tidak perlu seperti melukis atau membuat animasi hal yang berada di luar layar. Kabar buruknya adalah bahwa kecewaan matriks kami membuat Chrome berpikir bahwa gif kucing Nyan sebenarnya berada di luar layar. Semoga masalah ini bisa segera diperbaiki.

Seperti itu. Itu adalah pekerjaan yang merepotkan. Saya memuji Anda karena membaca semua hal. Ini adalah beberapa trik nyata agar cara ini berfungsi dan mungkin jarang sepadan dengan usaha, kecuali jika scrollbar yang disesuaikan adalah bagian penting dari pengalaman ini. Tapi senang rasanya mengetahui bahwa itu mungkin, bukan? Faktanya, sulit melakukan scrollbar kustom menunjukkan bahwa ada pekerjaan yang harus dilakukan di pihak CSS. Tapi jangan khawatir! Di masa mendatang, AnimationWorklet Houdini akan jauh lebih mudah membuat efek terkait scroll kesempurnaan frame.