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

Niestandardowe paski przewijania są bardzo rzadkie. Najczęściej wynika z tego, że są one jednym z pozostałych elementów internetu, które nie mają charakteru (patrzę na Ciebie – oto selektor daty). Możesz utworzyć własny kod za pomocą JavaScriptu, ale jest to kosztowny, mało dokładny i może działać z opóźnieniem. W tym artykule wykorzystamy niektóre niekonwencjonalne macierze CSS do stworzenia niestandardowego elementu przewijania, który podczas przewijania nie wymaga JavaScriptu, a jedynie trochę kodu konfiguracji.

TL;DR

Nie interesują Cię drobiazgi? Chcesz po prostu obejrzeć prezentację o kotach Nyan i pobrać bibliotekę? Kod wersji demonstracyjnej znajdziesz w repozytorium GitHub.

LAM;WRA (długie i matematyczne – można przeczytać mimo to)

Jakiś czas temu stworzyliśmy mechanizm przewijania z paralaksą (czy znasz ten artykuł? To naprawdę dobre, warte Twojego czasu). Odpychanie elementów za pomocą przekształceń CSS 3D sprawia, że przesuwają się one wolniej niż nasza rzeczywista szybkość przewijania.

Podsumowanie

Zacznijmy od podsumowania działania funkcji przewijania paralaksy.

Jak pokazano na animacji, uzyskaliśmy efekt paralaksy, przesuwając elementy do tyłu w przestrzeni 3D wzdłuż osi Z. Przewijanie dokumentu to tłumaczenie 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, które są bardziej oddalone od aparatu. Ponieważ są one bardziej oddalone od kamery, zaobserwowany ruch na ekranie będzie mniejszy niż 100 pikseli, co pozwoli uzyskać pożądany efekt paralaksy.

Oczywiście przesunięcie elementu w przestrzeń również spowoduje, że będzie się on wydawać mniejszy. To poprawimy, skalując go z powrotem w górę. Dokładne obliczenia zostały określone podczas tworzenia funkcji przewijania paralaksy, więc nie będę powtarzać wszystkich szczegółów.

Krok 0. Co chcesz zrobić?

Paski przewijania. To właśnie zamierzamy stworzyć. Ale czy naprawdę kiedyś myśleliście nad tym, co one robią? Ja nie. Paski przewijania wskazują, jaką część dostępnej treści są w danej chwili widoczne i jaki jest Twój postęp jako czytelnik. Jeśli przewiniesz w dół, pasek przewijania wskazuje, że idziesz do końca. Jeśli cała zawartość mieści się w widocznym obszarze, pasek przewijania jest zwykle ukryty. Jeśli treść jest 2 razy większa niż widoczny obszar, pasek przewijania wypełnia 1⁄2 wysokości widocznego obszaru. Treści warte 3 razy wysokość widocznego obszaru skalują pasek przewijania do 1⁄3 widocznego obszaru itd. Widać tutaj wzorzec. Zamiast przewijać, możesz też kliknąć i przeciągnąć pasek przewijania, aby szybciej poruszać się po stronie. Jak na taki niepozorny element, to co zaskakuje. Zagrajmy po jednej bitwie.

Krok 1. Odwrotność

Dzięki przekształceniom 3D CSS zgodnie z opisem w artykule o przewijaniu z paralaksą możemy sprawić, że elementy będą przesuwały się wolniej niż przewijanie. Czy możemy też odwrócić kierunek? Okazuje się, że możemy to zrobić – w ten sposób stworzyliśmy idealny niestandardowy pasek przewijania. Aby zrozumieć, jak to działa, musimy na początku poznać kilka podstawowych elementów CSS 3D.

Aby uzyskać jakiekolwiek odwzorowanie perspektywy w sensie matematycznym, najczęściej trzeba użyć jednorodnych współrzędnych. Nie wyjaśniam szczegółowo, czym są i dlaczego działają, ale mogą przypominać 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 musimy przejmować się szczegółami parametru w, ponieważ nie będziemy używać żadnej innej wartości niż 1. Dlatego wszystkie punkty pochodzą od teraz na 4-wymiarowe wektory [x, y, z, w=1] i dlatego macierze również muszą mieć wymiary 4 x 4.

Jedną z sytuacji, w której CSS jest stosowanie jednorodnych współrzędnych, jest zdefiniowanie własnych macierzy 4 x 4 we właściwości przekształcenia za pomocą funkcji matrix3d(). Funkcja matrix3d przyjmuje 16 argumentów (ponieważ macierz to 4 x 4), określając jedną kolumnę po drugiej. Za pomocą tej funkcji możemy więc ręcznie określić obroty, tłumaczenia itd. Pozwala nam to jednak pobawić się we współrzędnej w.

Zanim użyjemy funkcji matrix3d(), potrzebujemy kontekstu 3D, ponieważ bez niego nie byłoby żadnych zniekształceń perspektyw i nie potrzebowałyby one jednakowych współrzędnych. Aby utworzyć kontekst 3D, potrzebujemy kontenera z elementem perspective i kilkoma elementami, które możemy przekształcić w nowo utworzoną przestrzeń 3D. Na przykład:

Fragment kodu CSS zniekształcający element div za pomocą atrybutu perspektywa CSS.

Elementy wewnątrz kontenera perspektywy są przetwarzane przez wyszukiwarkę CSS w ten sposób:

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

Macierz przewijania to przesunięcie wzdłuż osi Y. Jeśli przewiniemy w dół o 400 pikseli, wszystkie elementy trzeba przesunąć w górę o 400 pikseli. Macierz perspektywa to macierz, która „przyciąga” punkty bliżej punktu znikającego w dalszym ciągu w przestrzeni 3D. Efektem jest zarówno pomniejszenie elementów, które się znajdują, jak i „wolniejsze poruszanie się” podczas tłumaczenia. Jeśli element jest przesunięty do tyłu, przesunięcie o 400 pikseli spowoduje przesunięcie go na ekranie o tylko 300 pikseli.

Jeśli chcesz poznać wszystkie szczegóły, zapoznaj się ze spec modelu renderowania z transformacją CSS, ale na potrzeby tego artykułu uprościliśmy powyższy algorytm.

Nasze pole znajduje się wewnątrz kontenera perspektywy z wartością p dla atrybutu perspective. Załóżmy, że kontener można przewijać o n pikseli.

Macierz perspektywa razy macierz przewijania pomnożona przez macierz przewijania elementu równa się macierze transformacji elementu równa 4 na 4, przy czym w 4 rzędzie w trzeciej kolumnie razy minus 1 nad p

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

Na pasku przewijania chcemy jednak widzieć przeciwieństwem – chcemy, by element przesunął się w dół, gdy przewijamy w dół. Oto nasz trik: odwrócenie współrzędnej W rogów prostokąta. Jeśli współrzędna W ma wartość -1, wszystkie tłumaczenia zaczynają działać w odwrotnym kierunku. Jak to zrobić? Mechanizm CSS konwertuje rogi pola na jednorodne współrzędne i ustawia w polu wartość 1. matrix3d() – czas zabłysnąć!

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

Ta tablica robi nic innego niż negowanie w. Gdy mechanizm CSS przekształci każdy róg w wektor o postaci [x,y,z,1], tablica przekonwertuje go na [x,y,z,-1].

Macierz tożsamości cztery na 4 z minus 1 nad p w 3 wierszu, 4 na 4 matryca tożsamości z minusem n w 4 rzędzie w czwartej kolumnie razy cztery na cztery: macierz tożsamości, w 4 rzędzie, w czwartej kolumnie razy 4 wymiary wektora tożsamości x, y, z, 1 równa się 4 na 4: matryca tożsamości w 4 wierszu minus 1 na 4 minus 1 na 4 w trzeciej kolumnie minus 4 na p w 4 wierszu minus 1 na p w 4 wierszu minus 1 na p w 4 wierszu minus 1 na p w 4 wierszu minus 1 na p w 4 wierszu.

Wskazałam krok pośredni, aby pokazać efekt naszej macierzy przekształcenia elementów. Jeśli nie czujesz się dobrze z matrycami matrycowymi, nic nie szkodzi. W ostatnim wierszu w ostatnim wierszu dodajmy przesunięcie n przewinięcia do naszej współrzędnej y, zamiast je odejmować. Element zostanie przesunięty w dół, gdy przewiniesz w dół.

Jeśli jednak umieścisz tę macierz w naszym przykładzie, element nie zostanie wyświetlony. Dzieje się tak, ponieważ specyfikacja CSS wymaga, aby każdy wierzchołek z w < 0 uniemożliwiał renderowanie elementu. Ponieważ nasza współrzędna Z wynosi obecnie 0, a p to 1, W będzie wynosić -1.

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

Oto nasze pudełko z powrotem!

Krok 2. Daj z siebie wszystko

Nasze pudełko jest w nim i wygląda tak samo jak bez żadnych zmian. Obecnie kontenera perspektywy nie można przewijać i nie widzimy go, ale wiemy, że po przewinięciu element będzie przesuwał się w innym kierunku. Może zaczniemy przewijać kontener? Możemy dodać element odstępu, 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. Dobierz odpowiedni rozmiar

Widać element, który przesuwa się w dół, gdy strona przewija się w dół. To bardzo trudny problem. Teraz musimy nadać jej styl, by wyglądał jak pasek przewijania i był bardziej interaktywny.

Pasek przewijania zazwyczaj składa się z „kciuka” i „ścieżki”, ale ś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, który można przewijać, a scroller.scrollHeight to łączna wysokość treści, które można przewijać. scrollerHeight/scroller.scrollHeight to część widocznej treści. Odsunięcie miejsca w pionie, które ma osłony na kciuki, powinno być równe proporcjom między widoczną częścią strony:

styl punkt
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Kciuk ma wygląd dobrze, ale porusza się za szybko. W tym miejscu możemy chwycić technikę z przewijania z paralaksą. Jeśli przesuniemy element dalej do tyłu, ruch będzie się wolniej poruszać podczas przewijania. Możemy go skorygować, skalując go w górę. Ale o ile mamy ją odsunąć? Czas na matematykę! To już ostatni raz.

Ważne jest, aby dolna krawędź kciuka po przewinięciu do końca pokrywała się z dolną krawędzią elementu, który można przewijać. Innymi słowy: jeśli przewinęliśmy scroller.scrollHeight - scroller.height piks., chcemy, by kciuk został przetłumaczony przez scroller.height - thumb.height. Na każdy piksel przewijania chcemy, by nasz kciuk przemieścił ułamek tego piksela:

Współczynnik jest równa wysokość punktu przewijania minus wysokość punktu przewijania nad elementem przewijania
  względem wysokości kropki przewijania minus wysokość punktu przewijania.

To nasz współczynnik skalowania. Teraz trzeba przekształcić współczynnik skalowania na przesunięcie wzdłuż osi Z, co zrobiliśmy już 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 określić, ile trzeba przesunąć kciukiem wzdłuż osi Z. Trzeba jednak pamiętać, że ze względu na koordynację działań musimy przełożyć dodatkowy element -2px razem z z. Pamiętaj też, że przekształcenia elementu są stosowane od prawej do lewej, co oznacza, że żadne tłumaczenia sprzed naszej specjalnej macierzy nie zostaną odwrócone, ale wszystkie tłumaczenia po naszej specjalnej macierzy – Zakodujmy 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. Jest to po prostu element DOM, który możemy dowolnie zmieniać. Jeśli chodzi o ułatwienia dostępu, ważne jest, aby kciuk reagował na kliknięcie i przeciągnięcie, ponieważ wielu użytkowników jest przyzwyczajonych do korzystania z paska przewijania w ten sposób. Aby nie wydłużać tego postu, nie będę wyłączać jego szczegółów. Aby dowiedzieć się, jak to zrobić, przeczytaj kod biblioteki.

A co z iOS?

Ach, mój stary przyjaciel Safari na iOS. Podobnie jak przy przewijaniu z efektem paralaksy, tutaj napotkaliśmy problem. Przewijamy element, więc musimy określić właściwość -webkit-overflow-scrolling: touch. Powoduje to wygładzanie 3D i cały efekt przewijania. Rozwiązaliśmy ten problem w narzędziu do przewijania paralaksy – wykryliśmy Safari w iOS i wykorzystaliśmy funkcję position: sticky jako obejście. W tym przypadku zrobimy to samo. Przeczytaj ten artykuł na temat paralaksy, aby odświeżyć swoją pamięć.

A co z paskiem przewijania w przeglądarce?

W przypadku niektórych systemów musimy korzystać ze stałego, natywnego paska przewijania. Dawniej paska przewijania nie dało się ukryć (z wyjątkiem użycia niestandardowego pseudoselektora). Aby ją ukryć, musimy uciekać się do jakiegoś ataku hakerów. Element przewijany umieszczamy w kontenerze jako element overflow-x: hidden i zwiększamy jego szerokość niż kontener. Natywny pasek przewijania w przeglądarce nie jest już widoczny.

Koniec

Po połączeniu wszystkich elementów możemy utworzyć idealny niestandardowy pasek przewijania, taki jak w prezentacji o kotach Nyan.

Jeśli nie widzisz kota Nyan, oznacza to, że masz błąd, który znaleźliśmy i zgłosiliśmy podczas tworzenia tej wersji demonstracyjnej (kliknij kciuk, by wyświetlić kota Nyan). Chrome świetnie radzi sobie z unikaniem niepotrzebnej pracy, takiej jak malowanie czy animowanie rzeczy, które nie są widoczne na ekranie. Zła wiadomość jest taka, że dzięki naszym matriksom Chrome uważa, że ten GIF z kotem Nyan nie pojawia się na ekranie. Mamy nadzieję, że wkrótce problem zostanie rozwiązany.

I o to chodzi. Wymagało to dużo pracy. Dziękuję za przeczytanie całości. Niekiedy trzeba w tym celu prawdziwych starań. W takim przypadku nie zawsze jest to warte wysiłku. Wyjątkiem jest niestandardowy pasek przewijania, który jest kluczowym elementem korzystania z aplikacji. Dobrze wiedzieć, że jest to możliwe, prawda? Trudno uwierzyć, że utworzenie niestandardowego paska przewijania jest bardzo trudne, i pokazuje, jak wiele jest jeszcze do zrobienia po stronie CSS. Ale nie bójcie się! W przyszłości narzędzie AnimationWorklet na kanale Houdini znacznie ułatwi takie działania jak ten z przewijaniem, które idealnie nadają się do zastosowania w klatce.