UNPKG

muijs-cuerpos

Version:

Librería para simular y graficar interacciones entre cuerpos rígidos

1,131 lines (1,111 loc) 97.6 kB
'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