CSS Deep-Dive – matrix3d() – niestandardowy pasek przewijania idealnie dopasowany do ramki

Niestandardowe suwaki są niezwykle rzadkie, głównie dlatego, że są jednymi z nielicznych elementów w internecie, których nie da się sformatować (patrz: selektor daty). Możesz użyć JavaScriptu, aby utworzyć własną, ale jest to drogie, ma niską jakość i może działać wolno. W tym artykule użyjemy niestandardowych macierzy CSS, aby utworzyć niestandardowy scroller, który nie wymaga żadnego kodu JavaScript podczas przewijania, tylko trochę kodu konfiguracyjnego.

TL;DR

Nie przejmujesz się szczegółami? Chcesz tylko obejrzeć prezentację Nyan Cat i pobrać bibliotekę? Kod demo znajdziesz w repozytorium GitHub.

LAM;WRA (długi i matematyczny; i tak przeczytasz)

Jakiś czas temu stworzyliśmy scroller z efektem paralaksy (czy czytałeś/czytałaś ten artykuł? To naprawdę świetne rozwiązanie, które warto wypróbować. Gdy elementy są przesuwane za pomocą trójwymiarowych transformacji CSS, poruszają się wolniej niż przy rzeczywistej prędkości przewijania.

Podsumowanie

Zacznijmy od podsumowania działania scrollera paralaksy.

Jak widać na animacji, efekt paralaksy uzyskaliśmy, przesuwając elementy „do tyłu” w przestrzeni 3D wzdłuż osi Z. Przewijanie dokumentu to w istocie przesunięcie wzdłuż osi Y. Jeśli przewiniesz w dół o 100 pikseli, każdy element zostanie przesunięty w górę o 100 pikseli. Dotyczy to wszystkich elementów, nawet tych, które są „dalej”. Ponieważ są one dalej od kamery, ich obserwowane na ekranie ruch będzie mniejszy niż 100 pikseli, co da pożądany efekt paralaksy.

Oczywiście przesunięcie elementu w przestrzeni powoduje, że wydaje się on mniejszy, co korygujemy przez zwiększenie jego rozmiaru. Dokładne obliczenia zostały przeprowadzone podczas tworzenia przewijacza paralaksy, więc nie będę powtarzać wszystkich szczegółów.

Krok 0. Co chcemy zrobić?

paski przewijania; To właśnie zamierzamy stworzyć. Czy zastanawiałeś/się kiedyś, co one robią? Ja na pewno nie. Paski przewijania wskazują, ile z dostępnych treści jest obecnie widocznych oraz jak duży postęp w czytaniu udało Ci się osiągnąć. Gdy przewijasz w dół, pasek przewijania również się przesuwa, aby pokazać, że zbliżasz się do końca. Jeśli wszystkie treści mieszczą się w widocznym obszarze, suwak jest zwykle ukryty. Jeśli wysokość treści jest 2 razy większa od wysokości widocznego obszaru, suwak wypełnia połowę wysokości widocznego obszaru. Treści o 3 razy większej wysokości niż widoczny obszar powodują, że suwak jest przeskalowany do 1/3 widocznego obszaru itd., czyli widzisz pewien schemat. Zamiast przewijać, możesz też kliknąć i przeciągnąć suwak, aby szybciej przewijać stronę. To zaskakujące zachowanie dla tak niepozornego elementu. Walczmy w jednym miejscu.

Krok 1. Wsteczny

Możemy sprawić, aby elementy poruszały się wolniej niż prędkość przewijania, za pomocą transformacji 3D w CSS, jak opisano w artykule o przewijaniu paralaktycznym. Czy możemy też odwrócić kierunek? Okazało się, że możemy, i właśnie tak powstała niestandardowa suwak do przewijania, która idealnie pasuje do ramki. Aby zrozumieć, jak to działa, musimy najpierw omówić kilka podstawowych zagadnień dotyczących CSS 3D.

Aby uzyskać dowolny rodzaj perspektywy w ujęciu matematycznym, prawdopodobnie użyjesz współrzędnych jednorodnych. Nie będę wchodzić w szczegóły, czym są i dlaczego działają, ale możesz je sobie wyobrazić jako współrzędne 3D z dodatkową, czwartą współrzędną o nazwie w. Ta współrzędna powinna mieć wartość 1, chyba że chcesz uzyskać zniekształcenie perspektywy. Nie musisz się martwić szczegółami parametru w, ponieważ nie użyjemy żadnej wartości innej niż 1. Dlatego od teraz wszystkie punkty są 4-wymiarowymi wektorami [x, y, z, w=1], a więc macierze też muszą mieć wymiary 4 × 4.

Jednym z przypadków, w którym widać, że CSS używa współrzędnych niejednorodnych, jest zdefiniowanie własnych macierzy 4 x 4 w właściwości transform za pomocą funkcji matrix3d(). Funkcja matrix3d przyjmuje 16 argumentów (ponieważ macierz ma wymiary 4 × 4), które określają kolejno po jednej kolumnie. Możemy więc użyć tej funkcji, aby ręcznie określić obroty, przesunięcia itp., ale pozwala ona też manipulować współrzędną w.

Zanim użyjemy funkcji matrix3d(), potrzebujemy kontekstu 3D, ponieważ bez niego nie byłoby żadnego zniekształcenia perspektywy ani potrzeby stosowania współrzędnych niejednorodnych. Aby utworzyć kontekst 3D, potrzebujemy kontenera z perspective i elementami, które możemy przekształcić w nowo utworzonej przestrzeni 3D. Na przykład:

Fragment kodu CSS, który zniekształca element div za pomocą atrybutu perspektywy.

Elementy w kontenerze perspektywy są przetwarzane przez mechanizm CSS w ten sposób:

  • Przekształcanie każdego narożnika (wierzchołka) elementu w współrzędne jednorodne [x,y,z,w] względem kontenera perspektywi.
  • Zastosuj wszystkie transformacje elementu jako macierze od prawa do lewej.
  • Jeśli element perspektywy można przewijać, zastosuj matrycę przewijania.
  • Zastosuj macierz perspektywy.

Macierz przewijania to przesunięcie wzdłuż osi y. Jeśli przewiniesz się w dół o 400 pikseli, wszystkie elementy trzeba przesunąć w górę o 400 pikseli. Macierz perspektywy to matryca, która „przyciąga” punkty do punktu zbiegu, im dalej są one w przestrzeni 3D. Dzięki temu obiekty są mniejsze, gdy znajdują się dalej, a także „poruszają się wolniej”, gdy są przenoszone. Jeśli więc element jest przesunięty w tył, przesunięcie o 400 pikseli spowoduje przesunięcie elementu tylko o 300 pikseli na ekranie.

Jeśli chcesz poznać wszystkie szczegóły, przeczytaj specyfikację modelu renderowania transformacji CSS. W tym artykule uprościłem powyższy algorytm.

Nasz element znajduje się w perspektywnym kontenerze o wartości p dla atrybutu perspective. Załóżmy, że kontener można przewijać i jest przewinięty w dół o n pikseli.

Macierz perspektywy × macierz przewijania × macierz transformacji elementu = macierz tożsamości 4 × 4 z wartością –1/p w 4. wierszu × 3. kolumnie macierz tożsamości 4 × 4 z wartością –n w 2. wierszu × 4. kolumnie macierz transformacji elementu.

Pierwsza to macierz perspektywy, a druga to macierz przewijania. Podsumujmy: zadaniem macierzy przewijania jest przesuwanie elementu w górę, gdy przewijamy w dół, stąd znak minus.

W przypadku suwaka chcemy jednak uzyskać efekt odwrotny – chcemy, aby element schodził, gdy przewijamy w dół. Tutaj możemy użyć sztuczki: odwrócić współrzędną w wierzchołków naszego pudełka. Jeśli współrzędna w ma wartość -1, wszystkie przesunięcia będą miały odwrotny kierunek. Jak to zrobić? Silnik CSS zajmuje się konwersją wierzchołków naszego prostokąta na współrzędne jednorodne i ustawia wartość w na 1. Nadszedł czas, aby matrix3d() zabłysła.

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

Ta matryca będzie tylko negować w. Gdy silnik CSS przekształci każdy wierzchołek w wektora o formie [x,y,z,1], macierz przekształci go w [x,y,z,-1].

macierz jednostkowa 4 × 4 z minus 1/p w 4. wierszu 3. kolumnie razy macierz jednostkowa 4 × 4 z minus n w 2. wierszu 4. kolumnie razy macierz jednostkowa 4 × 4 z minus 1 w 4. wierszu 4. kolumnie razy wektor 4-wymiarowy x, y, z, 1 jest równa macierzy jednostkowej 4 × 4 z minus 1/p w 4. wierszu 3. kolumnie, minus n w 2. wierszu 4. kolumnie i minus 1 w 4. wierszu 4. kolumnie jest równa wektorowi 4-wymiarowemu x, y + n, z, minus z/p minus 1.

Podałem pośredni krok, aby pokazać efekt transformacji elementu. matrix. Jeśli nie czujesz się pewnie w matematyce macierzy, to nic nie szkodzi. W ostatniej linii zamiast odejmowania do współrzędnej y dodajemy przesunięcie osi przesunięcia n. Element zostanie przeniesiony w dół, jeśli przewiniesz w dół.

Jeśli jednak umieścimy tę matrycę w przykładzie, element nie będzie się wyświetlał. Wynika to z tego, że specyfikacja CSS wymaga, aby każdy wierzchołek z w < 0 blokował renderowanie elementu. Ponieważ współrzędna z jest obecnie równa 0, a p równa się 1, w będzie równe –1.

Na szczęście możemy wybrać wartość z. Aby mieć pewność, że w=1, musimy ustawić z = -2.

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

I oto nasz box wrócił!

Krok 2. Porusz obiekt

Teraz nasza skrzynka jest widoczna i wygląda tak samo jak bez żadnych przekształceń. Obecnie kontener perspektywy nie jest przewijany, więc nie możemy go zobaczyć, ale wiemy, że podczas przewijania element będzie się poruszał w drugim kierunku. Zróbmy tak, aby kontener się przewijał. Możemy po prostu dodać element spacerowy, który zajmie miejsce:

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

Teraz przesuń pole. Czerwone pole przesuwa się w dół.

Krok 3. Określ rozmiar

Mamy element, który przesuwa się w dół, gdy przewijasz stronę w dół. To naprawdę trudna część. Teraz musimy nadać mu styl, aby wyglądał jak suwak, i uczynić go nieco bardziej interaktywnym.

Suwak składa się zwykle z elementu „suwaka” i „ścieżki”, przy czym ścieżka nie jest zawsze widoczna. Wysokość miniatury jest wprost proporcjonalna do tego, ile treści jest widocznych.

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

scrollerHeight to wysokość elementu, który można przewijać, a scroller.scrollHeight to łączna wysokość treści, które można przewijać. scrollerHeight/scroller.scrollHeight to ułamek treści, który jest widoczny. Stosunek pionowej przestrzeni, którą pokrywa miniatura, powinien być równy stosunkowi widocznych treści:

Wysokość kropki w stylu kropki w przycisku na tle scrollerHeight równa jest wysokości scrollerHeight
  na tle scroller dot scroll height, jeśli i tylko jeśli wysokość kropki w stylu kropki w przycisku na tle
  jest równa wysokości scrollerHeight razy scroller height na tle scroller dot scroll
  height.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Rozmiar miniatury wygląda dobrze, ale przesuwa się zbyt szybko. Tutaj możemy wykorzystać technikę z przewijacza paralaksy. Jeśli przesuniemy element dalej w tył, będzie się on wolniej przesuwał podczas przewijania. Możemy skorygować rozmiar, zwiększając go. Ale jak daleko powinniśmy przesunąć ten termin? Zgadliście – czas na odrobinę matematyki. To ostatni raz, obiecuję.

Najważniejsze jest to, aby dolna krawędź miniatury była wyrównana z dolną krawędzią elementu, który można przewijać, gdy jest on całkowicie przewinięty w dół. Inaczej mówiąc: jeśli przewinęliśmy scroller.scrollHeight - scroller.height pikseli, chcemy, aby palec przesunął się o scroller.height - thumb.height. W przypadku każdego piksela kółka przewijania chcemy, aby kciuk przesuwał się o ułamek piksela:

Współczynnik jest równy wysokości kropki w scrollerze pomniejszonym o wysokość miniatury kropki podzieloną przez wysokość scrollera pomniejszoną o wysokość kropki w scrollerze.

To jest nasz współczynnik skalowania. Teraz musimy przekształcić współczynnik skalowania w translację wzdłuż osi z, co już zrobiliśmy w artykule o przewijaniu paralaksy. Zgodnie z odpowiednią sekcją w specyfikacji: współczynnik skalowania jest równy p/(p – z). Możemy rozwiązać to równanie w przypadku z, aby dowiedzieć się, o ile musimy przesunąć kciuk wzdłuż osi z. Pamiętaj jednak, że ze względu na nasze sztuczki z współrzędną w musimy przesunąć dodatkowe -2px wzdłuż z. Pamiętaj też, że transformacje elementu są stosowane od prawej do lewej, co oznacza, że wszystkie translacje przed naszą specjalną macierzą nie będą odwrócone, ale wszystkie translacje po naszej specjalnej matrycy będą odwrócone. Zapisz to w kodzie.

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

Mamy pasek przewijania. Jest to po prostu element DOM, którego styl możemy dowolnie dostosować. W celu ułatwienia dostępu warto zadbać o to, aby suwak reagował na kliknięcie i przeciąganie, ponieważ wielu użytkowników w ten sposób korzysta z suwaka. Aby nie wydłużać tego posta, nie będę omawiać tej kwestii. Aby dowiedzieć się, jak to zrobić, zapoznaj się ze z kodem biblioteki.

A co z iOS?

Ah, moja stara znajoma, przeglądarka Safari na iOS. Podobnie jak w przypadku przewijania z efektem paralaksy, tutaj też mamy problem. Ponieważ przewijamy element, musimy określić -webkit-overflow-scrolling: touch, ale powoduje to spłaszczenie 3D i cały efekt przewijania przestaje działać. Rozwiązaliśmy ten problem w przypadku przewijania paralaksy, wykrywając iOS Safari i korzystając z funkcji position: sticky jako rozwiązania tymczasowego. Zrobimy dokładnie to samo w tym przypadku. Aby odświeżyć swoją wiedzę, przeczytaj artykuł o paralaksie.

A pasek przewijania w przeglądarce?

W niektórych systemach będziemy musieli korzystać z trwałego, natywnego paska przewijania. Do tej pory paska przewijania nie można było ukryć (z wyjątkiem niestandardowego pseudoselektora). Aby go ukryć, musimy uciec się do pewnych (bezmatematycznych) sztuczek. Element przewijania umieszczamy w kontenerze z wartością overflow-x: hidden i robimy go szerszym niż kontener. Natywna suwak przeglądarki jest teraz niewidoczny.

Fin

Po połączeniu wszystkich elementów możemy teraz utworzyć pasek przewijania, który idealnie pasuje do ramki – tak jak w naszym przykładzie z kota Nyan.

Jeśli nie widzisz Nyan cat, oznacza to, że podczas tworzenia tej wersji demonstracyjnej wystąpił problem, który znaleźliśmy i oznaczyliśmy (kliknij kciuk, aby wyświetlić Nyan cat). Chrome bardzo dobrze unika niepotrzebnej pracy, takiej jak rysowanie lub animowanie elementów, które znajdują się poza ekranem. Złe wieści są takie, że nasze sztuczki z Matriksem sprawiają, że Chrome myśli, że obrazek z kotem Nyan jest poza ekranem. Mamy nadzieję, że wkrótce uda się to naprawić.

I o to chodzi. To było bardzo dużo pracy. Gratuluję przeczytania całego tekstu. Jest to prawdziwa sztuczka, aby to zadziałało, i prawdopodobnie rzadko jest tego warte, chyba że dostosowany suwak jest istotną częścią interfejsu. Dobrze wiedzieć, że to możliwe, prawda? Fakt, że stworzenie niestandardowego paska przewijania jest tak trudne, pokazuje, że należy coś poprawić po stronie usługi porównywania cen. Nie martw się. W przyszłości AnimationWorklet w Houdini znacznie ułatwi tworzenie efektów związanych z przewijaniem, które będą idealnie dopasowane do poszczególnych klatek.