wandres.dev
EASING · cubic-bezier, steps y linear()

Construir tus propias curvas

Un procedimiento de tres decisiones para escribir la curva que necesitas, cómo organizarlas en un sistema y qué comprobar antes de darla por buena.

⏱ 17 min

Saber leer una curva sirve para diagnosticar; saber escribirla sirve para diseñar. El procedimiento cabe en tres decisiones y ninguna requiere abrir un editor: primero eliges el carácter del arranque, después el del aterrizaje, y por último ajustas el reparto entre ambos. Lo que suele fallar no es la curva sino el orden: mucha gente empieza ajustando la duración cuando el problema estaba en la pendiente final.

🎯 Al terminar esta lección sabrás
  • Construir una curva a partir de una intención descrita en palabras.
  • Aplicar las tres decisiones en su orden correcto.
  • Organizar un conjunto pequeño de curvas como sistema en lugar de improvisarlas.
  • Comprobar una curva con un método que no dependa de la impresión subjetiva.

Tres decisiones

Decisión uno: el arranque. Responde a la pregunta de si el elemento tiene masa. Se controla con el primer punto de control, y en la práctica se reduce a elegir la coordenada horizontal x1, dejando y1 en 0.

x1 con y1 a 0 Se lee como
0 Sin masa, arranca a velocidad máxima
0.2 Ligero
0.4 Con cuerpo
0.7 Pesado, tarda en arrancar

Cuando quieres una respuesta inmediata a una acción del usuario, x1 vale 0 y además y1 sube por encima de 0 para que la pendiente inicial sea grande. Cuando quieres masa, x1 sube y y1 se queda en 0.

Decisión dos: el aterrizaje. Responde a si el elemento llega por su propio impulso o es empujado. Se controla con el segundo punto, y aquí lo que se ajusta es sobre todo x2 con y2 en 1.

x2 con y2 a 1 Se lee como
0 Cola muy larga, llegada muy suave
0.2 Cola larga, llegada suave
0.6 Frenada moderada
1 Se para en seco, choca

Decisión tres: el reparto. Con los dos extremos elegidos, queda decidir cuánto de la animación se gasta en cada mitad. Se ajusta subiendo y1 —lo que adelanta el recorrido y hace que el final sea más largo y más lento— o bajando y2 —lo que retrasa el recorrido y concentra el movimiento al final.

La regla práctica: si la animación se siente lenta pero la duración es correcta, sube y1. Estás pidiendo que la mayor parte del desplazamiento ocurra pronto, con lo que el usuario percibe que llegó antes aunque el número no haya cambiado.

De la intención a los números

Cuatro intenciones frecuentes, resueltas con el procedimiento:

“Que entre desde fuera y se quede.” Arranque sin masa —viene lanzado desde fuera de la pantalla— y aterrizaje muy suave. x1 a 0, y1 a 0, x2 a 0, y2 a 1: cubic-bezier(0, 0, 0, 1). En la práctica se suaviza un poco el arranque para que no parezca un disparo: cubic-bezier(0.05, 0, 0, 1).

“Que se traslade de un sitio visible a otro.” Los dos extremos son visibles, así que hay masa a los dos lados. cubic-bezier(0.4, 0, 0.2, 1).

“Que responda a la pulsación.” Pendiente inicial grande y frenada rápida, porque lo que importa es el primer fotograma. cubic-bezier(0.05, 0.7, 0.1, 1).

“Que se vaya y desaparezca.” Arranque con algo de masa —tiene que despegarse— y sin necesidad de aterrizar bien, porque no se ve el final. cubic-bezier(0.4, 0, 1, 1).

:root {
  --ease-entra:  cubic-bezier(0.05, 0, 0, 1);
  --ease-mueve:  cubic-bezier(0.4, 0, 0.2, 1);
  --ease-pulsa:  cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-sale:   cubic-bezier(0.4, 0, 1, 1);
}

Cuatro curvas cubren prácticamente cualquier interfaz. Si tu sistema tiene doce, casi seguro que hay seis que nadie distingue de otras seis.

El sistema: curva y duración van juntas

Un error habitual al montar un sistema de movimiento es tener una escala de duraciones y una lista de curvas como dos ejes independientes que cada componente combina a su gusto. El resultado son componentes que usan la misma duración y se perciben con velocidades distintas, por la razón que ya vimos en el nivel de principios: lo que el usuario cronometra es cuándo el cambio se hace legible, no cuándo termina la interpolación.

La forma que funciona es emparejarlas: cada intención tiene su curva y su duración, y se usan como una unidad.

:root {
  --mov-entra: 260ms cubic-bezier(0.05, 0, 0, 1);
  --mov-mueve: 300ms cubic-bezier(0.4, 0, 0.2, 1);
  --mov-pulsa: 90ms  cubic-bezier(0.05, 0.7, 0.1, 1);
  --mov-sale:  180ms cubic-bezier(0.4, 0, 1, 1);
}

.panel {
  transition: translate var(--mov-sale), opacity var(--mov-sale);
}
.panel[data-abierto] {
  transition: translate var(--mov-entra), opacity var(--mov-entra);
}

Que la variable contenga los dos valores tiene una ventaja adicional que se aprecia con el tiempo: cuando alguien cambia una duración tiene que decidir también qué pasa con la curva, en lugar de cambiar un número aislado sin pensar en el efecto conjunto.

Una curva no se valida mirándola: se valida cronometrando el 80 por ciento

El método con el que la mayoría ajusta curvas —probarla, mirarla y decidir si “queda bien”— tiene un problema de método serio: cuando llevas veinte iteraciones mirando la misma animación, tu percepción está saturada y ya no distingues nada. Hace falta un número, y el número correcto no es la duración total sino el instante en que la animación ha recorrido el 80 por ciento, porque es aproximadamente cuando el sistema visual da el movimiento por terminado y pasa a otra cosa. Ese instante se calcula sin ejecutar nada: resuelves la curva y buscas la fracción de tiempo en la que el progreso llega a 0.8. Para cubic-bezier(0.2, 0, 0, 1) con 300 milisegundos, sale alrededor de 110; para linear con los mismos 300, sale 240. Dos animaciones con la misma duración escrita y una diferencia percibida de más del doble. Tener ese número cambia dos cosas en la práctica. La primera es que puedes igualar la velocidad percibida entre componentes que tienen duraciones distintas, que es lo que hace que un sistema se sienta coherente; dos componentes con duraciones de 240 y 380 milisegundos pueden tener el mismo instante del 80 por ciento si sus curvas lo compensan, y entonces se perciben igual de ágiles pese al número distinto. La segunda es que resuelve la discusión más frecuente sobre movimiento: cuando alguien dice que una animación se siente lenta, con el número puedes distinguir si el problema es que dura demasiado —hay que bajar la duración— o si dura bien pero reparte mal —hay que estirar la cola—, y son dos correcciones opuestas que sin el dato se eligen por intuición y se aciertan la mitad de las veces.

La función que da ese número, para tenerla a mano:

// Instante en que la curva alcanza un progreso dado, como fraccion
// de la duracion total. Resuelve x(t) = p por biseccion.
function instanteDeProgreso(x1, y1, x2, y2, objetivo = 0.8) {
  const bez = (a, b, t) =>
    3 * (1 - t) ** 2 * t * a + 3 * (1 - t) * t ** 2 * b + t ** 3;

  // Busca el parametro t donde y(t) = objetivo.
  let lo = 0, hi = 1, t = 0.5;
  for (let i = 0; i < 60; i++) {
    t = (lo + hi) / 2;
    if (bez(y1, y2, t) < objetivo) lo = t; else hi = t;
  }
  // Devuelve la coordenada temporal correspondiente a ese parametro.
  return bez(x1, x2, t);
}

const f = instanteDeProgreso(0.2, 0, 0, 1);
console.log('80% del recorrido a los', (f * 300).toFixed(0), 'ms de 300');

Con esa función, comparar dos curvas deja de ser una cuestión de impresión. Y montar la tabla de tu sistema —qué instante del 80 por ciento tiene cada pareja de curva y duración— es un ejercicio de diez minutos que revela inmediatamente qué componentes están descompensados.

Qué comprobar antes de dar una curva por buena

Cuatro comprobaciones, ninguna subjetiva.

El instante del 80 por ciento. Que esté dentro del tramo perceptivo que le corresponde a esa interacción.

El sobrepaso, si lo hay. Medido con la función de la lección anterior. Por encima del 3 por ciento del recorrido deja de ser inercia.

El comportamiento al interrumpir. Dispara la animación y cámbiala de destino a mitad. Con curvas de sobrepaso grande, las interrupciones producen movimientos incoherentes.

El aspecto sobre una distancia grande y sobre una pequeña. Una curva que funciona en un desplazamiento de 8 píxeles puede ser ilegible en uno de 400, porque en el pequeño no hay resolución suficiente para expresarla.

⚔️ Construye tu sistema de cuatro curvas
  1. Escribe cuatro parejas de curva y duración para las cuatro intenciones de esta lección, sin copiar las de aquí.
  2. Calcula el instante del 80 por ciento de cada una y comprueba que caen en el tramo perceptivo correcto.
  3. Aplícalas a un componente real y compáralas con lo que tenía antes.
  4. Mide el sobrepaso de las que lo tengan y ajusta hasta bajar del 3 por ciento.