28. Tiempo real, paso fijo e interpolación para animaciones fluidas

La pantalla se actualiza cuando el navegador puede dibujar, mientras la física necesita pasos controlados. Un acumulador desacopla ambos ritmos y la interpolación presenta movimientos suaves sin alterar la simulación.

28.1 Del cálculo aislado a una animación

Hasta ahora simulamos una duración mediante un bucle que podía terminar tan rápido como permitiera la computadora. Una animación interactiva debe relacionar el tiempo del modelo con el tiempo que transcurre para la persona que observa.

Además, el navegador comparte recursos con otras tareas. Dos dibujos consecutivos no están garantizados cada 16,67 ms: pueden separarse 14 ms, 20 ms o mucho más si la pestaña pierde actividad. El bucle debe admitir esas variaciones sin convertirlas directamente en pasos físicos arbitrarios.

28.2 Tres ritmos diferentes

Tiempo realDuración medida entre llamadas del navegador.
Tiempo simuladoInstante alcanzado mediante pasos del modelo físico.
Tiempo de presentaciónInstante visual que se muestra al interpolar estados.
Frecuencia de renderizadoCantidad de imágenes que llegan a dibujarse por segundo.

Estos valores pueden coincidir aproximadamente, pero cumplen funciones distintas. La física puede avanzar a 100 Hz mientras la pantalla dibuja a 60 Hz o cambia dinámicamente su frecuencia.

28.3 requestAnimationFrame

requestAnimationFrame solicita al navegador una llamada antes de un próximo repintado. La función recibe una marca temporal monotónica en milisegundos asociada al ciclo de animación.

function mostrarIntervalos(marcasMs) {
  const intervalosS = [];
  for (let i = 1; i < marcasMs.length; i++) {
    intervalosS.push((marcasMs[i] - marcasMs[i - 1]) / 1000);
  }
  return intervalosS;
}

console.log(mostrarIntervalos([1000, 1016.7, 1034.1, 1050]));

Las diferencias no son idénticas. También debemos convertir milisegundos a segundos antes de utilizarlas en ecuaciones con unidades SI.

28.4 El enfoque de paso variable

Una solución directa consiste en avanzar la física una vez por imagen usando el intervalo real:

estado siguiente = integrar(estado actual, Δt real)

Este enfoque mantiene aproximadamente la velocidad correcta en tiempo real, pero cada imagen utiliza un Δt distinto. La precisión y estabilidad cambian con el rendimiento, dos computadoras pueden generar trayectorias diferentes y un retraso grande puede causar un salto o desestabilizar el integrador.

Puede ser suficiente para movimientos visuales simples, siempre que se controlen los intervalos. Para una simulación verificable suele convenir un paso físico fijo.

28.5 Paso fijo

Elegimos un paso constante, por ejemplo pasoS=1/120, y toda actualización física utiliza exactamente ese valor:

estadoₙ₊₁ = integrar(estadoₙ, pasoS)

La secuencia depende de condiciones iniciales, entradas e integrador, no de la frecuencia momentánea de la pantalla. Esto simplifica pruebas, repetición y análisis de estabilidad.

El problema restante es decidir cuántos pasos ejecutar cuando llega una imagen después de un intervalo real cualquiera.

28.6 El acumulador de tiempo

El acumulador guarda tiempo real que todavía no fue consumido por la física. En cada imagen sumamos el intervalo transcurrido y ejecutamos todos los pasos completos disponibles:

acumulador += tiempoRealTranscurrido
mientras acumulador ≥ pasoS:
  avanzar física un pasoS
  acumulador −= pasoS

El resto siempre queda entre cero y un paso. No se pierde: participa en la siguiente imagen y también determina la interpolación visual.

28.7 Ejemplo manual del acumulador

Con paso fijo de 10 ms, supongamos intervalos de imagen de 16, 17 y 8 ms:

Consumo del tiempo acumulado.
ImagenSe agregaPasos físicosResto
116 ms16 ms
217 ms23 ms
38 ms11 ms

Después de 41 ms reales se ejecutaron cuatro pasos, es decir, 40 ms simulados, y quedó 1 ms pendiente. El tiempo se conserva aunque el número de pasos por imagen cambie.

28.8 Simular una secuencia de imágenes

function consumirIntervalos(intervalosMs, pasoMs) {
  let acumuladorMs = 0;
  let pasosTotales = 0;
  const resumen = [];
  for (const intervaloMs of intervalosMs) {
    acumuladorMs += intervaloMs;
    let pasos = 0;
    while (acumuladorMs >= pasoMs) {
      acumuladorMs -= pasoMs;
      pasos++;
      pasosTotales++;
    }
    resumen.push({ pasos, acumuladorMs });
  }
  return { pasosTotales, acumuladorMs, resumen };
}

console.log(consumirIntervalos([16, 17, 8], 10));

Este ejemplo trabaja con enteros en milisegundos para que el resultado manual sea exacto. El bucle real suele usar segundos de coma flotante y una pequeña tolerancia.

28.9 Por qué el estado actual queda atrasado

Después de consumir los pasos completos queda 0≤acumulador<pasoS. El estado físico actual corresponde al último límite discreto y queda detrás del tiempo real por ese resto.

Podríamos dibujarlo directamente, pero a bajas frecuencias físicas el móvil parecería permanecer quieto y saltar en cada actualización. Para suavizar la presentación usamos también el estado físico anterior.

28.10 Factor de interpolación

La fracción del paso que queda acumulada define:

α = acumulador / pasoS    con    0 ≤ α < 1

Interpolamos entre el estado anterior y el actual:

xvisual = (1−α)xanterior + αxactual

Con α=0 mostramos el anterior; con α próximo a 1 nos acercamos al actual. Esta convención presenta un instante suavizado con aproximadamente un paso de retraso, a cambio de no inventar un futuro que todavía no calculó la física.

28.11 Interpolación lineal

function interpolar(a, b, alfa) {
  if (alfa < 0 || alfa > 1) {
    throw new RangeError("alfa debe estar entre 0 y 1");
  }
  return a + (b - a) * alfa;
}

const anterior = { posicionM: 10, velocidadMps: 4 };
const actual = { posicionM: 14, velocidadMps: 6 };
const alfa = 0.25;

console.log({
  posicionM: interpolar(anterior.posicionM, actual.posicionM, alfa),
  velocidadMps: interpolar(anterior.velocidadMps, actual.velocidadMps, alfa)
});

La posición visual es 11 m. La interpolación es una operación de presentación: no debe escribirse de vuelta sobre actual ni usarse como punto de partida del siguiente paso físico.

28.12 Actividad interactiva: acumulador e interpolación

El laboratorio genera imágenes con intervalos ligeramente variables. El móvil azul representa el estado físico actual; el gris, el anterior; y el naranja, la posición interpolada que se dibuja.

Laboratorio de tiempo real

El móvil avanza con velocidad constante para aislar el funcionamiento temporal.

Los resultados se presentan debajo.
Estado anteriorEstado actualPosición interpolada
Paso físico0,100 s
Pasos en la última imagen0
Acumulador0,000 s
Factor α0,00

28.13 Estructura de un bucle con paso fijo

function crearBucle({ pasoS, actualizar, dibujar }) {
  let marcaAnteriorMs;
  let acumuladorS = 0;
  let anterior;
  let actual;

  function iniciar(estadoInicial) {
    anterior = structuredClone(estadoInicial);
    actual = structuredClone(estadoInicial);
    requestAnimationFrame(imagen);
  }

  function imagen(marcaActualMs) {
    if (marcaAnteriorMs === undefined) marcaAnteriorMs = marcaActualMs;
    const transcurridoS = (marcaActualMs - marcaAnteriorMs) / 1000;
    marcaAnteriorMs = marcaActualMs;
    acumuladorS += transcurridoS;

    while (acumuladorS >= pasoS) {
      anterior = actual;
      actual = actualizar(actual, pasoS);
      acumuladorS -= pasoS;
    }

    dibujar(anterior, actual, acumuladorS / pasoS);
    requestAnimationFrame(imagen);
  }

  return { iniciar };
}

console.log(typeof crearBucle);

Definir la función no inicia la animación del ejemplo. En una aplicación real, iniciar se llama una sola vez y cada callback solicita la imagen siguiente.

28.14 Copiar el estado anterior en el momento correcto

Antes de cada paso físico, anterior debe conservar el estado que está por reemplazarse. Después calculamos actual. Si ambos nombres apuntan al mismo objeto mutable, la interpolación verá dos estados iguales.

function avanzarPar(par, integrar, pasoS) {
  const anterior = par.actual;
  const actual = integrar(par.actual, pasoS);
  return { anterior, actual };
}

const integrarMRU = (estado, dt) => ({
  ...estado,
  tiempoS: estado.tiempoS + dt,
  posicionM: estado.posicionM + estado.velocidadMps * dt
});

const par = avanzarPar({
  actual: { tiempoS: 0, posicionM: 2, velocidadMps: 5 }
}, integrarMRU, 0.1);

console.log(par);

La interfaz inmutable facilita mantener dos fotografías distintas. Con mutación, hay que copiar explícitamente los componentes necesarios antes de actualizar.

28.15 Intervalos enormes y reanudación

Una pestaña oculta, una pausa del depurador o una tarea pesada puede producir un intervalo de varios segundos. Intentar recuperar todo de una vez requiere cientos de subpasos, retrasa aún más la siguiente imagen y puede iniciar la espiral de la muerte.

Una defensa consiste en limitar el intervalo incorporado al acumulador:

function limitarIntervalo(transcurridoS, maximoS = 0.25) {
  if (!Number.isFinite(transcurridoS) || transcurridoS < 0) return 0;
  return Math.min(transcurridoS, maximoS);
}

console.log(limitarIntervalo(0.016));
console.log(limitarIntervalo(4.2));

Limitar significa que parte del tiempo real no se simula. Es una política explícita: apropiada para muchas animaciones, pero quizá no para una simulación científica que deba conservar toda la cronología.

28.16 Límite de subpasos

Otra defensa fija un máximo de avances físicos por imagen. Si se alcanza, debemos decidir qué hacer con el tiempo restante:

Conservar el atrasoLa simulación intenta recuperarse en imágenes futuras, pero puede permanecer rezagada.
Descartar tiempoEvita saturación, pero el reloj simulado salta respecto del real.
Pausar y avisarAdecuado cuando perder tiempo invalida el resultado.
Reducir cargaDisminuir detalle visual o trabajo no esencial, sin cambiar el paso físico.

No existe una política universal. Debe corresponder a los requisitos de un juego, una visualización educativa o un análisis científico.

28.17 Pausar y reanudar correctamente

Al reanudar no conviene interpretar toda la pausa como tiempo a recuperar. Podemos reiniciar la marca anterior para que la primera imagen agregue cero:

function crearControlTemporal() {
  let marcaAnteriorMs;
  let pausado = true;
  return {
    pausar() { pausado = true; },
    reanudar() {
      pausado = false;
      marcaAnteriorMs = undefined;
    },
    intervalo(marcaMs) {
      if (pausado) return 0;
      if (marcaAnteriorMs === undefined) {
        marcaAnteriorMs = marcaMs;
        return 0;
      }
      const dt = (marcaMs - marcaAnteriorMs) / 1000;
      marcaAnteriorMs = marcaMs;
      return dt;
    }
  };
}

const reloj = crearControlTemporal();
reloj.reanudar();
console.log(reloj.intervalo(1000));
console.log(reloj.intervalo(1016));

28.18 Interpolar vectores y ángulos

Para una posición vectorial aplicamos la interpolación a cada componente. Los ángulos requieren cuidado: interpolar directamente de 350° a 10° recorrería 340° en lugar del camino corto de 20°.

function interpolarVector(a, b, alfa) {
  return {
    x: a.x + (b.x - a.x) * alfa,
    y: a.y + (b.y - a.y) * alfa
  };
}

console.log(interpolarVector(
  { x: 2, y: 5 },
  { x: 10, y: 1 },
  0.25
));

Rotaciones, orientaciones tridimensionales y trayectorias curvas pueden necesitar interpolaciones específicas. La interpolación lineal es un punto de partida, no una solución universal para todo tipo de estado.

28.19 Entradas y reproducibilidad

Un paso fijo ayuda a reproducir resultados, pero las entradas también deben asociarse a instantes o pasos. Si una tecla se aplica “cuando llegó una imagen”, distintas frecuencias de dibujo pueden asignarla a pasos diferentes.

Una estrategia consiste en registrar eventos con su marca temporal, incorporarlos antes del paso físico correspondiente y guardar la secuencia para repetirla. Las fuentes de aleatoriedad también necesitan una semilla controlada.

La reproducibilidad exacta puede depender además del entorno numérico, pero separar entradas, física y dibujo elimina una fuente importante de variación.

28.20 Medir rendimiento sin confundirlo con física

Los cuadros por segundo describen presentación, no precisión. Conviene registrar por separado:

  • intervalo real entre imágenes;
  • cantidad de pasos físicos ejecutados por imagen;
  • tiempo consumido por las actualizaciones;
  • tamaño del acumulador y atraso;
  • imágenes o tiempo descartados según la política;
  • error físico medido mediante las pruebas del tema anterior.

Una animación fluida puede esconder una física inestable; una física precisa puede dibujarse con saltos si falta interpolación.

28.21 Errores frecuentes

  • Suponer 60 Hz: usar la marca temporal entregada por el navegador.
  • Pasar milisegundos al integrador: convertir el intervalo a segundos.
  • Usar un paso variable sin límites: los retrasos grandes pueden volver inestable la física.
  • Descartar el resto del acumulador: provoca una pérdida sistemática de tiempo.
  • Interpolar y sobrescribir el estado físico: la interpolación pertenece solo al dibujo.
  • Compartir el mismo objeto como anterior y actual: ambos estados deben ser fotografías distintas.
  • Recuperar una pausa completa: reiniciar la marca temporal al reanudar.
  • No limitar subpasos: un retraso puede alimentar la espiral de la muerte.
  • Confundir FPS con actualizaciones físicas: son ritmos independientes.

28.22 Ejercicios propuestos

  1. Con paso físico de 20 ms y un intervalo real de 55 ms, ¿cuántos pasos se ejecutan y qué resto queda si el acumulador comenzó en cero?
  2. En la siguiente imagen transcurren 12 ms. Usá el resto anterior y repetí el cálculo.
  3. Si el acumulador vale 6 ms y el paso 20 ms, calculá α.
  4. Interpolá entre xanterior=10 m y xactual=14 m con α=0,3.
  5. Explicá por qué la posición interpolada no debe alimentar el integrador.
  6. Diseñá una política para una pestaña que estuvo oculta cinco segundos.
  7. ¿Qué ocurre si una actualización física tarda más que el propio paso?
  8. Proponé datos que registrarías para diagnosticar tirones visibles.
Ver soluciones y explicaciones
  1. Se ejecutan 2 pasos, se consumen 40 ms y quedan 15 ms.
  2. El acumulador llega a 27 ms; se ejecuta un paso y quedan 7 ms.
  3. α=6/20=0,3.
  4. x=10+(14−10)·0,3=11,2 m.
  5. Representa un estado visual entre dos estados físicos. Usarlo modificaría la secuencia y haría que el resultado dependiera del ritmo de dibujo.
  6. Para una animación educativa, reiniciar la marca al volver y no recuperar los cinco segundos. En un análisis que no pueda perder tiempo, pausar el modelo y requerir una reanudación explícita.
  7. El programa no puede mantener el ritmo: el acumulador crece y puede aparecer una espiral de actualizaciones atrasadas. Hay que reducir costo, aumentar responsablemente el paso o cambiar el modelo.
  8. Intervalo por imagen, subpasos ejecutados, acumulador, duración de actualización y renderizado, límites aplicados y cantidad de imágenes lentas.

28.23 Ideas para recordar

  • La frecuencia de dibujo no es constante ni define por sí sola el tiempo físico.
  • Un paso fijo vuelve más reproducible y verificable la secuencia de estados.
  • El acumulador conserva tiempo real pendiente y determina cuántos pasos ejecutar.
  • El resto normalizado α=acumulador/pasoS sirve para interpolar.
  • La interpolación suaviza la presentación y no modifica la física.
  • Los intervalos y subpasos deben limitarse mediante una política documentada.
  • Pausar, reanudar y procesar entradas requiere controlar las marcas temporales.
  • Rendimiento, suavidad visual y precisión física son cualidades diferentes.

En el próximo tema construiremos una estrategia de verificación con soluciones analíticas, gráficos y pruebas automáticas.