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
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:
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:
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:
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:
| Imagen | Se agrega | Pasos físicos | Resto |
|---|---|---|---|
| 1 | 16 ms | 1 | 6 ms |
| 2 | 17 ms | 2 | 3 ms |
| 3 | 8 ms | 1 | 1 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:
Interpolamos entre el estado anterior y el actual:
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.
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:
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
- 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?
- En la siguiente imagen transcurren 12 ms. Usá el resto anterior y repetí el cálculo.
- Si el acumulador vale 6 ms y el paso 20 ms, calculá α.
- Interpolá entre xanterior=10 m y xactual=14 m con α=0,3.
- Explicá por qué la posición interpolada no debe alimentar el integrador.
- Diseñá una política para una pestaña que estuvo oculta cinco segundos.
- ¿Qué ocurre si una actualización física tarda más que el propio paso?
- Proponé datos que registrarías para diagnosticar tirones visibles.
Ver soluciones y explicaciones
- Se ejecutan 2 pasos, se consumen 40 ms y quedan 15 ms.
- El acumulador llega a 27 ms; se ejecuta un paso y quedan 7 ms.
α=6/20=0,3.x=10+(14−10)·0,3=11,2 m.- 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.
- 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.
- 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.
- 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/pasoSsirve 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.