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

Scrollbar kustom sangat langka dan sebagian besar disebabkan oleh bilah gulir adalah salah satu bit yang tersisa di web yang cukup tidak bisa ditiru (saya sedang melihat Anda, pemilih tanggal). Anda dapat menggunakan JavaScript untuk membuatnya sendiri, tetapi itu mahal, rendah {i>fidelity <i}dan bisa terasa lambat. Dalam artikel ini, kami akan memanfaatkan beberapa matriks CSS non-konvensional untuk membuat scroller khusus yang tidak memerlukan JavaScript saat men-scroll, hanya beberapa kode penyiapan.

TL;DR

Kamu nggak peduli dengan hal-hal kecil? Anda hanya ingin melihat Demo kucing Nyan dan mendapatkan perpustakaan? Anda dapat menemukan kode demo di Repositori GitHub.

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

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

Rekap

Mari kita mulai dengan rangkuman cara kerja scroller paralaks.

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

Tentu saja, memindahkan elemen kembali ke luar ruangan juga akan membuatnya tampak lebih kecil, yang kita perbaiki dengan memperbesar kembali elemen tersebut. Kami menemukan perhitungan yang tepat saat kita membuat scroller paralaks, jadi saya tidak akan mengulangi semua detailnya.

Langkah 0: Apa yang ingin kita lakukan?

Scrollbar. Itulah yang akan kita bangun. Tetapi apakah Anda pernah berpikir tentang apa yang mereka lakukan? Saya tentu tidak tahu. Bilah gulir merupakan indikator dari berapa banyak konten tersedia yang saat ini terlihat dan berapa banyak progresnya Anda sebagai pembaca. Jika Anda menggulir ke bawah, {i>scrollbar<i} untuk menunjukkan bahwa Anda membuat kemajuan menjelang akhir proyek. Jika semua konten sesuai ke area pandang, scrollbar biasanya tersembunyi. Jika konten memiliki 2x tinggi area pandang, scrollbar mengisi 1⁄2 dari tinggi area pandang. Konten bernilai 3x tinggi area pandang menskalakan scrollbar ke 1⁄3 area pandang, dsb. Anda akan melihat polanya. Alih-alih menggulir, Anda juga dapat mengeklik dan menyeret {i>scrollbar<i} untuk situs lebih cepat. Itu adalah jumlah perilaku yang mengejutkan untuk orang yang tidak seperti itu. Ayo kita lawan satu per satu.

Langkah 1: Membalikkannya

Baiklah, kita dapat membuat elemen bergerak lebih lambat dari kecepatan scroll dengan CSS 3D seperti yang diuraikan dalam artikel {i>scrolling<i} paralaks. Dapatkah kita juga membalikkan arahnya? Ternyata kami bisa dan itulah jalan kami untuk membangun {i>frame-sempurna<i}, scrollbar khusus. Untuk memahami cara kerjanya, kita perlu membahas beberapa dasar 3D CSS terlebih dahulu.

Untuk mendapatkan proyeksi perspektif apa pun dalam arti matematis, Anda akan kemungkinan besar akan menggunakan koordinat homogen. Saya tidak akan membahas apa dan mengapa mereka bekerja secara rinci, tetapi bisa Anda pikirkan seperti koordinat 3D dengan koordinat tambahan keempat yang disebut w. Ini koordinat harus 1 kecuali jika Anda ingin mendapatkan distorsi perspektif. Rab tidak perlu khawatir tentang detail w karena kita tidak akan menggunakan nilai selain 1. Oleh karena itu, semua titik berasal dari vektor 4 dimensi [x, y, z, w=1] dan oleh karena itu matriks membutuhkan menjadi 4x4 juga.

Suatu kesempatan di mana Anda dapat melihat bahwa CSS menggunakan koordinat homogen di bawah adalah ketika Anda menentukan matriks 4 x 4 Anda sendiri dalam properti transformasi menggunakan Fungsi matrix3d(). matrix3d memerlukan 16 argumen (karena matriksnya adalah 4x4), yang menentukan satu kolom demi kolom. Kita bisa menggunakan fungsi ini untuk menentukan rotasi, terjemahan, dll. secara manual. Tapi hal itu juga memungkinkan kita mengotak-atik koordinat w itu.

Sebelum dapat menggunakan matrix3d(), kita memerlukan konteks 3D – karena tanpa Konteks 3D tidak akan terdistorsi perspektif dan tidak diperlukan koordinat homogen. Untuk membuat konteks 3D, kita membutuhkan penampung dengan perspective dan beberapa elemen di dalamnya yang dapat kita ubah dalam menciptakan ruang 3D. Sebagai contoh:

Potongan kode CSS yang mendistorsi div menggunakan elemen
    perspektif.

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

  • Mengubah 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 scroll ke bawah dengan 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 3D jarak yang tepat. Hal ini akan menghasilkan kedua hal, yaitu pembuatan hal-hal yang tampak lebih kecil mundur dan juga membuat mereka “bergerak lebih lambat” saat diterjemahkan. Jadi, jika sebuah elemen didorong ke belakang, terjemahan 400 px akan menyebabkan elemen bergerak hanya sejauh 300 px di layar.

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

Kotak kita ada di dalam penampung perspektif dengan nilai p untuk perspective , dan asumsikan container-nya dapat di-scroll dan di-scroll ke bawah oleh n piksel.

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

Matriks pertama adalah matriks perspektif, matriks kedua adalah {i>scroll<i} yang dihasilkan. Sebagai rangkuman: 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 turunkan ke bawah saat men-scroll ke bawah. Di sinilah kita dapat menggunakan trik: Membalik koordinat w dari sudut-sudut kotak kita. Jika koordinat w adalah -1, semua terjemahan akan berlaku ke arah yang berlawanan. Jadi bagaimana kita melakukan itu? Mesin CSS menangani konversi sudut dari kotak kita 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, ketika mesin CSS memiliki mengubah setiap sudut menjadi vektor bentuk [x,y,z,1], matriks akan mengonversinya menjadi [x,y,z,-1].

Empat kali empat matriks identitas dengan tanda minus satu di atas p di baris keempat
  kolom ketiga dikali empat kali empat matriks identitas dengan tanda minus n di kolom kedua
  baris keempat dikali empat kali empat matriks identitas dengan minus satu
  baris keempat kolom keempat dikali empat vektor dimensi x, y, z, 1 sama dengan empat
  dengan empat matriks identitas dengan minus satu di atas p di baris keempat kolom ketiga,
  dikurangi n di baris kedua kolom keempat dan dikurangi satu di baris keempat
  kolom keempat sama dengan vektor empat dimensi x, y plus n, z, dikurangi z lebih
  p dikurangi 1.

Saya mencantumkan langkah perantara untuk menunjukkan efek transformasi elemen kita yang dihasilkan. Jika Anda tidak terbiasa dengan matematika matriks, tidak apa-apa. Eureka di baris terakhir kita menambahkan offset scroll n ke y mengkoordinasikan alih-alih menguranginya. Elemen ini akan diterjemahkan ke bawah jika kita men-scroll ke bawah.

Namun, jika kita hanya memasukkan matriks ini ke dalam contoh, elemen tidak akan ditampilkan. Hal ini karena spesifikasi CSS mengharuskan verteks dengan w < 0 memblokir elemen agar tidak dirender. Dan karena z kita koordinat 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 untuk 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, box hadir kembali!

Langkah 2: Bergeraklah

Sekarang kotak kita ada di sana dan terlihat sama seperti sebelumnya tanpa transform. 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. Mari kita scroll container, bukan? Kita cukup menambahkan elemen pengatur jarak 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>

Dan 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. Itu yang sulit sedikit keluar. Sekarang kita perlu menata gayanya agar terlihat seperti {i>scrollbar<i} dan membuatnya sedikit lebih interaktif.

Scrollbar biasanya terdiri dari "jempol" dan "trek", sedangkan trek tidak selalu terlihat. Tinggi jempol berbanding lurus dengan seberapa besar konten dapat dilihat.

<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 ditutup ibu jari harus sama dengan rasio konten yang terlihat:

tinggi titik jempol di atas scrollerTinggi sama dengan tinggi scroller
  di atas tinggi scroller titik jika dan hanya jika tinggi titik jempol titik
  sama dengan tinggi scroller dikali tinggi scroller di atas scroller titik scroll
  tinggi.
<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 adalah tampak bagus, tapi 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 untuk men-scroll. Kita dapat memperbaiki ukuran dengan meningkatkan skalanya. Tapi seberapa banyak kita harus mendorong kembali dengan tepat? Ayo mulai matematika! Ini adalah terakhir kalinya, saya yang menjanjikan.

Informasi penting adalah bahwa kita ingin tepi bawah ibu jari untuk sejajar dengan tepi bawah elemen yang dapat di-scroll saat di-scroll sepenuhnya ke bawah. Dengan kata lain: Jika kita men-scroll scroller.scrollHeight - scroller.height piksel, kita ingin thumbnail diterjemahkan oleh scroller.height - thumb.height. Untuk setiap {i>pixel<i} scroller, kita ingin ibu jari kita memindahkan sepersekian piksel:

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

Itulah faktor penskalaan kami. Sekarang kita perlu mengonversi faktor penskalaan menjadi di sepanjang sumbu z, yang telah kita lakukan dalam scroll paralaks artikel. Menurut bagian yang relevan dalam spesifikasi: Faktor penskalaan sama dengan p/(p - z). Kita dapat menyelesaikan persamaan ini untuk z mencari tahu seberapa banyak kita perlu menerjemahkan jempol kita sepanjang sumbu z. Namun pertahankan diingat bahwa karena kesalahan koordinat {i>w<i}, kita perlu menerjemahkan sebuah -2px tambahan di sepanjang z. Perhatikan juga bahwa transformasi elemen diterapkan kanan ke kiri, yang berarti bahwa semua terjemahan sebelum matriks khusus kita tidak akan dibalik, tapi semua terjemahan setelah matriks khusus kita akan dibalik! Mari dapat menyusunnya!

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

Tersedia 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-seret, karena banyak pengguna yang terbiasa berinteraksi dengan scrollbar seperti itu. Supaya postingan blog ini tidak panjang, saya tidak akan menjelaskan detail untuk bagian tersebut. Lihat kode library untuk mengetahui detailnya jika ingin melihat cara melakukannya.

Bagaimana dengan iOS?

Ah, teman lama saya iOS Safari. Seperti halnya scroll paralaks, kita menemukan di sini. Karena kita men-scroll pada elemen, kita perlu menentukan -webkit-overflow-scrolling: touch, tetapi itu akan menyebabkan perataan 3D dan seluruh efek scroll berhenti berfungsi. Kita menyelesaikan masalah ini di scroller paralaks dengan mendeteksi iOS Safari dan mengandalkan position: sticky sebagai solusi, serta kami akan melakukan hal yang sama 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 pseudo non-standar). Jadi untuk menyembunyikannya, kita harus melakukan beberapa peretas (bebas matematika). Kita menggabungkan scroll dalam container dengan overflow-x: hidden, lalu buat elemen elemen scroll yang lebih lebar dari container. Scrollbar native browser sekarang tidak terlihat.

Fin

Dengan menggabungkan semuanya, sekarang kita dapat membuat frame kustom scrollbar – seperti yang ada di Demo kucing Nyan.

Jika Anda tidak dapat melihat kucing Nyan, Anda sedang 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-hal yang berada di luar layar. Kabar buruknya adalah kekejaman matriks membuat Chrome berpikir bahwa gif kucing Nyan sebenarnya ada di balik layar. Semoga masalah ini bisa segera diperbaiki.

Seperti itu. Itu adalah pekerjaan yang merepotkan. Saya memuji Anda karena telah membaca seluruh sesuatu. Ini adalah beberapa trik nyata agar cara ini berhasil dan mungkin jarang sepadan dengan usahanya, kecuali jika scrollbar yang disesuaikan adalah bagian penting dari pengalaman ini. Tapi senang mengetahui bahwa hal itu mungkin terjadi, bukan? Kenyataan bahwa sulit untuk melakukan scrollbar kustom menunjukkan bahwa ada pekerjaan yang harus dilakukan di sisi CSS. Tapi jangan khawatir! Di masa mendatang, Houdini AnimationWorklet akan membuat efek scroll-link {i>frame-sempurna<i} seperti ini yang jauh lebih mudah.