Pular para o conteúdo
Fundamento · TempoIniciante

Easing e tweening

Transforme o tempo num progresso de 0 a 1, curve esse progresso com uma curva de easing e monte com ele animações em etapas, escalonadas e de vaivém.

Como funciona

Um tween leva um valor de um estado a outro num tempo definido: uma cortina da esquerda para a direita, um clarão se apagando, um painel deslizando para dentro. Num shader são três passos. Transforme o tempo num progresso de 0 a 1, curve o progresso com uma curva de easing e faça um mix entre os valores inicial e final com o resultado.

float p = clamp((time - startTime) / duration, 0.0, 1.0); // progresso: 0 antes, 1 depois
float e = p * p * (3.0 - 2.0 * p);                         // com easing (a curva do smoothstep)
float x = mix(-0.2, 1.2, e);                               // o valor animado

Manter os três separados é o ponto: a curva de easing pode mudar sem mexer no tempo, e o mesmo progresso pode mover vários valores ao mesmo tempo.

Curvas de easing

Movimento em velocidade constante parece mecânico, porque objetos reais aceleram e desaceleram. Curvas de easing são curvas de modelagem que mantêm 0 em 0 e 1 em 1, com nomes que dizem onde elas são suaves:

float easeInQuad(float p)    { return p * p; }                         // começa devagar
float easeOutQuad(float p)   { return 1.0 - (1.0 - p) * (1.0 - p); }   // termina devagar
float easeInOutQuad(float p) {                                         // devagar nas duas pontas
    return p < 0.5 ? 2.0 * p * p : 1.0 - 2.0 * (1.0 - p) * (1.0 - p);
}
float easeOutCubic(float p)  { float q = 1.0 - p; return 1.0 - q * q * q; }

Potências maiores deixam a parte lenta mais longa e a parte rápida mais brusca. Ease-out combina com coisas que chegam à tela, porque elas se acomodam no lugar; ease-in combina com coisas que saem, porque elas ganham velocidade; ease-in-out combina com movimentos de uma posição de repouso para outra.

Duas famílias passam das pontas de propósito. Um easing “back” ultrapassa o alvo e volta, como um painel que passa um pouco do lugar e retorna:

float easeOutBack(float p) {
    const float k = 1.70158;          // o exagero clássico, cerca de 10%
    float q = p - 1.0;
    return 1.0 + (k + 1.0) * q * q * q + k * q * q;
}

Um easing elástico também ultrapassa, com um seno que vai diminuindo por cima. O de quique, uma série de arcos de queda, fica dentro de 0–1, mas bate na ponta várias vezes. Curvas que ultrapassam produzem valores acima de 1 ou abaixo de 0, então confira se o que elas movem aguenta isso: uma cor pode precisar de um clamp, enquanto uma posição pode simplesmente passar do alvo.

O CSS descreve a maioria dos easings como curvas cubic-bezier(). Um shader não consegue calcular essas curvas de forma barata, porque achar o valor num dado instante exige resolver uma equação cúbica, então os shaders escrevem a curva direto como um polinômio, como os de cima.

Animação em etapas e escalonada

Muitas animações têm fases: uma máscara cresce, depois uma cor muda, depois tudo some. Dê a cada fase a própria janela do progresso geral remapeando-o:

float stage(float p, float a, float b) { return clamp((p - a) / (b - a), 0.0, 1.0); }

float grow  = easeOutCubic(stage(p, 0.0, 0.5)); // primeira metade
float shift = stage(p, 0.3, 0.8);               // se sobrepõe ao fim do crescimento
float fade  = easeInQuad(stage(p, 0.7, 1.0));   // os últimos 30%

As janelas podem se sobrepor, o que costuma parecer mais suave do que uma depois da outra à risca. Escalonar é o mesmo remapeamento com um atraso por elemento: numa fileira de ladrilhos, stage(p, i * 0.05, i * 0.05 + 0.4) começa o ladrilho i um instante depois do anterior, a cascata usada em transições e em revelações de interface.

Vaivém e loops

Um tween que deve se repetir precisa de um progresso que se repita. fract(time / period) recomeça em 0 a cada período, o que dá um salto para trás. Para ir e voltar, dobre esse valor numa onda triangular que sobe e depois desce:

float loop     = fract(time / 2.0);           // 0..1 a cada 2 segundos
float pingPong = 1.0 - abs(loop * 2.0 - 1.0); // 0..1..0 a cada 2 segundos
float e        = easeInOutQuad(pingPong);     // com easing nos dois sentidos

Para segurar em cada ponta antes de virar, remapeie o valor dobrado: stage(pingPong, 0.1, 0.9) descansa em 0 e em 1 por um instante, um quinto de cada metade, antes de seguir.

Leitura complementar