30. Representar cuerpos, impulsos y colisiones con JavaScript

Un modelo de datos claro mantiene juntas las magnitudes relacionadas, evita mezclar cuerpos o unidades y permite reutilizar las mismas operaciones físicas en cálculos, simulaciones y pruebas.

30.1 Del modelo físico al modelo de datos

Una simulación necesita traducir cuerpos, vectores y contactos a valores organizados. La representación elegida debe conservar el significado físico y facilitar las operaciones que realizaremos con frecuencia.

Una buena estructura no reemplaza las ecuaciones: hace más difícil aplicarlas al cuerpo equivocado, perder una componente o mezclar estados de instantes diferentes.

30.2 Representar un vector

En dos dimensiones podemos usar un objeto con propiedades numéricas x e y:

const velocidad = { x: 3, y: -2 };
const impulso = { x: -4, y: 1.5 };

console.log("Velocidad:", velocidad, "m/s");
console.log("Impulso:", impulso, "N·s");

El mismo formato sirve para posición, velocidad, momento, fuerza e impulso. El nombre de la variable y la documentación indican qué magnitud y unidad representa.

30.3 Estructura mínima de un cuerpo

const cuerpo = {
  id: "A",
  masa: 2,
  radio: 0.5,
  posicion: { x: 1, y: 3 },
  velocidad: { x: 4, y: -1 }
};

console.log("Cuerpo creado:");
console.log(JSON.stringify(cuerpo, null, 2));
Identidadid permite relacionar registros y mensajes con el cuerpo.
Propiedadesmasa y radio describen parámetros del modelo.
Estadoposicion y velocidad cambian durante la simulación.

30.4 Nombres, unidades y convenciones

Antes de programar conviene documentar una convención única. Por ejemplo: kilogramos, metros, segundos, ángulos en radianes y ejes cartesianos con +y hacia arriba.

PropiedadTipoUnidad o regla
masanumberkg, estrictamente positiva.
radionumberm, positivo.
posicionvectorm, coordenadas globales.
velocidadvectorm/s, coordenadas globales.

30.5 Crear y validar cuerpos

Una función constructora centraliza valores predeterminados y rechaza estados inválidos:

function validarVector(vector, nombre = "vector") {
  if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
    throw new Error(`${nombre} debe tener componentes x e y finitas`);
  }
}

function crearCuerpo({ id, masa, radio, posicion, velocidad }) {
  if (!id) throw new Error("Falta el identificador");
  if (!Number.isFinite(masa) || masa <= 0) {
    throw new Error("La masa debe ser positiva y finita");
  }
  if (!Number.isFinite(radio) || radio <= 0) {
    throw new Error("El radio debe ser positivo y finito");
  }
  validarVector(posicion, "posición");
  validarVector(velocidad, "velocidad");

  return {
    id,
    masa,
    radio,
    posicion: { ...posicion },
    velocidad: { ...velocidad }
  };
}

const cuerpo = crearCuerpo({
  id: "A", masa: 2, radio: 0.5,
  posicion: { x: 1, y: 3 }, velocidad: { x: 4, y: -1 }
});
console.log("Cuerpo validado:", cuerpo);

30.6 Validar vectores

function validarVector(vector, nombre = "vector") {
  if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
    throw new Error(`${nombre} debe tener componentes x e y finitas`);
  }
  return vector;
}

console.log("Vector válido:", validarVector({ x: 3, y: -2 }, "velocidad"));
try {
  validarVector({ x: NaN, y: 1 }, "impulso");
} catch (error) {
  console.log("Vector rechazado:", error.message);
}

typeof NaN es "number"; por eso Number.isFinite es más útil que comprobar solamente el tipo.

30.7 Operaciones vectoriales puras

Funciones pequeñas permiten escribir las ecuaciones de forma cercana a su notación matemática:

const sumar = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const restar = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const dot = (a, b) => a.x * b.x + a.y * b.y;
const modulo = v => Math.hypot(v.x, v.y);

const a = { x: 3, y: 4 };
const b = { x: -1, y: 2 };
console.log("a + b =", sumar(a, b));
console.log("a - b =", restar(a, b));
console.log("2a =", escalar(2, a));
console.log("a · b =", dot(a, b));
console.log("|a| =", modulo(a));

Estas funciones devuelven objetos nuevos y no modifican sus argumentos, lo que facilita probarlas y combinarlas.

30.8 Magnitudes derivadas

El momento y la energía cinética se calculan desde el estado actual:

const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const dot = (a, b) => a.x * b.x + a.y * b.y;

function momento(cuerpo) {
  return escalar(cuerpo.masa, cuerpo.velocidad);
}

function energiaCinetica(cuerpo) {
  return 0.5 * cuerpo.masa * dot(cuerpo.velocidad, cuerpo.velocidad);
}

const cuerpo = { masa: 2, velocidad: { x: 3, y: 4 } };
console.log("Momento lineal:", momento(cuerpo), "kg·m/s");
console.log("Energía cinética:", energiaCinetica(cuerpo), "J");

No es necesario guardar estas magnitudes dentro del cuerpo. Si se derivan al solicitarlas, no pueden quedar desactualizadas respecto de la velocidad.

30.9 Representar y aplicar un impulso

Un impulso es un vector. Para masa constante:

v⃗f = v⃗i + J⃗/m
function validarVector(vector, nombre = "vector") {
  if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
    throw new Error(`${nombre} debe tener componentes finitas`);
  }
}

function aplicarImpulso(cuerpo, impulso) {
  validarVector(impulso, "impulso");
  cuerpo.velocidad.x += impulso.x / cuerpo.masa;
  cuerpo.velocidad.y += impulso.y / cuerpo.masa;
}

const cuerpo = { masa: 2, velocidad: { x: 3, y: -1 } };
console.log("Velocidad inicial:", { ...cuerpo.velocidad });
aplicarImpulso(cuerpo, { x: 4, y: 2 });
console.log("Velocidad final:", cuerpo.velocidad);

La función modifica intencionalmente el estado del cuerpo. Su nombre debe hacer visible ese efecto.

30.10 Mutación o actualización inmutable

EnfoqueVentajaPrecaución
MutableMenos asignaciones y acceso directo al estado actual.Debe controlarse exactamente cuándo se escribe.
InmutableConserva estados anteriores y facilita depuración.Requiere crear copias, incluidas las propiedades anidadas.
const sumar = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });

function conImpulso(cuerpo, impulso) {
  return {
    ...cuerpo,
    velocidad: sumar(cuerpo.velocidad, escalar(1 / cuerpo.masa, impulso))
  };
}

const original = { id: "A", masa: 2, velocidad: { x: 3, y: -1 } };
const actualizado = conImpulso(original, { x: 4, y: 2 });
console.log("Original sin cambios:", original);
console.log("Nuevo estado:", actualizado);
console.log("¿Son objetos distintos?", original !== actualizado);

30.11 Representar un contacto

El contacto describe una relación temporal entre dos cuerpos, no una propiedad permanente de uno solo:

const contacto = {
  cuerpoA: "A",
  cuerpoB: "B",
  normal: { x: 0.8, y: 0.6 },
  punto: { x: 4.2, y: 1.1 },
  penetracion: 0.003,
  restitucion: 0.7
};

console.log("Contacto detectado:");
console.log(JSON.stringify(contacto, null, 2));

La normal debe tener una orientación documentada, por ejemplo desde A hacia B.

30.12 Separar detección y respuesta

La detección decide si existe contacto y calcula su geometría. La respuesta usa esa información para obtener impulsos y actualizar velocidades.

Estado de cuerpos
Detección geométrica
Respuesta impulsiva
Nuevo estado

Esta separación permite cambiar la forma de detectar sin reescribir las ecuaciones físicas.

30.13 Registrar una colisión resuelta

Un registro facilita depuración, reproducción y pruebas:

const evento = {
  tipo: "colision",
  tiempo: 1.275,
  participantes: ["A", "B"],
  normal: { x: 1, y: 0 },
  impulsoEscalar: 8.4,
  velocidadesAntes: {
    A: { x: 5, y: 0 },
    B: { x: 0, y: 0 }
  },
  velocidadesDespues: {
    A: { x: 0.8, y: 0 },
    B: { x: 2.8, y: 0 }
  }
};

console.log("Evento registrado:");
console.log(JSON.stringify(evento, null, 2));

30.14 Identificadores y referencias

Guardar identificadores en los eventos evita duplicar cuerpos completos y luego modificar una copia equivocada. Una colección puede indexarse con Map:

const cuerpos = [
  { id: "A", masa: 2 },
  { id: "B", masa: 3 }
];
const contacto = { cuerpoA: "A", cuerpoB: "B" };

const porId = new Map(cuerpos.map(cuerpo => [cuerpo.id, cuerpo]));
const cuerpoA = porId.get(contacto.cuerpoA);
const cuerpoB = porId.get(contacto.cuerpoB);

console.log("Cuerpo referido como A:", cuerpoA);
console.log("Cuerpo referido como B:", cuerpoB);
console.log("Identificadores indexados:", [...porId.keys()]);

Los identificadores deben ser únicos y estables durante la vida del cuerpo.

30.15 Colecciones y estado de la simulación

const simulacion = {
  tiempo: 0,
  cuerpos: [],
  eventos: [],
  configuracion: {
    gravedad: { x: 0, y: 0 },
    tolerancia: 1e-9,
    maximoEventosPorPaso: 100
  }
};

console.log("Estado inicial de la simulación:");
console.log(JSON.stringify(simulacion, null, 2));

Separar configuración, estado dinámico y registro evita usar valores mágicos dispersos en el programa.

30.16 Copias superficiales y alias compartidos

El operador de propagación solo copia el primer nivel:

const cuerpo = {
  id: "A",
  posicion: { x: 1, y: 3 },
  velocidad: { x: 4, y: -1 }
};

const copiaIncompleta = { ...cuerpo };
copiaIncompleta.velocidad.x = 99;
// También cambia cuerpo.velocidad.x: ambos comparten el mismo vector.

console.log("Tras modificar la copia superficial:", cuerpo.velocidad);

const copiaCorrecta = {
  ...cuerpo,
  posicion: { ...cuerpo.posicion },
  velocidad: { ...cuerpo.velocidad }
};
copiaCorrecta.velocidad.x = -5;

console.log("Copia independiente:", copiaCorrecta.velocidad);
console.log("Original sin el segundo cambio:", cuerpo.velocidad);

Este detalle es crítico al guardar estados “antes” y “después”.

30.17 Serializar con JSON

JSON.stringify permite guardar o transmitir estructuras compuestas por datos simples:

const simulacion = {
  tiempo: 1.5,
  cuerpos: [{ id: "A", masa: 2, velocidad: { x: 3, y: 1 } }],
  eventos: [{ tipo: "impulso", tiempo: 1.5 }]
};

const texto = JSON.stringify(simulacion, null, 2);
const restaurada = JSON.parse(texto);

console.log("JSON serializado:");
console.log(texto);
console.log("Estado restaurado:", restaurada);

JSON no conserva funciones, instancias especiales, Map, Infinity ni NaN de manera útil. Conviene serializar solo el estado de datos y reconstruir las herramientas del programa.

30.18 Pruebas e invariantes

Las estructuras permiten escribir comprobaciones pequeñas:

  • masa y radio son finitos y positivos;
  • cada vector posee componentes finitas;
  • los identificadores no se repiten;
  • una normal declarada unitaria tiene módulo próximo a uno;
  • aplicar J⃗ cambia el momento exactamente en J⃗ dentro de la tolerancia;
  • un evento conserva copias independientes de los estados anterior y posterior.

30.19 Actividad interactiva: inspector de estado

Configurá un cuerpo y un impulso. Podés aplicarlo varias veces para observar la actualización acumulativa del objeto y sus magnitudes derivadas.

Inspector de cuerpo e impulso

Azul: velocidad actual. Naranja: impulso configurado. Verde: momento actual.


          
Velocidad actual(+3,00; +1,00) m/s
Momento actual(+6,00; +2,00) kg·m/s
Energía cinética10,00 J
Cambio esperado de velocidad(−2,00; +3,00) m/s
Impulsos aplicados0

El objeto está validado y listo para recibir el impulso.

30.20 Un módulo físico reutilizable

Podemos reunir creación, actualización y auditoría sin mezclar código de interfaz:

function crearCuerpo(datos) {
  if (!datos.id || !Number.isFinite(datos.masa) || Math.sign(datos.masa) !== 1) {
    throw new Error("Datos del cuerpo no válidos");
  }
  return {
    ...datos,
    posicion: { ...datos.posicion },
    velocidad: { ...datos.velocidad }
  };
}

const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const restar = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const momento = cuerpo => escalar(cuerpo.masa, cuerpo.velocidad);

function aplicarImpulso(cuerpo, impulso) {
  cuerpo.velocidad.x += impulso.x / cuerpo.masa;
  cuerpo.velocidad.y += impulso.y / cuerpo.masa;
}

function crearModeloDeCuerpo(datos) {
  const cuerpo = crearCuerpo(datos);

  return {
    obtenerEstado() {
      return {
        ...cuerpo,
        posicion: { ...cuerpo.posicion },
        velocidad: { ...cuerpo.velocidad }
      };
    },
    aplicar(impulso) {
      const momentoAntes = momento(cuerpo);
      aplicarImpulso(cuerpo, impulso);
      const momentoDespues = momento(cuerpo);
      return {
        momentoAntes,
        momentoDespues,
        cambio: restar(momentoDespues, momentoAntes)
      };
    }
  };
}

const modelo = crearModeloDeCuerpo({
  id: "A", masa: 2, radio: 0.5,
  posicion: { x: 1, y: 3 }, velocidad: { x: 4, y: -1 }
});
console.log("Estado inicial:", modelo.obtenerEstado());
console.log("Auditoría del impulso:", modelo.aplicar({ x: -4, y: 6 }));
console.log("Estado final:", modelo.obtenerEstado());

La interfaz gráfica consume este modelo, pero las ecuaciones pueden probarse sin navegador.

30.21 Errores frecuentes y ejercicios

Errores frecuentes

  • Representar un vector como texto y luego intentar calcular con él.
  • Guardar momento y energía sin actualizarlos al cambiar la velocidad.
  • Compartir accidentalmente objetos de posición o velocidad entre cuerpos.
  • Mezclar unidades o convenciones de ejes dentro de la misma colección.
  • Aceptar masas nulas, componentes NaN o identificadores duplicados.
  • Mezclar detección geométrica, respuesta física y dibujo en una sola función.
  • Guardar referencias mutables como si fueran fotografías del estado anterior.

Ejercicios propuestos

  1. Creá un objeto para un cuerpo de 3 kg en (2, −1) m con velocidad (4, 5) m/s.
  2. Implementá una función normalizar(v) que rechace vectores casi nulos.
  3. Calculá con funciones el momento y la energía del cuerpo del ejercicio 1.
  4. Aplicá J⃗ = (−6, 3) N·s y obtené la nueva velocidad.
  5. Escribí una versión inmutable de avanzar(cuerpo, dt).
  6. Diseñá un registro para una colisión entre A y B a t = 2,4 s.
  7. Explicá por qué { ...cuerpo } no crea una copia profunda.
  8. Agregá al laboratorio un historial de los impulsos aplicados.
Ver soluciones y orientaciones
  1. { id: "A", masa: 3, posicion: {x: 2, y: -1}, velocidad: {x: 4, y: 5} }; falta elegir un radio positivo.
  2. Calculá longitud = Math.hypot(v.x, v.y), comprobá una tolerancia y devolvé {x: v.x/longitud, y: v.y/longitud}.
  3. p⃗ = (12, 15) kg·m/s y K = ½·3·(4² + 5²) = 61,5 J.
  4. Δv⃗ = J⃗/m = (−2, 1) m/s; v⃗f = (2, 6) m/s.
  5. Debe devolver un objeto nuevo, copiar posicion y sumar velocidad·dt sin modificar el original.
  6. Incluí tipo, tiempo, identificadores, normal, restitución y copias de velocidades antes y después.
  7. Las propiedades anidadas continúan apuntando a los mismos objetos.
  8. Guardá una copia del impulso y del estado resultante después de cada aplicación.

30.22 Ideas para recordar

  • Los vectores pueden representarse uniformemente como objetos {x, y}.
  • Parámetros, estado dinámico y eventos cumplen funciones diferentes.
  • Las magnitudes derivadas se calculan desde el estado actual.
  • Las operaciones vectoriales puras reducen errores y facilitan pruebas.
  • Una función mutable debe hacer explícito que modifica el cuerpo.
  • Las copias de objetos anidados deben realizarse de manera intencional.
  • Detección, respuesta física y presentación conviene mantenerlas separadas.
  • La validación impide que un dato inválido contamine toda la simulación.

En el próximo tema usaremos estas estructuras para calcular velocidades posteriores y comprobar los resultados.