1 pontos por GN⁺ 2024-03-09 | 1 comentários | Compartilhar no WhatsApp
  • 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
    • Ex.: 1/44100, 1/48000

1 comentários

 
GN⁺ 2024-03-09
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 regular
    Se 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

    • Talvez dê para resolver isso interpretando a suavização exponencial como uma equação diferencial ordinária em relação à distância até o destino D. A suavização exponencial é uma atualização de Euler de dD/dt=-C*D, cuja solução é D(t)=A*exp(-C*t), então ela se aproxima de 0 assintoticamente sem alcançá-lo
      Basta 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 desejado
    • Não entendo por que parar a animação quando o passo cai abaixo de um limite não seria elegante. É simples, fácil de implementar e verificar, e ainda produz o efeito visual desejado, então me parece suficientemente limpo
    • Se existe o problema dos 400ms nas transições em CSS, suspeito que o parâmetro de velocidade da função exponencial tenha o mesmo problema
  • Como 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 caso
    Pense 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 sqrt ou suavização exponencial
    Tirando 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

    • Intuitivamente, a função cúbica também me pareceu ter uma sensação melhor. Interruptores mecânicos têm um certo grau de inércia, e isso parece aparecer nessa abordagem cúbica
  • 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

    • Lembro de estar sentado na primeira fileira de uma conferência em que Yugo apresentava, e quando ele passou rapidamente pelos demos, os outros especialistas em Flash foram à loucura
  • 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

    • https://jsfiddle.net/u1vybhqg/
      input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }
      input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }
    • Já vi muitos usuários idosos tentando arrastar toggles, sem que nada acontecesse. Então, se for fazer assim, o ideal é permitir que o toggle realmente possa ser arrastado. Caso contrário, é melhor simplesmente usar uma caixa de seleção
  • 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 requestAnimationFrame em paralelo pelo site inteiro. Também não colocaria de propósito um elemento quebrado
    També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?

    • Nesse caso, muita gente não consegue ler o texto. Quando você rola cerca de um terço da página, a maioria dos navegadores não Chromium trava
    • Se você está perguntando “Desde quando o HN virou o Reddit?”, basta ler pelo menos o último parágrafo de https://news.ycombinator.com/newsguidelines.html
    • Essa mudança já vem acontecendo há algum tempo, mas as diretrizes do HN proíbem falar disso diretamente, então as pessoas não comentam. É bem estranho
    • Nesse caso, o cinismo não parece muito justo
      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