Animowanie rozmycia

Rozmycie to świetny sposób na przekierowanie uwagi użytkownika. Rozmycie niektórych elementów wizualnych przy jednoczesnym pozostawieniu ostrości na innych elementach w naturalny sposób kieruje uwagę użytkownika. Użytkownicy ignorują zamazane treści i skupiają się na treściach, które mogą przeczytać. Przykładem może być lista ikon, które po najechaniu na nie kursorem myszy wyświetlają szczegółowe informacje o poszczególnych elementach. W tym czasie pozostałe opcje mogą być niewyraźne, aby przekierować użytkownika do nowo wyświetlanych informacji.

TL;DR

Animacja rozmycia nie jest dobrym rozwiązaniem, ponieważ jest bardzo powolna. Zamiast tego z góry oblicz serię coraz bardziej rozmytych wersji i przełączaj się między nimi. Moja koleżanka Yi Gu napisała bibliotekę, która zadba o wszystko za Ciebie. Obejrzyj nasze demo.

Jednak zastosowanie tej metody bez okresu przejściowego może być dość drastyczne. Animowanie rozmycia (przejście od nierozmytego do rozmytego) wydaje się rozsądnym rozwiązaniem, ale jeśli kiedykolwiek próbowałeś/próbowałaś to zrobić w internecie, prawdopodobnie zauważyłeś/zauważyłaś, że animacje są dalekie od płynności, jak pokazuje to demo, jeśli nie masz mocnego komputera. Czy możemy coś zrobić lepiej?

Problem

Oznaczenia są przekształcane w tekstury przez procesor. Tekstury są przesyłane do GPU. GPU rysuje te tekstury na framebufferze za pomocą shaderów. Rozmywanie odbywa się w shaderze.

Obecnie nie możemy sprawić, aby animacja rozmycia działała efektywnie. Możemy jednak znaleźć obejście, które wygląda wystarczająco dobrze, ale technicznie nie jest to animowane rozmycie. Na początek wyjaśnijmy, dlaczego animacja rozmycia jest powolna. Aby rozmyć elementy w internecie, możesz użyć jednej z 2 technik: właściwości CSS filterlub filtrów SVG. Dzięki większej obsłudze i łatwości użycia filtry CSS są często używane. Jeśli musisz obsługiwać Internet Explorera, musisz używać filtrów SVG, ponieważ IE 10 i 11 je obsługują, ale nie filtry CSS. Dobra wiadomość jest taka, że nasze obejście problemu dotyczące animacji rozmycia działa w przypadku obu tych technik. Spróbujmy znaleźć wąskie gardło, korzystając z Narzędzi deweloperskich.

Jeśli w Narzędziach deweloperskich włączysz opcję „Błyski w ramach malowania”, nie zobaczysz żadnych błysków. Wygląda na to, że nie ma żadnych ponownych malowań. Jest to technicznie poprawne, ponieważ „przemalowanie” oznacza, że procesor musi ponownie pomalować teksturę promowanego elementu. Gdy element jest wyeksponowany irozmyty, rozmycie jest stosowane przez GPU za pomocą shadera.

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

I właśnie w tym tkwi problem. W każdej klatce wykonujemy dość kosztowną operację na karcie graficznej, co powoduje przekroczenie budżetu na klatkę wynoszącego 16 ms, w efekcie FPS spada poniżej 60.

W głąb króliczej nory

Co możemy zrobić, aby wszystko działało prawidłowo? Możemy użyć sztuczki. Zamiast animować rzeczywistą wartość rozmycia (promień rozmycia), obliczamy wstępnie kilka zamazanych kopii, w których wartość rozmycia wzrasta wykładniczo, a następnie stosujemy przejście między nimi za pomocą funkcji opacity.

Przejście polega na nałożeniu na siebie kilku przejść z różną przezroczystością. Jeśli mamy na przykład 4 stopnie rozmycia, pierwszy z nich znika, a drugi pojawia się w tym samym czasie. Gdy drugi etap osiągnie 100% przezroczystości, a pierwszy – 0%, drugi etap zacznie zanikać, a trzeci – pojawiać się. Gdy to zrobimy, trzeci etap zniknie, a na jego miejsce pojawi się czwarta i ostateczna wersja. W takim przypadku każdy etap zajmowałby ¼ całkowitego oczekiwanego czasu. Wizualnie wygląda to bardzo podobnie do prawdziwego, animowanego rozmycia.

Z naszych eksperymentów wynika, że najlepszy efekt wizualny uzyskuje się, zwiększając promień rozmycia wykładniczo na każdym etapie. Przykład: jeśli mamy 4 etapy rozmywania, do każdego z nich zastosujemy filter: blur(2^n), czyli etap 0: 1 piksel, etap 1: 2 piksele, etap 2: 4 piksele i etap 3: 8 pikseli. Jeśli spowodujemy, że każda z tych niewyraźnych kopii zostanie umieszczona na osobnej warstwie (co nazywamy „promowaniem”) za pomocą funkcji will-change: transform, zmiana przezroczystości tych elementów powinna być bardzo szybka. Teoretycznie pozwoliłoby nam to zwiększyć wydajność kosztownej pracy związanej z rozmazywaniem. Okazało się, że logika jest wadliwa. Jeśli uruchomisz to demo, zobaczysz, że liczba klatek na sekundę nadal jest poniżej 60 FPS, a rozmycie jest w fakcie gorsze niż wcześniej.

DevTools pokazujące ślad, w którym GPU ma długie okresy zajętości.

Szybki rzut oka na DevTools pokazuje, że procesor graficzny jest nadal bardzo zajęty i rozciąga każdy kadr do około 90 ms. Ale dlaczego? Nie zmieniamy już wartości rozmycia, tylko przezroczystości. Co się dzieje? Problem leży z powrotem w naturze efektu rozmycia: jak już wyjaśnialiśmy, jeśli element jest jednocześnie podświetlony i rozmyty, efekt jest stosowany przez procesor graficzny. Mimo że nie animujemy już wartości rozmycia, sama tekstura nadal nie jest rozmyta i musi być ponownie rozmyta w każdej klatce przez procesor graficzny. Wynika to z tego, że w porównaniu z prostą implementacją GPU ma więcej pracy, ponieważ większość czasu widoczne są 2 tekstury, które trzeba osobno rozmywać.

Nasze rozwiązanie nie jest ładne, ale dzięki niemu animacja jest błyskawicznie szybka. Wracamy do nie promowania elementu, który ma być zamazany, a zamiast tego promujemy element nadrzędny. Jeśli element jest rozmyty i wypromowany, efekt jest stosowany przez procesor graficzny. To spowolniło naszą prezentację. Jeśli element jest rozmyty, ale nie został wypromowany, rozmycie jest rastrowane do najbliższej tekstury nadrzędnej. W naszym przypadku jest to promowany element nadrzędny. Rozmyty obraz jest teraz teksturą elementu nadrzędnego i może być ponownie użyty we wszystkich kolejnych klatkach. Działa to tylko dlatego, że wiemy, że rozmyte elementy nie są animowane i ich buforowanie jest korzystne. Oto prezentacja, która pokazuje tę technikę. Ciekawe, co o tym myśli Moto G4? Uwaga, spoiler: wydaje mu się, że jest świetny:

DevTools pokazujące ślad, w którym GPU ma dużo czasu bezczynności.

Teraz mamy dużo zapasu na karcie graficznej i płynne 60 FPS. Udało się!

Wprowadzanie w wersję produkcyjną

W naszym pokazie wielokrotnie powieliliśmy strukturę DOM, aby móc zamazać zawartość z różną intensywnością. Możesz się zastanawiać, jak to będzie działać w środowisku produkcyjnym, ponieważ może to mieć niezamierzone skutki uboczne w przypadku stylów CSS autora czy nawet jego kodu JavaScript. Masz rację. Oto Shadow DOM.

Większość osób uważa, że shadow DOM to sposób na dołączanie „wewnętrznych” elementów do elementów niestandardowych, ale jest to też prosty mechanizm izolacji i zwiększania wydajności. Kod JavaScript i CSS nie może przekraczać granic cienia DOM, co pozwala nam powielać zawartość bez zakłócania stylów dewelopera ani logiki aplikacji. Mamy już element <div> dla każdej kopii, na którą ma być rzutowany obraz, i teraz używamy tych elementów <div> jako hostów zastępczych. Tworzymy ShadowRoot za pomocą attachShadow({mode: 'closed'}) i dołączamy kopię treści do ShadowRoot zamiast do <div>. Musimy też skopiować wszystkie arkusze stylów do folderu ShadowRoot, aby mieć pewność, że nasze kopie będą miały taki sam styl co oryginał.

Niektóre przeglądarki nie obsługują Shadow DOM w wersji 1. W ich przypadku po prostu duplikujemy treści i mamy nadzieję, że nic się nie zepsuje. Moglibyśmy użyć zastępnika Shadow DOMShadyCSS, ale nie wdrożyliśmy tego w naszej bibliotece.

I to wszystko. Po przeanalizowaniu łańcucha przetwarzania Chrome udało nam się znaleźć sposób na wydajne animowanie rozmycia w różnych przeglądarkach.

Podsumowanie

Tego typu efektów nie należy używać lekkomyślnie. Kopiujemy elementy DOM i przesuwamy je na własną warstwę, dzięki czemu możemy przesunąć granice możliwości tańszych urządzeń. Kopiowanie wszystkich arkuszy stylów do każdego ShadowRoot niesie ze sobą również ryzyko pogorszenia wydajności, dlatego musisz zdecydować, czy wolisz dostosować logikę i style tak, aby nie były one modyfikowane przez kopie w LightDOM, czy też skorzystać z naszej techniki ShadowDOM. Czasami jednak warto zastosować naszą technikę. Zapoznaj się z kodem w naszym repozytorium GitHub oraz obejrzyj prezentację. Jeśli masz pytania, skontaktuj się ze mną na Twitter.