23. Modelar el estado cinemático con objetos en JavaScript

Una simulación necesita representar qué sabemos de cada móvil en un instante. Un buen modelo de estado reúne esos datos, explicita unidades y referencias, y evita combinaciones incoherentes.

23.1 De las ecuaciones a los datos

Hasta ahora describimos movimientos mediante funciones como x(t), v(t) y a(t). Para programar una simulación también necesitamos guardar sus valores en instantes concretos. El conjunto de valores que describe al móvil en uno de esos instantes es su estado cinemático.

Un estado no explica por qué el móvil se comporta así ni contiene necesariamente toda su historia. Es una fotografía numérica suficiente para consultar el movimiento o calcular un estado posterior según el modelo elegido.

23.2 Qué debe contener un estado

Para una partícula en dos dimensiones con aceleración conocida utilizaremos:

tiempoSInstante al que pertenecen todos los demás valores.
posicionMVector que ubica el móvil en la referencia elegida.
velocidadMpsVector que indica cómo cambia la posición.
aceleracionMps2Vector que indica cómo cambia la velocidad.
referenciaNombre del sistema respecto del cual se expresan los vectores.

La cantidad de campos depende del modelo. Un movimiento uniforme no necesita una aceleración variable, pero conservar el vector aceleración en cero puede dar una interfaz uniforme a distintas clases de movimiento.

23.3 Un primer objeto JavaScript

const estado = {
  tiempoS: 2,
  posicionM: { x: 12, y: 5 },
  velocidadMps: { x: 4, y: -1 },
  aceleracionMps2: { x: 0, y: -9.8 },
  referencia: "suelo"
};

console.log(estado.posicionM.x);
console.log(estado.velocidadMps.y);

Los objetos anidados representan vectores y permiten escribir estado.posicionM.x, más expresivo que recordar qué significa cada elemento de un arreglo. Los sufijos M, Mps y Mps2 documentan las unidades esperadas.

23.4 Estado, propiedades e identidad

No todo dato del móvil es parte de su estado cinemático. El nombre, color o radio de dibujo pueden acompañarlo, pero cumplen otra función. Conviene separar la identidad y presentación de las magnitudes que evolucionan.

const movil = {
  id: "pelota-1",
  apariencia: { color: "#e56b36", radioPx: 8 },
  estado: {
    tiempoS: 0,
    posicionM: { x: 0, y: 2 },
    velocidadMps: { x: 7, y: 9 },
    aceleracionMps2: { x: 0, y: -9.8 },
    referencia: "suelo"
  }
};

console.log(movil.id, movil.estado.posicionM);

Así podemos cambiar el color sin alterar la física, o sustituir el estado conservando la identidad del móvil. También resulta más fácil guardar, mostrar y probar cada responsabilidad por separado.

23.5 Todos los campos deben pertenecer al mismo instante

Un objeto es coherente solo si posición, velocidad y aceleración describen el mismo instante. Combinar la posición de t=2 s con la velocidad de t=3 s crea un estado que el modelo quizá nunca tuvo.

estado(t) = { t, r(t), v(t), a(t), referencia }

Por eso el tiempo pertenece al estado y no debe mantenerse como una variable global desconectada. En una simulación con varios móviles, todos pueden compartir el mismo reloj o incluir explícitamente su marca temporal para detectar desincronizaciones.

23.6 La referencia también forma parte del significado

Los valores {x: 10, y: 0} no bastan para ubicar un móvil: necesitamos saber origen, orientación y referencia. Después del tema anterior, sabemos además que posición y velocidad cambian al pasar del suelo a un vehículo.

No debemos etiquetar como “tren” una posición calculada desde el suelo. Cambiar el texto de referencia no transforma los vectores; es necesario aplicar las ecuaciones correspondientes y crear un nuevo estado coherente.

23.7 Diseñar vectores uniformes

Aunque un movimiento sea unidimensional, usar siempre objetos con componentes x e y puede simplificar funciones compartidas. Otra opción es definir modelos distintos para 1D y 2D. Lo importante es no mezclar representaciones sin una conversión explícita.

Representaciones posibles de una posición bidimensional.
RepresentaciónVentajaRiesgo
{ x: 3, y: 4 }Componentes legibles por nombre.Requiere objetos anidados.
[3, 4]Compacta y fácil de recorrer.Hay que recordar el orden.
{ xM: 3, yM: 4 }Unidades muy explícitas.Nombres más largos en operaciones vectoriales.

En este curso usaremos {x, y} dentro de propiedades cuyo nombre declara la unidad.

23.8 Crear estados mediante una función

Escribir objetos literales en muchos lugares facilita olvidos y diferencias de formato. Una función constructora centraliza valores iniciales y validaciones.

function crearEstado({
  tiempoS = 0,
  posicionM = { x: 0, y: 0 },
  velocidadMps = { x: 0, y: 0 },
  aceleracionMps2 = { x: 0, y: 0 },
  referencia = "mundo"
} = {}) {
  return {
    tiempoS,
    posicionM: { ...posicionM },
    velocidadMps: { ...velocidadMps },
    aceleracionMps2: { ...aceleracionMps2 },
    referencia
  };
}

console.log(crearEstado());
console.log(crearEstado({
  posicionM: { x: 10, y: 3 },
  velocidadMps: { x: 2, y: 0 }
}));

El parámetro es un objeto, de modo que el orden de los argumentos no importa. Las copias con ... evitan compartir por accidente los vectores recibidos.

23.9 Validar números y estructura

JavaScript permite construir {x: "rápido"} o producir NaN sin detenerse. Cuanto antes se detecte un dato inválido, más cerca estaremos de su causa.

function esVectorFinito(vector) {
  return vector !== null
    && typeof vector === "object"
    && Number.isFinite(vector.x)
    && Number.isFinite(vector.y);
}

function validarEstado(estado) {
  if (!Number.isFinite(estado?.tiempoS)) {
    throw new TypeError("tiempoS debe ser un número finito");
  }
  for (const campo of [
    "posicionM", "velocidadMps", "aceleracionMps2"
  ]) {
    if (!esVectorFinito(estado[campo])) {
      throw new TypeError(`${campo} debe contener x e y finitos`);
    }
  }
  if (typeof estado.referencia !== "string" || !estado.referencia.trim()) {
    throw new TypeError("referencia debe ser un texto no vacío");
  }
  return true;
}

console.log(validarEstado({
  tiempoS: 0,
  posicionM: { x: 0, y: 0 },
  velocidadMps: { x: 0, y: 0 },
  aceleracionMps2: { x: 0, y: 0 },
  referencia: "mundo"
}));

Number.isFinite rechaza NaN, infinitos y textos numéricos. Un valor como "3" debe convertirse deliberadamente antes de llegar al modelo.

23.10 Mutar o producir un estado nuevo

Podemos actualizar un objeto cambiando sus propiedades o construir otro. La mutación usa menos asignaciones, pero dificulta saber qué valores pertenecían al estado anterior. Crear estados nuevos conserva cada fotografía y suele hacer las pruebas más claras.

function trasladarEstado(estado, desplazamientoM) {
  return {
    ...estado,
    posicionM: {
      x: estado.posicionM.x + desplazamientoM.x,
      y: estado.posicionM.y + desplazamientoM.y
    }
  };
}

const inicial = { posicionM: { x: 2, y: 3 } };
const trasladado = trasladarEstado(inicial, { x: 5, y: -1 });

console.log(inicial.posicionM);
console.log(trasladado.posicionM);

El operador de propagación realiza una copia superficial. Por eso reemplazamos también posicionM; si modificáramos el vector anidado compartido, cambiarían ambos estados.

23.11 Copias superficiales y referencias compartidas

const original = { posicionM: { x: 1, y: 2 } };
const copiaSuperficial = { ...original };

copiaSuperficial.posicionM.x = 99;
console.log(original.posicionM.x); // también vale 99

La copia crea un objeto exterior nuevo, pero ambos apuntan al mismo objeto posicionM. Para esta estructura conocida podemos copiar cada vector. Para datos compatibles más complejos, structuredClone(original) produce una copia profunda, aunque copiar todo en cada operación puede tener un costo innecesario.

23.12 Inmutabilidad defensiva

Object.freeze impide reasignar propiedades directas, pero también es superficial. Podemos congelar cada nivel del estado durante el desarrollo:

function congelarEstado(estado) {
  Object.freeze(estado.posicionM);
  Object.freeze(estado.velocidadMps);
  Object.freeze(estado.aceleracionMps2);
  return Object.freeze(estado);
}

const estadoSeguro = congelarEstado({
  posicionM: { x: 0, y: 0 },
  velocidadMps: { x: 5, y: 1 },
  aceleracionMps2: { x: 0, y: 0 }
});

console.log(Object.isFrozen(estadoSeguro));
console.log(Object.isFrozen(estadoSeguro.velocidadMps));

Congelar datos ayuda a descubrir mutaciones accidentales, especialmente en modo estricto. No reemplaza un diseño correcto ni convierte automáticamente en inmutable cualquier objeto profundo.

23.13 Actividad interactiva: construir e inspeccionar un estado

Configurá una fotografía inicial y elegí cuánto tiempo después querés consultar el móvil. La predicción usa las ecuaciones analíticas de aceleración constante y genera un objeto nuevo; no modifica el inicial.

Inspector de estado cinemático

Las flechas representan velocidad y aceleración; el punto tenue marca la posición inicial.

El estado se presenta como texto debajo.

          
Instante consultado2,0 s
Posición(2,0; 6,0) m
Velocidad(5,0; −1,0) m/s
Rapidez5,1 m/s

23.14 Consultar otro instante sin alterar el original

La función utilizada conceptualmente por el laboratorio aplica la solución exacta para una aceleración constante:

function estadoConAceleracionConstante(inicial, deltaTiempoS) {
  if (!Number.isFinite(deltaTiempoS)) {
    throw new TypeError("deltaTiempoS debe ser finito");
  }

  const { posicionM: r, velocidadMps: v, aceleracionMps2: a } = inicial;
  return {
    ...inicial,
    tiempoS: inicial.tiempoS + deltaTiempoS,
    posicionM: {
      x: r.x + v.x * deltaTiempoS + 0.5 * a.x * deltaTiempoS ** 2,
      y: r.y + v.y * deltaTiempoS + 0.5 * a.y * deltaTiempoS ** 2
    },
    velocidadMps: {
      x: v.x + a.x * deltaTiempoS,
      y: v.y + a.y * deltaTiempoS
    },
    aceleracionMps2: { ...a }
  };
}

const inicio = {
  tiempoS: 0,
  posicionM: { x: -8, y: 4 },
  velocidadMps: { x: 5, y: 3 },
  aceleracionMps2: { x: 0, y: -2 },
  referencia: "laboratorio"
};
console.log(estadoConAceleracionConstante(inicio, 2));

Esta consulta analítica no es todavía un algoritmo de integración. En los próximos temas estudiaremos cómo avanzar por pasos cuando no disponemos de una solución exacta sencilla.

23.15 Colecciones de móviles

Una simulación suele contener muchos objetos. Un arreglo conserva el orden de recorrido; un Map facilita buscar por identificador.

const estadoEnReposo = (x, y) => ({
  tiempoS: 0,
  posicionM: { x, y },
  velocidadMps: { x: 0, y: 0 },
  aceleracionMps2: { x: 0, y: 0 },
  referencia: "mundo"
});
const moviles = new Map();

moviles.set("nave", {
  id: "nave",
  estado: {
    ...estadoEnReposo(20, 8),
    velocidadMps: { x: -2, y: 0 }
  }
});

moviles.set("sonda", {
  id: "sonda",
  estado: estadoEnReposo(-5, 3)
});

console.log(moviles.get("nave").estado.posicionM);
console.log(moviles.size);

El identificador debe ser estable y único. Usar el índice de un arreglo como identidad puede fallar si eliminamos o reordenamos elementos.

23.16 Serializar y recuperar estados

Los objetos formados por números finitos, textos, booleanos, arreglos y objetos simples se pueden convertir a JSON para almacenarlos o enviarlos:

const estado = {
  tiempoS: 1.5,
  posicionM: { x: 4, y: 7 },
  velocidadMps: { x: 0, y: 0 },
  aceleracionMps2: { x: 0, y: 0 },
  referencia: "suelo"
};

function validarEstado(e) {
  const vectorValido = v => v && Number.isFinite(v.x) && Number.isFinite(v.y);
  if (!Number.isFinite(e?.tiempoS)
      || !vectorValido(e.posicionM)
      || !vectorValido(e.velocidadMps)
      || !vectorValido(e.aceleracionMps2)
      || typeof e.referencia !== "string") {
    throw new TypeError("Estado inválido");
  }
  return true;
}

const texto = JSON.stringify(estado);
const recuperado = JSON.parse(texto);
validarEstado(recuperado);

console.log(texto);
console.log(recuperado.posicionM);

JSON no conserva métodos, prototipos ni valores como undefined; además transforma NaN e infinitos en null. Por eso debemos validar después de leer datos externos y, si el formato evolucionará, incluir un número de versión.

23.17 Comparar estados y tratar el redondeo

Los cálculos con coma flotante pueden producir valores extremadamente cercanos sin que sean idénticos. Las pruebas físicas suelen comparar con una tolerancia:

function casiIguales(a, b, tolerancia = 1e-9) {
  return Math.abs(a - b) <= tolerancia;
}

function vectoresCasiIguales(a, b, tolerancia = 1e-9) {
  return casiIguales(a.x, b.x, tolerancia)
    && casiIguales(a.y, b.y, tolerancia);
}

console.log(casiIguales(0.1 + 0.2, 0.3));
console.log(vectoresCasiIguales(
  { x: 3.0000000001, y: 4 },
  { x: 3, y: 4 }
));

La tolerancia debe elegirse según la escala y precisión requeridas, no como un número mágico que oculte errores grandes.

23.18 Pruebas e invariantes útiles

  • Todos los campos numéricos del estado deben ser finitos.
  • Cada vector debe poseer exactamente las componentes esperadas por el modelo.
  • Todos los vectores deben usar las unidades y la referencia declaradas.
  • Una operación inmutable no debe cambiar el objeto de entrada.
  • Consultar con Δt=0 debe conservar posición y velocidad.
  • Con aceleración cero, la velocidad debe permanecer constante.
  • Serializar, recuperar y validar debe conservar los datos representables.

Un invariante es una condición que debe mantenerse válida. Expresarlos como pruebas permite detectar rápidamente cuándo un cambio de código rompe el modelo.

23.19 Errores frecuentes

  • Guardar números sin unidades: nombres como velocidadMps reducen ambigüedades.
  • Mezclar instantes: todos los campos de una fotografía deben pertenecer al mismo tiempo.
  • Mezclar referencias: posición y velocidad deben estar expresadas respecto del mismo sistema.
  • Compartir vectores sin querer: una copia superficial no duplica objetos anidados.
  • Confiar en datos externos: JSON válido no garantiza un estado físicamente ni estructuralmente válido.
  • Mezclar física y dibujo: metros y píxeles requieren una transformación explícita.
  • Usar igualdad exacta para decimales calculados: comparar con una tolerancia adecuada.
  • Guardar información derivada innecesaria: la rapidez puede calcularse desde la velocidad y duplicarla exige mantener ambos valores sincronizados.

23.20 Ejercicios propuestos

  1. Escribí un objeto que represente a un móvil en t=3 s, posición (4,−2) m, velocidad (1,5) m/s y aceleración nula respecto del suelo.
  2. Explicá por qué {...estado} no crea una copia totalmente independiente.
  3. Extendé validarEstado para rechazar tiempos negativos si el modelo comienza en t=0.
  4. Con r₀=(1,2) m, v₀=(3,4) m/s, a=(2,−2) m/s² y Δt=2 s, calculá el nuevo estado usando aceleración constante.
  5. ¿Conviene guardar la rapidez además de la velocidad? Justificá.
  6. Diseñá una prueba que demuestre que trasladarEstado no modifica su argumento.
  7. Indicá qué datos separarías entre apariencia y estado para un automóvil animado.
Ver soluciones y explicaciones
  1. {tiempoS:3, posicionM:{x:4,y:-2}, velocidadMps:{x:1,y:5}, aceleracionMps2:{x:0,y:0}, referencia:"suelo"}.
  2. La propagación copia las propiedades exteriores, pero los vectores anidados continúan compartidos.
  3. Agregar if (estado.tiempoS < 0) throw new RangeError("tiempoS no puede ser negativo").
  4. La posición es (11,6) m, la velocidad (7,0) m/s y el tiempo final 2 s.
  5. Normalmente no: Math.hypot(v.x,v.y) la obtiene de la velocidad. Guardarla duplica información y puede volverse incoherente.
  6. Guardar una copia de las componentes iniciales, ejecutar la función y comprobar que el estado original conserva esos valores y que el resultado es otro objeto.
  7. Color, dimensiones en píxeles e imagen pertenecen a apariencia; tiempo, posición, velocidad, aceleración y referencia pertenecen al estado cinemático.

23.21 Ideas para recordar

  • Un estado cinemático es una fotografía numérica del móvil en un instante.
  • Tiempo, posición, velocidad y aceleración deben ser coherentes entre sí.
  • Unidades y sistema de referencia forman parte del significado de los datos.
  • Separar identidad, apariencia y estado evita responsabilidades mezcladas.
  • Las funciones constructoras y validaciones mantienen una estructura uniforme.
  • Los objetos anidados exigen atención a copias, mutación e inmutabilidad.
  • Los estados simples se serializan bien, pero siempre deben validarse al recuperarlos.
  • Las pruebas con invariantes y tolerancias protegen el modelo.

En el próximo tema distinguiremos tiempo continuo, tiempo discreto y paso de simulación.