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ą wierność i może działać wolno. W tym artykule użyjemy niekonwencjonalnych 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 naszym 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ć. Wypchnięcie elementów do tyłu za pomocą transformacji 3D w CSS spowodowało, że elementy poruszały 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 spowoduje, że będzie on mniejszy, co skorygujemy, powiększając go. Dokładne obliczenia zostały wykonane podczas tworzenia przewijania 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ą? Nie, nie zrobiłem tego. Suwaki są wskaźnikiem tego, ile z dostępnych treści jest obecnie widocznych, oraz jak duży postęp w czytaniu udało Ci się osiągnąć. Jeśli przewiniesz w dół, suwak też się przesunie, 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 pasek przewijania jest w 1/3 szerokości widocznego obszaru itd., czyli w ogóle widać tu 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 jednej bitwie naraz.

Krok 1. Włączanie biegu wstecznego

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 paralaksy. 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 rozumieniu 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 dotyczącymi 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), podając kolejno po jednej kolumnie. Dzięki tej funkcji możemy ręcznie określać obroty, przesunięcia itp., ale pozwala nam 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 w kodzie CSS.

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 perspektywy.
  • 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 macierz, która „przyciąga” punkty do punktu zbiegu, im bardziej oddalają się 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 do tyłu, 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 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. kolumny × macierz tożsamości 4 × 4 z wartością –n w 2. wierszu 4. kolumny × macierz transformacji elementu.

Pierwsza matryca to matryca perspektywy, a druga to matryca 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ócenie współrzędnej w narożnikó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 niejednorodne i ustawia wartość w równaniu 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 mechanizm 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 poziomej 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 ma obecnie wartość 0, a p – 1, w będzie równa –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 nasza pudełko wróciło!

Krok 2. Spraw, aby się poruszał

Teraz nasza skrzynka 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ę przesuwał w drugą stronę. Zróbmy tak, aby kontener się przewijał. Możemy po prostu dodać element spacerowy, 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>

Teraz przewiń 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.

Scrollbarka składa się zwykle z elementu „palca” 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 przeniesiemy element dalej w tył, będzie się on wolniej przesuwał podczas przewijania. Możemy poprawić 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 kciuk 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 przesunięcie 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ąć dodatkowy -2px wzdłuż z. Pamiętaj też, że transformacje elementu są stosowane od prawej do lewej, co oznacza, że wszystkie przesunięcia przed naszą specjalną macierzą nie będą odwrócone, ale wszystkie przesunięcia po naszej specjalnej matrycy będą odwrócone. Zacznijmy kodować.

<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 zakresie ułatwień dostępu ważne jest, aby suwak reagował na klikanie 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ć szczegółów tej części. Aby dowiedzieć się, jak to zrobić, zapoznaj się ze zapisem kodu biblioteki.

A co z iOS?

Ach, moja stara znajoma, przeglądarka Safari na iOS. Podobnie jak w przypadku przewijania z efektem paralaksy, tutaj też napotykamy 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 (niematematycznych) 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 tych elementów możemy stworzyć pasek przewijania, który idealnie pasuje do ramki, tak jak w naszym przykładzie z Nian Cat.

Jeśli nie widzisz Nyan cat, oznacza to, że podczas tworzenia tej wersji demonstracyjnej wystąpił problem, który został przez nas znaleziony i zgłoszony (kliknij miniaturę, 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ć.

To wszystko. 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 jeszcze sporo pracy wykonać 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.