- Suavização exponencial é uma técnica simples que pode ser usada amplamente em animações que precisam seguir um valor-alvo de forma suave, como botões de alternância, câmera, elementos de UI e volume de áudio
- A fórmula central é
position += (target - position) * (1 - exp(- speed * dt)), e ela reage de forma natural a mudanças no alvo usando apenas a posição atual e a posição-alvo
- Movimento linear e easing tradicional costumam exigir tratamento para cliques no meio da animação, entrada rápida da câmera e
dt grande, como saltos, tremores ou filas de entrada, mas a suavização exponencial reduz a complexidade com a mesma estrutura
1 - exp(- speed * dt) vem da solução da equação diferencial resolvida pela fórmula de atualização proporcional; com dt pequeno ela é quase igual à fórmula existente, e com dt grande evita overshoot
- Embora matematicamente nunca chegue exatamente ao valor-alvo, na prática ela se comporta como concluída em animações reais por causa da precisão de ponto flutuante e do limite mínimo de mudança perceptível para o usuário
O problema que aparece em um botão de alternância
- A posição do seletor de um botão toggle pode ser calculada de forma simples como
turned_on ? max_x : min_x, mas quando o estado muda a posição se teletransporta instantaneamente e falta vivacidade
- Uma animação linear pode ser implementada atualizando a posição a uma velocidade constante e limitando o intervalo
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
- Movimento com velocidade constante faz com que a posição seja uma função linear do tempo, o que pode deixar o movimento sem graça
- Se você adicionar uma função de easing, o movimento pode ficar mais suave
- cubic smoothstep clássico:
3t² - 2t³
- easing de raiz quadrada:
sqrt(t)
smoothstep tem a simetria 1 - f(t) = f(1 - t), então o mesmo código pode ser usado para animações de ida e volta
sqrt exige fórmulas diferentes dependendo da direção
- ao ligar:
sqrt(t)
- ao desligar:
1 - sqrt(1 - t)
sqrt se move rápido no início e desacelera suavemente perto do alvo, mas mesmo em um toggle simples de 2 estados ainda exige gerenciamento de estado como t, direção e cálculo do easing
- Se o usuário clicar de novo no meio da animação, o easing tradicional pode criar uma descontinuidade em que a posição salta de repente
A fórmula da suavização exponencial
- Na suavização exponencial, você define a posição-alvo e então puxa a posição atual em direção a ela pouco a pouco
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
- A quantidade de atualização é determinada com base em
target - position.x, a diferença entre a posição atual e a posição-alvo
1 - exp(- dt * speed) é o coeficiente de interpolação que decide quanto avançar em direção ao alvo a cada frame
- Mesmo que o alvo mude no meio do caminho, o movimento continua imediatamente em direção ao novo alvo sem precisar de tempo de progresso separado nem de estado de direção da animação
- No exemplo do botão toggle, assim como no easing com
sqrt, ele começa rápido e desacelera perto do alvo, mas reduz o problema de saltos em cliques intermediários
Vantagens ainda maiores no movimento da câmera
- O mesmo problema também aparece em uma câmera que se move sobre um mapa
- Se você implementar uma interpolação de velocidade constante de forma simples, pode usar o sinal da direção-alvo em cada eixo
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
- No ponto em que a animação termina, o sinal de
target - position pode alternar entre positivo e negativo, causando tremor
- Para evitar isso, é necessária uma função de atualização separada que limite
delta ao intervalo de max_delta
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
- Para aplicar easing cúbico ao movimento da câmera, é preciso colocar em fila os eventos de movimento solicitados e processá-los um por um, o que complica a estrutura
- Ignorar a entrada do usuário enquanto a animação está em andamento pode ser extremamente frustrante do ponto de vista do usuário
- Com suavização exponencial, o movimento da câmera pode ser tratado com praticamente o mesmo código do botão toggle
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
- Se o usuário clicar rapidamente, a diferença entre o alvo e a posição atual aumenta, então a câmera fica naturalmente mais rápida
Por que 1 - exp(- speed * dt)?
- Uma atualização proporcional simples pode ser escrita da seguinte forma
position += (target - position) * speed * dt;
- Essa fórmula move mais rápido quanto maior for a diferença entre o alvo e a posição atual, e não precisa de estado adicional além da posição atual e da posição-alvo
- Escrevendo com
lerp, fica a mesma coisa
position = lerp(position, target, speed * dt);
- Se
speed * dt estiver próximo de 0, move devagar; se estiver próximo de 1, se aproxima rapidamente do alvo
- Se
speed * dt for maior que 1, a interpolação gera overshoot, ultrapassando o alvo
- No exemplo, quando
speed = 220 e dt = 1 / 125, speed * dt fica maior que 1 e aparece tremor
- Se
speed * dt < 2, a diferença absoluta ainda pode diminuir, mas se speed * dt > 2, o comportamento deixa de ser útil
- É possível limitar o coeficiente de interpolação com
min(1, speed * dt), mas isso não resolve de forma suave os casos em que dt cresce
- o código pode ficar lento e a taxa de frames pode cair
- o usuário pode mudar para outra aba ou janela, o código pode parar e depois voltar com um
dt de vários segundos
- Em simulações físicas, um
dt grande pode ser limitado ou dividido em várias atualizações, mas em animações é melhor para a experiência do usuário que câmera e botões se comportem naturalmente mesmo com dt grande
A solução vista como equação diferencial
- Uma forma
A += B * dt geralmente corresponde a uma solução numérica da equação diferencial dA/dt = B
- A fórmula de atualização simples equivale a resolver a seguinte equação
d(position) / dt = (target - position) * speed
- Se definirmos as variáveis como
x = position, a = target, c = speed, temos o seguinte
dx / dt = (a - x) * c
- Resolvendo essa equação diretamente, obtemos a forma abaixo
x = x0 + (a - x0) * (1 - exp(-c * t))
- Portanto, se a fórmula correta para
dt pequeno é position += (target - position) * speed * dt, então a fórmula que pode ser usada para qualquer dt é a seguinte
position += (target - position) * (1 - exp(- speed * dt));
- Pela expansão de Taylor,
exp(x) ≈ 1 + x, então para dt pequeno temos 1 - exp(-speed * dt) ≈ speed * dt, o que torna a nova fórmula equivalente à fórmula simples existente
- Mesmo que
speed * dt fique muito grande, exp(-speed * dt) se aproxima de 0 e 1 - exp(...) se aproxima de 1, movendo-se de forma estável para um valor próximo ao alvo
- A mesma fórmula também pode ser escrita com
lerp
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));
Como escolher o valor de velocidade
- Em animações comuns, é fácil pensar em duração, como “mover em 0,125 segundo”, mas tecnicamente a suavização exponencial leva tempo infinito para chegar exatamente ao alvo
exp(- speed * time) diminui com o tempo, mas nunca chega a 0, então se o valor inicial e o alvo forem diferentes, position também nunca se torna matematicamente idêntica a target
- Na prática, ela chega ao limite da precisão de ponto flutuante, ou a diferença da posição da câmera fica pequena demais para o usuário perceber, então a animação parece concluída
- O significado de
speed é que, no intervalo de tempo 1 / speed, position fica exatamente e = 2.71828... vezes mais próxima de target
- Em uso real, normalmente
speed fica no intervalo 5..50, e para uma sensação parecida com animações lineares ou cúbicas, algo como 2 * speed na suavização exponencial pode parecer adequado
Relação com processamento de sinais
- Se você buscar por “exponential smoothing” ou “exponential moving average”, encontrará o artigo Exponential smoothing da Wikipedia
- Quando
dt é fixo e target muda a cada iteração, o valor é atualizado de forma semelhante à seguinte, com base no índice da repetição
factor = 1 - exp(- speed * dt)
- normalmente
factor é definido diretamente como um valor entre 0 e 1
- A suavização exponencial discreta é o análogo discreto da técnica usada em animação
- Ela também é usada em processamento de sinais porque funciona apenas com a média atual, sem manter uma lista de valores anteriores nem um estado complexo
- Em áudio digital,
dt normalmente é fixo como o inverso da frequência de amostragem, 1 / freq
1 comentários
Comentários do Hacker News
Parece que o ponto principal não está sendo tratado o suficiente aqui. Isso não é apenas mais uma curva de easing ou
smoothstep()entre 0 e 1, mas uma abordagem sem estado realmente útil que lida com quase qualquer entrada de forma regularSe você já usou transições em CSS, esse problema deve ser familiar. Se a duração for 400ms, por que justamente 400ms? Não deveria variar conforme a distância a ser percorrida?
Como outros mencionaram, a suavização exponencial tem o problema de apenas se aproximar assintoticamente do destino, sem nunca realmente chegar lá. Existe a solução óbvia de parar a animação quando o passo ficar abaixo de um certo limite, mas isso não é elegante
Ao usar algo parecido para rolagem com inércia, foi útil adicionar um termo de pseudoatrito. Esse termo compensa o termo exponencial e acaba funcionando como uma velocidade mínima. Exemplo no Desmos: https://www.desmos.com/calculator/98ufbuzxhj
D. A suavização exponencial é uma atualização de Euler dedD/dt=-C*D, cuja solução éD(t)=A*exp(-C*t), então ela se aproxima de 0 assintoticamente sem alcançá-loBasta trocar por uma expressão que vá a 0 em tempo finito, por exemplo
dD/dt=-C*sqrt(D). A solução se comporta como uma parábola pela metade até atingir 0, e então permanece lá. A atualização de Euler dessa equação também é sem estado, do jeito desejadoComo desenvolvedor de jogos, eu diria que, para a maior parte da UI, um tween com easing e duração predefinida é melhor. Mas esse outro tipo de animação é muito útil quando você quer suavizar movimentos contínuos e imprevisíveis, sem um ponto inicial e final claramente definidos
Por exemplo, quando o jogador arrasta peças de um grid com o mouse e você quer que elas se encaixem na grade, ou ao mover a câmera como no exemplo do texto
Nesses casos, o truque de interpolação exponencial é muito útil, mas não é amplamente conhecido. Muitos jogos usam interpolação linear, que é menos precisa, e então, diferente da época em que 60fps era o padrão, alguém roda em um monitor de 240Hz e a sensação da animação fica completamente estranha
Por isso gostei de ver este texto. Esse tipo de conhecimento específico demais costuma ser transmitido oralmente dentro da equipe, de sênior para júnior, quase como um aprendizado artesanal, então é difícil ter acesso
Gostei do texto, mas quero dizer que a avaliação do autor de que
sqrté melhor do que uma cúbica no caso de um interruptor tipo toggle está objetivamente errada. Observando como um interruptor tipo toggle real normalmente funciona, a função cúbica é a melhor escolha nesse casoPense em um disjuntor elétrico residencial, ou nos interruptores comuns em sintetizadores analógicos e equipamentos de áudio. São dispositivos com uma estética própria, e meu pequeno amplificador de guitarra Hughes & Kettner também tem dois interruptores com uma sensação tátil muito boa
Esses interruptores oferecem um pouco de resistência no começo e depois, por causa do mecanismo de mola, de repente saltam para a nova posição. Esse comportamento é melhor modelado por uma função cúbica do que por
sqrtou suavização exponencialTirando essa pequena observação, o texto é muito bom. Ele mostra bem que animações bem aplicadas, como com a função de easing adequada, melhoram a experiência do usuário, mas, quando implementadas sem cuidado, como no exemplo de interpolação linear, podem incomodar e piorar a experiência
[0] Isso varia conforme o tipo de interruptor toggle. Ainda assim, o tipo que você vê em equipamentos como um Minimoog também mostra esse comportamento de “resistir e depois saltar para a nova posição”, o que torna o uso prazeroso. E, para constar, isso não é ostentação de equipamento: eu não tenho um Minimoog
Continuo me surpreendendo com a frequência com que truques não lineares simples acrescentam prazer às interações online. No caso da percepção de cor, isso também é essencial para entender por que duas cores não são suficientemente distinguíveis para algumas pessoas
O estranho é que os humanos nem sempre entendem bem aceleração. Você não deve fugir de um incêndio subindo uma ladeira achando que ele vai avançar quase em velocidade constante como em terreno plano. O fogo acelera ao subir
Crianças aprendem rapidamente a velocidade com que uma bola lançada se move ao longo do chão, mas nem sempre entendem bem com que velocidade ela vai atingir sua mão por causa da gravidade
É interessante que a maior parte deste texto acabe convergindo para easing. Parece que cada nova geração precisa redescobrir isso por conta própria
Lembro de ter ficado fascinado, no fim dos anos 90, pelos sites experimentais de Yugo Nakamura. Foi um dos primeiros sites que vi dar uma sensação orgânica com uso livre de easing: https://www.youtube.com/watch?v=NLt7Gwnt3WY
Por algum motivo, acho que seria legal ter um toggle assim. Enquanto você toca ou clica e segura, ele se move lentamente até cerca de 75% do caminho até o alvo, e, quando solta, faz o resto de uma vez
Não sei bem qual seria o sentido disso do ponto de vista de UX. Talvez pudesse significar que a configuração só é realmente aplicada ou salva no final
Ou poderia fazer parte de uma caixa de diálogo do tipo “tem certeza mesmo?”. A configuração é aplicada enquanto você mantém pressionado, mas pode ser revertida com Escape antes de encaixar definitivamente no lugar
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }Gostei deste texto. Escrevi quase exatamente a mesma técnica há cerca de 10 anos, na época eu a chamava de
lazy-easy, e ainda a uso hojeÀs vezes você só quer animações suaves sem precisar gerenciar todo o estado: https://www.hailpixel.com/articles/lazy-animation-with-lazy-...
O texto em si é realmente bom. A demo parece funcionar bem no Chrome, mas no Firefox trava durante a rolagem e a renderização da página para completamente
Na verdade é uma abordagem bem boa, e também funciona bem como prova de conceito de técnica de animação/easing. Lembra bastante o Flickity
https://metafizzy.co/blog/initial-demos/
https://metafizzy.co/blog/math-time-resting-position/
https://metafizzy.co/blog/particle-to-slider/
https://metafizzy.co/blog/flickity-begins/
Especialmente esta demo: https://codepen.io/desandro/pen/myXdej
Essa técnica não é útil só para switches. Na prática, você também não usaria esse tipo de elemento em um switch, nem rodaria 20 loops de
requestAnimationFrameem paralelo pelo site inteiro. Também não colocaria de propósito um elemento quebradoTambém não há otimização para parar a renderização quando o delta fica pequeno o suficiente, e provavelmente há dezenas de outros pequenos ajustes para torná-la pronta para produção
Olhando as reações aqui, parece que as pessoas não leram o texto e estão supondo coisas, ou não conseguem ver a floresta por causa das árvores, ou são tendenciosas e cínicas demais e compartilham julgamentos nada profissionais para parecer inteligentes
Desde quando o HN virou o Reddit?
Quanto à parte de o HN estar virando o Reddit, talvez exista um clima geral de cinismo vago em relação à tecnologia como um todo. Há uma desconfiança de fundo sobre como governos e empresas pretendem usar tecnologias poderosas que estão surgindo, se estão sendo construídas com a intenção de restringir e controlar indivíduos em vez de ampliar a liberdade individual
Está muito perto da essência do design emocional(https://en.wikipedia.org/wiki/Emotional_Design). Há muito o que dizer mesmo por trás de uma animação minúscula