Animowanie rozmycia

Rozmycie to świetny sposób na przekierowanie uwagi użytkownika. Rozmycie niektórych elementów wizualnych przy jednoczesnym zachowaniu ostrości innych elementów w naturalny sposób przyciąga uwagę użytkownika. Użytkownicy ignorują rozmazane treści i skupiają się na tych, które mogą przeczytać. Przykładem może być lista ikon, które po najechaniu na nie wskaźnikiem myszy wyświetlają szczegóły poszczególnych elementów. W tym czasie pozostałe opcje mogą być rozmyte, aby przekierować użytkownika do nowo wyświetlanych informacji.

TL;DR

Animowanie rozmycia nie jest dobrym rozwiązaniem, ponieważ jest bardzo powolne. Zamiast tego oblicz serię coraz bardziej rozmytych wersji i płynnie przechodź między nimi. Mój kolega Yi Gu napisał bibliotekę, która zajmie się wszystkim za Ciebie. Zapoznaj się z naszą wersją demonstracyjną.

Jeśli jednak zastosujesz tę technikę bez okresu przejściowego, może to być dość nieprzyjemne. Animowanie rozmycia – przejście od obrazu nierozmytego do rozmytego – wydaje się rozsądnym wyborem, ale jeśli kiedykolwiek próbowałeś to zrobić w internecie, prawdopodobnie zauważyłeś, że animacje nie są płynne. Pokazuje to demo, jeśli nie masz wydajnego urządzenia. Czy możemy to zrobić lepiej?

.

Problem

Procesor przekształca znaczniki w tekstury. Tekstury są przesyłane do procesora graficznego. Procesor graficzny rysuje te tekstury w buforze ramki za pomocą shaderów. Rozmycie następuje w cieniowaniu.

Obecnie nie możemy skutecznie animować rozmycia. Możemy jednak znaleźć obejście, które wygląda wystarczająco dobrze, ale technicznie nie jest animowanym rozmyciem. Na początek dowiedzmy się, dlaczego animowane rozmycie działa wolno. Aby rozmyć elementy w internecie, można zastosować 2 techniki: właściwość CSS filter i filtry SVG. Dzięki większej obsłudze i łatwości użycia zwykle stosuje się filtry CSS. Jeśli musisz obsługiwać Internet Explorer, nie masz wyboru i musisz używać filtrów SVG, ponieważ IE 10 i 11 je obsługują, ale nie obsługują filtrów CSS. Dobra wiadomość jest taka, że nasze obejście problemu z animowaniem rozmycia działa w przypadku obu tych technik. Spróbujmy więc znaleźć wąskie gardło, korzystając z Narzędzi deweloperskich.

Jeśli włączysz w Narzędziach deweloperskich opcję „Paint Flashing”, nie zobaczysz żadnych błysków. Wygląda na to, że nie ma żadnych ponownych rysowań. Jest to technicznie poprawne, ponieważ „ponowne rysowanie” oznacza, że procesor musi ponownie narysować teksturę promowanego elementu. Gdy element jest zarówno promowany , jak i rozmyty, rozmycie jest stosowane przez procesor graficzny za pomocą cieniowania.

Zarówno filtry SVG, jak i filtry CSS używają filtrów splotowych do zastosowania rozmycia. Filtry splotowe są dość kosztowne, ponieważ w przypadku każdego piksela wyjściowego należy wziąć pod uwagę pewną liczbę pikseli wejściowych. Im większy obraz lub promień rozmycia, tym większy koszt efektu.

I tu tkwi problem – w każdej klatce wykonujemy dość kosztowną operację na GPU, przekraczając budżet klatki wynoszący 16 ms, a w rezultacie uzyskujemy znacznie mniej niż 60 klatek na sekundę.

Wpaść w króliczą norę

Co możemy zrobić, aby wszystko przebiegało sprawnie? Możemy użyć sztuczki! Zamiast animować rzeczywistą wartość rozmycia (promień rozmycia), wstępnie obliczamy kilka rozmytych kopii, w których wartość rozmycia rośnie wykładniczo, a następnie płynnie przechodzimy między nimi za pomocą funkcji opacity.

Przenikanie to seria nakładających się na siebie efektów stopniowego pojawiania się i znikania. Jeśli na przykład mamy 4 etapy rozmycia, wygaszamy pierwszy etap, jednocześnie włączając drugi. Gdy drugi etap osiągnie 100% krycia, a pierwszy – 0%, wygaszamy drugi etap, a w tym samym czasie włączamy trzeci. Gdy to zrobimy, stopniowo wycofamy trzeci etap i wprowadzimy czwartą, ostatnią wersję. W tym przypadku każdy etap trwałby ¼ całkowitego czasu trwania. Wizualnie wygląda to bardzo podobnie do prawdziwego, animowanego rozmycia.

Z naszych eksperymentów wynika, że najlepsze efekty wizualne uzyskuje się, zwiększając promień rozmycia wykładniczo na każdym etapie. Przykład: jeśli mamy 4 etapy rozmycia, do każdego z nich zastosujemy wartość filter: blur(2^n), czyli etap 0: 1 piksel, etap 1: 2 piksele, etap 2: 4 piksele i etap 3: 8 pikseli. Jeśli wymusimy umieszczenie każdej z tych rozmytych kopii na osobnej warstwie (tzw. „promowanie”) za pomocą will-change: transform, zmiana krycia tych elementów powinna być bardzo szybka. Teoretycznie pozwoliłoby to nam z wyprzedzeniem wykonać kosztowne zadanie rozmycia. Okazuje się, że ta logika jest wadliwa. Jeśli uruchomisz tę wersję demonstracyjną, zobaczysz, że liczba klatek na sekundę jest nadal mniejsza niż 60, a rozmycie jest gorsze niż wcześniej.

Narzędzia deweloperskie pokazujące ślad, w którym GPU przez długi czas jest zajęty.

Szybki rzut oka na Narzędzia deweloperskie pokazuje, że procesor graficzny jest nadal bardzo obciążony i wydłuża każdą klatkę do około 90 ms. Ale dlaczego? Nie zmieniamy już wartości rozmycia, a jedynie krycie. Co się dzieje? Problem polega ponownie na naturze efektu rozmycia: jak już wspomnieliśmy, jeśli element jest promowany i rozmyty, efekt jest stosowany przez GPU. Dlatego, mimo że nie animujemy już wartości rozmycia, tekstura nadal jest nierozmyta i musi być rozmywana przez procesor graficzny w każdej klatce. Przyczyna jeszcze gorszej liczby klatek na sekundę niż wcześniej wynika z faktu, że w porównaniu z prostą implementacją procesor graficzny ma więcej pracy, ponieważ przez większość czasu widoczne są 2 tekstury, które muszą być rozmyte niezależnie od siebie.

Nie jest to zbyt estetyczne rozwiązanie, ale dzięki niemu animacja działa błyskawicznie. Wracamy do niepromowania elementu, który ma zostać rozmyty, ale zamiast tego promujemy element nadrzędny. Jeśli element jest zarówno rozmyty, jak i promowany, efekt jest stosowany przez procesor graficzny. To właśnie spowolniło naszą wersję demonstracyjną. Jeśli element jest rozmyty, ale nie jest promowany, rozmycie jest rasteryzowane do najbliższej tekstury nadrzędnej. W naszym przypadku jest to promowany element opakowania nadrzędnego. Rozmyty obraz jest teraz teksturą elementu nadrzędnego i może być ponownie używany w przyszłych klatkach. Działa to tylko dlatego, że wiemy, że rozmyte elementy nie są animowane i ich buforowanie jest korzystne. Oto demo, które wykorzystuje tę technikę. Ciekawe, co na to Moto G4? Uwaga spojler: uważa, że jest świetny:

Narzędzia deweloperskie pokazujące ślad, w którym GPU ma dużo czasu bezczynności.

Teraz mamy duży zapas mocy na GPU i płynne 60 klatek na sekundę. Udało się!

Wdrażanie do środowiska produkcyjnego

W naszej wersji demonstracyjnej wielokrotnie powieliliśmy strukturę DOM, aby mieć kopie treści do rozmycia z różną intensywnością. Możesz się zastanawiać, jak to będzie działać w środowisku produkcyjnym, ponieważ może to mieć niepożądane skutki uboczne w postaci stylów CSS autora lub nawet jego kodu JavaScript. Masz rację. Witaj w Shadow DOM!

Większość osób uważa Shadow DOM za sposób dołączania „wewnętrznych” elementów do elementów niestandardowych, ale jest to też element izolacji i wydajności. JavaScript i CSS nie mogą przenikać granic Shadow DOM, co pozwala nam duplikować treści bez zakłócania stylów ani logiki aplikacji dewelopera. Mamy już element <div> dla każdej kopii, na której można przeprowadzić rasteryzację, a teraz używamy tych elementów <div> jako hostów cieni. Tworzymy ShadowRoot za pomocą attachShadow({mode: 'closed'}) i dołączamy kopię treści do ShadowRoot zamiast do samego <div>. Musimy też skopiować wszystkie arkusze stylów do elementu ShadowRoot, aby mieć pewność, że nasze kopie będą miały taki sam styl jak oryginał.

Niektóre przeglądarki nie obsługują Shadow DOM w wersji 1, więc w ich przypadku po prostu duplikujemy treść i liczymy na to, że nic się nie zepsuje. Moglibyśmy użyć wypełnienia Shadow DOMShadyCSS, ale nie zaimplementowaliśmy tego w naszej bibliotece.

To wszystko. Po przejściu przez proces renderowania w Chrome dowiedzieliśmy się, jak skutecznie animować rozmycie w różnych przeglądarkach.

Podsumowanie

Tego rodzaju efektu nie należy używać pochopnie. Kopiujemy elementy DOM i umieszczamy je w osobnej warstwie, dzięki czemu możemy wykorzystać pełnię możliwości urządzeń z niższej półki. Kopiowanie wszystkich arkuszy stylów do każdego elementu ShadowRoot również może negatywnie wpłynąć na wydajność, dlatego musisz zdecydować, czy wolisz dostosować logikę i style, aby nie miały na nie wpływu kopie w elemencie LightDOM, czy użyć naszej techniki ShadowDOM. Ale czasami nasza technika może być opłacalną inwestycją. Zapoznaj się z kodem w naszym repozytorium GitHub, a także z wersją demonstracyjną. Jeśli masz jakieś pytania, napisz do mnie na Twitter.