Skuteczna paralaksa

Robert Lewiński
Robert Flack

Kochasz ją czy nienawidzisz – paralaksa będzie trwała. Stosowana z rozwagą może nadać aplikacji internetowej głębię i subtelny charakter. Problem polega jednak na tym, że efektywne wdrożenie paralaksy może być trudne. W tym artykule omówimy rozwiązanie, które jest zarówno wydajne, jak i co ważne, działa w różnych przeglądarkach.

Ilustracja z paralaksą

TL;DR

  • Do tworzenia animacji paralaksy nie używaj zdarzeń przewijania ani parametru background-position.
  • Aby uzyskać dokładniejszy efekt paralaksy, użyj przekształceń 3D CSS.
  • W przeglądarce mobilnej Safari użyj position: sticky, aby zapewnić rozpowszechnienie efektu paralaksy.

Jeśli potrzebujesz rozwiązania w postaci rozwiązania, przejdź do repozytorium UI Element Samples na GitHubie i pobierz plik JS z paralaksą dla paralaksy. Demonstrację na żywo działania przewijającego z paralaksą możesz zobaczyć w repozytorium GitHub.

Rozwiąż problemy z paralaksami

Na początek przyjrzymy się dwóm typowym sposobom osiągania efektu paralaksy oraz wyjaśnimy, dlaczego są one nieodpowiednie do naszych celów.

Nieprawidłowo: używanie zdarzeń przewijania

Kluczowym wymogiem paralaksy jest to, aby była ona połączona z przewijaniem. W przypadku każdej zmiany położenia przewijania strony pozycja elementu paralaksy powinna być aktualizowana. Choć brzmi to prosto, ważnym mechanizmem nowoczesnych przeglądarek jest możliwość pracy asynchronicznej. W naszym przypadku dotyczy to przewijania zdarzeń. W większości przeglądarek zdarzenia przewijania są realizowane w ramach najlepszych starań i nie możemy zagwarantować, że będą się pojawiać w każdej klatce animacji.

Ta ważna informacja wyjaśnia nam, dlaczego należy unikać rozwiązania opartego na JavaScript, które przenosi elementy na podstawie zdarzeń przewijania: JavaScript nie gwarantuje, że paralaksa będzie uwzględniała pozycję przewijania strony. W starszych wersjach mobilnych Safari zdarzenia przewijania odbywały się na końcu przewijania, co uniemożliwiało utworzenie efektu przewijania opartego na języku JavaScript. Nowsze wersje obsługują zdarzenia przewijania w trakcie animacji, ale – podobnie jak w Chrome – w zasadzie najlepszych starań. Jeśli wątek główny jest zajęty innymi zadaniami, zdarzenia przewijania nie są natychmiast dostarczane, co oznacza, że efekt paralaksy zostanie utracony.

Błąd: aktualizowanie pliku background-position

Inną sytuacją, której chcemy uniknąć, jest malowanie każdej klatki. Wiele rozwiązań próbuje zmienić background-position, aby uzyskać wygląd paralaksy, co powoduje, że przeglądarka ponownie maluje uszkodzone części strony podczas przewijania, co może być dość kosztowne, aby znacząco zakłócić animację.

Jeśli chcemy spełnić oczekiwania paralaksy, potrzebujemy czegoś, co można by zastosować jako właściwość przyspieszoną (co obecnie oznacza stosowanie przekształceń i nieprzezroczystości), która nie opierała się na zdarzeniach przewijania.

CSS w 3D

Zarówno Scott Kellum, jak i Keith Clark podjęli znaczne wysiłki w dziedzinie stosowania paralaksy w CSS 3D. Technika, którą stosują, to:

  • Skonfiguruj element zawierający przewijanie za pomocą funkcji overflow-y: scroll (i prawdopodobnie overflow-x: hidden).
  • Do tego samego elementu zastosuj wartość perspective, a perspective-origin ustawioną na top left lub 0 0.
  • Do elementów podrzędnych tego elementu zastosuj przesunięcie w punkcie Z i przeskaluj je z powrotem, aby uzyskać efekt paralaksy bez wpływu na ich rozmiar na ekranie.

Kod CSS tego podejścia wygląda tak:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

Zakładamy więc, że fragment kodu HTML wygląda tak:

<div class="container">
    <div class="parallax-child"></div>
</div>

Dostosowywanie skali pod kątem perspektywy

Wepchnięcie elementu podrzędnego z powrotem spowoduje, że zmniejszy się on proporcjonalnie do wartości perspektywy. Aby obliczyć, jak należy go przeskalować, użyj tego równania: (perspektywa – odległość) / perspektywa. Najbardziej potrzebna jest funkcja paralaksy, która ma się pojawiać w rozmiarze, w którym go stworzyliśmy, więc trzeba go skalować w górę, a nie pozostawiać bez zmian.

W przypadku powyższego kodu perspektywa wynosi 1px, a odległość Z elementu parallax-child to 1px. Oznacza to, że element trzeba skalować w górę o 3x, co będzie widać w kodzie: scale(3).

W treściach, do których nie zastosowano wartości translateZ, możesz wstawić wartość zero. Oznacza to, że skala jest (perspektywa - 0) / perspektywa, która pomniejsza wartość 1, co oznacza, że została przeskalowana ani w górę, ani w dół. Naprawdę przydatne.

Jak działa to podejście

Trzeba wyraźnie ustalić, dlaczego to działa, ponieważ wkrótce wykorzystamy tę wiedzę. Przewijanie jest w praktyce przekształceniem, dlatego można je przyspieszyć. Najczęściej polega na przemieszczaniu warstw za pomocą GPU. W typowym przewijaniu, czyli takim, w którym nie ma pojęcia o perspektywie, przewijanie odbywa się w sposób 1:1 podczas porównywania elementu przewijającego się z jego elementami podrzędnymi. Jeśli przewiniesz element w dół o 300px, jego elementy podrzędne zostaną przekształcone o tę samą wartość: 300px.

Jednak zastosowanie wartości perspektywy do przewijanego elementu dezorientuje ten proces – zmienia macierz, który leży u podstaw przekształcenia przewijania. Teraz zwój o długości 300 pikseli może przesunąć elementy podrzędne tylko o 150 pikseli w zależności od wybranych wartości perspective i translateZ. Jeśli element translateZ ma wartość 0, będzie przewijany w proporcji 1:1 (tak jak wcześniej), ale element podrzędny przesunięty w kierunku Z poza punkt początkowy będzie przewijany z inną szybkością. Wynik netto: ruch paralaksy. Bardzo ważne jest, że odbywa się to automatycznie w ramach wewnętrznego mechanizmu przewijania w przeglądarce, więc nie trzeba odsłuchiwać zdarzeń scroll ani zmieniać elementu background-position.

Lot w maście: Mobile Safari

Każdy efekt ma pewne zastrzeżenia, a jednym ważnym z nich jest zachowanie efektów 3D w elementach podrzędnych. Jeśli w hierarchii między elementem z perspektywą a elementami podrzędnymi powodującymi paralaksację występują elementy, perspektywa 3D jest „spłaszczona”, co oznacza, że efekt się nie pojawia.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

W powyższym kodzie HTML .parallax-container jest nowy i skutecznie spłaszcza wartość perspective, przez co tracimy efekt paralaksy. Rozwiązanie w większości przypadków jest dość proste: wystarczy dodać do elementu transform-style: preserve-3d, co spowoduje rozpowszechnienie wszystkich efektów 3D (np. wartości z perspektywy), które zostały zastosowane wyżej w drzewie.

.parallax-container {
  transform-style: preserve-3d;
}

Sytuacja w mobilnej wersji Safari jest trochę bardziej zawinięta. Stosowanie metody overflow-y: scroll do elementu kontenera jest techniczne, ale kosztem możliwości rzucenia przewijanego elementu. Rozwiązaniem jest dodanie parametru -webkit-overflow-scrolling: touch, ale jednocześnie spłaszczenie perspective i uniknięcie paralaksy.

Jeśli chodzi o stopniowe udoskonalanie, raczej nie jest to najistotniejszy problem. Jeśli nie możemy zastosować efektu paralaksy w każdej sytuacji, nasza aplikacja nadal działa, ale warto poszukać sposobu obejścia tego problemu.

position: sticky na ratunek!

Pomocna jest w rzeczywistości funkcja position: sticky, która umożliwia „przyklejanie się” elementów do górnej części widocznego obszaru lub określonego elementu nadrzędnego podczas przewijania. Specyfikacja, jak większość z nich, jest dość duża, ale zawiera przydatny klejnot:

Na pierwszy rzut oka może to nie wydawać się wielkim, ale najważniejszym stwierdzeniem w tym zdaniu jest to, jak dokładnie oblicza się trwałość elementu: „odsunięcie jest obliczane w odniesieniu do najbliższego elementu nadrzędnego z przewijanym polem”. Inaczej mówiąc, odległość, na jaką ma zostać przesunięty element przyklejony (aby wyglądał na przyłączony do innego elementu lub widocznego obszaru), jest obliczana przed zastosowaniem innych przekształceń, a nie po. Oznacza to, że podobnie jak w przypadku przewijania w poprzednim przykładzie, jeśli przesunięcie zostało obliczone na 300 pikselach, pojawi się nowa możliwość użycia perspektyw (lub dowolnego innego przekształcenia) do manipulowania wartością przesunięcia 300 pikseli, zanim zostanie zastosowana do elementów przyklejonych.

Stosując właściwość position: -webkit-sticky do elementu paralaksowego, możemy skutecznie „odwrócić” efekt wygładzania elementu -webkit-overflow-scrolling: touch. Dzięki temu element paralaksowy odwołuje się do najbliższego elementu nadrzędnego z polem przewijania. W tym przypadku jest to .container. Następnie, podobnie jak wcześniej, .parallax-container stosuje wartość perspective, która zmienia obliczone przesunięcie przewijania i stwarza efekt paralaksy.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

Przywraca to efekt paralaksy w mobilnej wersji Safari – to świetna wiadomość.

Zastrzeżenia dotyczące pozycji przyklejonych

Jednak istnieje różnica: position: sticky zmienia mechanikę paralaksy. Pozycjonowanie przyklejone ma na celu (w przeciwieństwie do wersji nieprzyklejonej) element przyklejony do kontenera przewijanego. Oznacza to, że paralaksa z przyklejonym elementem jest odwrotnością tej bez:

  • Przy wartości position: sticky element jest bliżej z=0, tym mniej się porusza.
  • Bez wartości position: sticky im bliżej z=0, tym bardziej się porusza.

Jeśli wydaje się to trochę abstrakcyjne, obejrzyj demonstrację Roberta Flacka, która pokazuje, jak różne elementy zachowują się w pozycji przyklejonej i bez niej. Aby zobaczyć różnicę, musisz mieć Chrome Canary (w chwili utworzenia wersji 56) lub Safari.

Zrzut ekranu z perspektywy paralaksy

Demonstracja autorstwa Roberta Flacka pokazująca, jak position: sticky wpływa na przewijanie z użyciem paralaksy.

Różne błędy i sposoby obejścia problemu

Jak zawsze, zawsze są guzki, które trzeba wygładzić:

  • Ciągła obsługa klienta jest niespójna. Obsługa jest wciąż wprowadzana w Chrome, Edge nie obsługujemy w pełni, a w Firefoksie występują błędy mające miejsce w przypadku połączenia elementów przyklejonych z przekształceniami perspektywy. W takich przypadkach warto dodać niewielki kod, aby dodać position: sticky (wersja z prefiksem -webkit-) tylko wtedy, gdy jest potrzebna. Ta opcja jest dostępna tylko w mobilnej przeglądarce Safari.
  • Efekt nie „po prostu działa” w Edge. Edge stara się obsługiwać przewijanie na poziomie systemu operacyjnego, co jest zwykle dobrym rozwiązaniem, ale w tym przypadku uniemożliwia wykrywanie zmian perspektywy podczas przewijania. Aby rozwiązać ten problem, możesz dodać element o stałej pozycji, który prawdopodobnie przełącza Edge na metodę przewijania poza systemem operacyjnym i zapewnia uwzględnianie zmian perspektyw.
  • „Zawartość strony właśnie stała się ogromna” Wiele przeglądarek decyduje o wielkości strony, biorąc pod uwagę skalę treści, ale Chrome i Safari nie biorą pod uwagę perspektywy. Jeśli więc do elementu zastosowano skalę 3x, możesz zobaczyć paski przewijania itp., nawet jeśli po zastosowaniu perspective element będzie miał wartość 1x. Można rozwiązać ten problem, skalując elementy z prawego dolnego rogu (za pomocą funkcji transform-origin: bottom right), co działa, ponieważ powoduje to, że zbyt duże elementy rozwijają się do „obszaru wykluczającego” (zwykle jest to lewy górny obszar przewijanego obszaru). W przypadku przewijanych regionów nigdy nie można zobaczyć ani przewinąć treści znajdujących się w wykluczonym obszarze.

Podsumowanie

Przemyślana paralaksa daje zabawny efekt. Jak widać, można ją wdrożyć w sposób wydajny, połączony z funkcją przewijania i działający w różnych przeglądarkach. Wymaga trochę pracy matematycznej i niewielkiej ilości powtarzalnego kodu do uzyskania oczekiwanego efektu, dlatego stworzyliśmy małą bibliotekę pomocniczą i przykład, które znajdziesz w naszym repozytorium UI Element Samples na GitHubie.

Baw się dobrze i daj nam znać, jak Ci poszło.