- 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
1 comentarios
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 útilSi 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
D. El suavizado exponencial es una actualización de Euler dedD/dt=-C*D, y como su solución esD(t)=A*exp(-C*t), se aproxima a 0 sin llegar nuncaSe 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 deseaComo 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
sqrtes 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 casoBasta 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
sqrto el suavizado exponencialFuera 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
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
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
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }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-easyy 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
requestAnimationFramecorriendo 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?
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.