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

Niestandardowe paski przewijania są niezwykle rzadkie i najczęściej wynika to z faktu, że pasek przewijania to jeden z pozostałych elementów w internecie, których styl jest dość mało estetyczny (zajmuję się selektorem dat). Do tworzenia własnych stron możesz użyć JavaScriptu, ale jest to kosztowne, mało precyzyjne i późniejsze rozwiązanie. W tym artykule wykorzystamy niekonwencjonalne macierze CSS, aby utworzyć niestandardowy przewijak, który nie wymaga JavaScriptu podczas przewijania, tylko trochę kodu konfiguracji.

TL;DR

Nie zależy Ci na drobiazgach? Chcesz po prostu obejrzeć prezentację o kotach Nyan i pobrać bibliotekę? Kod wersji demonstracyjnej znajdziesz w naszym repozytorium GitHub.

LAM;WRA (tekst długi i matematyczny; mimo to odczytany)

Jakiś czas temu stworzyliśmy przewijak z paralaksą (czy wiesz, ten artykuł? Warto poświęcić na to czas!). Odpychanie elementów za pomocą przekształceń CSS 3D sprawia, że poruszają się one wolniej niż rzeczywiste przewijanie.

Podsumowanie

Zacznijmy od ogólnego opisu działania przewijania z paralaksą.

Jak pokazano w animacji, uzyskaliśmy efekt paralaksy, przesuwając elementy do tyłu w przestrzeni 3D wzdłuż osi Z. Przewinięcie dokumentu jest w rzeczywistości przesunięciem wzdłuż osi Y. Jeśli więc przewiniesz w dół o, np. 100 pikseli, każdy element zostanie przesunięty w górę o 100 pikseli. Dotyczy to wszystkich elementów, nawet tych znajdujących się „daleko od siebie”. Jednak ponieważ są one oddalone od kamery, obserwowany ruch na ekranie będzie krótszy niż 100 pikseli, co da oczekiwany efekt paralaksy.

Oczywiście przesunięcie elementu z powrotem w kosmos również spowoduje, że wyjdzie on mniejszy, co korygujemy, skalując z powrotem w górę. Podczas tworzenia przeglądarki z paralaksą obliczyliśmy dokładnie, jak to działa, dlatego nie będę powtarzać szczegółów.

Krok 0. Co chcemy zrobić?

Paski przewijania. To właśnie będziemy tworzyć. Ale czy naprawdę zastanawialiście się, do czego służą? Na pewno nie. Paski przewijania informują o tym, jak duża część dostępnych treści jest w danym momencie widoczna oraz jaki jest Twój postęp jako czytelnik. Pasek przewijania w dół wskazuje, że robisz postępy. Jeśli cała zawartość mieści się w widocznym obszarze, pasek przewijania jest zwykle ukryty. Jeśli treść ma 2 x wysokość widocznego obszaru, pasek przewijania wypełnia połowę wysokości widocznego obszaru. Zawartość 3 razy wysokości widocznego obszaru skaluje pasek przewijania do 1⁄3 jego powierzchni itd. Widać tu wzór. Zamiast przewijać stronę, możesz też kliknąć i przeciągnąć pasek przewijania, by szybciej poruszać się w witrynie. To zaskakujące zachowanie przy takim niepozornym elemencie. Toczymy walkę po jednej bitwie.

Krok 1. Umieszczenie ich na odwrót

Dzięki przekształceniom CSS 3D elementy mogą poruszać się wolniej niż przewijanie zgodnie z opisem w artykule dotyczącym przewijania paralaksy. Czy możemy też odwrócić kierunek? Okazuje się, że możemy to zrobić i w ten sposób utworzyć idealny pasek przewijania, który będzie idealnie pasował do kadru. Aby zrozumieć, jak to działa, musimy najpierw omówić podstawy CSS 3D.

Aby uzyskać dowolne odwzorowanie perspektywowe w sensie matematycznym, musisz użyć jednorodnych współrzędnych. Nie opowiadam szczegółowo, czym one są i dlaczego działają, ale można je porównać do współrzędnych 3D z dodatkową, czwartą współrzędną o nazwie w. Ta współrzędna powinna wynosić 1, chyba że chcesz uzyskać zniekształcenie perspektywy. Nie musimy przejmować się szczegółami parametru w, ponieważ nie użyjemy innej wartości niż 1. Tak więc wszystkie punkty są od tej pory wektorami 4-wymiarowymi [x, y, z, w=1], a tym samym macierze też muszą mieć rozmiar 4 x 4.

Gdy zauważysz w CSS jednorodnych współrzędnych, możesz np. zdefiniować własne macierze 4 x 4 we właściwości przekształcenia za pomocą funkcji matrix3d(). matrix3d przyjmuje 16 argumentów (ponieważ macierz to 4 x 4), wskazując jedną kolumnę po drugiej. Funkcja ta pozwala na ręczne określenie obrotów, tłumaczeń itp. Jednak poza tym umożliwia manipulację współrzędną W.

Zanim będziemy mogli użyć obiektu matrix3d(), potrzebujemy kontekstu 3D, ponieważ bez niego nie dałoby się zniekształcić perspektywy i nie byłoby potrzebne jednorodne współrzędne. Aby utworzyć kontekst 3D, potrzebujemy kontenera z elementem perspective i elementami, które można przekształcić w nowo utworzonej przestrzeni 3D. Przykład:

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

Elementy znajdujące się w kontenerze perspektywy są przetwarzane przez mechanizm CSS w ten sposób:

  • Zamień każdy wierzchołek elementu w jednorodne współrzędne [x,y,z,w] względem kontenera perspektywy.
  • Zastosuj wszystkie przekształcenia elementu jako macierze od prawej do lewej.
  • Jeśli element perspektywy można przewijać, zastosuj macierz przewijania.
  • Zastosuj macierz perspektywy.

Macierz przewijania to przesunięcie wzdłuż osi Y. Jeśli przewiniemy w dół o 400 pikseli, wszystkie elementy muszą zostać przesunięte w górę o 400 pikseli. Macierz perspektyw to macierz, która „ciągnie” punkty bliżej znikającego punktu tym dalej w przestrzeni 3D, w której się znajdują. Dzięki temu materiały stają się pomniejszone, gdy znajdują się dalej, i są „wolniejsze” podczas tłumaczenia. Jeśli więc element zostanie odsunięty do tyłu, przesunięcie go o 400 pikseli spowoduje przesunięcie go o tylko 300 pikseli na ekranie.

Jeśli chcesz poznać wszystkie szczegóły, przeczytaj spec modelu renderowania przekształconego CSS, ale na potrzeby tego artykułu uprościliśmy algorytm opisany powyżej.

Nasze pole znajduje się w kontenerze perspektywy, w którym atrybut perspective ma wartość p. Załóżmy, że kontener można przewijać i przewinąć w dół o n pikseli.

Macierz wielokąta razy macierz wielokrotnych przewinięcia macierzy równa się 4 na 4 macierz tożsamości elementu, z minus 1 na p w 4 wierszu, 3 kolumna razy cztery na cztery macierz tożsamości, z minusem n w drugim wierszu czwartej kolumny razy macierz przekształcenia elementu.

Pierwsza tablica to tablica perspektyw, a druga – tablica przewijania. Podsumowując: zadaniem macierzy przewijania jest ruch elementu w górę, gdy przewijamy stronę w dół. Dlatego jest to znak minus.

Dla paska przewijania potrzebny jest jednak przeciwieństwo – chcemy, by element przesunął się w dół, gdy przewijamy w dół. Tutaj możemy zastosować sztuczkę: Odwrócenie współrzędnych W na rogach pola. Jeśli współrzędna w to -1, wszystkie tłumaczenia mają zastosowanie w przeciwnym kierunku. Jak to zrobić? Mechanizm CSS przekształca rogi pola w jednorodne współrzędne i ustawia w na wartość 1. Czas, by firma matrix3d() zabłysnęła!

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

Ta macierz będzie negowała tylko w. Gdy wyszukiwarka CSS przekształci każdy róg w wektor o postaci [x,y,z,1], macierz przekonwertuje go na [x,y,z,-1].

macierz 4 na 4 macierzy tożsamości z minus 1 na p w 4 wierszu, 3 kolumna razy 4 po 4 macierz tożsamości, z minusem n w drugim wierszu, 4 kolumna, 4 mnożnik, z minus 1 w 4 wierszu, 4 mnożnik skalarny, 1 wektorem wymiarowym x, y, Z, 1 równa się 4 po 4 macierz tożsamości, 4 m 4 m 4, 4, 4 m 4, 4 m 4, 4, 4, 4, 4 w drugim, 4 wiersz, 4. kolumna, 4. kolumna

Podałem krok pośredni, aby pokazać efekt macierzy przekształcenia elementów. Jeśli nie czujesz się dobrze z matematyką macierzową, nie szkodzi. W czasie Eureki w ostatnim wierszu dodajemy przesunięcie n do współrzędnej y, zamiast odejmować. Jeśli przewiniesz w dół, element zostanie przesunięty w dół.

Jeśli jednak umieścisz tę macierz w przykładzie, element nie zostanie wyświetlony. Dzieje się tak, ponieważ specyfikacja CSS wymaga, aby każdy wierzchołek o wartości w < 0 blokuje renderowanie elementu. A ponieważ nasza współrzędna z wynosi obecnie 0, a p to 1, w wynosi -1.

Na szczęście możemy wybrać wartość z! Aby uzyskać 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);
}

Nasz skrzynia wróciła!

Krok 2. Zrób ruch

Otwarta skrzynia wygląda tak samo bez żadnych przekształceń. W tej chwili kontenera perspektywy nie można przewijać, więc nie możemy go zobaczyć. Wiemy, że po przewinięciu element obróci się w inny kierunek. Przewińmy kontener. Możemy dodać odstęp, który zajmuje 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>

A teraz przewiń pole! Czerwone pole przesuwa się w dół.

Krok 3. Dodaj rozmiar

Mamy element, który przesuwa się w dół, gdy strona przewija się w dół. To naprawdę trudne. Teraz musimy nadać mu styl, by wyglądał jak pasek przewijania, i sprawił, że będzie bardziej interaktywny.

Pasek przewijania składa się zwykle z kciuka i ścieżki, choć ścieżka nie jest zawsze widoczna. Wysokość kciuka jest wprost proporcjonalna do tego, jak duża część treści jest widoczna.

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

scrollerHeight to wysokość elementu przewijanego, a scroller.scrollHeight to wysokość treści przewijanej. scrollerHeight/scroller.scrollHeight to część treści, która jest widoczna. Proporcje między obszarem w pionie, którym objęte są kciuki, powinny być równy współczynnikowi proporcji widocznej treści:

styl kciuka wysokość kropki nad ScrollerHeight równa się wysokość przewijania nad elementem przewijania kropka i wysokość przewijania tylko wtedy, gdy styl kciuka i wysokość kropki równa się wysokość przewijania pomnożone przez wysokość przewijania nad elementem przewijania kropka kropka przewijania.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Rozmiar kciuka wygląda dobrze, ale porusza się za szybko. Tutaj możemy pobrać naszą technikę za pomocą przewijania z paralaksą. Jeśli przesuniesz element dalej, ruch będzie wolniejszy podczas przewijania. Możemy poprawić jego rozmiar, skalując go w górę. Ale o ile dokładnie musimy ją wycofać? Zgadzacie się – matematyka! Obiecuję, że to ostatni raz.

Co ważne, dolna krawędź kciuka ma się wyrównać z dolną krawędzią przewijanego elementu podczas przewijania do końca. Inaczej mówiąc: jeśli przewinęliśmy scroller.scrollHeight - scroller.height pikseli, nasz kciuk powinien zostać przetłumaczony przez scroller.height - thumb.height. Chcemy, by na każdym pikselu przewijania przesuwał się kciuk:

Rozłóż na czynniki wysokość punktu przewijania pomniejszony o wysokość kropki kciuka nad elementem przewijanym
  kropka przewijanie wysokość minus wysokość kropki przewijaka.

To nasz czynnik skalowania. Teraz musimy przekonwertować współczynnik skalowania na przesunięcie wzdłuż osi Z, tak jak w artykule dotyczącym przewijania paralaksy. Zgodnie z odpowiednią sekcją specyfikacji: współczynnik skalowania jest równy p/(p − z). Możemy rozwiązać równanie z, aby obliczyć, jak bardzo trzeba przesunąć kciuk wzdłuż osi Z. Pamiętaj jednak, że ze względu na tę koordynację musimy przetłumaczyć dodatkowe -2px poza z. Pamiętaj też, że przekształcenia elementu są stosowane od prawej do lewej. Oznacza to, że nie odwrócone zostaną wszystkie tłumaczenia przed naszą specjalną macierzem. Zmienią się natomiast wszystkie tłumaczenia po specjalnej macierzy. Skodujmy to.

<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! To po prostu element DOM, który możemy dowolnie stylizować. Jeśli chodzi o ułatwienia dostępu, trzeba zadbać o to, aby kciuk reagują na kliknięcie i przeciągnięcie – wielu użytkowników jest przyzwyczajonych do interakcji z paskiem przewijania w ten sposób. Nie zamierzam jednak opisywać szczegółów tej części, bo ten post nie będzie jeszcze dłuższy. Aby dowiedzieć się, jak to działa, zajrzyj do kodu biblioteki.

A co z iOS?

Oto moja dawna znajoma Safari na iOS. Podobnie jak przy przewijaniu paralaksy, mamy tutaj jakiś problem. Przewijamy element, więc musimy określić właściwość -webkit-overflow-scrolling: touch, ale powoduje to wygładzanie 3D i całkowity efekt przewijania. Problem ten rozwiązaliśmy w przypadku przewijania z paralaksą, wykrywając iOS Safari i ostawiając obejście position: sticky. Robimy dokładnie to samo. Przeczytaj artykuł o paralaksie, aby odświeżyć wspomnienie.

A co z paskiem przewijania w przeglądarce?

W niektórych systemach będziemy musieli dostrzegać stały, natywny pasek przewijania. Do tej pory paska przewijania nie można było ukryć (oprócz niestandardowego pseudoselektora). Aby to ukryć, musimy skorzystać z hakerów (bez matematyki). Element przewijany pakujemy do kontenera z atrybutem overflow-x: hidden i sprawiamy, że element przewijany jest szerszy niż kontener. Natywny pasek przewijania w przeglądarce nie jest już widoczny.

Koniec

Po połączeniu wszystkich elementów możemy teraz utworzyć niestandardowy pasek przewijania, który będzie idealnie pasować do ramki, jak ten w prezentacji o kotach Nyan.

Jeśli nie widzisz kota Nyan, podczas tworzenia prezentacji wystąpił błąd, który wykryliśmy i zgłosiliśmy (kliknij kciuk, aby wyświetlić kota Nyan). Chrome doskonale unika niepotrzebnych czynności, np. malowania czy animowania elementów, które nie znajdują się na ekranie. Zła wiadomość jest taka, że nasze macierzowe żarty sprawiają, że Chrome uważa, że GIF-y z kotem Nyan nie wyświetlają się na ekranie. Mamy nadzieję, że wkrótce zostanie on rozwiązany.

I o to chodzi. To wymagało dużo pracy. Dziękuję za przeczytanie całego materiału. Korzystanie z tej funkcji jest bardzo trudne i prawdopodobnie rzadko się opłaci – z wyjątkiem sytuacji, gdy dostosowany pasek przewijania jest kluczowym elementem interfejsu. Dobrze jednak wiedzieć, że jest to możliwe, nie? Fakt, że utworzenie niestandardowego paska przewijania jest niełatwy, świadczy o tym, że wiele do zrobienia jest do zrobienia po stronie CSS. Ale nie martw się! W przyszłości narzędzie AnimationWorklet firmy Houdini znacznie ułatwi takie efekty związane z przewijaniem, które idealnie pasują do ramki.