muijs-cuerpos
Version:
Librería para simular y graficar interacciones entre cuerpos rígidos
1,131 lines (1,111 loc) • 97.6 kB
JavaScript
'use strict';
/**MÓDULO DE GEOMETRÍA EN ESPAÑOL
* Útilitario para mui.js
* Incluye métodos de conversión de grados y distancia entre puntos.
*/
class Geometria {
/**Retorna el doble del valor de PI.*/
static get DOS_PI() {
return Math.PI * 2;
}
/**Retorna la mitad del valor de PI.*/
static get PI_MEDIO() {
return Math.PI / 2;
}
//GRADOS
/**Transforma grados sexagesimales a radianes.*/
static gradoARadian(grado) {
return (grado / 180) * Math.PI;
}
/**Transfoma radianes a grados sexagesimales.*/
static radianAGrado(rad) {
return (rad / Math.PI) * 180;
}
//PITAGÓRICA
/**Retorna la longitud de la hipotenusa según la longitud de los dos catetos ingresados.*/
static hipotenusa(cateto1, cateto2) {
return (cateto1 ** 2 + cateto2 ** 2) ** (1 / 2);
}
/**Retorna la longitud de un cateto según la longitud de la hipotenusa y del otro cateto.*/
static cateto(hipotenusa, cateto) {
return (hipotenusa ** 2 - cateto ** 2) ** (1 / 2);
}
//COORDENADAS
/**Retorna el valor de la distancia entre dos puntos de un plano cartesiano.*/
static distanciaEntrePuntos(puntoUno, puntoDos) {
return this.hipotenusa(puntoDos.x - puntoUno.x, puntoDos.y - puntoUno.y);
}
/**Retorna el punto medio entre dos puntos de un plano cartesiano.*/
static puntoMedio(puntoUno, puntoDos) {
return { x: (puntoUno.x / 2 + puntoDos.x / 2), y: (puntoUno.y / 2, +puntoDos.y / 2) };
}
/**Compara las coordenadas de dos puntos.
* Retorna true si son iguales y false si no lo son.
*/
static compararPuntos(puntoUno, puntoDos) {
if (puntoUno.x == puntoDos.x && puntoUno.y == puntoDos.y) {
return true;
}
return false;
}
}
/**Clase que permite capturar lo que se proyecte en un canvas. */
class Grabador {
constructor() { }
/**Graba la animación de un canvas en formato .avi y asocia la grabación a un link de descarga en el documento HTML.
* Permite definir la duración de la grabación, en milisegundos, el número de FPS y la id del <anchor> HTML que iniciará la descarga.
* Si no se define un elemento <anchor>, el método creará uno.
*/
static grabarCanvas(canvas, milisegundos, fps, anchor) {
let chunks = [];
let videoStream = canvas.captureStream(fps);
let mediaRecorder = new MediaRecorder(videoStream);
mediaRecorder.ondataavailable = function (e) {
chunks.push(e.data);
};
mediaRecorder.start();
setTimeout(function () { mediaRecorder.stop(); }, milisegundos);
mediaRecorder.onstop = function (e) {
let blob = new Blob(chunks, { 'type': 'video/avi' });
chunks = [];
let videoURL = URL.createObjectURL(blob);
let link;
if (anchor) {
link = document.getElementById(anchor);
}
else {
link = document.createElement("a");
link.style.color = 'skyblue';
link.innerHTML = 'Descargar';
}
link.href = videoURL;
link.download = "Captura Canvas";
};
}
}
/**
* MÓDULO MATEMÁTICO EN ESPAÑOL
* Reducido. Contiene solo funciones útiles de números aleatorios.
*/
class Matematica {
/**Retorna un número aleatorio entre dos números.*/
static aleatorio(min, max) {
let rango = max - min;
return (Math.random() * rango) + min;
}
/**Retorna un número aleatorio entero entre dos números, ambos incluídos.*/
static aleatorioEntero(min, max) {
let rango = 1 + max - min;
return Math.trunc((Math.random() * rango) + min);
}
static compararNumeros(numeroUno, numeroDos, epsilon = Number.EPSILON) {
return (Math.abs(numeroUno - numeroDos) < epsilon);
}
}
exports.TipoFormas = void 0;
(function (TipoFormas) {
TipoFormas["circunferencia"] = "circunferencia";
TipoFormas["poligono"] = "poligono";
TipoFormas["linea"] = "linea";
TipoFormas["vector"] = "vector";
})(exports.TipoFormas || (exports.TipoFormas = {}));
//POR REVISAR
class Vector {
x;
y;
origen;
id;
constructor(x, y) {
this.x = x;
this.y = y;
this.origen = { x: 0, y: 0 };
this.id = 0;
}
get magnitud() {
// return Vector.magnitud(this);
return (this.x ** 2 + this.y ** 2) ** (1 / 2);
}
get angulo() {
// return Vector.angulo(this);
if (this.x == 0 && this.y == 0) {
return 0;
}
else if (this.y > 0 && this.x == 0) {
return Geometria.PI_MEDIO;
}
else if (this.y < 0 && this.x == 0) {
return (3 / 2) * Math.PI;
}
else {
if (this.y > 0 && this.x > 0) {
return Math.atan(this.y / this.x);
}
else if (this.y > 0 && this.x < 0) {
return Math.acos(this.x / this.magnitud);
}
else if (this.y < 0 && this.x < 0) {
return Math.PI - Math.asin(this.y / this.magnitud);
}
return Geometria.DOS_PI - Math.acos(this.x / this.magnitud);
}
}
// static magnitud(vector: Vector): number {
// return (vector.x ** 2 + vector.y ** 2) ** (1 / 2)
// }
//REVISARRRRRRRRRRRRRRRR
// static angulo(vector: Vector): number {
// if (vector.x == 0 && vector.y == 0) {
// return 0;
// }
// else if (vector.y > 0 && vector.x == 0) {
// return Geometria.PI_MEDIO;
// }
// else if (vector.y < 0 && vector.x == 0) {
// return (3 / 2) * Math.PI;
// }
// else {
// if (vector.y > 0 && vector.x > 0) {
// return Math.atan(vector.y / vector.x);
// }
// else if (vector.y > 0 && vector.x < 0) {
// return Math.acos(vector.x / Vector.magnitud(vector));
// }
// else if (vector.y < 0 && vector.x < 0) {
// return Math.PI - Math.asin(vector.y / Vector.magnitud(vector));
// }
// return Geometria.DOS_PI - Math.acos(vector.x / Vector.magnitud(vector));;
// }
// }
static cero() {
return new Vector(0, 0);
}
static arriba(magnitud = 1) {
return new Vector(0, -1 * magnitud);
}
static abajo(magnitud = 1) {
return new Vector(0, 1 * magnitud);
}
static izquierda(magnitud = 1) {
return new Vector(-1 * magnitud, 0);
}
static derecha(magnitud = 1) {
return new Vector(1 * magnitud, 0);
}
/**Retorna un Vector aleatorio con magnitud definida.
* Si no se determina una magnitud, retorna un vector normalizado.
*/
static aleatorio(magnitud = 1) {
let x = Math.random() * 2 - 1;
let y = Math.random() * 2 - 1;
return Vector.crear(x, y).normalizar().escalar(magnitud);
}
/**Retorna un vector nuevo a partir de las componentes x e y ingresadas.*/
static crear(x, y) {
return new Vector(x, y);
}
/**Retorna un vector nuevo que va desde un punto origen a un punto extremo.*/
static segunPuntos(origen, extremo) {
let vector = new Vector(extremo.x - origen.x, extremo.y - origen.y);
return vector;
}
/**Retorna una copia de un conjunto de vectores.*/
static clonarConjunto(vectores) {
let conjuntoCopia = [];
for (let vector of vectores) {
conjuntoCopia.push(vector.clonar());
}
return conjuntoCopia;
}
/**Retorna el vector normal de un segmento formado por dos vectores.
* El ángulo de la normal va en sentido antihorario según la dirección del primer al segundo vector.
* (Según la inverción de ejes de las coordenadas de JS, donde los ángulos crecen en sentido horario).
*/
static normal(vectorUno, vectorDos) {
let vectorSegmento = Vector.segunPuntos(vectorUno, vectorDos);
return vectorSegmento.rotar(-Geometria.PI_MEDIO);
}
/**Retorna una copia del vector.*/
clonar() {
let x = this.x;
let y = this.y;
return new Vector(x, y);
}
/**Retorna la suma de dos vectores como un vector nuevo.*/
sumar(vectorSumar) {
let vectorSuma = new Vector((this.x + vectorSumar.x), (this.y + vectorSumar.y));
return vectorSuma;
}
/**Retorna la resta de dos vectores como un vector nuevo.*/
restar(vectorRestar) {
let vectorResta = new Vector((this.x - vectorRestar.x), (this.y - vectorRestar.y));
return vectorResta;
}
/**Retorna un vector nuevo resultante de multiplicar las componentes de un vector por un escalar.*/
escalar(escalar) {
let vectorEscalado = new Vector((this.x * escalar), (this.y * escalar));
return vectorEscalado;
}
/**Retorna una copia del vector ingresado con magnitud 1.*/
normalizar() {
return new Vector(this.x / this.magnitud, this.y / this.magnitud);
}
/**Retorna un vector resultante de invertir la dirección del vector ingresado.*/
invertir() {
return new Vector(-this.x, -this.y);
}
/**Retorna el producto punto, o escalar, entre dos vectores.*/
punto(vectorProducto) {
return (this.x * vectorProducto.x) + (this.y * vectorProducto.y);
}
/**Retorna el módulo del producto cruz, o vectorial, entre dos vectores de 2 dimensiones.*/
cruz(vectorProducto) {
return this.x * vectorProducto.y - this.y * vectorProducto.x;
}
/**Retorna el valor de la proyección de un vector sobre un eje representado por otro vector.*/
proyeccion(vectorEje) {
return (this.punto(vectorEje) / vectorEje.magnitud);
}
/**Retorna el valor del ángulo entre dos vectores.*/
anguloVectores(vectorAngulo) {
let punto = this.punto(vectorAngulo);
let magnitudes = this.magnitud * vectorAngulo.magnitud;
return Math.acos(punto / magnitudes);
}
/**Retorna un vector nuevo a partir de un vector rotado.*/
rotar(angulo) {
let x = (Math.cos(angulo) * this.x) - (Math.sin(angulo) * this.y);
let y = (Math.sin(angulo) * this.x) + (Math.cos(angulo) * this.y);
return new Vector(x, y);
}
}
/**
=============================================
* MÓDULO DE TRANSFORMACIONES *
=============================================
Trabaja sobre conjuntos de vectores.
Almacena las transformaciones como atributos.
Siempre retorna copias nuevas de los conjuntos de vectores ingresados.
*/
/**Aplica transformaciones de escala, rotación y desplazamiento sobre arreglos de vectores.
* Siempre retorna copias nuevas de los arreglos.
* Almacena en sus atributos los valores de las transformaciones que aplica.
*/
class Transformacion {
escala;
rotacion;
posicion;
constructor(x = 0, y = 0, rotacion = 0, escala = 1) {
this.escala = escala;
this.rotacion = rotacion;
this.posicion = Vector.crear(x, y);
}
/**Retorna el arreglo de vectores resultante de aplicar las transformaciones de escala, rotación y desplazamiento
* sobre un arreglo de vectores de entrada.
* Permite aumentar puntualmente la rotación en un ángulo específico sin modificar la propiedad de rotación de la transformación.*/
transformarConjuntoVectores(vectores) {
let vectoresTransformados = Vector.clonarConjunto(vectores);
vectoresTransformados = this.aplicarEscalaVectores(vectoresTransformados);
vectoresTransformados = this.aplicarRotacionVectores(vectoresTransformados);
vectoresTransformados = this.aplicarDesplazamientoVectores(vectoresTransformados);
return vectoresTransformados;
}
/**Escala cada uno de los vectores del arreglo ingresado y los retorna en un arreglo nuevo.*/
aplicarEscalaVectores(vectores) {
let vectoresEscalados = [];
for (let vector of vectores) {
let vectorEscalado = vector.escalar(this.escala);
vectoresEscalados.push(vectorEscalado);
}
return vectoresEscalados;
}
/**Desplaza cada uno de los vectores del arreglo ingresado y los retorna en un arreglo nuevo.*/
aplicarDesplazamientoVectores(vectores) {
let vectoresDesplazados = [];
for (let vector of vectores) {
let x = vector.x + this.posicion.x;
let y = vector.y + this.posicion.y;
vectoresDesplazados.push(Vector.crear(x, y));
}
return vectoresDesplazados;
}
/**Rota cada uno de los vectores del arreglo ingresado según el ángulo de rotación almacenado y los retorna en un arreglo nuevo.
*/
aplicarRotacionVectores(vectores) {
let vectoresRotados = [];
for (let vector of vectores) {
let x = vector.x * Math.cos(this.rotacion) - vector.y * Math.sin(this.rotacion);
let y = vector.x * Math.sin(this.rotacion) + vector.y * Math.cos(this.rotacion);
vectoresRotados.push(Vector.crear(x, y));
}
return vectoresRotados;
}
/**Rota cada uno de los vectores de un arreglo según el ángulo ingresado y los retorna en un arreglo nuevo.
*/
static rotarVectores(vectores, angulo) {
let vectoresRotados = [];
for (let vector of vectores) {
let x = vector.x * Math.cos(angulo) - vector.y * Math.sin(angulo);
let y = vector.x * Math.sin(angulo) + vector.y * Math.cos(angulo);
vectoresRotados.push(Vector.crear(x, y));
}
return vectoresRotados;
}
/**Retorna una copia de la transformación.*/
clonarTransformación() {
return new Transformacion(this.posicion.x, this.posicion.y, this.rotacion, this.escala);
}
}
//POR INTEGRAR
// Para una forma personalizada, ya sea abierta o cerrada, agregar un método para calcular su radio o su centro
// Función de reflejar
// SUMAR FORMAS
//Agregar propiedad de vértices transformados, normales rotadas y apotema, para no estar calculándolo en cada momento,
//ademas de una propiedad que avise cuando haya que aplicar la transformación.
/**MÓDULO FORMA
* Instancias de formas geométricas.
* Permite cambiar su posición, rotar, escalar, crear formas básicas y personalizadas, y dibujarlas.
*/
class Forma {
_vertices = [];
_verticesTransformados = [];
_transformacion = new Transformacion();
verticesTransformadosAnteriores = [];
transformacionAnterior = new Transformacion();
transformar = true;
id;
radio = 0;
lados = 0;
tipo = exports.TipoFormas.poligono;
colorTrazo;
colorRelleno;
/**Determina si la forma debe ser trazada al dibujar.*/
trazada = true;
/**Determina si la forma debe ser rellenada al dibujar.*/
rellenada = true;
grosorTrazo;
opacidad;
constructor() { }
/**Retorna el valor del radio con la transformación de escala aplicada.*/
get radioTransformado() {
let radioTransformado = this.radio * this._transformacion.escala;
return radioTransformado;
}
/**Retorna una copia de la transformación de la forma.*/
get transformacion() {
return new Transformacion(this._transformacion.posicion.x, this._transformacion.posicion.y, this._transformacion.rotacion, this._transformacion.escala);
}
/**Retorna una copia del vector de la posición después de aplicar las transformaciones.*/
get posicion() {
return this._transformacion.posicion.clonar();
}
/**Retorna una copia del vector de la posición antes de aplicar las transformaciones.*/
get posicionAnterior() {
return this.transformacionAnterior.posicion.clonar();
}
/**Retorna el ángulo de rotación actual de la forma.*/
get rotacion() {
return this._transformacion.rotacion;
}
/**Retorna el valor de la escala de la forma.*/
get escala() {
return this._transformacion.escala;
}
/**Retorna una copia del arreglo de vértices sin transformaciones.*/
get vertices() {
return Vector.clonarConjunto(this._vertices);
}
/**Retorna una copia del arreglo de vértices después de aplicar las transformaciones de escala, rotación y desplazamiento.*/
get verticesTransformados() {
if (this.transformar) {
this.verticesTransformadosAnteriores = Vector.clonarConjunto(this._verticesTransformados);
this.transformarVertices();
}
return Vector.clonarConjunto(this._verticesTransformados);
}
/**Retorna el conjunto de vectores normales de cada arista del polígono.
* El orden de las aristas es en sentido horario.
*/
get normales() {
let normales = [];
for (let i = 0; i < this.verticesTransformados.length; i++) {
if (i != this.verticesTransformados.length - 1) {
let normal = Vector.normal(this.verticesTransformados[i], this.verticesTransformados[i + 1]);
normales.push(normal);
}
else {
let normal = Vector.normal(this.verticesTransformados[i], this.verticesTransformados[0]);
normales.push(normal);
}
}
return normales;
}
/**Retorna la distancia entre el centro del polígono y el punto más cercano de sus aristas.*/
get apotema() {
if (this.tipo == exports.TipoFormas.circunferencia) {
return this.radioTransformado;
}
return Math.cos(Math.PI / this.lados) * this.radio;
}
/**Reemplaza la transformación de la forma.*/
set transformacion(transformacion) {
this.transformar = true;
this.transformacionAnterior = this._transformacion.clonarTransformación();
this._transformacion = transformacion;
}
/**Reemplaza el vector posición de la forma.*/
set posicion(nuevaPosicion) {
this.transformar = true;
this.transformacionAnterior.posicion = this._transformacion.posicion;
this._transformacion.posicion = nuevaPosicion.clonar();
}
/**Modifica el valor de la rotación de la figura con respecto a su forma sin transformaciones.*/
set rotacion(rotacion) {
this.transformar = true;
this.transformacionAnterior.rotacion = this._transformacion.rotacion;
this._transformacion.rotacion = rotacion;
}
/**Reemplaza el valor de la escala de la forma.*/
set escala(nuevaEscala) {
this.transformar = true;
this.transformacionAnterior.escala = this._transformacion.escala;
this._transformacion.escala = nuevaEscala;
}
/**Reemplaza el conjunto de vértices base de la forma.*/
set vertices(vertices) {
this._vertices = Vector.clonarConjunto(vertices);
}
/**Permite modificar las opciones gráficas con la interfaz OpcionesGraficasForma*/
set estiloGrafico(opciones) {
Object.assign(this, opciones);
}
////////Agregar control de errores para índices mayores al número de vértices
moverVertice(indice, punto) {
this._vertices[indice] = Vector.crear(punto.x, punto.y);
}
/**Inicia los vértices de la forma creada.*/
crearVertices() {
if (this.lados == 0) {
this._vertices = [];
}
let theta = Geometria.DOS_PI / this.lados;
let offset = theta * 0.5;
let nVertices = [];
for (let i = 0; i < this.lados; i++) {
let angulo = offset + (i * theta);
let xx = Math.cos(angulo) * this.radio;
let yy = Math.sin(angulo) * this.radio;
let vertice = Vector.crear(xx, yy);
nVertices.push(vertice);
}
this._vertices = nVertices;
}
/**Retorna una forma de tipo polígono. El radio es el valor de la distancia entre el centro y cualquiera de sus vértices.*/
static poligono(x, y, lados, radio, opciones) {
let nuevoPoligono = new Forma();
nuevoPoligono.lados = lados;
nuevoPoligono.radio = radio;
nuevoPoligono.crearVertices();
nuevoPoligono.tipo = exports.TipoFormas.poligono;
if (opciones) {
Object.assign(nuevoPoligono, opciones);
}
nuevoPoligono.iniciarTransformacion(x, y);
return nuevoPoligono;
}
/**Retorna una forma de tipo circunferencia. */
static circunferencia(x, y, radio, opciones) {
let nuevaCircunferencia = new Forma();
nuevaCircunferencia.radio = radio;
let lados = 10 + Math.trunc(radio / 10);
if (lados % 2 == 1) {
lados++;
}
if (lados > 30) {
lados = 30;
}
nuevaCircunferencia.lados = lados;
nuevaCircunferencia.crearVertices();
nuevaCircunferencia.tipo = exports.TipoFormas.circunferencia;
if (opciones) {
Object.assign(nuevaCircunferencia, opciones);
}
nuevaCircunferencia.iniciarTransformacion(x, y);
return nuevaCircunferencia;
}
/**Retorna una forma de tipo rectángulo. El radio es el valor de la distancia entre el centro y cualquiera de sus vértices.*/
static rectangulo(x, y, base, altura, opciones) {
let rectangulo = new Forma();
rectangulo.lados = 4;
rectangulo.radio = Geometria.hipotenusa(base * 0.5, altura * 0.5);
let ver1 = Vector.crear(base / 2, altura / 2);
let ver2 = Vector.crear(-base / 2, altura / 2);
let ver3 = Vector.crear(-base / 2, -altura / 2);
let ver4 = Vector.crear(base / 2, -altura / 2);
rectangulo.vertices = [ver1, ver2, ver3, ver4];
rectangulo.tipo = exports.TipoFormas.poligono;
if (opciones) {
Object.assign(rectangulo, opciones);
}
rectangulo.iniciarTransformacion(x, y);
return rectangulo;
}
/**Crea una recta centrada en la posición ingresada.*/
static recta(puntoUno, puntoDos, opciones) {
let linea = new Forma();
linea.lados = 1;
linea.radio = Geometria.distanciaEntrePuntos(puntoUno, puntoDos) / 2;
let centro = Vector.crear(puntoUno.x / 2 + puntoDos.x / 2, puntoUno.y / 2 + puntoDos.y / 2);
linea.vertices = [Vector.crear(puntoUno.x - centro.x, puntoUno.y - centro.y), Vector.crear(puntoDos.x - centro.x, puntoDos.y - centro.y)];
linea.tipo = exports.TipoFormas.linea;
if (opciones) {
Object.assign(linea, opciones);
}
linea.iniciarTransformacion(centro.x, centro.y);
return linea;
}
/**
* Crea un conjunto de rectas a partir de un grupo de vértices.
* Calcula el centro de los vértices y centra el trazo en la posición ingresada.
*/
static trazo(vertices, opciones) {
let centro = Vector.crear(0, 0);
let trazo = new Forma();
let verticesTrazo = [];
vertices.forEach((vertice) => centro = centro.sumar(vertice.escalar(1 / vertices.length)));
vertices.forEach((vertice) => verticesTrazo.push(vertice.restar(centro)));
trazo.vertices = verticesTrazo;
trazo.lados = vertices.length - 1;
trazo.tipo = exports.TipoFormas.linea;
if (opciones) {
Object.assign(trazo, opciones);
}
trazo.iniciarTransformacion(centro.x, centro.y);
return trazo;
}
/**
* Crea un polígono a partir de un grupo de vértices.
* Calcula el centro de los vértices ingresados y lo asigna a su posición.
*/
static poligonoSegunVertices(vertices, opciones) {
let centro = Vector.crear(0, 0);
let poligono = new Forma();
let verticesPoligono = [];
vertices.forEach((vertice) => centro = centro.sumar(vertice.escalar(1 / vertices.length)));
vertices.forEach((vertice) => verticesPoligono.push(vertice.restar(centro)));
poligono.vertices = verticesPoligono;
poligono.lados = vertices.length - 1;
poligono.tipo = exports.TipoFormas.poligono;
if (opciones) {
Object.assign(poligono, opciones);
}
poligono.iniciarTransformacion(centro.x, centro.y);
return poligono;
}
/**Crea una transformación nueva para formas nuevas, con la posición ingresada.*/
iniciarTransformacion(x, y) {
this._transformacion.posicion = Vector.crear(x, y);
this.transformacionAnterior = this._transformacion.clonarTransformación();
}
/**Actualiza el conjunto de vectores transformados.*/
transformarVertices() {
this.verticesTransformadosAnteriores = Vector.clonarConjunto(this._verticesTransformados);
this._verticesTransformados = this._transformacion.transformarConjuntoVectores(this._vertices);
this.transformar = false;
}
/**Retorna una copia de la forma como una forma nueva.*/
clonar() {
const clonForma = new Forma();
Object.assign(clonForma, this);
clonForma.iniciarTransformacion(this.posicion.x, this.posicion.y);
return clonForma;
}
/**Suma el ángulo ingresado al ángulo de rotación de la forma.*/
rotar(angulo) {
this.transformacionAnterior.rotacion = this._transformacion.rotacion;
this._transformacion.rotacion += angulo;
this.transformar = true;
}
/**Suma el vector ingresado al vector de posición de la forma.*/
desplazar(vector) {
this.transformacionAnterior.posicion = this._transformacion.posicion;
this._transformacion.posicion = this._transformacion.posicion.sumar(vector);
}
/**Rota la forma alrededor del punto (0, 0)*/
rotarSegunOrigen(angulo) {
this.transformacionAnterior.posicion = this._transformacion.posicion;
this._transformacion.posicion = this._transformacion.posicion.rotar(angulo);
}
/**Rota la forma alrededor del punto ingresado.*/
rotarSegunPunto(punto, angulo) {
let vectorAcomodador = Vector.crear(punto.x, punto.y);
this.transformacionAnterior.posicion = this._transformacion.posicion;
this._transformacion.posicion = this._transformacion.posicion.restar(vectorAcomodador);
this.rotarSegunOrigen(angulo);
this._transformacion.posicion = this._transformacion.posicion.sumar(vectorAcomodador);
}
/**Traza el contorno de la forma. Usa una instancia de la clase Lapiz o Dibujante.*/
trazar(lapiz) {
lapiz.trazar(this);
}
/**Rellena el interior de la forma. Usa una instancia de la clase Lapiz o Dibujante.*/
rellenar(lapiz) {
lapiz.rellenar(this);
}
/**Rellena el interior de la forma. Usa una instancia de la clase Lapiz o Dibujante.*/
dibujar(lapiz) {
lapiz.dibujar(this);
}
}
/**
=============================================
* MÓDULO DE COLISIONES *
=============================================
Trabaja usando objetos de tipo Forma.
Usa el Teorema de ejes de separación (SAT) para detectar colisiones.
*/
/**MÓDULO DE COLISIONES
* Trabaja usando objetos de tipo Forma.
* Usa el Teorema de ejes de separación (SAT) para detectar colisiones.
*/
class Colision {
static get iteraciones() {
return 2;
}
/**Detecta colisiones usando el teorema SAT entre formas de tipo circunferencia y/o polígono.
* Retorna true si detecta una colisión.
* Retorna false si no detecta colisión.
*/
static detectar(formaUno, formaDos) {
//Pondré acá la detección de la distancia límite de colisión
if (Geometria.distanciaEntrePuntos(formaUno.posicion, formaDos.posicion) <= (formaUno.radio + formaDos.radio) * 1.05) {
if (formaUno.tipo == exports.TipoFormas.poligono && formaDos.tipo == exports.TipoFormas.poligono) {
return Colision.poligonos(formaUno, formaDos);
}
else if (formaUno.tipo == exports.TipoFormas.circunferencia && formaDos.tipo == exports.TipoFormas.poligono) {
return Colision.circunferenciaPoligono(formaUno, formaDos);
}
else if (formaUno.tipo == exports.TipoFormas.poligono && formaDos.tipo == exports.TipoFormas.circunferencia) {
return Colision.circunferenciaPoligono(formaDos, formaUno);
}
else {
return Colision.circunferencias(formaUno, formaDos);
}
}
return false;
}
/**Detecta la intersección entre dos circunferencias.
* Retorna true si hay intersección.
* Retorna false si no hay intersección.
* Compara la distancia entre ambos centros con la suma de sus radios.
*/
static circunferencias(circunferenciaUno, circunferenciaDos) {
let sumaRadios = circunferenciaUno.radioTransformado + circunferenciaDos.radioTransformado;
let distanciaCentros = Geometria.distanciaEntrePuntos(circunferenciaUno.posicion, circunferenciaDos.posicion);
if (distanciaCentros > sumaRadios) {
return false;
}
return true;
}
/**Detecta la colisión entre dos polígonos.
* Retorna true si hay colisión.
* Retorna false si no hay colisión.
* Usa el teorema SAT. Proyecta los vértices sobre las normales de las caras de ambos polígonos y busca ejes de separación.
*/
static poligonos(poligonoUno, poligonoDos) {
for (let normal of poligonoUno.normales) {
/**Búsqueda de proyecciones mínimas y máximas de los vértices de los polígonos sobre las normales del polígono uno.*/
let menorUno = Colision.proyeccionMenor(poligonoUno.verticesTransformados, normal);
let mayorUno = Colision.proyeccionMayor(poligonoUno.verticesTransformados, normal);
let menorDos = Colision.proyeccionMenor(poligonoDos.verticesTransformados, normal);
let mayorDos = Colision.proyeccionMayor(poligonoDos.verticesTransformados, normal);
/**Comparación. Si se encuentra una separación, retorna false.*/
if (menorUno > mayorDos || mayorUno < menorDos) {
return false;
}
}
for (let normal of poligonoDos.normales) {
/**Búsqueda de proyecciones mínimas y máximas de los vértices de los polígonos sobre las normales del polígono uno.*/
let menorUno = Colision.proyeccionMenor(poligonoUno.verticesTransformados, normal);
let mayorUno = Colision.proyeccionMayor(poligonoUno.verticesTransformados, normal);
let menorDos = Colision.proyeccionMenor(poligonoDos.verticesTransformados, normal);
let mayorDos = Colision.proyeccionMayor(poligonoDos.verticesTransformados, normal);
/**Comparación. Si se encuentra una separación, retorna false.*/
if (menorUno > mayorDos || mayorUno < menorDos) {
return false;
}
}
return true;
}
/**Detecta la colisión entre una circunferencia y un polígono.
* Retorna true si hay colisión.
* Retorna false si no hay colisión.
* Usa el teorema SAT. Proyecta los vértices del polígono y dos puntos de la circunferencia sobre las normales de las caras del polígono y busca ejes de separación.
*/
static circunferenciaPoligono(circunferencia, poligono) {
for (let normal of poligono.normales) {
/**Búsqueda de proyecciones mínimas y máximas de los vértices de los polígonos sobre las normales del polígono uno.*/
let menorPoli = Colision.proyeccionMenor(poligono.verticesTransformados, normal);
let mayorPoli = Colision.proyeccionMayor(poligono.verticesTransformados, normal);
let menorCirc = circunferencia.posicion.proyeccion(normal) - circunferencia.radioTransformado;
let mayorCirc = circunferencia.posicion.proyeccion(normal) + circunferencia.radioTransformado;
/**Comparación. Si se encuentra una separación, retorna false.*/
if (menorPoli > mayorCirc || mayorPoli < menorCirc) {
return false;
}
}
return true;
}
/**Retorna el valor menor entre las proyecciones de un conjunto de vértices sobre un eje representado por un vector normal.*/
static proyeccionMenor(vertices, normal) {
let menor = vertices[0].proyeccion(normal);
/**Búsqueda de proyecciones mínimas de los vértices del polígono uno.*/
for (let vertice of vertices) {
if (vertice.proyeccion(normal) < menor) {
menor = vertice.proyeccion(normal);
}
}
return menor;
}
/**Retorna el valor mayor entre las proyecciones de un conjunto de vértices sobre un eje representado por un vector normal.*/
static proyeccionMayor(vertices, normal) {
let mayor = vertices[0].proyeccion(normal);
/**Búsqueda de proyecciones máximas de los vértices del polígono uno.*/
for (let vertice of vertices) {
if (vertice.proyeccion(normal) > mayor) {
mayor = vertice.proyeccion(normal);
}
}
return mayor;
}
/**Retorna un arreglo de dos vectores correspondiente a las normales de las caras de contacto entre dos formas.
* El primero vector del arreglo corresponde a la normal de la primera forma.
* El segundo vector del arreglo corresponde a la normal de la segunda forma.
*/
static normalesContacto(formaUno, formaDos) {
let normales = [];
let normalUno;
let normalDos;
let vectorUnoADos = Vector.segunPuntos(formaUno.posicion, formaDos.posicion);
let vectorDosAUno = Vector.segunPuntos(formaDos.posicion, formaUno.posicion);
if (formaUno.tipo == exports.TipoFormas.circunferencia) {
normalUno = vectorUnoADos;
}
else {
normalUno = formaUno.normales[0].clonar();
for (let normal of formaUno.normales) {
if (vectorUnoADos.punto(normal) > vectorUnoADos.punto(normalUno)) {
normalUno = normal.clonar();
}
}
}
if (formaDos.tipo == exports.TipoFormas.circunferencia) {
normalDos = vectorDosAUno.clonar();
}
else {
normalDos = formaDos.normales[0].clonar();
for (let normal of formaDos.normales) {
if (vectorDosAUno.punto(normal) > vectorDosAUno.punto(normalDos)) {
normalDos = normal.clonar();
}
}
}
normales.push(normalUno);
normales.push(normalDos);
return normales;
}
/**Detecta la colisión entre una circunferencia y su entorno que la contiene.
* Retorna el valor de solapamiento.
* Retorna null si no hay colisión.
* Usa el teorema SAT. Proyecta los vértices del entorno y dos puntos de la circunferencia sobre las normales de las caras del polígono
* y verifica si hay proyecciones de la circunferencia mayores a la de los vértices del entorno.
*/
static circunferenciaEntorno(circunferencia, entorno) {
let distanciaCicunferenciaCentro = Geometria.distanciaEntrePuntos(circunferencia.posicion, entorno.posicion);
if (distanciaCicunferenciaCentro + circunferencia.radio * 1.2 > entorno.apotema) {
for (let normal of entorno.normales) {
/**Búsqueda de proyecciones mínimas y máximas de los vértices de los polígonos sobre las normales del polígono uno.*/
let menorPoli = Colision.proyeccionMenor(entorno.verticesTransformados, normal);
let mayorPoli = Colision.proyeccionMayor(entorno.verticesTransformados, normal);
let menorCirc = circunferencia.posicion.proyeccion(normal) - circunferencia.radioTransformado;
let mayorCirc = circunferencia.posicion.proyeccion(normal) + circunferencia.radioTransformado;
/**Comparación. Si se encuentra una separación, retorna true.*/
if (menorPoli > menorCirc) {
return menorPoli - menorCirc;
}
if (mayorPoli < mayorCirc) {
return mayorCirc - mayorPoli;
}
}
}
return null;
}
/**Retorna la normal del borde del entorno contra el que ha colisionado una forma.*/
static normalContactoConEntorno(forma, entorno) {
let numeroVertices = entorno.verticesTransformados.length;
let normalEntorno = entorno.normales[numeroVertices - 1];
let vectorCentroAForma = Vector.segunPuntos(entorno.posicion, forma.posicion);
for (let i = 0; i < numeroVertices - 1; i++) {
let vectorCentroAVerticeUno = Vector.segunPuntos(entorno.posicion, entorno.verticesTransformados[i]);
let vectorCentroAVerticeDos = Vector.segunPuntos(entorno.posicion, entorno.verticesTransformados[i + 1]);
let anguloVertices = vectorCentroAVerticeDos.anguloVectores(vectorCentroAVerticeUno);
if (vectorCentroAForma.anguloVectores(vectorCentroAVerticeUno) < anguloVertices
&& vectorCentroAForma.anguloVectores(vectorCentroAVerticeDos) < anguloVertices) {
normalEntorno = entorno.normales[i];
}
}
return normalEntorno;
}
}
//Momento lineal, movimiento acelerado, momento angular, energía cinética y potencial.
class Cinematica {
/**Retorna un vector velocidad de un cuerpo que colisiona con una superficie.*/
static reboteSimple(cuerpo, normal) {
let vectorRebotado = cuerpo.velocidad;
if (vectorRebotado.anguloVectores(normal) > Geometria.PI_MEDIO) {
vectorRebotado = vectorRebotado.invertir();
}
return vectorRebotado.rotar((normal.angulo - vectorRebotado.angulo) * 2);
}
/**Retorna en un arreglo las velocidades finales después de un choque elástico entre dos cuerpos.*/
static reboteElastico(cuerpoUno, cuerpoDos) {
return [Cinematica.velocidadUnoFinal(cuerpoUno, cuerpoDos), Cinematica.velocidadDosFinal(cuerpoUno, cuerpoDos)];
}
static velocidadUnoFinal(cuerpoUno, cuerpoDos) {
const velUnoInicial = cuerpoUno.velocidad;
const divisionMasas = (2 * cuerpoDos.masa) / (cuerpoUno.masa + cuerpoDos.masa);
const restaVelocidades = cuerpoDos.velocidad.restar(cuerpoUno.velocidad);
const restaPosiciones = cuerpoDos.posicion.restar(cuerpoUno.posicion);
const puntoVelocidadesPosiciones = restaVelocidades.punto(restaPosiciones);
const moduloPosicionesCuadrado = restaPosiciones.magnitud ** 2;
const velUnoFinal = velUnoInicial.sumar(restaPosiciones.escalar(divisionMasas * puntoVelocidadesPosiciones / moduloPosicionesCuadrado));
return velUnoFinal;
}
static velocidadDosFinal(cuerpoUno, cuerpoDos) {
const velDosInicial = cuerpoDos.velocidad;
const divisionMasas = (2 * cuerpoUno.masa) / (cuerpoUno.masa + cuerpoDos.masa);
const restaVelocidades = cuerpoUno.velocidad.restar(cuerpoDos.velocidad);
const restaPosiciones = cuerpoUno.posicion.restar(cuerpoDos.posicion);
const puntoVelocidadesPosiciones = restaVelocidades.punto(restaPosiciones);
const moduloPosicionesCuadrado = restaPosiciones.magnitud ** 2;
const velDosFinal = velDosInicial.sumar(restaPosiciones.escalar(divisionMasas * puntoVelocidadesPosiciones / moduloPosicionesCuadrado));
return velDosFinal;
}
}
//Interacciones entre cuerpos.
class Interaccion {
static get iteraciones() {
return 1;
}
/**Retorna una copia del conjunto de cuerpos con la resolución de rebote para cuerpos que han colisionado. */
static reboteEntreCuerpos(cuerpos) {
for (let iteracion = 0; iteracion < Interaccion.iteraciones; iteracion++) {
// let cuerposRebotados: Cuerpo[] = [];
for (let i = 0; i < cuerpos.length - 1; i++) {
for (let j = i + 1; j < cuerpos.length; j++) {
if (Colision.detectar(cuerpos[i], cuerpos[j])) {
let normales = Colision.normalesContacto(cuerpos[i], cuerpos[j]);
let velocidadesFinales = Cinematica.reboteElastico(cuerpos[i], cuerpos[j]);
cuerpos[i].velocidad = velocidadesFinales[0];
cuerpos[j].velocidad = velocidadesFinales[1];
// cuerpos[i].velocidad = Cinematica.reboteSimple(cuerpos[i], normales[1])
// cuerpos[j].velocidad = Cinematica.reboteSimple(cuerpos[j], normales[0])
if (cuerpos[i].fijo) {
cuerpos[j].posicion = cuerpos[j].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[j], cuerpos[i], normales[0]));
}
else if (cuerpos[j].fijo) {
cuerpos[i].posicion = cuerpos[i].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[i], cuerpos[j], normales[1]));
}
else {
cuerpos[i].posicion = cuerpos[i].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[i], cuerpos[j], normales[1]));
cuerpos[j].posicion = cuerpos[j].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[j], cuerpos[i], normales[0]));
}
}
}
// cuerposRebotados.push(cuerpos[i])
}
// cuerposRebotados.push(cuerpos[cuerpos.length - 1])
}
return cuerpos;
// return cuerposRebotados;
}
/**Retorna una copia del conjunto de cuerpos con la resolución de contacto sólido para cuerpos que han colisionado. */
static contactoSimple(cuerpos) {
// console.log('Comprobando')
for (let iteracion = 0; iteracion < Interaccion.iteraciones; iteracion++) {
// let cuerposRebotados: Cuerpo[] = [];
for (let i = 0; i < cuerpos.length - 1; i++) {
for (let j = i + 1; j < cuerpos.length; j++) {
if (Colision.detectar(cuerpos[i], cuerpos[j])) {
let normales = Colision.normalesContacto(cuerpos[i], cuerpos[j]);
// let velocidadesFinales: Vector[] = Cinematica.reboteElastico(cuerpos[i], cuerpos[j])
if (cuerpos[i].fijo) {
cuerpos[j].posicion = cuerpos[j].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[j], cuerpos[i], normales[0]));
}
else if (cuerpos[j].fijo) {
cuerpos[i].posicion = cuerpos[i].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[i], cuerpos[j], normales[1]));
}
else {
cuerpos[i].posicion = cuerpos[i].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[i], cuerpos[j], normales[1]));
cuerpos[j].posicion = cuerpos[j].posicion.sumar(Interaccion.resolverSolapamiento(cuerpos[j], cuerpos[i], normales[0]));
}
}
}
// cuerposRebotados.push(cuerpos[i])
}
// cuerposRebotados.push(cuerpos[cuerpos.length - 1])
}
return cuerpos;
// return cuerposRebotados;
}
static resolverSolapamiento(cuerpoUno, cuerpoDos, normal) {
let vectorDesplazamiento = normal.normalizar();
let solapamiento = (cuerpoDos.radio + cuerpoUno.radio) - Geometria.distanciaEntrePuntos(cuerpoDos.posicion, cuerpoUno.posicion);
if (cuerpoDos.fijo) {
vectorDesplazamiento = vectorDesplazamiento.escalar(solapamiento);
return vectorDesplazamiento;
}
vectorDesplazamiento = vectorDesplazamiento.escalar(0.5 * solapamiento);
return vectorDesplazamiento;
}
/**Retorna una copia del conjunto de circunferencias con la resolución de rebote para cuerpos que han colisionado con los bordes de un entorno. */
static reboteCircunferenciasConEntorno(circunferencias, entorno) {
let cuerposRebotados = [];
for (let i = 0; i < circunferencias.length; i++) {
let solapamiento = Colision.circunferenciaEntorno(circunferencias[i], entorno);
if (solapamiento != null) {
let normal = Colision.normalContactoConEntorno(circunferencias[i], entorno);
let normalInvertida = normal.invertir();
// circunferencias[i].velocidad = Cinematica.reboteElastico(circunferencias[i], entorno)[0]
// circunferencias[i].velocidad = Vector.invertir(Cinematica.reboteElastico(circunferencias[i], entorno)[0])
circunferencias[i].velocidad = Cinematica.reboteSimple(circunferencias[i], normalInvertida);
circunferencias[i].posicion = circunferencias[i].posicion.sumar(Interaccion.resolverSolapamientoEntorno(normalInvertida, solapamiento));
}
cuerposRebotados.push(circunferencias[i]);
}
return cuerposRebotados;
}
static resolverSolapamientoEntorno(normal, solapamiento) {
let vectorDesplazamiento = normal.normalizar();
vectorDesplazamiento = vectorDesplazamiento.escalar(1 * solapamiento);
return vectorDesplazamiento;
}
}
//REPENSAR ESTA CLASE
class Contenedor {
cuerpo;
cuerposContenidos = [];
constructor(cuerpo) {
this.cuerpo = cuerpo;
this.cuerpo.fijo = true;
}
/**Retorna el conjunto de vectores normales de cada arista del contenedor. */
get normales() {
return Vector.clonarConjunto(this.cuerpo.normales);
}
/**Retorna un objeto Contenedor a partir de un cuerpo.*/
static crearContenedor(cuerpo) {
return new Contenedor(cuerpo);
}
/**Agrega cuerpos al conjunto de cuerpos que estarán dentro del contenedor.*/
agregarCuerposContenidos(...cuerpos) {
this.cuerposContenidos.push(...cuerpos);
}
rebotarCircunferenciasConBorde() {
Interaccion.reboteCircunferenciasConEntorno(this.cuerposContenidos, this.cuerpo);
}
/**Suma la aceleración a la velocidad y la velocidad a la posición.*/
mover() {
this.cuerpo.mover();
}
}
/**
=============================================
* MÓDULO DE CUERPOS *
=============================================
Trabaja usando objetos de tipo Forma.
Crea cuerpos geométricos con masa y densidad.
Contiene métodos para mover según velocidad y aceleración.
*/
//TAREAS
//Una propiedad que defina si es necesario actualizar la posición y la rotación.
//Un solo método para aplicar transformar y actualizar transformaciones
//Buscar un modo de anclar un vértice a otro vector. Así se puede acoplar un ala a otro cuerpo. Método anclar(vector)
/**MÓDULO DE CUERPOS
* Trabaja usando objetos de tipo Forma.
*/
class Cuerpo extends Forma {
_velocidad = Vector.cero();
_aceleracion = Vector.cero();
/**Determina si el cuerpo rotará o no según la dirección y sentido de su velocidad.*/
rotarSegunVelocidad = false;
/**Propiedad útil para determinar si un cuerpo será controlado por el usuario.*/
controlable = false;
/**Determina si un cuerpo se moverá o no producto de la interacción con otros cuerpos.*/
fijo = false;
masa = 1;
densidad = 1;
/**Propiedades para activar y desactivar acciones relacionadas con el control del movimiento de cuerpos por parte del usuario.*/
controles = {
arriba: false,
abajo: false,
izquierda: false,
derecha: false,
rotarIzquierda: false,
rotarDerecha: false,
rapidez: 1,
anguloRotacion: Geometria.PI_MEDIO / 30
};
constructor() {
super();
}
/**Retorna una copia del vector velocidad.*/
get velocidad() {
return this._velocidad.clonar();
}
/**Retorna una copia del vector aceleración.*/
get aceleracion() {
return this._aceleracion.clonar();
}
get verticesTransformados() {
if (this.rotarSegunVelocidad) {
this.transformacionAnterior.rotacion = this._transformacion.rotacion;
this.rotacion = this._velocidad.angulo - this._vertices[0].angulo;
return super.verticesTransformados;
}
return super.verticesTransformados;
}
/**Modifica el vector velocidad.*/
set velocidad(velocidad) {
this._velocidad = velocidad.clonar();
}
/**Modifica el vector aceleración.*/
set aceleracion(aceleracion) {
this._aceleracion = aceleracion.clonar();
}
/**Retorna un cuerpo geométrico regular.
* El radio corresponde a la distancia entre el centro y cualquiera de sus vértices.*/
static poligono(x, y, lados, radio, opciones) {
let poliForma = super.poligono(x, y, lados, radio);
let poligono = Cuerpo.cuerpoSegunForma(poliForma);
if (opciones) {
Object.assign(poligono, opciones);
// poligono.aplicarOpciones(opciones)
}
return poligono;
}
/**Retorna un cuerpo geométrico regular.
* El radio corresponde a la distancia entre el centro y cualquiera de sus vértices.*/
static poligonoSegunVertices(vertices, opciones) {
let poliForma = super.poligonoSegunVertices(vertices);
let poligono = Cuerpo.cuerpoSegunForma(poliForma);
if (opciones) {
Object.assign(poligono, opciones);
// poligono.aplicarOpciones(opciones)
}
return poligono;
}
/**Retorna un cuerpo rectangular.*/
static rectangulo(x, y, base, altura, opciones) {
let rectForma = super.rectangulo(x, y, base, altura);
let rectangulo = Cuerpo.cuerpoSegunForma(rectForma);
i