36. Representar ángulos, velocidades y cuerpos rotatorios con JavaScript

Programar una rotación exige separar el estado físico, los cálculos derivados y el dibujo. Una convención coherente permite transformar metros y radianes en coordenadas de pantalla sin perder el significado físico.

36.1 Del modelo físico a los datos

En una simulación, un cuerpo rotatorio no se representa únicamente mediante una imagen girada. Necesitamos almacenar las variables que definen su estado físico y calcular a partir de ellas lo que debe mostrarse.

Estado

Posición, orientación, velocidades, tamaño y propiedades inerciales.

Cálculos derivados

Coordenadas de puntos, vectores de velocidad, energía y otras magnitudes.

Representación

Conversión a píxeles y actualización de SVG, Canvas o elementos HTML.

Mantener estas capas separadas facilita verificar la física, cambiar la escala visual y reutilizar el mismo modelo con distintas formas de dibujo.

36.2 Elegir una convención

Antes de escribir fórmulas conviene fijar una convención y documentarla. En este tema usaremos:

  • eje x positivo hacia la derecha;
  • eje y físico positivo hacia arriba;
  • ángulos medidos desde +x;
  • ángulos y velocidades angulares positivos en sentido antihorario;
  • unidades del Sistema Internacional para el estado físico.
Regla práctica: la convención puede ser diferente, pero debe ser única. Mezclar un eje y ascendente en los cálculos con signos de una pantalla cuyo y crece hacia abajo produce errores difíciles de detectar.

36.3 Usar radianes internamente

Las funciones trigonométricas de JavaScript reciben radianes. Además, las ecuaciones v = Rω, α = dω/dt y θ = θ₀ + ωt suponen que los ángulos están expresados en radianes.

radianes = grados·π/180     grados = radianes·180/π
const gradosARadianes = grados => grados * Math.PI / 180;
const radianesAGrados = radianes => radianes * 180 / Math.PI;
console.log({
  grados180: gradosARadianes(180),
  piSobreDos: radianesAGrados(Math.PI / 2)
});

La interfaz puede mostrar grados para comodidad del usuario, pero el estado interno debe conservar radianes.

36.4 Una estructura para el estado

Un objeto agrupa las variables relacionadas y hace explícitas sus unidades:

const cuerpo = {
  posicion: { x: 2, y: 1 },       // m
  velocidad: { x: 1, y: 0.5 },    // m/s
  angulo: Math.PI / 4,             // rad
  velocidadAngular: 2,             // rad/s
  radio: 1.5,                      // m
  masa: 3,                         // kg
  momentoInercia: 3.375            // kg·m²
};
console.log(cuerpo);

Los nombres descriptivos reducen errores. Una propiedad llamada angulo es preferible a una abreviatura ambigua; los comentarios o tipos pueden documentar unidades.

36.5 Estado fundamental y valores derivados

Conviene almacenar solo los datos necesarios para definir el estado y calcular el resto. Si se guardan simultáneamente ángulo, seno y coseno, esos tres valores pueden quedar desincronizados.

const cuerpo = {
  angulo: Math.PI / 4,
  velocidadAngular: 2
};
const coseno = Math.cos(cuerpo.angulo);
const seno = Math.sin(cuerpo.angulo);
const rapidezAngular = Math.abs(cuerpo.velocidadAngular);
console.log({ coseno, seno, rapidezAngular });

Los valores derivados se recomputan cuando se necesitan. En una optimización avanzada podrían almacenarse temporalmente, pero deben invalidarse de manera controlada.

36.6 Coordenadas locales del cuerpo

Una forma se describe cómodamente en un sistema local cuyo origen coincide con el centro del cuerpo. Por ejemplo, un punto marcado en el borde a ángulo local φ = 0 tiene:

qlocal = (R, 0)

Los vértices de un rectángulo o de un polígono también pueden almacenarse como pares (x, y) locales. Mover o girar el cuerpo no modifica esta geometría original.

36.7 Matriz de rotación en dos dimensiones

Para girar un vector local (qx, qy) un ángulo θ en sentido antihorario:

q′x = qxcosθ − qysenθ
q′y = qxsenθ + qycosθ
function rotar({ x, y }, angulo) {
  const c = Math.cos(angulo);
  const s = Math.sin(angulo);
  return { x: x * c - y * s, y: x * s + y * c };
}
console.log(rotar({ x: 1, y: 0 }, Math.PI / 2));

36.8 Transformar de local a global

Después de rotar el punto local, se suma la posición global del centro:

pglobal = posicióncentro + Rotar(θ)qlocal
function rotar({ x, y }, angulo) {
  const c = Math.cos(angulo);
  const s = Math.sin(angulo);
  return { x: x * c - y * s, y: x * s + y * c };
}

function localAGlobal(cuerpo, puntoLocal) {
  const girado = rotar(puntoLocal, cuerpo.angulo);
  return {
    x: cuerpo.posicion.x + girado.x,
    y: cuerpo.posicion.y + girado.y
  };
}

const cuerpo = { posicion: { x: 2, y: 1 }, angulo: Math.PI / 2 };
console.log(localAGlobal(cuerpo, { x: 1.5, y: 0 }));

El orden importa: primero se rota alrededor del origen local y después se traslada. Trasladar primero haría que el punto orbitara alrededor del origen global.

36.9 Un punto marcado sobre una rueda

Si el punto está en el borde y θ representa su orientación desde +x:

xP = xCM + Rcosθ     yP = yCM + Rsenθ

Estas ecuaciones son un caso particular de la transformación local a global con qlocal = (R, 0). Para un punto interior basta utilizar un radio local menor.

36.10 Del mundo físico a la pantalla

En SVG y Canvas, el origen suele estar arriba a la izquierda y la coordenada y aumenta hacia abajo. Podemos conservar y físico hacia arriba mediante una función de cámara:

xpantalla = xorigen + escala·xfísico
ypantalla = yorigen − escala·yfísico
function mundoAPantalla(punto, camara) {
  return {
    x: camara.origenX + punto.x * camara.pixelesPorMetro,
    y: camara.origenY - punto.y * camara.pixelesPorMetro
  };
}

const camara = { origenX: 320, origenY: 240, pixelesPorMetro: 70 };
console.log(mundoAPantalla({ x: 2, y: 1 }, camara));

36.11 La escala no cambia la física

pixelesPorMetro pertenece a la representación, no al cuerpo. Cambiar la escala debe agrandar o reducir el dibujo sin alterar posición física, velocidad, energía ni trayectoria.

Modelo físico

R = 1,5 m

Se conserva en metros y participa en las ecuaciones.

Representación

radio = R·70 px/m

Solo determina el tamaño visible.

36.12 Velocidad por rotación de un punto

En el plano xy, con ω positiva antihoraria y r = (rx, ry) desde el centro hasta el punto:

vrot = ω × r = (−ωry, ωrx)
function velocidadRotacional(omega, radioVector) {
  return {
    x: -omega * radioVector.y,
    y:  omega * radioVector.x
  };
}
console.log(velocidadRotacional(2, { x: 1.5, y: 0 }));

El vector resultante es tangente a la circunferencia y su módulo es |ω|r.

36.13 Sumar traslación y rotación

Si el centro también se mueve, la velocidad global del punto es:

vP = vCM + ω × rP/CM
function velocidadRotacional(omega, radioVector) {
  return {
    x: -omega * radioVector.y,
    y:  omega * radioVector.x
  };
}

function velocidadDePunto(cuerpo, radioVectorGlobal) {
  const rotacional = velocidadRotacional(
    cuerpo.velocidadAngular,
    radioVectorGlobal
  );
  return {
    x: cuerpo.velocidad.x + rotacional.x,
    y: cuerpo.velocidad.y + rotacional.y
  };
}

const cuerpo = {
  velocidad: { x: 1, y: 0.5 },
  velocidadAngular: 2
};
console.log(velocidadDePunto(cuerpo, { x: 1.5, y: 0 }));

36.14 Representar aceleraciones

Para un cuerpo con aceleración del centro aCM, velocidad angular ω y aceleración angular α, la aceleración de P es:

aP = aCM + α × r + ω × (ω × r)

En dos dimensiones, los aportes relativos son:

atang = (−αry, αrx)     acentr = −ω²(rx, ry)

Guardar α y aCM en el estado permite dibujar o verificar estas componentes sin cambiar la geometría.

36.15 Representar una forma mediante puntos

Un cuerpo no circular puede describirse con un arreglo de vértices locales:

function rotar({ x, y }, angulo) {
  const c = Math.cos(angulo);
  const s = Math.sin(angulo);
  return { x: x * c - y * s, y: x * s + y * c };
}

function localAGlobal(cuerpo, puntoLocal) {
  const girado = rotar(puntoLocal, cuerpo.angulo);
  return {
    x: cuerpo.posicion.x + girado.x,
    y: cuerpo.posicion.y + girado.y
  };
}

const cuerpo = { posicion: { x: 2, y: 1 }, angulo: Math.PI / 4 };
const verticesLocales = [
  { x: -1, y: -0.5 },
  { x:  1, y: -0.5 },
  { x:  1, y:  0.5 },
  { x: -1, y:  0.5 }
];

const verticesGlobales = verticesLocales.map(
  punto => localAGlobal(cuerpo, punto)
);
console.log(verticesGlobales);

La misma transformación sirve para centros de masa, puntos de aplicación de fuerzas, sensores, articulaciones y marcas visuales.

36.16 Dibujar con SVG

SVG resulta útil cuando cada elemento necesita identidad, etiquetas o interacción. Sus atributos pueden actualizarse con coordenadas ya convertidas:

function mundoAPantalla(punto, camara) {
  return {
    x: camara.origenX + punto.x * camara.pixelesPorMetro,
    y: camara.origenY - punto.y * camara.pixelesPorMetro
  };
}

const cuerpo = { posicion: { x: 2, y: 1 }, radio: 1.5 };
const camara = { origenX: 320, origenY: 240, pixelesPorMetro: 70 };
const puntoPantalla = { x: 456, y: 166 };
const atributos = { rueda: {}, marca: {} };
const rueda = {
  setAttribute: (nombre, valor) => atributos.rueda[nombre] = valor
};
const marca = {
  setAttribute: (nombre, valor) => atributos.marca[nombre] = valor
};

const centroPantalla = mundoAPantalla(cuerpo.posicion, camara);
rueda.setAttribute("cx", centroPantalla.x);
rueda.setAttribute("cy", centroPantalla.y);
rueda.setAttribute("r", cuerpo.radio * camara.pixelesPorMetro);

marca.setAttribute("cx", puntoPantalla.x);
marca.setAttribute("cy", puntoPantalla.y);
console.log(atributos);

El modelo entrega números físicos; la función de renderizado es responsable de transformarlos en atributos SVG.

36.17 Dibujar con Canvas

Canvas es conveniente para muchas partículas o dibujos que se renuevan completos. Puede usarse la transformación del contexto, recordando que los ángulos visuales se invierten si el eje y no fue corregido:

const operaciones = [];
const ctx = {
  save: () => operaciones.push("save"),
  translate: (x, y) => operaciones.push(["translate", x, y]),
  rotate: angulo => operaciones.push(["rotate", angulo]),
  beginPath: () => operaciones.push("beginPath"),
  arc: (...argumentos) => operaciones.push(["arc", ...argumentos]),
  stroke: () => operaciones.push("stroke"),
  restore: () => operaciones.push("restore")
};
const centroPantalla = { x: 460, y: 170 };
const cuerpo = { angulo: Math.PI / 4 };
const radioEnPixeles = 105;

ctx.save();
ctx.translate(centroPantalla.x, centroPantalla.y);
ctx.rotate(-cuerpo.angulo); // y de pantalla apunta hacia abajo
ctx.beginPath();
ctx.arc(0, 0, radioEnPixeles, 0, 2 * Math.PI);
ctx.stroke();
ctx.restore();
console.log(operaciones);

save() y restore() evitan que una transformación aplicada a un cuerpo contamine los dibujos siguientes.

36.18 No usar el dibujo como estado físico

Leer una transformación CSS o un atributo SVG para descubrir dónde está el cuerpo mezcla presentación y simulación. El flujo recomendable es unidireccional:

entrada → estado físico → cálculos → representación

El estado es la fuente de verdad. El dibujo se puede reconstruir completamente a partir de él y nunca debe introducir modificaciones físicas ocultas.

36.19 Separar actualización y renderizado

Una función actualiza el modelo; otra lo dibuja. En este tema el laboratorio modifica directamente el estado con controles, mientras que el próximo tema integrará su evolución temporal.

function calcularRepresentacion(modelo, camara) {
  return {
    angulo: modelo.angulo,
    omega: modelo.velocidadAngular,
    escala: camara.pixelesPorMetro
  };
}

function actualizarSVG(vista, datos) {
  vista.ultimoDibujo = datos;
}

function actualizar(modelo, entrada) {
  modelo.angulo = entrada.angulo;
  modelo.velocidadAngular = entrada.omega;
}

function renderizar(modelo, vista) {
  const datos = calcularRepresentacion(modelo, vista.camara);
  actualizarSVG(vista, datos);
}

const modelo = { angulo: 0, velocidadAngular: 0 };
const vista = { camara: { pixelesPorMetro: 70 } };
actualizar(modelo, { angulo: Math.PI / 4, omega: 2 });
renderizar(modelo, vista);
console.log({ modelo, representacion: vista.ultimoDibujo });

Esta separación permite probar actualizar y los cálculos sin necesidad de abrir un navegador.

36.20 Precisión, formato y validación

Los cálculos deben conservar la precisión de punto flotante disponible. El redondeo se aplica solo al mostrar resultados:

const valor = 2.3456;
const salida = { textContent: "" };
const formato = new Intl.NumberFormat("es-AR", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

salida.textContent = `${formato.format(valor)} rad/s`;
console.log(salida.textContent);

También deben validarse radios positivos, masas no negativas y números finitos. Number.isFinite(valor) ayuda a detectar entradas inválidas antes de que produzcan coordenadas NaN.

No compares resultados de punto flotante con igualdad exacta cuando provienen de trigonometría. Usá una tolerancia, por ejemplo Math.abs(a - b) < 1e-9.

36.21 Actividad interactiva: del estado a la pantalla

Modificá el estado del cuerpo. El punto naranja está fijo en sus coordenadas locales (R, 0), pero cambia de posición global al variar θ. El vector violeta suma la velocidad del centro y la velocidad tangencial por rotación.

Laboratorio de representación de un cuerpo rotatorio

El panel muestra simultáneamente magnitudes físicas en SI y coordenadas de pantalla en píxeles.

+2,0 m
+1,0 m
1,5 m
45° · 0,785 rad
+2,0 rad/s
+1,0 m/s
+0,5 m/s
70 px/m
Transformación de un cuerpo rotatorio desde coordenadas físicas a pantalla Cuerpo circular con centro en dos coma uno metros, punto de borde a cuarenta y cinco grados y vector de velocidad resultante. +x+yorigen visual θ = 45°CM (2,00; 1,00)P (3,06; 2,06) vCMvP = (−1,12; +2,62) m/s Transformacioneslocal: q = (1,50; 0,00) mglobal: P = (3,06; 2,06) mpantalla: P = (614,2; 185,8) px
cuerpo y centroorientación y puntovelocidad resultanteejes de pantalla
Ángulo interno0,7854 rad
Vector radial global(+1,06; +1,06) m
Posición global P(+3,06; +2,06) m
Posición en pantalla(614,2; 185,8) px
Velocidad rotacional(−2,12; +2,12) m/s
Velocidad total P(−1,12; +2,62) m/s

El estado se conserva en metros y radianes. La escala solo modifica el tamaño en pantalla; la inversión del eje y ocurre exclusivamente al representar.

36.22 Implementación completa del cálculo

function rotar({ x, y }, angulo) {
  const c = Math.cos(angulo);
  const s = Math.sin(angulo);
  return { x: x * c - y * s, y: x * s + y * c };
}

function mundoAPantalla(punto, camara) {
  return {
    x: camara.origenX + punto.x * camara.pixelesPorMetro,
    y: camara.origenY - punto.y * camara.pixelesPorMetro
  };
}

function calcularRepresentacion(cuerpo, camara) {
  if (!Number.isFinite(cuerpo.radio) || cuerpo.radio <= 0) {
    throw new Error("Radio no válido");
  }

  const puntoLocal = { x: cuerpo.radio, y: 0 };
  const radioGlobal = rotar(puntoLocal, cuerpo.angulo);
  const puntoGlobal = {
    x: cuerpo.posicion.x + radioGlobal.x,
    y: cuerpo.posicion.y + radioGlobal.y
  };
  const velocidadRotacion = {
    x: -cuerpo.velocidadAngular * radioGlobal.y,
    y:  cuerpo.velocidadAngular * radioGlobal.x
  };
  const velocidadPunto = {
    x: cuerpo.velocidad.x + velocidadRotacion.x,
    y: cuerpo.velocidad.y + velocidadRotacion.y
  };

  return {
    puntoLocal,
    radioGlobal,
    puntoGlobal,
    puntoPantalla: mundoAPantalla(puntoGlobal, camara),
    centroPantalla: mundoAPantalla(cuerpo.posicion, camara),
    velocidadRotacion,
    velocidadPunto
  };
}

const cuerpo = {
  posicion: { x: 2, y: 1 },
  velocidad: { x: 1, y: 0.5 },
  angulo: Math.PI / 4,
  velocidadAngular: 2,
  radio: 1.5
};
const camara = { origenX: 320, origenY: 240, pixelesPorMetro: 70 };
console.log(calcularRepresentacion(cuerpo, camara));

La función no toca el DOM y siempre devuelve los mismos resultados para las mismas entradas. Esa propiedad facilita pruebas automatizadas. Otra función puede recibir el objeto devuelto y actualizar SVG o Canvas.

36.23 Ejemplos, errores frecuentes y ejercicios

Ejemplos resueltos

Ejemplo 1: 90° equivalen a π/2 ≈ 1,5708 rad. Un punto local (2, 0) girado ese ángulo queda aproximadamente en (0, 2).

Ejemplo 2: un centro está en (3, −1) m y un punto girado local queda en (0,5; 1) m. Su posición global es (3,5; 0) m.

Ejemplo 3: para ω = 4 rad/s y r = (0, 2) m:

vrot = (−ωry, ωrx) = (−8, 0) m/s

Ejemplo 4: con origen de pantalla (400, 300), escala 50 px/m y punto físico (2, 1):

xpx = 400 + 50·2 = 500    ypx = 300 − 50·1 = 250

Ejemplo 5: si vCM = (1, 0) m/s y vrot = (−2, 3) m/s, entonces vP = (−1, 3) m/s.

Errores frecuentes

  • pasar grados directamente a Math.sin o Math.cos;
  • mezclar metros con píxeles dentro de las ecuaciones físicas;
  • olvidar que la coordenada y de pantalla crece hacia abajo;
  • trasladar un punto antes de rotarlo cuando debe girar alrededor del centro;
  • usar (ωry, −ωrx) e invertir la velocidad tangencial;
  • guardar valores derivados que luego quedan desactualizados;
  • leer el estado físico desde estilos CSS o atributos gráficos;
  • redondear el estado después de cada cálculo;
  • comparar números de punto flotante mediante igualdad exacta;
  • aplicar transformaciones Canvas sin save() y restore().

Ejercicios propuestos

  1. Convertí 30°, 180° y 270° a radianes.
  2. Rotá el vector local (1, 0) un ángulo de 60°.
  3. Un cuerpo está en (2, 3) m y un punto rotado queda a (−1, 2) m del centro. Hallá su posición global.
  4. Calculá la velocidad rotacional para ω = −3 rad/s y r = (2, 1) m.
  5. Sumá al resultado anterior una velocidad del centro (4, −1) m/s.
  6. Transformá el punto físico (−2, 3) a pantalla con origen (400, 300) y escala 40 px/m.
  7. Explicá por qué cambiar de 40 a 80 px/m no debe modificar la energía calculada.
  8. Escribí los cuatro vértices locales de un rectángulo de ancho 4 m y alto 2 m centrado en su origen.
  9. Calculá la aceleración centrípeta vectorial si ω = 2 rad/s y r = (3, −1) m.
  10. Proponé una prueba para verificar que rotar conserva la longitud de un vector.
Ver soluciones y explicaciones
  1. π/6 ≈ 0,5236 rad; π ≈ 3,1416 rad; 3π/2 ≈ 4,7124 rad.
  2. (cos60°, sen60°) = (0,5; √3/2) ≈ (0,5; 0,866).
  3. (2, 3) + (−1, 2) = (1, 5) m.
  4. vrot = (−ωry, ωrx) = (3, −6) m/s.
  5. vP = (4, −1) + (3, −6) = (7, −7) m/s.
  6. xpx = 400 + 40(−2) = 320; ypx = 300 − 40·3 = 180; punto (320, 180) px.
  7. La escala pertenece a la cámara y solo convierte metros a píxeles; las magnitudes físicas usan los valores en SI.
  8. (−2, −1), (2, −1), (2, 1) y (−2, 1) m.
  9. acentr = −ω²r = −4(3, −1) = (−12, 4) m/s².
  10. Comparar Math.hypot(x, y) antes y después de rotar para varios ángulos, aceptando una diferencia menor que una tolerancia.

36.24 Ideas para recordar

  • Separá estado físico, cálculos derivados y representación.
  • Documentá ejes, signos y unidades antes de programar.
  • Usá radianes dentro de las ecuaciones y convertí solo para la interfaz.
  • Describí la geometría en coordenadas locales.
  • Rotá primero y trasladá después para pasar de local a global.
  • Convertí de mundo a pantalla en una función de cámara independiente.
  • En 2D, vrot = (−ωry, ωrx).
  • La velocidad global de un punto suma traslación y rotación.
  • El estado es la fuente de verdad; SVG, Canvas y CSS solo lo representan.
  • Conservá precisión en los cálculos y redondeá únicamente al mostrar.

En el próximo tema aprenderemos a integrar la rotación, normalizar ángulos y verificar resultados, haciendo que el estado evolucione de manera estable a lo largo del tiempo.