1 puntos por GN⁺ 2024-03-09 | 1 comentarios | Compartir por WhatsApp
  • El suavizado exponencial es una técnica simple que puede usarse ampliamente en animaciones que deben seguir suavemente un valor objetivo, como botones de alternancia, cámaras, elementos de UI o volumen de audio
  • La fórmula clave es position += (target - position) * (1 - exp(- speed * dt)), y responde de forma natural a los cambios del objetivo usando solo la posición actual y la posición objetivo
  • El movimiento lineal y el easing habitual suelen requerir manejo extra para clics a mitad de animación, entradas rápidas de cámara o dt grandes, como saltos, vibración o colas de entrada, pero el suavizado exponencial reduce la complejidad con la misma estructura
  • 1 - exp(- speed * dt) surge de la solución de una ecuación diferencial que resuelve la fórmula de actualización proporcional; con dt pequeños es casi igual a la fórmula existente y con dt grandes evita el overshoot
  • Aunque matemáticamente no llega al valor objetivo con exactitud absoluta, en animaciones reales se comporta como si hubiera terminado, debido a la precisión de punto flotante y al límite mínimo de cambio perceptible para el usuario

El problema que se hace visible en un botón de alternancia

  • La posición del interruptor de un botón toggle puede calcularse de forma simple como turned_on ? max_x : min_x, pero cuando cambia el estado la posición se teletransporta al instante y le falta viveza
  • La animación lineal suele implementarse actualizando la posición a una velocidad constante y limitando el rango
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
  • El movimiento a velocidad constante hace que la posición sea una función lineal del tiempo, por lo que el movimiento puede verse algo rígido
  • Si se le agrega una función de easing, el movimiento puede sentirse más suave
    • smoothstep cúbico clásico: 3t² - 2t³
    • easing de raíz cuadrada: sqrt(t)
  • smoothstep tiene la simetría 1 - f(t) = f(1 - t), así que puede usarse el mismo código para la animación de ida y vuelta
  • sqrt necesita una fórmula distinta según la dirección
    • Al encender: sqrt(t)
    • Al apagar: 1 - sqrt(1 - t)
  • sqrt se mueve rápido al inicio y se ralentiza suavemente al acercarse al objetivo, pero incluso en un toggle simple de dos estados requiere manejo de estado como t, dirección y cálculo del easing
  • Si el usuario vuelve a hacer clic a mitad de la animación, el easing tradicional puede generar una discontinuidad donde la posición salta de golpe

La fórmula del suavizado exponencial

  • El suavizado exponencial funciona fijando una posición objetivo y atrayendo gradualmente la posición actual hacia ella
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
  • La cantidad de actualización se determina a partir de target - position.x, es decir, la diferencia entre la posición actual y el objetivo
  • 1 - exp(- dt * speed) es el factor de interpolación que decide cuánto se avanza hacia el objetivo en cada frame
  • Aunque el objetivo cambie a mitad del proceso, el movimiento continúa de inmediato hacia el nuevo destino sin necesidad de guardar tiempo de progreso ni el estado de la dirección de la animación
  • En el ejemplo del botón toggle, comienza rápido como el easing sqrt, se desacelera cerca del objetivo y además reduce el problema de los saltos al hacer clic a mitad de la animación

Ventajas aún mayores en el movimiento de cámara

  • El mismo problema aparece también en una cámara que se mueve sobre un mapa
  • Si la interpolación a velocidad constante se implementa de forma simple, se usa el signo de la dirección objetivo en cada eje
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
  • Al llegar al final de la animación, el signo de target - position puede alternar entre positivo y negativo, provocando vibración
  • Para evitarlo, hace falta una función de actualización separada que limite delta al rango 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;
}
  • Si se quiere aplicar easing cúbico al movimiento de cámara, hay que poner en cola los eventos de movimiento solicitados y procesarlos uno por uno, lo que complica la estructura
  • Ignorar la entrada del usuario mientras la animación está en curso resulta muy frustrante desde la perspectiva del usuario
  • Con suavizado exponencial, el movimiento de cámara puede manejarse con casi el mismo código que el botón toggle
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
  • Si el usuario hace clic rápidamente, la diferencia entre el objetivo y la posición actual aumenta, así que la cámara se acelera de forma natural

¿Por qué 1 - exp(- speed * dt)?

  • Una actualización proporcional simple puede escribirse así
position += (target - position) * speed * dt;
  • Esta fórmula hace que el movimiento sea más rápido cuanto mayor sea la diferencia entre el objetivo y la posición actual, y no necesita estado adicional aparte de la posición actual y la objetivo
  • Es la misma fórmula si se escribe con lerp
position = lerp(position, target, speed * dt);
  • Si speed * dt está cerca de 0, se mueve lentamente; si está cerca de 1, se acerca rápido al objetivo
  • Si speed * dt es mayor que 1, la interpolación produce overshoot y se pasa del objetivo
    • En el ejemplo, cuando speed = 220 y dt = 1 / 125, speed * dt supera 1 y aparece vibración
    • Si speed * dt < 2, la diferencia absoluta aún puede reducirse, pero si speed * dt > 2, deja de tener un comportamiento útil
  • Se puede limitar el factor de interpolación con min(1, speed * dt), pero eso no resuelve suavemente los casos donde dt crece mucho
    • El código puede volverse lento y bajar el frame rate
    • El usuario puede cambiar a otra pestaña o ventana, el código quedarse detenido, y luego reanudarse con un dt de varios segundos
  • En una simulación física, un dt grande puede limitarse o dividirse en varias actualizaciones, pero en animación es mejor para la experiencia de usuario que la cámara y los botones sigan comportándose de forma natural incluso con dt grandes

La solución vista desde ecuaciones diferenciales

  • Una forma A += B * dt suele corresponder a una solución numérica de la ecuación diferencial dA/dt = B
  • La fórmula simple de actualización está resolviendo una ecuación de este tipo
d(position) / dt = (target - position) * speed
  • Si definimos las variables como x = position, a = target, c = speed, queda así
dx / dt = (a - x) * c
  • Si resolvemos directamente esa ecuación, obtenemos una forma como esta
x = x0 + (a - x0) * (1 - exp(-c * t))
  • Por lo tanto, si la fórmula correcta para dt pequeños es position += (target - position) * speed * dt, entonces la fórmula que puede usarse con cualquier dt es la siguiente
position += (target - position) * (1 - exp(- speed * dt));
  • Como en el desarrollo de Taylor exp(x) ≈ 1 + x, para dt pequeños se cumple 1 - exp(-speed * dt) ≈ speed * dt, así que coincide con la fórmula simple anterior
  • Incluso si speed * dt es muy grande, exp(-speed * dt) se acerca a 0 y 1 - exp(...) se acerca a 1, por lo que se mueve de forma estable hacia un valor cercano al objetivo
  • La misma fórmula también puede escribirse con lerp
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));

Cómo elegir el valor de velocidad

  • En la animación común suele pensarse en términos de duración, como “moverse en 0.125 segundos”, pero técnicamente el suavizado exponencial necesita un tiempo infinito para llegar exactamente al objetivo
  • exp(- speed * time) se hace más pequeño con el paso del tiempo, pero nunca llega a 0, así que si el valor inicial y el objetivo son distintos, matemáticamente position nunca será exactamente igual a target
  • En la práctica, se alcanza el límite de precisión de punto flotante o la diferencia en la posición de la cámara se vuelve tan pequeña que el usuario ya no puede verla, y la animación parece haber terminado
  • El significado de speed es que, en un tiempo de 1 / speed, position se acerca exactamente e = 2.71828... veces más al target
  • En uso real, speed suele estar en el rango 5..50, y para una sensación equivalente a la de una animación lineal o cúbica, un valor cercano a 2 * speed para el suavizado exponencial puede sentirse adecuado

Relación con el procesamiento de señales

  • Si se busca “exponential smoothing” o “exponential moving average”, se encuentra el artículo Exponential smoothing de Wikipedia
  • Cuando dt es fijo y target cambia en cada iteración, el valor se actualiza con algo parecido a esto según el índice de iteración
    • factor = 1 - exp(- speed * dt)
    • Normalmente factor se fija directamente como un valor entre 0 y 1
  • El suavizado exponencial discreto es el análogo discreto del método usado en animación
  • En procesamiento de señales también se usa porque funciona manteniendo solo el valor promedio actual, sin listas de valores previos ni estado complejo
  • En audio digital, dt suele estar fijo como el inverso de la frecuencia de muestreo, 1 / freq
    • Ej.: 1/44100, 1/48000

1 comentarios

 
GN⁺ 2024-03-09
Comentarios de Hacker News
  • Parece que aquí no se está abordando lo suficiente el punto clave. Esto no es simplemente otra curva de easing entre 0 y 1 ni smoothstep(), sino un método sin estado que maneja casi cualquier entrada de forma consistente, así que es realmente útil
    Si has usado transiciones CSS, este problema te resultará familiar. Si defines una duración de 400 ms, ¿por qué precisamente 400 ms? ¿No debería depender de la distancia que hay que recorrer?
    Como dijeron otros, el suavizado exponencial tiene el problema de que solo se acerca asintóticamente al destino y nunca llega del todo. La solución obvia es detener la animación cuando el paso cae por debajo de un umbral, pero no es elegante
    Cuando usaba un método parecido para el desplazamiento con inercia, me resultó útil agregar un término de seudofricción. Ese término compensa al exponencial y en la práctica actúa como una velocidad mínima. Aquí está el ejemplo en Desmos: https://www.desmos.com/calculator/98ufbuzxhj

    • Tal vez se pueda resolver interpretando el suavizado exponencial como una ecuación diferencial ordinaria respecto a la distancia al destino D. El suavizado exponencial es una actualización de Euler de dD/dt=-C*D, y como su solución es D(t)=A*exp(-C*t), se aproxima a 0 sin llegar nunca
      Se puede cambiar por una ecuación que llegue a 0 en tiempo finito, por ejemplo dD/dt=-C*sqrt(D). La solución se comporta como una especie de cuadrática truncada y, al llegar a 0, se queda ahí. La actualización de Euler para esta ecuación también es sin estado, justo como se desea
    • No veo por qué detener la animación cuando el paso cae por debajo de un umbral no sería elegante. Es simple, fácil de implementar y de verificar, y además da el efecto visual deseado, así que me parece bastante limpio
    • Si existe el problema de los 400 ms en las transiciones CSS, me parece que el parámetro de velocidad de una función exponencial tendría exactamente el mismo problema
  • Como desarrollador de juegos, diría que para la mayoría de las UI convienen más los tweens con easing y duración predefinida. Pero cuando quieres suavizar movimientos continuos e impredecibles donde no hay un punto de inicio y fin claramente definidos, este otro tipo de animación resulta muy útil
    Por ejemplo, cuando el jugador arrastra con el mouse una pieza sobre una cuadrícula y quieres que encaje en la grilla, o cuando mueves la cámara como en el ejemplo del artículo
    En esos casos, este truco de interpolación exponencial es muy útil, pero no es algo muy conocido. Muchos juegos usan una interpolación lineal menos precisa y luego, a diferencia de la época en que 60 fps era el estándar, alguien los ejecuta en un monitor de 240 Hz y toda la sensación de la animación se vuelve rarísima
    Por eso me alegra ver este artículo. Este tipo de conocimiento demasiado específico suele transmitirse oralmente, casi como un oficio, de alguien senior del equipo a alguien más nuevo, así que no es fácil acceder a él

  • Me gustó el artículo, pero quiero decir que el juicio del autor de que sqrt es mejor que una cúbica para un interruptor de palanca es objetivamente incorrecto. Si ves cómo suelen funcionar los interruptores de palanca reales, una función cúbica es mejor opción en este caso
    Basta pensar en los interruptores de los disyuntores eléctricos de casa, o en los que aparecen a menudo en sintetizadores analógicos y equipo de audio. Son dispositivos que apuntan a una estética particular, y hasta mi pequeño amplificador de guitarra Hughes & Kettner tiene dos interruptores con una sensación muy satisfactoria al accionarlos
    Estos interruptores ofrecen algo de resistencia al principio y luego, por la estructura del resorte, saltan de golpe a la nueva posición. Ese comportamiento lo modela mejor una función cúbica que sqrt o el suavizado exponencial
    Fuera de esa observación menor, el artículo me pareció muy bueno. Muestra bien que usar animaciones con una función de easing adecuada puede mejorar la experiencia de usuario, mientras que implementarlas sin cuidado, como en el ejemplo de interpolación lineal, puede volverse molesto y perjudicar la experiencia
    [0] Claro, depende del tipo de interruptor de palanca. Pero los del estilo que ves en equipos como un Minimoog también muestran ese comportamiento de “resistirse y luego saltar a la nueva posición”, y por eso da gusto manipularlos. Aclaro que no es por presumir equipo; no tengo un Minimoog

    • Intuitivamente, también sentí que la función cúbica se veía mejor. Los interruptores mecánicos tienen cierta inercia, y eso parece reflejarse en el enfoque cúbico
  • Me sigue sorprendiendo lo seguido que un truco no lineal simple puede añadir placer a una interacción en línea. En el caso de la percepción del color, también es clave para entender por qué dos colores no son lo bastante distinguibles para algunas personas
    Lo raro es que los humanos no siempre entienden bien la aceleración. No deberías huir cuesta arriba de un incendio creyendo que avanza casi a velocidad constante como en un terreno plano. El fuego acelera al subir una pendiente
    Los niños aprenden bastante rápido la velocidad con la que una pelota lanzada se mueve a lo largo del suelo, pero no siempre entienden bien qué tan rápido se moverá al golpearles la mano debido a la gravedad

  • Me parece interesante que casi todo en este artículo termine reduciéndose al easing. Da la impresión de que cada nueva generación tiene que redescubrirlo por su cuenta
    Recuerdo haber quedado fascinado a finales de los 90 con los sitios web experimentales de Yugo Nakamura. Fue uno de los primeros sitios que vi que usaba easing libremente para dar una sensación orgánica: https://www.youtube.com/watch?v=NLt7Gwnt3WY

    • Recuerdo estar sentado en la primera fila de una conferencia donde Yugo presentaba, y cuando fue mostrando rápidamente sus demos, los demás especialistas en Flash estaban totalmente enloquecidos
  • Siento que me gustaría un toggle así. Algo que, mientras lo tocas o haces clic, se mueva lentamente hasta alrededor del 75% del objetivo, y al soltarlo complete de golpe el resto
    No estoy muy seguro de qué significado tendría eso desde el punto de vista de UX. Tal vez podría implicar que la configuración solo se aplica o se guarda realmente al final
    O podría ser parte de un diálogo de “¿de verdad estás seguro?”. La configuración se aplica mientras mantienes presionado, pero todavía se puede revertir con Escape antes de que encaje por completo en su 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; }
    • He visto a muchos usuarios mayores intentar empujar un toggle y que no pase nada. Así que, si vas a hacerlo así, también deberías permitir que el toggle realmente se pueda empujar. Si no, mejor usa una casilla de verificación
  • Me gustó este artículo. Es casi exactamente la misma técnica que escribí hace unos 10 años; en ese entonces la llamé lazy-easy y todavía la uso.
    A veces solo quieres animación suave sin hacer toda la gestión de estado: https://www.hailpixel.com/articles/lazy-animation-with-lazy-...

  • El artículo en sí es realmente bueno. La demo parece funcionar bien en Chrome, pero en Firefox se congela mientras haces scroll y el renderizado de la página se detiene por completo.

  • En realidad es un enfoque bastante bueno, y también sirve como prueba de concepto de técnicas de animación y easing. Me recuerda mucho a 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/
    En particular esta demo: https://codepen.io/desandro/pen/myXdej
    Esta técnica no solo es útil para interruptores. De hecho, tampoco usarías este tipo de elementos para un interruptor, ni pondrías 20 bucles de requestAnimationFrame corriendo en paralelo en todo el sitio. Tampoco meterías elementos intencionalmente rotos.
    Además, no hay una optimización para detener el renderizado cuando el delta se vuelve lo suficientemente pequeño, y probablemente haya decenas de pequeños ajustes más para llevarlo a un nivel de producción.
    Viendo las reacciones aquí, parece que algunos no leyeron el artículo y están suponiendo cosas, o no ven el bosque por fijarse en el árbol, o están tan sesgados y cínicos que comparten juicios poco profesionales para parecer inteligentes.
    ¿Desde cuándo HN se volvió como Reddit?

    • En este caso, mucha gente no puede leer el artículo, porque la mayoría de los navegadores que no son Chromium se congelan al bajar aproximadamente un tercio de la página.
    • Si preguntas “¿Desde cuándo HN se volvió como Reddit?”, bastaría con leer al menos el último párrafo de https://news.ycombinator.com/newsguidelines.html
    • Ese cambio lleva tiempo ocurriendo, pero como las pautas de HN prohíben hablar directamente de eso, la gente no lo dice. Es bastante extraño.
    • En este caso, el cinismo no parece muy justo.
      Sobre la parte de que HN se volvió como Reddit, quizá sí haya un ambiente general de cinismo difuso en torno a la tecnología. Hay una desconfianza de fondo sobre cómo gobiernos y empresas pretenden usar tecnologías poderosas que están surgiendo, y si las están construyendo no para ampliar la libertad individual sino para restringir y controlar a las personas.
  • Está muy cerca de la esencia del diseño emocional(https://en.wikipedia.org/wiki/Emotional_Design). Incluso detrás de una animación muy pequeña hay mucho de qué hablar.