Hacer que las medusas se muevan en Compose: Animar ImageVectors y aplicar AGSL RenderEffects

Me encanta seguir a personas inspiradoras en Internet y ver lo que hacen; una de esas personas es Cassie Codes , hace animaciones increíbles para la web. Uno de sus ejemplos inspiradores es esta linda medusa animada .
Después de ver esto y obsesionarme con él por un tiempo, seguí pensando que esta linda criaturita también necesita cobrar vida en Compose. Entonces, esta publicación de blog describe cómo hice esto en Jetpack Compose, el código final se puede encontrar aquí . Las técnicas aquí no solo son relevantes para las medusas, por supuesto... ¡cualquier otro pez también lo hará! Es broma: esta publicación de blog cubrirá:
- Vectores de imagen personalizados
- Animación de rutas o grupos de ImageVector
- Aplicando un efecto de ruido de distorsión en un Composable con AGSL RenderEffect.
Analizando el SVG
Para implementar esta medusa, primero debemos ver de qué está compuesto el SVG e intentar replicar las diferentes partes. La mejor manera de averiguar qué está dibujando un SVG es comentar varias partes y ver el resultado visual de lo que representa cada sección del svg. Para hacer esto, puede cambiarlo en el codepen vinculado anteriormente o descargar y abrir un SVG en un editor de texto (es un formato de texto legible).
Así que echemos un vistazo general a este 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>
- Caminos y Grupos de caminos que componen el SVG:
- tentáculos
- Cara: gota y gelatina exterior
- Ojos: el animado abierto y cerrado.
- Burbujas: animación aleatoria alrededor de la medusa: el tamaño y la animación alfa
- M, m : Mover a
- L, l, H, h, V, v : Línea a
- C, c, S, s : Curva de Bézier cúbica a
- Q, q, T, t: Curva de Bézier cuadrática a
- A, a: Curva de arco elíptico a
- Z, z — Cierra el camino
- La medusa debe moverse hacia arriba y hacia abajo lentamente.
- Los ojos deben parpadear al hacer clic en la medusa.
- El cuerpo de la medusa debe tener un efecto tambaleante/ruido aplicado.
Ahora que entendemos de qué está compuesto este SVG, vamos a renderizar la versión estática en Compose.
Creación de ImageVector personalizado
Compose tiene un concepto de ImageVector , donde puede crear un vector mediante programación, similar a SVG. Para los vectores/SVG que solo desea renderizar sin cambiarlos, también puede cargar un VectorDrawable usando pintorResource(R.drawable.vector_image). Esto se encargará de convertirlo en un ImageVector que Compose renderizará.
Ahora puede que se pregunte: ¿por qué no simplemente importar la medusa como un SVG en un archivo xml y cargarlo usando painterResource(R.drawable.jelly_fish)
?
Esa es una gran pregunta, y es posible cargar la medusa de esta manera, eliminando el aspecto de turbulencia del SVG y la imagen se renderizará con un XML cargado (como se explica en la documentación aquí ). Pero queremos hacer un poco más con las partes individuales de la ruta, como animar partes al hacer clic y aplicar un efecto de ruido al cuerpo, por lo que construiremos nuestra ImageVector
programación.
Para renderizar esta medusa en Compose, podemos copiar los datos de la ruta (o la d
etiqueta “ ” en la ruta) que forman el pez, por ejemplo, el primer tentáculo tiene los siguientes datos de la ruta:
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
Ahora probablemente esté pensando: ¿tengo que dibujar en mi cabeza y saber todas las posiciones y comandos a mano? No, en absoluto. Puede crear un vector en la mayoría de los programas de diseño, como Figma o Inkscape, y exportar el resultado de su dibujo a un SVG para obtener esta información por sí mismo. ¡Uf!
Para crear el vector en Compose: llamamos rememberVectorPainter
, que crea un ImageVector
, y creamos un Group
llamado jellyfish
, luego otro Group
llamado tentacles
y colocamos el primero Path
dentro de él para el primer tentáculo. También establecemos a RadialGradient
como fondo para toda la medusa.
¡Y el resultado de lo siguiente es un pequeño tentáculo dibujado en la pantalla con un fondo degradado radial!

Repetimos este proceso para todos los elementos del SVG: tomando los bits de la ruta del archivo SVG y aplicando el color y alfa a la ruta que se dibujará, también agrupamos lógicamente las rutas en los tentáculos, la cara, el burbujas, etc.:
Ahora tenemos todo nuestro renderizado de medusas con lo anterior ImageVector
:

Animación de rutas y grupos de ImageVector
Queremos animar partes de este vector:
Entonces, veamos cómo podemos animar partes individuales del archivo ImageVector
.
Moviendo la medusa arriba y abajo
Mirando el copen, podemos ver que la medusa se mueve con una traslación hacia arriba y hacia abajo (traslación y). Para hacer esto en composición, creamos una transición infinita y una translationY
que se animará a más de 3000 milisegundos, luego configuramos el grupo que contiene la medusa y la cara para que tenga un translationY
, esto producirá la animación hacia arriba y hacia abajo.

Genial: parte de ImageVector
ahora se está animando hacia arriba y hacia abajo, notará que las burbujas permanecen en la misma posición.
Ojos parpadeantes ️
Mirando el codepen, podemos ver que hay una scaleY
animación opacity
en cada uno de los ojos. Vamos a crear estas dos variables y aplicar la escala al Group
y el alfa en el Path
. También los aplicaremos solo al hacer clic en la medusa, para hacer de esta una animación más interactiva.
Creamos dos Animatables que mantendrán el estado de animación y una función de suspensión que llamaremos al hacer clic en la medusa: animamos estas propiedades para escalar y desvanecer los ojos.
Ahora tenemos una linda animación parpadeante al hacer clic, ¡y nuestra medusa está casi completa!

Aplicando un efecto de distorsión/ruido
Así que tenemos la mayoría de las cosas que queremos animar: el movimiento hacia arriba y hacia abajo y el parpadeo. Veamos cómo el cuerpo de la medusa tiene ese efecto tambaleante aplicado, el cuerpo y los tentáculos se mueven con el ruido aplicado para darle una sensación de movimiento.

Al observar el SVG y el código de animación, podemos ver que feTurbulence
genera ruido que luego se aplica al SVG como un archivo 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)">
Podemos usar sombreadores AGSL para lograr esto, vale la pena señalar que esto solo es compatible con Tiramisu y versiones posteriores (API 33+). Primero, necesitamos crear un sombreador que actúe como un bamboleo, no usaremos ruido al principio, solo una función de mapeo en su lugar para simplificar.
La forma en que funcionan los sombreadores es que actúan sobre píxeles individuales; obtenemos una coordenada ( fragCoord
) y se espera que produzcamos un resultado de color que se representará en esa coordenada. A continuación se muestra el sombreador inicial que usaremos para transformar el componible:
En nuestro caso, la entrada que usaremos son los píxeles actualmente representados en la pantalla. Obtenemos acceso a esto a través de la uniform shader contents;
variable que enviaremos como entrada. Tomamos la coordenada de entrada ( fragCoord
) y aplicamos algunas transformaciones en esta coordenada, moviéndola con el tiempo y, en general, realizando algunos cálculos matemáticos para moverla.
Esto produce una nueva coordenada, por lo que en lugar de devolver el color exacto en la fragCoord
posición, cambiamos de donde obtenemos el píxel de entrada. Por ejemplo, si tuviéramos return contents.eval(fragCoord)
, no produciría ningún cambio, sería un traspaso. Ahora obtenemos el color del píxel de un punto diferente del componible, lo que creará un efecto de distorsión tambaleante en el contenido del componible.
Para usar esto en nuestro componible, podemos aplicar este shader como un RenderEffect
al contenido del componible:
Usamos createRuntimeShaderEffect
, pasando WOBBLE_SHADER
como entrada. Esto toma el contenido actual del componible y lo proporciona como entrada en el shader, con el nombre de parámetro “ contents
”. Luego consultamos el contenido dentro del archivo WOBBLE_SHADER
. La time
variable cambia la oscilación con el tiempo (creando la animación).
Al ejecutar esto, podemos ver que el conjunto Image
ahora está distorsionado y se ve un poco más tambaleante, como una medusa.

Si no queremos que el efecto se aplique a la cara y las burbujas, podemos extraerlos por separado ImageVectors
y omitir la aplicación del efecto de renderizado a esos vectores:

Aplicando Efecto de Ruido
El sombreador que especificamos arriba no usa una función de ruido para aplicar un desplazamiento al contenido del componible. El ruido es una forma de aplicar un desplazamiento, con una función aleatoria más estructurada. Uno de esos tipos de ruido es el ruido de Perlin (que es lo que se feTurbulence
usa debajo del capó), así es como se vería si representamos el resultado de ejecutar la función de ruido de Perlin:

Usamos el valor de ruido para cada coordenada en el espacio y lo usamos para consultar una nueva coordenada en el contents
sombreador “ ”.
Actualicemos nuestro sombreador para usar una función de ruido de Perlin (adaptada de este repositorio de Github ). Luego lo usaremos para determinar el mapeo de coordenadas desde la coordenada de entrada hasta la coordenada de salida (es decir, un mapa de desplazamiento).
¡Aplicando esta función de ruido, obtenemos un resultado mucho mejor! La medusa parece moverse dentro del agua.

Pero, ¿por qué usaría esto?
En este punto, es posible que se esté preguntando, esto es genial, pero muy especial en su caso de uso, Rebecca. Claro, tal vez no estés haciendo una medusa animada todos los días en el trabajo (podemos soñar, ¿verdad?). Pero RenderEffects
se puede aplicar a cualquier árbol componible, lo que le permite aplicar efectos a casi cualquier cosa que desee.
Por ejemplo, ¿por qué no querrías que tu texto degradado o toda la pantalla componible tuvieran un efecto de ruido o cualquier otro efecto AGSL que tu corazón desee?

Termina
Por lo tanto, hemos cubierto muchos conceptos interesantes en esta publicación de blog: creación personalizada ImageVectors
a partir de SVG, animación de partes de un ImageVector
y aplicación de sombreadores AGSL RenderEffects
en nuestra interfaz de usuario en Compose.
Para obtener el código completo de Jellyfish, consulte la esencia completa aquí . Para obtener más información sobre AGSL RenderEffects , consulte la documentación o el JetLagged Sample para ver otro ejemplo de uso.
Si tiene alguna pregunta, no dude en comunicarse con Mastodon androiddev.social/@riggaroo o Twitter .
Gracias a Jolanda Verhoef , Nick Butcher , Florina Muntenescu , Romain Guy , Nader Jawad por los valiosos comentarios sobre esta publicación.