Far muovere le meduse in Compose: animazione di ImageVector e applicazione di AGSL RenderEffects

Nov 25 2022
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.

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>

  1. Vie e Gruppi di Vie che compongono la SVG:
  2. Tentacoli
  3. Faccia - blob e gelatina esterna
  4. Occhi — l'animato aperto e chiuso
  5. Bolle - animate in modo casuale attorno alla medusa - le dimensioni e l'alfa si animano
  6. 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 ImageVectorprogrammaticamente.

    Per rendere questa medusa in Compose, possiamo copiare i dati del percorso (o il dtag " " 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
    

    • 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

    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 Groupcalled jellyfish, poi un altro Groupcalled tentaclese posizioniamo il primo Pathal suo interno per il primo tentacolo. Impostiamo anche a RadialGradientcome sfondo per l'intera medusa.

    E il risultato di quanto segue è un piccolo tentacolo disegnato sullo schermo con uno sfondo sfumato radiale!

    Primo tentacolo reso

    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:

    Rendering completo di meduse statiche in Compose

    Animazione di percorsi e gruppi ImageVector

    Vogliamo animare parti di questo vettore:

    • 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.

    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 translationYche 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ù.

    Traduzione Su e giù

    Fantastico: parte ImageVectordell'ora si sta animando su e giù, noterai che le bolle rimangono nella stessa posizione.

    Occhi lampeggianti ️

    Guardando il codepen, possiamo vedere che c'è scaleYun'animazione opacitysu ciascuno degli occhi. Creiamo queste due variabili e applichiamo la scala a Groupe 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!

    Lampeggiante al clic di ImageVector

    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.

    Codepen: Jellyfish senza rumore vs con rumore applicato

    Osservando l'SVG e il codice di animazione, possiamo vedere che utilizza feTurbulenceper 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 fragCoordposizione, 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 RenderEffecta al contenuto del componibile:

    Usiamo createRuntimeShaderEffect, passando WOBBLE_SHADERcome 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 timevariabile modifica l'oscillazione nel tempo (creando l'animazione).

    Eseguendo questo, possiamo vedere che il tutto Imageora è distorto e sembra un po' più traballante, proprio come una medusa.

    Wobble applicato su tutta la medusa

    Se volessimo che l'effetto non si applichi alla faccia e alle bolle, possiamo estrarli in separate ImageVectorse saltare l'applicazione dell'effetto di rendering a quei vettori:

    Wobble Applicato senza intaccare il viso

    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 feTurbulenceusa sotto il cofano), ecco come sarebbe se rendiamo il risultato dell'esecuzione della funzione rumore Perlin:

    Perlin Uscita del rumore

    Usiamo il valore del rumore per ogni coordinata nello spazio e lo usiamo per interrogare una nuova coordinata nello contentsshader " ".

    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.

    Perlin Noise applicato al corpo della medusa

    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 RenderEffectspuò 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?

    Perlin Noise applicato all'intero Composable

    Concludi

    Quindi abbiamo trattato molti concetti interessanti in questo post del blog: creazione personalizzata ImageVectorsda SVG, animazione di parti di un ImageVectore applicazione di shader AGSL RenderEffectsalla 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.