Fazendo a água-viva se mover no Compose: Animando ImageVectors e aplicando AGSL RenderEffects

Nov 25 2022
Adoro seguir pessoas inspiradoras na internet e ver o que elas fazem — uma delas é a Cassie Codes, ela faz animações incríveis para a web. Um de seus exemplos inspiradores é esta linda água-viva animada.

Adoro seguir pessoas inspiradoras na internet e ver o que elas fazem — uma delas é a Cassie Codes , ela faz animações incríveis para a web. Um de seus exemplos inspiradores é esta linda água-viva animada .

Depois de ver isso e ficar obcecado com isso por um tempo, fiquei pensando comigo mesmo que essa criaturinha fofa também precisa ganhar vida no Compose. Portanto, esta postagem do blog descreve como fiz isso no Jetpack Compose, o código final pode ser encontrado aqui . As técnicas aqui não são relevantes apenas para águas-vivas, é claro... qualquer outro peixe também serve! Brincadeirinha - esta postagem do blog abordará:

  • ImageVectors personalizados
  • Animando Caminhos ou Grupos de ImageVector
  • Aplicando um efeito de ruído de distorção em um Composable com AGSL RenderEffect.

Analisando o SVG

Para implementar essa água-viva, precisamos primeiro ver do que o SVG é feito — e tentar replicar as diferentes partes dele. A melhor maneira de descobrir o que um SVG está desenhando é comentar várias partes dele e ver o resultado visual do que cada seção do SVG renderiza. Para fazer isso, você pode alterá-lo no codepen vinculado acima ou baixar e abrir um SVG em um editor de texto (é um formato de texto legível).

Então, vamos dar uma olhada geral neste 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. Caminhos e grupos de caminhos que compõem o SVG:
  2. tentáculos
  3. Rosto — bolha e gelatina externa
  4. Olhos — a animação aberta e fechada
  5. Bolhas — animações aleatórias ao redor da água-viva — animações de tamanho e alfa
  6. Agora que entendemos do que esse SVG é feito, vamos renderizar a versão estática no Compose.

    Criando ImageVector personalizado

    O Compose tem um conceito de ImageVector , onde você pode criar um vetor programaticamente — semelhante ao SVG. Para vetores/SVGs que você deseja apenas renderizar sem alterar, você também pode carregar um VectorDrawable usando painterResource(R.drawable.vector_image). Isso cuidará de convertê-lo em um ImageVector que o Compose renderizará.

    Agora você deve estar se perguntando - por que não importar a água-viva como um SVG para um arquivo xml e carregá-lo usando painterResource(R.drawable.jelly_fish)?

    Essa é uma ótima pergunta — e é possível carregar a água-viva dessa forma, removendo o aspecto de turbulência do SVG e a imagem será renderizada com um XML carregado (conforme explicado na documentação aqui ). Mas queremos fazer um pouco mais com as partes individuais do caminho, como animar partes ao clicar e aplicar um efeito de ruído ao corpo, para que possamos construir nosso ImageVectorprogramaticamente.

    Para renderizar essa água-viva no Compose, podemos copiar os dados do caminho (ou a dtag “ ” no caminho) que compõe o peixe, por exemplo, o primeiro tentáculo tem os seguintes dados do caminho:

    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 : Mover para
    • L, l, H, h, V, v : Linha para
    • C, c, S, s : Curva cúbica de Bézier para
    • Q, q, T, t: curva quadrática de Bézier para
    • A, a: Curva de arco elíptico para
    • Z, z — Fechar o caminho

    Agora você provavelmente está pensando - eu tenho que desenhar na minha cabeça e saber todas as posições e comandos à mão? Não - de jeito nenhum. Você pode criar um vetor na maioria dos programas de design — como Figma ou Inkscape, e exportar o resultado de seu desenho para um SVG para obter essas informações por conta própria. Ufa!

    Para criar o vetor no Compose: chamamos rememberVectorPainter, que cria um ImageVector, e criamos um Groupchamado jellyfish, depois outro Groupchamado tentaclese colocamos o primeiro Pathdentro dele para o primeiro tentáculo. Também definimos um RadialGradientcomo plano de fundo para toda a água-viva.

    E o resultado a seguir é um pequeno tentáculo desenhado na tela com um fundo gradiente radial!

    Primeiro tentáculo renderizado

    Repetimos esse processo para todos os elementos do SVG — pegando os bits do caminho do arquivo SVG e aplicando a cor e o alfa ao caminho que será desenhado, também agrupamos logicamente os caminhos nos tentáculos, no rosto, no bolhas etc:

    Agora temos toda a renderização da água-viva com o exemplo acima ImageVector:

    Renderização de água-viva estática inteira no Compose

    Animando Caminhos e Grupos de ImageVector

    Queremos animar partes deste vetor:

    • A água-viva deve mover-se para cima e para baixo lentamente
    • Os olhos devem piscar no clique da água-viva
    • O corpo da água-viva deve ter um efeito oscilante/ruído aplicado a ele.

    Então, vamos ver como podemos animar bits individuais do arquivo ImageVector.

    Movendo a água-viva para cima e para baixo

    Olhando para o codepen, podemos ver que a água-viva está se movendo com uma translação para cima e para baixo (translação y). Para fazer isso na composição, criamos uma transição infinita e um translationYque será animado em 3000 millis, então definimos o grupo contendo a água-viva e o rosto para ter um translationY, isso produzirá a animação para cima e para baixo.

    Tradução para cima e para baixo

    Ótimo — parte do ImageVectoragora está animando para cima e para baixo, você notará que as bolhas permanecem na mesma posição.

    Olhos Piscando ️

    Olhando para o codepen, podemos ver que há uma animação scaleYe opacityem cada um dos olhos. Vamos criar essas duas variáveis ​​e aplicar a escala no Groupe o alfa no Path. Também iremos aplicá-los apenas no clique da água-viva, para tornar esta animação mais interativa.

    Criamos dois Animatables que manterão o estado de animação e uma função de suspensão que chamaremos ao clicar na água-viva — animamos essas propriedades para dimensionar e esmaecer os olhos.

    Agora temos uma linda animação piscando ao clicar - e nossa água-viva está quase completa!

    Piscando ao clicar em ImageVector

    Aplicando um efeito de distorção/ruído

    Portanto, temos a maioria das coisas que queremos animar - o movimento para cima e para baixo e o piscar. Vejamos como o corpo da água-viva tem esse efeito oscilante aplicado a ele, o corpo e os tentáculos se movem com ruído aplicado a eles para dar uma sensação de movimento.

    Codepen: água-viva sem ruído vs com ruído aplicado

    Observando o SVG e o código de animação, podemos ver que ele usa feTurbulencepara gerar ruído que é aplicado ao SVG como um arquivo 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 conseguir isso, vale a pena notar que isso só é suportado no Tiramisu e superior (API 33+). Primeiro, precisamos criar um shader que atuará como uma oscilação, não usaremos ruído no início - apenas uma função de mapeamento para simplificar.

    A maneira como os sombreadores funcionam é que eles agem em pixels individuais — obtemos uma coordenada ( fragCoord) e esperamos produzir um resultado de cor que será renderizado nessa coordenada. Abaixo está o shader inicial que usaremos para transformar o que pode ser composto:

    Em nosso caso, a entrada que usaremos são nossos pixels atualmente renderizados na tela. Temos acesso a isso através da uniform shader contents;variável que enviaremos como entrada. Pegamos a coordenada de entrada ( fragCoord) e aplicamos algumas transformações nessa coordenada — movendo-a com o tempo e, em geral, realizando alguns cálculos nela para movê-la.

    Isso produz uma nova coordenada, portanto, em vez de retornar a cor exata na fragCoordposição, mudamos de onde obtemos o pixel de entrada. Por exemplo, se tivéssemos return contents.eval(fragCoord), não produziria nenhuma mudança — seria um pass-through. Agora obtemos a cor do pixel de um ponto diferente do que pode ser composto — o que criará um efeito de distorção instável no conteúdo do que pode ser composto.

    Para usar isso em nosso elemento que pode ser composto, podemos aplicar esse sombreador como um RenderEffectao conteúdo do elemento que pode ser composto:

    Usamos createRuntimeShaderEffect, passando o WOBBLE_SHADERcomo entrada. Isso pega o conteúdo atual do que pode ser composto e o fornece como entrada no sombreador, com o nome do parâmetro “ contents”. Em seguida, consultamos o conteúdo dentro do arquivo WOBBLE_SHADER. A timevariável altera a oscilação ao longo do tempo (criando a animação).

    Executando isso, podemos ver que o todo Imageagora está distorcido e parece um pouco mais instável - como uma água-viva.

    Wobble aplicado em toda a água-viva

    Se quisermos que o efeito não seja aplicado ao rosto e às bolhas, podemos extraí-los em separado ImageVectorse pular a aplicação do efeito de renderização a esses vetores:

    Wobble aplicado sem afetar o rosto

    Aplicando efeito de ruído

    O sombreador que especificamos acima não está usando uma função de ruído para aplicar um deslocamento ao conteúdo do que pode ser composto. O ruído é uma forma de aplicar um deslocamento, com uma função aleatória mais estruturada. Um desses tipos de ruído é o ruído Perlin (que é feTurbulenceusado sob o capô), é assim que pareceria se renderizássemos o resultado da execução da função de ruído Perlin:

    Saída de Ruído Perlin

    Usamos o valor de ruído para cada coordenada no espaço e o usamos para consultar uma nova coordenada no contentssombreador “ ”.

    Vamos atualizar nosso shader para usar uma função de ruído Perlin (adaptado deste repositório do Github ). Nós o usaremos para determinar o mapeamento de coordenadas da coordenada de entrada para a coordenada de saída (ou seja, um mapa de deslocamento).

    Aplicando esta função de ruído, obtemos um resultado muito melhor! A água-viva parece estar se movendo dentro da água.

    Perlin Noise aplicado ao corpo da água-viva

    Mas por que eu usaria isso?

    Neste ponto, você deve estar se perguntando, isso é legal - mas muito nicho em seu caso de uso, Rebecca. Claro - talvez você não esteja fazendo uma água-viva animada todos os dias no trabalho (podemos sonhar, certo?). Mas RenderEffectspode ser aplicado a qualquer árvore combinável - permitindo que você aplique efeitos a praticamente qualquer coisa que desejar.

    Por exemplo, por que você não gostaria que seu texto gradiente ou toda a tela que pode ser composta tivesse um efeito de ruído ou qualquer outro efeito AGSL que seu coração desejar?

    Perlin Noise aplicado a todo o Composable

    Encerrar

    Portanto, abordamos muitos conceitos interessantes nesta postagem do blog — criar ImageVectorsSVGs personalizados, animar partes de um ImageVectore aplicar sombreadores AGSL RenderEffectsà nossa interface do usuário no Compose.

    Para o código completo do Jellyfish — confira a essência completa aqui . Para obter mais informações sobre o AGSL RenderEffects — verifique a documentação ou o JetLagged Sample para outro exemplo de uso dele.

    Se você tiver alguma dúvida, sinta-se à vontade para entrar em contato no Mastodon androiddev.social/@riggaroo ou no Twitter .

    Obrigado a Jolanda Verhoef , Nick Butcher , Florina Muntenescu , Romain Guy e Nader Jawad pelo feedback valioso sobre este post.