Animação dentro e fora da caixa com o Jetpack Compose
Conteudo
Introdução
As animações têm o poder de fazer com que as interfaces de usuário pareçam vivas e envolventes. No Android, com o Jetpack Compose, esse poder está na ponta de seus dedos, oferecendo ferramentas avançadas para criar interfaces de usuário verdadeiramente dinâmicas. Neste artigo, iremos além do básico e exploraremos os aspectos mais profundos das animações no Jetpack Compose.
Abordaremos uma série de técnicas, desde a criação de movimentos fluidos e baseados em física que adicionam um toque de realismo até sequências coreografadas complexas que trazem uma qualidade narrativa às suas interfaces. Quer você esteja aperfeiçoando suas habilidades ou apenas curioso para saber o que é possível fazer, esta jornada fornecerá insights práticos para que seus aplicativos não apenas funcionem sem problemas, mas também encantem os usuários em cada interação.
Vamos mergulhar de cabeça e descobrir como essas animações podem transformar sua abordagem ao design da interface do usuário, tornando-a mais intuitiva, responsiva e agradável para os usuários.
Seção 1 – Manipuladores de animação personalizados no Jetpack Compose
Abraçando a interatividade dinâmica com animações personalizadas
Nesta seção, exploramos o uso de manipuladores de animação personalizados avançados no Jetpack Compose para criar elementos de interface do usuário dinâmicos e interativos. Nosso foco está em um exemplo do mundo real que demonstra como a interação do usuário pode influenciar uma animação de forma significativa.
Exemplo – Movimento interativo de personagens de jogos
Ilustraremos esse conceito com um exemplo em que um personagem de jogo (representado por um ícone de rosto) segue um caminho determinado por um ponto de controle arrastável pelo usuário.
@Composable fun GameCharacterMovement() { val startPosition = Offset(100f, 100f) val endPosition = Offset(250f, 400f) val controlPoint = remember { mutableStateOf(Offset(200f, 300f)) } val position = remember { Animatable(startPosition, Offset.VectorConverter) } LaunchedEffect(controlPoint.value) { position.animateTo( targetValue = endPosition, animationSpec = keyframes { durationMillis = 5000 controlPoint.value at 2500 // midway point controlled by the draggable control point } ) } val onControlPointChange: (offset: Offset) -> Unit = { controlPoint.value = it } Box(modifier = Modifier.fillMaxSize()) { Icon( Icons.Filled.Face, contentDescription = "Localized description", modifier = Modifier .size(50.dp) .offset(x = position.value.x.dp, y = position.value.y.dp) ) DraggableControlPoint(controlPoint.value, onControlPointChange) } }
Explicação
- O
GameCharacterMovement
anima um ícone que representa um personagem do jogo. O caminho da animação é controlado pelo controlPoint, que é definido e atualizado pela interação do usuário. Animatable
é usado para fazer uma transição suave da posição do ícone de startPosition para endPosition.- O
LaunchedEffect
escuta as alterações no valor do controlPoint, reativando a animação sempre que o ponto de controle é movido. - animationSpec – É uma configuração que define a duração, o atraso e a atenuação de uma animação. Ela determina como os valores animados mudam ao longo do tempo.
- keyframes – Permite especificar valores em momentos específicos durante a animação, dando-lhe controle sobre os pontos intermediários da animação. É particularmente útil para criar animações complexas e coreografadas.
- O bloco de
keyframes
define a animação como uma sequência de keyframes. Em 2500 milissegundos (o ponto intermediário), o personagem atinge o ponto de controle e continua até a posição final.
Composable fun DraggableControlPoint(controlPoint: Offset, onControlPointChange: (Offset) -> Unit) { var localPosition by remember { mutableStateOf(controlPoint) } Box( modifier = Modifier .offset { IntOffset( x = localPosition.x.roundToInt() - 15, y = localPosition.y.roundToInt() - 15 ) } .size(30.dp) .background(Color.Red, shape = CircleShape) .pointerInput(Unit) { detectDragGestures(onDragEnd = { onControlPointChange(localPosition) }) { _, dragAmount -> // adjust based on screen bounds val newX = (localPosition.x + dragAmount.x).coerceIn(0f, 600f) val newY = (localPosition.y + dragAmount.y).coerceIn(0f, 600f) localPosition = Offset(newX, newY) } } ) }
Explicação
DraggableControlPoint
é um composable que permite que o usuário altere interativamente a posição do ponto de controle.- Arrastar o ponto de controle atualiza
localPosition
, que é então refletido de volta para oGameCharacterMovement
após a conclusão do gesto de arrastar (onDragEnd
). Essa interação altera o caminho do ícone animado.
Casos de uso no mundo real
- Aplicativos educacionais interativos: em um aplicativo educacional, as animações podem ser usadas para tornar o aprendizado mais envolvente. Por exemplo, arrastar um planeta ao longo de sua órbita em um aplicativo de astronomia para ver diferentes constelações.
- Narração de histórias e jogos interativos: Em aplicativos de jogos ou histórias digitais, permitir que os usuários influenciem a história ou o ambiente do jogo por meio de elementos arrastáveis pode criar uma experiência mais envolvente.
Seção 2 – Coreografando animações complexas no Jetpack Compose
Sincronização de vários elementos para obter efeitos harmoniosos
Nesta seção, vamos nos aprofundar na arte de coreografar animações complexas no Jetpack Compose. Concentramo-nos na criação de animações sincronizadas em que vários elementos interagem perfeitamente, aprimorando a experiência geral do usuário.
A) Animações de reação em cadeia – O efeito dominó
É possível criar um efeito dominó na interface do usuário configurando uma série de animações em que a conclusão de uma aciona o início da próxima.
@Composable fun DominoEffect() { val animatedValues = List(6) { remember { Animatable(0f) } } LaunchedEffect(Unit) { animatedValues.forEachIndexed { index, animate -> animate.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = 1000, delayMillis = index * 100) ) } } Box (modifier = Modifier.fillMaxSize()){ animatedValues.forEachIndexed { index, value -> Box( modifier = Modifier .size(50.dp) .offset(x = ((index+1) * 50).dp, y = ((index+1) * 30).dp) .background(getRandomColor(index).copy(alpha = value.value)) ) } } } fun getRandomColor(seed: Int): Color { val random = Random(seed = seed).nextInt(256) return Color(random, random, random) }
Explicação
animatedValues
é uma lista de valores Animatable, cada um controlando o alfa (opacidade) de uma caixa.- O
LaunchedEffect
aciona uma sequência de animações para esses valores, criando um efeito escalonado em que cada caixa desaparece após a anterior, semelhante à queda de um dominó. - A função
getRandomColor
gera um tom aleatório de cinza para cada caixa, acrescentando um elemento visual exclusivo a cada componente da sequência. - As caixas são posicionadas diagonalmente na tela, aprimorando o efeito dominó.
B) Interactive Scrollable Timeline
Nessa linha do tempo, cada elemento aparecerá e se moverá para sua posição à medida que o usuário rolar pela linha do tempo. Usaremos LazyColumn
para a lista rolável e Animatable
para a animação.
@Composable fun InteractiveTimeline(timelineItems: List<String>) { val scrollState = rememberLazyListState() LazyColumn(state = scrollState) { itemsIndexed(timelineItems) { index, item -> val animatableAlpha = remember { Animatable(0f) } val isVisible = remember { derivedStateOf { scrollState.firstVisibleItemIndex <= index } } LaunchedEffect(isVisible.value) { if (isVisible.value) { animatableAlpha.animateTo( 1f, animationSpec = tween(durationMillis = 1000) ) } } TimelineItem( text = item, alpha = animatableAlpha.value, ) } } } @Composable fun TimelineItem(text: String, alpha: Float) { Row( modifier = Modifier .fillMaxWidth() .background(Color.DarkGray.copy(alpha = alpha)) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = text, color = Color.White, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 18.sp, fontWeight = FontWeight.SemiBold ) } }
Explicação
animatableAlpha
controla o alfa (opacidade) de cada item da linha do tempo, inicialmente definido como 0 (totalmente transparente).- O estado
isVisible
é derivado da posição de rolagem atual, determinando se o item deve estar visível. - À medida que o usuário rola a tela, o
LaunchedEffect
aciona a animação de fade-in para os itens que entram na janela de visualização.
Caso de uso
Essa linha do tempo interativa é ideal para aplicativos em que você deseja apresentar uma série de eventos ou etapas de uma forma visualmente atraente. A animação aumenta o envolvimento do usuário, chamando a atenção para os itens à medida que eles são exibidos.
Essas animações não são apenas cativantes, mas também podem ser usadas para orientar a atenção do usuário por meio de uma sequência de eventos ou ações em seu aplicativo.
Seção 3 – Animações baseadas em física para realismo no Jetpack Compose
Aproveitamento da física para aprimorar a dinâmica da interface do usuário
Nesta seção, exploramos como integrar princípios de física em animações com o Jetpack Compose, adicionando uma camada de realismo e interatividade à interface do usuário. Vamos nos concentrar em um exemplo de interação de arrasto elástico.
Efeito elástico no arrasto
Este exemplo ilustra uma interação de arrasto elástico em um ícone. Quando arrastado verticalmente, o ícone se estica e se recupera com um efeito elástico, simulando o comportamento de uma mola ou de um elástico.
@Composable fun ElasticDraggableBox() { var animatableOffset by remember { mutableStateOf(Animatable(0f)) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFFFA732)), contentAlignment = Alignment.Center) { Box( modifier = Modifier .offset(y = animatableOffset.value.dp) .draggable( orientation = Orientation.Vertical, state = rememberDraggableState { delta -> animatableOffset = Animatable(animatableOffset.value + delta) }, onDragStopped = { animatableOffset.animateTo(0f, animationSpec = spring()) } ) .size(350.dp), contentAlignment = Alignment.Center ) { Icon( Icons.Filled.Favorite, contentDescription = "heart", modifier = Modifier.size(animatableOffset.value.dp + 150.dp), tint = Color.Red ) } } }
Explicação
- Um
Box
composable, que contém um ícone, é tornado arrastável usando o modificadordraggable
. - O
animatableOffset
rastreia o deslocamento vertical do ícone devido ao arrastamento. - Durante o arrasto, o tamanho do ícone muda com base na quantidade arrastada, criando um efeito de alongamento.
- Quando o arrasto é interrompido (
onDragStopped
), oanimatableOffset
é animado de volta para0f
usando uma animação de mola, resultando no retorno do ícone ao seu tamanho e posição originais.
Seção 4 – Animações baseadas em gestos no Jetpack Compose
Aprimorando a experiência do usuário com gestos responsivos
Nesta seção, exploramos como o Jetpack Compose pode ser usado para criar animações que são controladas por gestos do usuário. Vamos nos concentrar em dois exemplos: uma imagem transformável multitoque e uma forma de onda de áudio controlada por gestos.
A) Imagem transformável multitoque
Neste exemplo, criaremos uma visualização de imagem com a qual os usuários podem interagir usando gestos multitoque, como pinçar, aplicar zoom e girar.
@Composable fun TransformableImage(imageId: Int = R.drawable.android) { var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color.DarkGray), contentAlignment = Alignment.Center) { Image( painter = painterResource(id = imageId), contentDescription = "Transformable image", contentScale = ContentScale.Crop, modifier = Modifier .size(300.dp) .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) .pointerInput(Unit) { detectTransformGestures { _, pan, zoom, rotate -> scale *= zoom rotation += rotate offset += pan } } ) } }
Explicação
- O
Image
composable é modificado comgraphicsLayer
para aplicar transformações como escala, rotação e translação. - O
pointerInput
comdetectTransformGestures
é usado para lidar com gestos multitoque, atualizando a escala, a rotação e o deslocamento de acordo.
B) Forma de onda controlada por gestos
Esta é uma visualização de forma de onda que muda sua aparência com base nos gestos do usuário, como deslizar e apertar, para controlar aspectos como amplitude e frequência.
@Composable fun GestureControlledWaveform() { var amplitude by remember { mutableStateOf(100f) } var frequency by remember { mutableStateOf(1f) } Canvas(modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectDragGestures { _, dragAmount -> amplitude += dragAmount.y frequency += dragAmount.x / 500f // Adjusting frequency based on drag } } .background( Brush.verticalGradient( colors = listOf(Color(0xFF003366), Color.White, Color(0xFF66B2FF)) ) )) { val width = size.width val height = size.height val path = Path() val halfHeight = height / 2 val waveLength = width / frequency path.moveTo(0f, halfHeight) for (x in 0 until width.toInt()) { val theta = (2.0 * Math.PI * x / waveLength).toFloat() val y = halfHeight + amplitude * sin(theta.toDouble()).toFloat() path.lineTo(x.toFloat(), y) } val gradient = Brush.horizontalGradient( colors = listOf(Color.Blue, Color.Cyan, Color.Magenta) ) drawPath( path = path, brush = gradient ) } }
Explicação
amplitude
efrequency
são variáveis de estado que controlam a amplitude e a frequência da forma de onda, respectivamente.- O
Canvas
composable é usado para desenhar a forma de onda. A lógica de desenho dentro doCanvas
calcula a posição Y para cada posição X com base na função senoidal, criando um efeito de onda. - O modificador
detectDragGestures
é usado para atualizar aamplitude
efrequency
com base nos gestos de arrastar do usuário. Os arrastamentos horizontais ajustam a frequência e os arrastamentos verticais ajustam a amplitude. - À medida que o usuário arrasta pela tela, o formato da forma de onda muda de acordo, criando uma experiência interativa.
Observação
- Esta é uma implementação básica. Para obter uma forma de onda de áudio mais realista, você precisaria integrar dados de áudio reais.
- A capacidade de resposta da forma de onda aos gestos pode ser ajustada com precisão, ajustando como a
amplitude
efrequency
são modificadas durante o arrasto.
Este exemplo demonstra como criar uma forma de onda interativa básica no Compose e pode ser estendido ou modificado para casos de uso mais complexos ou para lidar com gestos mais complexos.
Seção 5 – Padrões de animação orientados por estado no Jetpack Compose
Animação da IU com base em dados e alterações de estado
Esta seção se concentra na criação de animações que são acionadas por alterações nos dados ou no estado da interface do usuário, aprimorando a interatividade e a capacidade de resposta do aplicativo. Exploraremos dois exemplos específicos: a animação de um gráfico de dados e a implementação de transições de estado em uma IU de vários estados.
A) Animação de gráfico orientada por dados
Este exemplo demonstra um gráfico de linhas animado em que o caminho do gráfico é animado em resposta a alterações no conjunto de dados.
@Composable fun AnimatedGraphExample() { var dataPoints by remember { mutableStateOf(generateRandomDataPoints(5)) } Column( modifier = Modifier .fillMaxSize() .background(Color.DarkGray) ) { AnimatedLineGraph(dataPoints = dataPoints) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { dataPoints = generateRandomDataPoints(5) }, modifier = Modifier.align(Alignment.CenterHorizontally), colors = ButtonDefaults.buttonColors(containerColor = Color.Green) ) { Text( "Update Data", fontWeight = FontWeight.Bold, color = Color.DarkGray, fontSize = 18.sp ) } } } @Composable fun AnimatedLineGraph(dataPoints: List<Float>) { val animatableDataPoints = remember { dataPoints.map { Animatable(it) } } val path = remember { Path() } LaunchedEffect(dataPoints) { animatableDataPoints.forEachIndexed { index, animatable -> animatable.animateTo(dataPoints[index], animationSpec = TweenSpec(durationMillis = 500)) } } Canvas( modifier = Modifier .fillMaxWidth() .height(400.dp) ) { path.reset() animatableDataPoints.forEachIndexed { index, animatable -> val x = (size.width / (dataPoints.size - 1)) * index val y = size.height - (animatable.value * size.height) if (index == 0) path.moveTo(x, y) else path.lineTo(x, y) } drawPath(path, Color.Green, style = Stroke(5f)) } } fun generateRandomDataPoints(size: Int): List<Float> { return List(size) { Random.nextFloat() } }
Explicação
- O composable
AnimatedGraphExample
cria um ambiente em que os pontos de dados do gráfico de linhas podem ser atualizados. - O gráfico é desenhado em uma
Canvas
, onde o métododrawPath
usa valores animados deanimatableDataPoints
. - Para cada ponto de dados no gráfico, precisamos calcular as posições
x
(horizontal) ey
(vertical) correspondentes na tela. - Cálculo de
x
– A posiçãox
é calculada com base no índice do ponto de dados e na largura total da tela. Distribuímos uniformemente os pontos de dados ao longo da largura da tela.
val x = (size.width / (dataPoints.size - 1)) * index
- Cálculo y – A posição
y
é calculada com base no valor do ponto de dados (animatable.value
) e na altura da tela.
val y = size.height - (animatable.value * size.height)
- O caminho começa no primeiro ponto de dados e, em seguida,
lineTo
é usado para desenhar uma linha para cada ponto subsequente, criando a linha do gráfico. - O caminho é desenhado com base nos valores animados dos pontos de dados, o que cria o efeito de animação quando os dados são alterados.
B) Transição de estado em uma interface de usuário com vários estados
A implementação de transições de estado em uma IU com vários estados pode ser feita usando o Animatable para animar entre diferentes estados da IU.
enum class UIState { StateA, StateB, StateC } @Composable fun StateTransitionUI() { var currentState by remember { mutableStateOf(UIState.StateA) } Box( modifier = Modifier .fillMaxSize() .background(getBackgroundColorForState(currentState)), contentAlignment = Alignment.Center ) { AnimatedContent(currentState = currentState) Button( onClick = { currentState = getNextState(currentState) }, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("Next State") } } } @Composable fun AnimatedContent(currentState: UIState) { AnimatedVisibility( visible = currentState == UIState.StateA, enter = fadeIn(animationSpec = tween(durationMillis = 2000)) + expandVertically(), exit = fadeOut(animationSpec = tween(durationMillis = 2000)) + shrinkVertically() ) { Text("This is ${currentState.name}", fontSize = 32.sp) } // Similar blocks for B and C } fun getBackgroundColorForState(state: UIState): Color { return when (state) { UIState.StateA -> Color.Red UIState.StateB -> Color.Green UIState.StateC -> Color.Blue } } fun getNextState(currentState: UIState): UIState { return when (currentState) { UIState.StateA -> UIState.StateB UIState.StateB -> UIState.StateC UIState.StateC -> UIState.StateA } }
Explicação
- Neste exemplo, o
AnimatedVisibility
é usado para animar o aparecimento e o desaparecimento do conteúdo de cada estado. Isso adiciona um efeito de transição suave quando o estado muda. - Para cada estado (
StateA
,StateB
,StateC
), há um blocoAnimatedVisibility
que controla a visibilidade de seu conteúdo com animações de esmaecimento e expansão/redução. - Os parâmetros de
enter
eexit
doAnimatedVisibility
definem as animações para quando o conteúdo se torna visível ou oculto, respectivamente.
Seção 6 – Morphing Shapes no Compose
A animação da transformação entre formas envolve a interpolação das propriedades dessas formas.
fun ShapeMorphingAnimation() { val animationProgress = remember { Animatable(0f) } LaunchedEffect(Unit) { animationProgress.animateTo( targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(2000, easing = LinearOutSlowInEasing), repeatMode = RepeatMode.Reverse ) ) } Canvas(modifier = Modifier.padding(40.dp).fillMaxSize()) { val sizeValue = size.width.coerceAtMost(size.height) / 2 val squareRect = Rect(center = center, sizeValue) val morphedPath = interpolateShapes(progress = animationProgress.value, squareRect = squareRect) drawPath(morphedPath, color = Color.Blue, style = Fill) } } fun interpolateShapes(progress: Float, squareRect: Rect): Path { val path = Path() val cornerRadius = CornerRadius( x = lerp(start = squareRect.width / 2, stop = 0f, fraction = progress), y = lerp(start = squareRect.height / 2, stop = 0f, fraction = progress) ) path.addRoundRect( roundRect = RoundRect(rect = squareRect, cornerRadius = cornerRadius) ) return path } fun lerp(start: Float, stop: Float, fraction: Float): Float { return (1 - fraction) * start + fraction * stop }
Explicação
ShapeMorphingAnimation
configura uma animação infinita que alterna o valoranimationProgress
entre 0 e 1.- O composable
Canvas
é usado para desenhar a forma. Aqui, definimos as dimensões de um quadrado (squareRect
) com base no tamanho da tela. interpolateShapes
usa o progresso atual da animação e o retângulo do quadrado para interpolar entre um círculo e um quadrado. Ele usalerp
(interpolação linear) para ajustar gradualmente ocornerRadius
de um retângulo arredondado, que representa nossa forma de transformação.- Quando o
progress
é 0, ocornerRadius
é a metade do tamanho do retângulo, tornando a forma um círculo. Quando oprogress
é 1, ocornerRadius
é 0, transformando a forma em um quadrado.
Casos de uso no mundo real
- Indicadores de carregamento e progresso – as formas de transformação podem ser usadas para criar indicadores de carregamento ou progresso mais atraentes, proporcionando uma maneira visualmente interessante de indicar o progresso ou os estados de carregamento.
- Transições de ícones na interface do usuário – os ícones de transformação podem ser usados para fornecer feedback visual em resposta às ações do usuário. Por exemplo, um botão de reprodução que se transforma em um botão de pausa quando clicado, ou um ícone de menu de hambúrguer que se transforma em uma seta para trás.
- Visualização de dados – Em visualizações de dados complexas, a transformação pode ajudar na transição entre diferentes exibições ou estados de dados, facilitando o acompanhamento e a compreensão das alterações ao longo do tempo ou entre categorias.
Alguém quer uma queda de neve?
Demonstraremos um sistema de partículas simples para criar um efeito de queda de neve.
data class Snowflake( var x: Float, var y: Float, var radius: Float, var speed: Float ) @Composable fun SnowfallEffect() { val snowflakes = remember { List(100) { generateRandomSnowflake() } } val infiniteTransition = rememberInfiniteTransition(label = "") val offsetY by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1000f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 5000, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "" ) Canvas(modifier = Modifier.fillMaxSize().background(Color.Black)) { snowflakes.forEach { snowflake -> drawSnowflake(snowflake, offsetY % size.height) } } } fun generateRandomSnowflake(): Snowflake { return Snowflake( x = Random.nextFloat(), y = Random.nextFloat() * 1000f, radius = Random.nextFloat() * 2f + 2f, // Snowflake size speed = Random.nextFloat() * 1.2f + 1f // Falling speed ) } fun DrawScope.drawSnowflake(snowflake: Snowflake, offsetY: Float) { val newY = (snowflake.y + offsetY * snowflake.speed) % size.height drawCircle(Color.White, radius = snowflake.radius, center = Offset(snowflake.x * size.width, newY)) }
Explicação
- O
SnowfallEffect
configura um sistema de partículas com vários flocos de neve (objetosSnowflake
). - Cada
Snowflake
tem propriedades como posição (x
,y
),radius
(tamanho) espeed
. rememberInfiniteTransition
eanimateFloat
são usados para criar um efeito de movimento vertical contínuo, simulando a queda de neve.- O
Canvas
composable é usado para desenhar cada floco de neve. A funçãodrawSnowflake
calcula a nova posição de cada floco de neve com base em sua velocidade e nooffsetY
animado. - Os flocos de neve reaparecem na parte superior depois de cair da parte inferior, criando um efeito de queda de neve em loop.
Conclusão
Ao concluirmos essa exploração das animações no Jetpack Compose, fica claro que as animações são mais do que apenas enfeites visuais. Elas são ferramentas cruciais para criar experiências de usuário envolventes, intuitivas e agradáveis.
Abraçando a interatividade
Desde o movimento dinâmico do personagem do jogo até a linha do tempo interativa, vimos como as animações podem tornar as interações com o usuário mais envolventes e informativas.
Criando experiências realistas
O efeito de queda de neve e as formas que se transformam mostram a capacidade desse kit de ferramentas de trazer realismo e fluidez para o mundo digital. Essas animações ajudam a criar experiências imersivas que repercutem nos usuários.
Simplificando a complexidade
Seja na coreografia de vários elementos ou na animação de transições de estado, a simplicidade com que isso pode ser feito se destaca.