Far muovere le meduse in Compose: animazione di ImageVector e applicazione di AGSL RenderEffects
Adoro seguire persone stimolanti su Internet e vedere cosa fanno: una di queste persone è Cassie Codes , crea animazioni incredibili per il web. Uno dei suoi esempi ispiratori è questa simpatica medusa animata .
Dopo averlo visto e averci ossessionato per un po', ho continuato a pensare tra me e me che anche questa piccola creatura carina doveva prendere vita in Compose. Quindi questo post sul blog descrive come ho fatto a farlo in Jetpack Compose, il codice finale può essere trovato qui . Le tecniche qui descritte non sono rilevanti solo per le meduse ovviamente... anche qualsiasi altro pesce andrà bene! Sto solo scherzando: questo post sul blog coprirà:
- ImageVector personalizzati
- Animazione di percorsi o gruppi ImageVector
- Applicazione di un effetto di rumore di distorsione su un oggetto Composable con AGSL RenderEffect.
Analisi dell'SVG
Per implementare questa medusa, dobbiamo prima vedere di cosa è composto l'SVG e provare a replicare le diverse parti di esso. Il modo migliore per capire cosa sta disegnando un file SVG è commentare varie parti di esso e vedere il risultato visivo di ciò che rende ogni sezione del file svg. Per fare ciò, puoi modificarlo nella codepen collegata sopra o scaricare e aprire un SVG in un editor di testo (è un formato leggibile dal testo).
Quindi diamo uno sguardo d'insieme a questo 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>
- Vie e Gruppi di Vie che compongono la SVG:
- Tentacoli
- Faccia - blob e gelatina esterna
- Occhi — l'animato aperto e chiuso
- Bolle - animate in modo casuale attorno alla medusa - le dimensioni e l'alfa si animano
- M, m : Sposta a
- L, l, H, h, V, v : Linea a
- C, c, S, s : Curva di Bézier cubica a
- Q, q, T, t: curva di Bézier quadratica a
- A, a: Curva ad arco ellittico a
- Z, z — Chiude il percorso
- La medusa dovrebbe muoversi su e giù lentamente
- Gli occhi dovrebbero lampeggiare al clic della medusa
- Al corpo della medusa dovrebbe essere applicato un effetto traballante/rumore.
Ora che abbiamo capito di cosa è composto questo SVG, procediamo al rendering della versione statica in Compose.
Creazione di ImageVector personalizzato
Compose ha un concetto di ImageVector , in cui è possibile creare un vettore a livello di codice, simile a SVG. Per i vettori/SVG che vuoi solo renderizzare senza modifiche, puoi anche caricare un VectorDrawable usando painterResource(R.drawable.vector_image). Questo si occuperà di convertirlo in un ImageVector che Compose renderà.
Ora potresti chiederti: perché non importare semplicemente la medusa come SVG in un file xml e caricarla usando painterResource(R.drawable.jelly_fish)
?
Questa è un'ottima domanda - ed è possibile caricare la medusa in questo modo, rimuovendo l'aspetto di turbolenza dell'SVG e l'immagine verrà renderizzata con un XML caricato (come spiegato nella documentazione qui ). Ma vogliamo fare un po' di più con le singole parti del percorso, come animare parti al clic e applicare un effetto rumore al corpo, quindi costruiremo il nostro ImageVector
programmaticamente.
Per rendere questa medusa in Compose, possiamo copiare i dati del percorso (o il d
tag " " sul percorso) che compongono il pesce, ad esempio, il primo tentacolo ha i seguenti dati del percorso:
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
Ora probabilmente stai pensando: devo disegnare nella mia testa e conoscere tutte le posizioni e i comandi a mano? No, per niente. Puoi creare un vettore nella maggior parte dei programmi di progettazione, come Figma o Inkscape, ed esportare il risultato del tuo disegno in un file SVG per ottenere queste informazioni per te stesso. Accidenti!
Per creare il vettore in Compose: chiamiamo rememberVectorPainter
, che crea un ImageVector
, e creiamo un Group
called jellyfish
, poi un altro Group
called tentacles
e posizioniamo il primo Path
al suo interno per il primo tentacolo. Impostiamo anche a RadialGradient
come sfondo per l'intera medusa.
E il risultato di quanto segue è un piccolo tentacolo disegnato sullo schermo con uno sfondo sfumato radiale!
Ripetiamo questo processo per tutti gli elementi dell'SVG: prendendo i bit del percorso dal file SVG e applicando il colore e l'alfa al percorso che verrà disegnato, raggruppiamo anche logicamente i percorsi nei tentacoli, la faccia, il bolle ecc:
Ora abbiamo il nostro intero rendering di meduse con quanto sopra ImageVector
:
Animazione di percorsi e gruppi ImageVector
Vogliamo animare parti di questo vettore:
Quindi vediamo come possiamo animare i singoli bit del file ImageVector
.
Muovere la medusa su e giù
Osservando il codepen, possiamo vedere che la medusa si muove con una traslazione su e giù (traduzione y). Per fare questo in composizione, creiamo una transizione infinita e una translationY
che sarà animata di oltre 3000 millis, quindi impostiamo il gruppo contenente la medusa e la faccia per avere un translationY
, questo produrrà l'animazione su e giù.
Fantastico: parte ImageVector
dell'ora si sta animando su e giù, noterai che le bolle rimangono nella stessa posizione.
Occhi lampeggianti ️
Guardando il codepen, possiamo vedere che c'è scaleY
un'animazione opacity
su ciascuno degli occhi. Creiamo queste due variabili e applichiamo la scala a Group
e l'alfa su Path
. Li applicheremo anche solo al clic della medusa, per rendere questa animazione più interattiva.
Creiamo due Animatables che manterranno lo stato di animazione e una funzione di sospensione che chiameremo al clic sulla medusa: animiamo queste proprietà per ridimensionare e sfumare gli occhi.
Ora abbiamo una simpatica animazione lampeggiante al clic e la nostra medusa è quasi completa!
Applicazione di un effetto distorsione/rumore
Quindi abbiamo la maggior parte delle cose che vogliamo animare: il movimento su e giù e il battito delle palpebre. Diamo un'occhiata a come il corpo della medusa ha quell'effetto traballante applicato su di esso, il corpo e i tentacoli si muovono con il rumore applicato a loro per dargli un senso di movimento su di esso.
Osservando l'SVG e il codice di animazione, possiamo vedere che utilizza feTurbulence
per generare rumore che viene quindi applicato all'SVG come file 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)">
Possiamo utilizzare gli shader AGSL per raggiungere questo obiettivo, vale la pena notare che questo è supportato solo su Tiramisu e versioni successive (API 33+). Per prima cosa dobbiamo creare uno shader che funga da oscillazione, all'inizio non useremo il rumore, ma solo una funzione di mappatura per semplicità.
Il modo in cui funzionano gli shader è che agiscono su singoli pixel: otteniamo una coordinata ( fragCoord
) e ci si aspetta che produciamo un risultato cromatico che verrà renderizzato a quella coordinata. Di seguito è riportato lo shader iniziale che useremo per trasformare il componibile:
Nel nostro caso, l'input che useremo sono i nostri pixel attualmente renderizzati sullo schermo. Otteniamo l'accesso a questo tramite la uniform shader contents;
variabile che invieremo come input. Prendiamo la coordinata di input ( fragCoord
) e applichiamo alcune trasformazioni su questa coordinata, spostandola nel tempo e generalmente eseguendo alcuni calcoli su di essa per spostarla.
Questo produce una nuova coordinata, quindi invece di restituire il colore esatto nella fragCoord
posizione, ci spostiamo da dove otteniamo il pixel di input. Ad esempio, se avessimo return contents.eval(fragCoord)
, non produrrebbe alcun cambiamento: sarebbe un pass-through. Ora otteniamo il colore dei pixel da un punto diverso del componibile, che creerà un effetto di distorsione traballante sul contenuto del componibile.
Per usarlo sul nostro componibile, possiamo applicare questo shader come RenderEffect
a al contenuto del componibile:
Usiamo createRuntimeShaderEffect
, passando WOBBLE_SHADER
come input. Questo prende il contenuto corrente del componibile e lo fornisce come input nello shader, con il nome del parametro " contents
". Quindi interroghiamo il contenuto all'interno del file WOBBLE_SHADER
. La time
variabile modifica l'oscillazione nel tempo (creando l'animazione).
Eseguendo questo, possiamo vedere che il tutto Image
ora è distorto e sembra un po' più traballante, proprio come una medusa.
Se volessimo che l'effetto non si applichi alla faccia e alle bolle, possiamo estrarli in separate ImageVectors
e saltare l'applicazione dell'effetto di rendering a quei vettori:
Applicazione dell'effetto rumore
Lo shader che abbiamo specificato sopra non utilizza una funzione noise per applicare uno spostamento al contenuto del componibile. Il rumore è un modo per applicare uno spostamento, con una funzione casuale più strutturata. Uno di questi tipi di rumore è il rumore Perlin (che è quello che feTurbulence
usa sotto il cofano), ecco come sarebbe se rendiamo il risultato dell'esecuzione della funzione rumore Perlin:
Usiamo il valore del rumore per ogni coordinata nello spazio e lo usiamo per interrogare una nuova coordinata nello contents
shader " ".
Aggiorniamo il nostro shader per utilizzare una funzione di rumore Perlin (adattata da questo repository Github ). Lo useremo quindi per determinare la mappatura delle coordinate dalla coordinata di input alla coordinata di output (ovvero una mappa di spostamento).
Applicando questa funzione di rumore, otteniamo un risultato molto migliore! La medusa sembra muoversi all'interno dell'acqua.
Ma perché dovrei usare questo?
A questo punto ti starai chiedendo, è bello, ma molto di nicchia nel suo caso d'uso, Rebecca. Certo, forse non stai realizzando una medusa animata ogni giorno al lavoro (possiamo sognare, vero?). Ma RenderEffects
può essere applicato a qualsiasi albero componibile, consentendoti di applicare effetti praticamente a tutto ciò che desideri.
Ad esempio, perché non vorresti che il tuo testo sfumato o l'intero schermo componibile avesse un effetto rumore o qualsiasi altro effetto AGSL che il tuo cuore desidera?
Concludi
Quindi abbiamo trattato molti concetti interessanti in questo post del blog: creazione personalizzata ImageVectors
da SVG, animazione di parti di un ImageVector
e applicazione di shader AGSL RenderEffects
alla nostra interfaccia utente in Compose.
Per il codice completo di Jellyfish, dai un'occhiata all'essenza completa qui . Per ulteriori informazioni su AGSL RenderEffects , consulta la documentazione o JetLagged Sample per un altro esempio di utilizzo.
Se hai domande, non esitare a contattarci su Mastodon androiddev.social/@riggaroo o su Twitter .
Grazie a Jolanda Verhoef , Nick Butcher , Florina Muntenescu , Romain Guy , Nader Jawad per il prezioso feedback su questo post.