Wprawianie meduz w ruch w Compose: animowanie wektorów obrazu i stosowanie AGSL RenderEffects

Uwielbiam śledzić inspirujących ludzi w Internecie i patrzeć, co robią — jedną z takich osób jest Cassie Codes , która tworzy niesamowite animacje do sieci. Jednym z jej inspirujących przykładów jest ta urocza animowana meduza .
Po obejrzeniu tego i obsesji na jego punkcie przez chwilę pomyślałem sobie, że to urocze małe stworzenie musi ożyć również w Compose. Więc ten post na blogu opisuje, jak zrobiłem to w Jetpack Compose, ostateczny kod można znaleźć tutaj . Opisane tu techniki dotyczą oczywiście nie tylko meduz… każda inna ryba też się nada! Żartuję — ten post na blogu obejmie:
- Niestandardowe wektory obrazu
- Animowanie ścieżek lub grup ImageVector
- Stosowanie efektu szumu zniekształceń na Composable z AGSL RenderEffect.
Analiza pliku SVG
Aby zaimplementować tę meduzę, musimy najpierw zobaczyć, z czego składa się plik SVG — i spróbować odtworzyć różne jego części. Najlepszym sposobem, aby dowiedzieć się, co rysuje plik SVG, jest skomentowanie różnych jego części i zobaczenie wizualnego wyniku renderowania każdej sekcji pliku svg. Aby to zrobić, możesz albo zmienić go w edytorze, do którego link znajduje się powyżej, albo pobrać i otworzyć plik SVG w edytorze tekstu (jest to format czytelny dla tekstu).
Przyjrzyjmy się zatem temu plikowi SVG:
<!--
Jellyfish SVG, path data removed for brevity
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
<defs>
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle" />
<path class="tentacle" />
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="face" />
<path class="outerJelly"/>
<path id="freckle" />
<path id="freckle"/>
<path id="freckle-4"/>
</g>
<g id="bubbles" fill="#fff">
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
</g>
<g class="jellyfish face">
<path class="eye lefteye" fill="#b4bebf" d=""/>
<path class="eye righteye" fill="#b4bebf" d=""/>
<path class="mouth" fill="#d3d3d3" opacity=".72"/>
</g>
</svg>
- Ścieżki i grupy ścieżek tworzących plik SVG:
- Macki
- Twarz — kropelka i zewnętrzna galaretka
- Oczy — ożywione otwarte i zamknięte
- Bąbelki — animowane losowo wokół meduzy — animacje rozmiaru i alfa
- M, m : Przejdź do
- L, l, H, h, V, v : Linia do
- C, c, S, s : Sześcienna krzywa Béziera do
- Q, q, T, t: Kwadratowa krzywa Béziera do
- A, a: Eliptyczna krzywa łuku do
- Z, z — Zamknij ścieżkę
- Meduza powinna poruszać się powoli w górę iw dół
- Oczy powinny mrugać na kliknięcie meduzy
- Ciało meduzy powinno mieć nałożony efekt chwiejności/hałasu.
Teraz, gdy rozumiemy, z czego składa się ten plik SVG, przejdźmy do renderowania wersji statycznej w programie Compose.
Tworzenie niestandardowego ImageVector
Compose ma koncepcję ImageVector , w której można programowo zbudować wektor — podobnie jak SVG. W przypadku wektorów/plików SVG, które chcesz renderować bez zmian, możesz również załadować VectorDrawable za pomocą painterResource(R.drawable.vector_image). To zajmie się przekształceniem go w ImageVector, który wyrenderuje Compose.
Teraz możesz zadać sobie pytanie — dlaczego po prostu nie zaimportować meduzy jako pliku SVG do pliku xml i załadować go za pomocą painterResource(R.drawable.jelly_fish)
?
To świetne pytanie — i możliwe jest załadowanie meduzy w ten sposób, usuwając aspekt turbulencji SVG, a obraz będzie renderowany z załadowanym XML (jak wyjaśniono w dokumentacji tutaj ). Ale chcemy zrobić trochę więcej z poszczególnymi częściami ścieżki, na przykład animować części po kliknięciu i zastosować efekt szumu do ciała, więc będziemy rozwijać ImageVector
programowo.
Aby wyrenderować tę meduzę w Compose, możemy skopiować dane ścieżki (lub d
znacznik „ ” na ścieżce), z których składa się ryba, na przykład pierwsza macka ma następujące dane ścieżki:
M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
Teraz pewnie myślisz — czy muszę rysować w głowie i znać wszystkie pozycje i komendy od ręki? Nie — wcale. Możesz utworzyć wektor w większości programów do projektowania — takich jak Figma lub Inkscape, i wyeksportować wynik rysunku do pliku SVG, aby uzyskać te informacje dla siebie. Uff!
Aby utworzyć wektor w Compose: wywołujemy rememberVectorPainter
, co tworzy ImageVector
, a my tworzymy Group
wywołany jellyfish
, następnie kolejny Group
wywołany tentacles
i umieszczamy w Path
nim pierwszy dla pierwszej macki. Ustawiliśmy również RadialGradient
jako tło dla całej meduzy.
Rezultatem tego jest mała macka narysowana na ekranie z radialnym gradientem tła!

Powtarzamy ten proces dla wszystkich elementów SVG — pobierając bity ścieżki z pliku SVG i stosując kolor i alfa do ścieżki, która zostanie narysowana, logicznie grupujemy również ścieżki w macki, twarz, bąbelki itp.:
Mamy teraz całe nasze renderowanie meduzy z powyższym ImageVector
:

Animowanie ścieżek i grup ImageVector
Chcemy animować części tego wektora:
Zobaczmy więc, jak możemy animować poszczególne fragmenty pliku ImageVector
.
Przesuwanie meduzy w górę iw dół
Patrząc na codepen, widzimy, że meduza porusza się z translacją w górę iw dół (translacja y). Aby to zrobić w komponowaniu, tworzymy nieskończone przejście i translationY
animację na ponad 3000 milisekund, a następnie ustawiamy grupę zawierającą meduzę, a twarz ma translationY
, co spowoduje animację w górę iw dół.

Świetnie — część ImageVector
jest teraz animowana w górę iw dół, zauważysz, że bąbelki pozostają w tej samej pozycji.
Mrugające oczy ️
Patrząc na codepen, widzimy, że na każdym z oczu jest animacja scaleY
i . opacity
Stwórzmy te dwie zmienne i zastosujmy skalę do Group
i alfa na Path
. Zastosujemy je również tylko po kliknięciu meduzy, aby uczynić tę animację bardziej interaktywną.
Tworzymy dwa Animatables , które będą przechowywać stan animacji, oraz funkcję zawieszenia, którą wywołamy po kliknięciu meduzy — animujemy te właściwości, aby skalować i zanikać oczy.
Mamy teraz uroczą migającą animację po kliknięciu — a nasza meduza jest prawie gotowa!

Stosowanie efektu zniekształcenia/szumu
Mamy więc większość rzeczy, które chcemy animować — ruch w górę iw dół oraz mruganie. Przyjrzyjmy się, w jaki sposób ciało meduzy ma efekt chwiejności, ciało i macki poruszają się z zastosowanym hałasem, aby nadać mu wrażenie ruchu.

Patrząc na SVG i kod animacji, widzimy, że używa go feTurbulence
do generowania szumu, który jest następnie stosowany do SVG jako plik feDisplacementMap
.
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
Aby to osiągnąć, możemy użyć shaderów AGSL , warto zauważyć, że jest to obsługiwane tylko w Tiramisu i nowszych (API 33+). Najpierw musimy stworzyć moduł cieniujący, który będzie działał jak kołysanie, na początku nie będziemy używać szumu — zamiast tego dla uproszczenia użyjemy funkcji mapowania.
Sposób działania shaderów polega na tym, że działają one na pojedyncze piksele — otrzymujemy współrzędną ( fragCoord
) i oczekuje się, że uzyskamy wynik w kolorze, który zostanie wyrenderowany na tej współrzędnej. Poniżej znajduje się początkowy moduł cieniujący, którego użyjemy do przekształcenia komponowalnego:
W naszym przypadku dane wejściowe, których będziemy używać, to nasze aktualnie renderowane piksele na ekranie. Dostęp do tego uzyskujemy poprzez uniform shader contents;
zmienną, którą wyślemy jako dane wejściowe. Bierzemy współrzędną wejściową ( fragCoord
) i stosujemy pewne przekształcenia na tej współrzędnej — przesuwając ją w czasie i ogólnie wykonując na niej obliczenia matematyczne, aby ją przesunąć.
Daje to nową współrzędną, więc zamiast zwracać dokładny kolor w fragCoord
pozycji, przesuwamy miejsce, z którego pobieramy piksel wejściowy. Na przykład, gdybyśmy mieli return contents.eval(fragCoord)
, nie spowodowałoby to żadnej zmiany — byłoby przejściem. Teraz uzyskujemy kolor piksela z innego punktu elementu komponowalnego — co spowoduje efekt chwiejnego zniekształcenia zawartości elementu komponowalnego.
Aby użyć tego w naszym komponowalnym, możemy zastosować ten moduł cieniujący jako a RenderEffect
do zawartości komponowalnego:
Używamy createRuntimeShaderEffect
, przekazując WOBBLE_SHADER
jako dane wejściowe. Spowoduje to pobranie bieżącej zawartości elementu komponowalnego i przekazanie jej jako danych wejściowych do modułu cieniującego z nazwą parametru „ contents
”. Następnie sprawdzamy zawartość wewnątrz pliku WOBBLE_SHADER
. Zmienna time
zmienia chybotanie w czasie (tworząc animację).
Uruchamiając to, widzimy, że całość Image
jest teraz zniekształcona i wygląda nieco bardziej chwiejnie — zupełnie jak meduza.

Jeśli nie chcemy, aby efekt dotyczył twarzy i bąbelków, możemy wyodrębnić je do oddzielnych ImageVectors
plików i pominąć stosowanie efektu renderowania do tych wektorów:

Stosowanie efektu szumu
Moduł cieniujący, który wymieniliśmy powyżej, nie używa funkcji szumu do zastosowania przesunięcia zawartości elementu komponowalnego. Hałas to sposób zastosowania przemieszczenia z bardziej ustrukturyzowaną funkcją losową. Jednym z takich rodzajów szumu jest szum Perlina (który jest feTurbulence
używany pod maską), tak by to wyglądało, gdybyśmy wyrenderowali wynik działania funkcji szumu Perlina:

Używamy wartości szumu dla każdej współrzędnej w przestrzeni i używamy jej do zapytania o nową współrzędną w contents
module cieniującym „ ”.
Zaktualizujmy nasz moduł cieniujący, aby używał funkcji szumu Perlina (zaadaptowanej z tego repozytorium Github ). Następnie użyjemy go do określenia mapowania współrzędnych od współrzędnych wejściowych do współrzędnych wyjściowych (tj. mapa przemieszczenia).
Stosując tę funkcję szumu, uzyskujemy znacznie lepszy wynik! Meduza wygląda, jakby poruszała się w wodzie.

Ale po co miałbym tego używać?
W tym momencie możesz się zastanawiać, to jest fajne — ale bardzo niszowe w przypadku użycia, Rebecca. Jasne — może nie robisz codziennie w pracy animowanej meduzy (możemy pomarzyć, prawda?). Można go jednak RenderEffects
zastosować do dowolnego drzewa, które można komponować — co pozwala zastosować efekty do niemal wszystkiego, co chcesz.
Na przykład, dlaczego nie chcesz, aby tekst gradientu lub cały ekran, który można komponować, miał efekt szumu lub inny efekt AGSL, którego dusza zapragnie?

Podsumuj
Dlatego w tym poście omówiliśmy wiele interesujących koncepcji — tworzenie niestandardowych ImageVectors
plików SVG, animowanie części pliku ImageVector
i stosowanie shaderów AGSL w RenderEffects
naszym interfejsie użytkownika w programie Compose.
Aby uzyskać pełny kod Meduzy — sprawdź pełną istotę tutaj . Aby uzyskać więcej informacji na temat AGSL RenderEffects — zapoznaj się z dokumentacją lub próbką JetLagged , aby zobaczyć inny przykład użycia.
Jeśli masz jakieś pytania — skontaktuj się z Mastodon androiddev.social/@riggaroo lub na Twitterze .
Podziękowania dla Jolandy Verhoef , Nicka Butchera , Floriny Muntenescu , Romaina Guya i Nadera Jawada za cenne uwagi dotyczące tego posta.