30.1 Objetivo del proyecto
Construiremos un simulador bidimensional de proyectiles. La persona podrá configurar el lanzamiento, elegir un integrador, avanzar automática o manualmente, observar la trayectoria, inspeccionar estados, medir el error y exportar los datos.
El objetivo no es solamente obtener una parábola. La aplicación debe mantener separadas las responsabilidades, declarar sus supuestos y producir resultados verificables.
30.2 Requisitos funcionales
30.3 Alcance y supuestos físicos
Representaremos el móvil como una partícula, con eje x horizontal y eje y positivo hacia arriba. La aceleración será constante a=(0,aᵧ), sin resistencia del aire. El movimiento termina al cruzar y=0 durante el descenso.
El modelo permite comparar con una solución analítica. La aceleración es un dato del simulador: estudiar sus causas queda fuera de este curso de cinemática.
v(t)=v₀+at
30.4 Arquitectura por responsabilidades
La interfaz coordina estas piezas, pero no debe contener escondidas las ecuaciones. Separarlas permite probar la física sin ejecutar la animación.
30.5 Modelo de configuración
La configuración describe una ejecución y permanece estable hasta reiniciar:
function crearConfiguracion(datos) {
const configuracion = {
rapidezInicialMps: Number(datos.rapidezInicialMps),
anguloGrados: Number(datos.anguloGrados),
alturaInicialM: Number(datos.alturaInicialM),
aceleracionYMps2: Number(datos.aceleracionYMps2),
pasoS: Number(datos.pasoS),
integrador: datos.integrador
};
const numeros = Object.values(configuracion).filter(v => typeof v === "number");
if (!numeros.every(Number.isFinite) || configuracion.pasoS <= 0) {
throw new TypeError("Configuración numérica inválida");
}
return Object.freeze(configuracion);
}
console.log(crearConfiguracion({
rapidezInicialMps: 20,
anguloGrados: 45,
alturaInicialM: 2,
aceleracionYMps2: -9.8,
pasoS: 0.02,
integrador: "velocity-verlet"
}));30.6 Crear el estado inicial
Convertimos el ángulo a radianes y descomponemos la rapidez. El estado conserva tiempo, posición, velocidad, aceleración y referencia:
function crearEstadoInicial(config) {
const anguloRad = config.anguloGrados * Math.PI / 180;
return {
tiempoS: 0,
posicionM: { x: 0, y: config.alturaInicialM },
velocidadMps: {
x: config.rapidezInicialMps * Math.cos(anguloRad),
y: config.rapidezInicialMps * Math.sin(anguloRad)
},
aceleracionMps2: { x: 0, y: config.aceleracionYMps2 },
referencia: "suelo"
};
}
console.log(crearEstadoInicial({
rapidezInicialMps: 20,
anguloGrados: 30,
alturaInicialM: 2,
aceleracionYMps2: -9.8
}));30.7 Una interfaz común para integradores
Cada integrador recibe un estado y Δt y devuelve otro estado con la misma forma. Así el reloj y el dibujo no necesitan conocer la fórmula elegida.
function integrarEuler(estado, dt) {
const { posicionM: r, velocidadMps: v, aceleracionMps2: a } = estado;
return {
...estado,
tiempoS: estado.tiempoS + dt,
posicionM: { x: r.x + v.x * dt, y: r.y + v.y * dt },
velocidadMps: { x: v.x + a.x * dt, y: v.y + a.y * dt }
};
}
function integrarSemiImplicito(estado, dt) {
const { posicionM: r, velocidadMps: v, aceleracionMps2: a } = estado;
const nuevaV = { x: v.x + a.x * dt, y: v.y + a.y * dt };
return {
...estado,
tiempoS: estado.tiempoS + dt,
posicionM: { x: r.x + nuevaV.x * dt, y: r.y + nuevaV.y * dt },
velocidadMps: nuevaV
};
}
const inicial = {
tiempoS: 0,
posicionM: { x: 0, y: 2 },
velocidadMps: { x: 6, y: 8 },
aceleracionMps2: { x: 0, y: -9.8 }
};
console.log(integrarEuler(inicial, 0.1));
console.log(integrarSemiImplicito(inicial, 0.1));30.8 Solución exacta y Velocity Verlet
Como la aceleración es constante, Velocity Verlet coincide con las ecuaciones analíticas paso a paso. Conservamos ambas opciones porque conceptualmente cumplen papeles distintos: una consulta desde el estado inicial y un integrador desde el estado actual.
function integrarVelocityVerlet(estado, dt) {
const { posicionM: r, velocidadMps: v, aceleracionMps2: a } = estado;
return {
...estado,
tiempoS: estado.tiempoS + dt,
posicionM: {
x: r.x + v.x * dt + 0.5 * a.x * dt ** 2,
y: r.y + v.y * dt + 0.5 * a.y * dt ** 2
},
velocidadMps: { x: v.x + a.x * dt, y: v.y + a.y * dt }
};
}
const estado = {
tiempoS: 0,
posicionM: { x: 0, y: 2 },
velocidadMps: { x: 6, y: 8 },
aceleracionMps2: { x: 0, y: -9.8 }
};
console.log(integrarVelocityVerlet(estado, 0.1));30.9 Seleccionar la estrategia
Un objeto de funciones evita una cadena extensa de condiciones y facilita agregar métodos:
const integradores = {
euler: (x, v, a, dt) => ({
x: x + v * dt,
v: v + a * dt
}),
semimplicito: (x, v, a, dt) => {
const nuevaV = v + a * dt;
return { x: x + nuevaV * dt, v: nuevaV };
},
verlet: (x, v, a, dt) => ({
x: x + v * dt + 0.5 * a * dt ** 2,
v: v + a * dt
})
};
const metodo = "verlet";
console.log(integradores[metodo](0, 3, -2, 0.5));Antes de llamar debemos comprobar que la clave existe. Una configuración externa no debe poder ejecutar propiedades arbitrarias.
30.10 Detectar el contacto con el suelo
El móvil puede pasar de y positiva a y negativa entre dos estados. Detectamos el cambio de signo e interpolamos el cruce:
function ajustarAlSuelo(anterior, actual) {
if (anterior.posicionM.y < 0 || actual.posicionM.y >= 0) return actual;
const fraccion = anterior.posicionM.y
/ (anterior.posicionM.y - actual.posicionM.y);
const mezclar = (a, b) => a + (b - a) * fraccion;
return {
...actual,
tiempoS: mezclar(anterior.tiempoS, actual.tiempoS),
posicionM: {
x: mezclar(anterior.posicionM.x, actual.posicionM.x),
y: 0
},
velocidadMps: {
x: mezclar(anterior.velocidadMps.x, actual.velocidadMps.x),
y: mezclar(anterior.velocidadMps.y, actual.velocidadMps.y)
}
};
}
console.log(ajustarAlSuelo(
{ tiempoS: 1, posicionM: { x: 8, y: 1 }, velocidadMps: { x: 8, y: -3 } },
{ tiempoS: 1.5, posicionM: { x: 12, y: -2 }, velocidadMps: { x: 8, y: -5 } }
));La interpolación lineal aproxima el evento para métodos numéricos. La solución exacta permitiría resolver la raíz cuadrática; compararlas forma parte de la verificación.
30.11 Registrar la historia
Cada estado aceptado se agrega a un arreglo. Las métricas derivadas se calculan desde esa fuente para evitar duplicar valores que podrían quedar desincronizados.
function resumirHistoria(historia) {
if (historia.length === 0) return null;
let alturaMaximaM = -Infinity;
let rapidezMaximaMps = 0;
for (const estado of historia) {
alturaMaximaM = Math.max(alturaMaximaM, estado.posicionM.y);
rapidezMaximaMps = Math.max(
rapidezMaximaMps,
Math.hypot(estado.velocidadMps.x, estado.velocidadMps.y)
);
}
const final = historia.at(-1);
return { alturaMaximaM, rapidezMaximaMps, duracionS: final.tiempoS };
}
console.log(resumirHistoria([
{ tiempoS: 0, posicionM: { x: 0, y: 2 }, velocidadMps: { x: 3, y: 4 } },
{ tiempoS: 1, posicionM: { x: 3, y: 4 }, velocidadMps: { x: 3, y: -2 } }
]));30.12 Aplicación interactiva completa
Configurá el lanzamiento y compará métodos. Al modificar un parámetro la ejecución se reinicia para que toda la historia pertenezca a una única configuración.
Simulador y analizador de trayectorias
Modelo ideal sin aire; eje y positivo hacia arriba; final al cruzar el suelo.
Últimos estados registrados
| t (s) | x (m) | y (m) | vₓ (m/s) | vᵧ (m/s) |
|---|
30.13 Escala física y escala gráfica
El modelo conserva metros; Canvas utiliza píxeles. La vista calcula una transformación según el alcance y altura exactos esperados:
píxelY = alto − margenInferior − y·escalaY
El signo vertical se invierte porque Canvas crece hacia abajo. Esta transformación no debe modificar los estados ni introducir píxeles en las ecuaciones físicas.
30.14 El reloj de la aplicación
La reproducción usa requestAnimationFrame, acumula el tiempo real y consume pasos fijos. Se limita el intervalo por imagen y la cantidad de subpasos para evitar una espiral de atraso.
mientras acumulador ≥ Δt y subpasos < límite:
avanzar(Δt)
acumulador −= Δt
El botón de avance manual llama a la misma función física. Así no existen dos implementaciones distintas para reproducción y depuración.
30.15 Interpolación y trayectoria registrada
Durante la reproducción puede interpolarse el móvil entre los dos últimos estados para suavizarlo. La línea de trayectoria, en cambio, representa muestras físicas registradas. No debemos agregar posiciones interpoladas a la historia porque dependerían de la frecuencia de dibujo.
En este proyecto el punto actual se presenta desde el último estado calculado para mantener evidente qué valores aparecen en la tabla. Como extensión se puede añadir una posición visual interpolada separada.
30.16 Exportar CSV
El encabezado declara nombres y unidades. Cada fila corresponde a un estado físico:
function historiaACsv(historia) {
const filas = ["tiempo_s,posicion_x_m,posicion_y_m,velocidad_x_mps,velocidad_y_mps"];
for (const e of historia) {
filas.push([
e.tiempoS,
e.posicionM.x,
e.posicionM.y,
e.velocidadMps.x,
e.velocidadMps.y
].join(","));
}
return filas.join("\n");
}
console.log(historiaACsv([{
tiempoS: 0,
posicionM: { x: 0, y: 2 },
velocidadMps: { x: 3, y: 4 }
}]));La descarga se inicia solamente cuando la persona pulsa el botón. La configuración del ensayo debería guardarse junto con el archivo o en sus metadatos para poder reproducirlo.
30.17 Verificaciones automáticas del proyecto
- El estado inicial coincide con los controles y usa valores finitos.
- El tiempo aumenta y ningún paso supera el Δt configurado.
- vₓ permanece constante porque aₓ=0.
- La velocidad vertical coincide con
vᵧ₀+aᵧtpara todos los métodos. - Velocity Verlet y la opción exacta coinciden dentro del redondeo.
- Al finalizar, y es cero y la velocidad vertical es negativa.
- Al reducir Δt, el error de Euler debe disminuir.
- El CSV contiene encabezado y tantas filas de datos como estados.
30.18 Prueba de convergencia integrada
function posicionEuler(v0, a, duracion, dtMaximo) {
let x = 0, v = v0, t = 0;
while (t < duracion - 1e-12) {
const dt = Math.min(dtMaximo, duracion - t);
x += v * dt;
v += a * dt;
t += dt;
}
return x;
}
const exacta = 3 * 4 + 0.5 * 2 * 4 ** 2;
const error1 = Math.abs(posicionEuler(3, 2, 4, 0.2) - exacta);
const error2 = Math.abs(posicionEuler(3, 2, 4, 0.1) - exacta);
console.log({ error1, error2, razon: error1 / error2 });La razón cercana a dos confirma el primer orden de Euler en este ensayo. Esta prueba se ejecuta sin interfaz ni reloj real.
30.19 Estados y transiciones de la interfaz
| Estado | Acciones | Transición |
|---|---|---|
| Preparado | Configurar, avanzar o iniciar | Iniciar → ejecutando |
| Ejecutando | Pausar | Pausar → pausado; suelo → finalizado |
| Pausado | Continuar, avanzar o reiniciar | Continuar → ejecutando |
| Finalizado | Exportar o reiniciar | Reiniciar → preparado |
Modelar explícitamente las transiciones evita que “iniciar” duplique bucles o que “avanzar” continúe después del impacto.
30.20 Accesibilidad y uso responsable de Canvas
Los resultados esenciales aparecen como texto y tabla, no solamente por color o posición. Los controles poseen etiquetas, los botones funcionan con teclado y las actualizaciones importantes utilizan regiones accesibles.
Canvas ofrece una representación complementaria. Una aplicación más extensa puede proporcionar una descripción resumida de la trayectoria y respetar prefers-reduced-motion pausando la reproducción automática.
30.21 Extensiones posibles
- Agregar aceleraciones dependientes del tiempo, posición o velocidad.
- Comparar varios integradores simultáneamente.
- Incorporar obstáculos, eventos y rebotes documentados.
- Mostrar gráficos de x(t), y(t), velocidad y error.
- Importar configuraciones y reproducir ensayos guardados.
- Ejecutar estudios automáticos con varios valores de Δt.
- Separar el núcleo físico en módulos y agregar un entorno formal de pruebas.
- Ejecutar cálculos pesados fuera del hilo de la interfaz.
Cada extensión debe preservar unidades, referencia, marcas temporales y verificaciones.
30.22 Errores frecuentes en el proyecto
- Mezclar metros y píxeles: transformar solamente al dibujar.
- Cambiar controles durante una historia: reiniciar o registrar el cambio como evento.
- Duplicar fórmulas: reproducción y avance manual deben compartir el mismo núcleo.
- Detectar el suelo con y===0: comprobar y ajustar el cruce entre estados.
- Guardar posiciones visuales: el registro debe contener estados físicos.
- Comparar métodos en condiciones distintas: reutilizar exactamente la misma configuración.
- Confiar solo en la parábola dibujada: medir error y ejecutar invariantes.
- Exportar sin unidades ni parámetros: documentar el significado de cada columna y ensayo.
30.23 Ejercicios de ampliación
- Agregá al resumen el alcance y la rapidez al llegar al suelo.
- Mostrá una gráfica independiente de vᵧ(t).
- Implementá un botón que ejecute el mismo lanzamiento con Δt, Δt/2 y Δt/4 y presente una tabla de errores.
- Agregá una referencia móvil y mostrale la trayectoria transformada.
- Diseñá una prueba que detecte si se intercambian seno y coseno al descomponer v₀.
- Incluí la configuración como líneas comentadas al comienzo del CSV.
- Implementá interpolación visual sin agregar muestras falsas a la historia.
- Proponé una arquitectura de archivos para separar modelo, vista, reloj y pruebas.
Ver orientaciones
- El alcance es x final menos x inicial; la rapidez es
Math.hypot(vx,vy). - Usá tiempo en el eje horizontal y escalá entre el mínimo y máximo de vᵧ registrados.
- Conservá constantes todas las condiciones y compará contra la solución analítica en instantes comunes.
- Aplicá las transformaciones de Galileo a copias de los estados; no cambies la historia original.
- A 0° debe cumplirse vₓ=v₀ y vᵧ=0; a 90°, vₓ≈0 y vᵧ=v₀.
- Anteponé filas como
# paso_s=0.05y documentá que son metadatos, no datos tabulares. - Conservá estados anterior y actual, calculá α desde el acumulador y dibujá una copia interpolada.
- Por ejemplo:
modelo.js,integradores.js,reloj.js,vista.js,exportar.jsy una carpetapruebas.
30.24 Cierre del curso
Una simulación de movimiento no es solamente un objeto que cambia de píxel. Es una cadena de decisiones: sistema de referencia, unidades, estado, ecuaciones, paso temporal, integrador, tratamiento de eventos, presentación y verificación.
- Modelá explícitamente qué representa cada dato.
- Separá física, reloj, dibujo y controles.
- Elegí el integrador y Δt mediante evidencia.
- Conservá estados y parámetros suficientes para reproducir resultados.
- Usá soluciones conocidas, invariantes, gráficos y pruebas automáticas.
- Documentá los límites del modelo antes de interpretar sus predicciones.
Con estas herramientas podés extender el proyecto hacia movimientos más complejos sin perder la claridad física ni la capacidad de comprobar el programa.