Un segmento es una línea recta acotada por un punto de inicio (A) y un punto de fin (B). Para colocar objetos en un segmento lo dividiremos en un cierto número de partes iguales.
Como podemos ver en el ejemplo anterior, dividiendo el segmento en cuatro partes obtenemos cinco puntos equidistantes donde colocar nuestros objetos.
Nuestros puntos vienen dados por una posición $x$ (horizontal) y una posición $y$ (vertical). Restando la posición $x$ de B con la posición $x$ de A obtenemos el ancho del segmento, que podría ser negativo si B estuviera a la izquierda de A. De forma similar, restando las posiciones $y$ obtenemos el alto del segmento.
Dividiendo el ancho y el alto entre un número de partes (en el ejemplo, cuatro) obtendremos el ancho y el alto de cada parte.
Nuestros puntos equidistantes se obtendrán sumando a A el tamaño de una parte entre cero y cuatro veces (el número de partes). A continuación se muestra un ejemplo de cómo se podría implementar este algoritmo.
function drawSegment(ax, ay, bx, by, numPoints) { // Calculamos el alto y ancho del segmento var w = bx - ax; var h = by - ay; // Calculamos el alto y ancho de las partes var numParts = numPoints - 1; var partW = w / numParts; var partH = h / numParts; // Para i entre 0 y `numPoints` (no incluido) ... for (var i = 0; i < numPoints; i++) { // Dibujamos un punto que será A sumado i // veces el ancho y alto de la parte. drawPoint({ x: ax + partW * i, y: ay + partH * i }); }}
Un polígono es un conjunto de segmentos unidos entre sí. En general, todo segmento tiene un punto de inicio y un punto de fin. En el caso de un polígono, el punto de inicio de un segmento es el punto de fin del anterior.
Aquí vamos a centrarnos en los polígonos regulares, es decir, aquellos que todos los segmentos que los forman tienen la misma longitud, puesto que son los que utilizaremos después como base para hacer estrellas.
Un polígono regular se construye a partir de una circunferencia, dividiéndola en tantos arcos como lados queramos para nuestro polígono. Todos los arcos deben tener el mismo ángulo. La siguiente imagen muestra como ejemplo la construcción de un pentágono.
El proceso para obtener estos puntos es exactamente el mismo que veíamos para representar un círculo en el artículo anterior. Dividimos el ángulo de la circunferencia completa ($2\pi$ radianes o 360º) entre el número de lados y utilizamos seno y coseno para calcular los puntos que definen el polígono.
function calcPolygonVertices(numVertices, centerX, centerY, radius, rotation){ // Dividimos el ángulo de la circunferencia completa // (2pi=360°) entre el número de vértices del polígono. // De esta manera obtendremos el ángulo de los lados. var angInterval = 2 * Math.PI / numVertices; // Inicializamos una lista de vértices var vertices = []; for (var i = 0; i < numVertices; i++) { // Multiplicamos el número de vértice por el ángulo // de los lados var x = centerX + Math.cos(i * angInterval + rotation) * radius; var y = centerY + Math.sin(i * angInterval + rotation) * radius; vertices.push({x: x, y: y}); } return vertices;}
Sin embargo, en este caso no nos conformaremos con pintar los puntos, sino que los utilizaremos de entrada para la función anterior de manera que podamos dibujar segmentos.
function drawPolygon(numVertices, centerX, centerY, radius, rotation, numPointsSegment){ var vertices = calcPolygonVertices(numVertices, centerX, centerY, radius, rotation); // Unimos cada vértice con el siguiente for (var numVertex = 0; numVertex < numVertices; numVertex++) { var vertexA = vertices[numVertex]; var vertexB = vertices[numVertex + 1]; drawPolygonSegment(vertexA.ax, vertexA.ay, vertexB.ax, vertexB.ay, numPointsSegment); }}
La función drawPolygonSegment
es igual que la función drawSegment
explicada antes, con una sutil diferencia: el último punto del segmento no es dibujado, ya que coincidirá con el primer punto del siguiente lado del polígono.
function drawPolygonSegment(ax, ay, bx, by, numPoints) { // Calculamos el alto y ancho del segmento var w = bx - ax; var h = by - ay; // Calculamos el alto y ancho de las partes var numParts = numPoints - 1; var partW = w / numParts; var partH = h / numParts; // Para i entre 0 y `numPoints - 1` (no incluido) ... // El último punto no se dibuja porque coincide con // el primer punto del siguiente segmento for (var i = 0; i < numPoints - 1; i++) { // Dibujamos un punto que será A sumado i // veces el ancho y alto de la parte. drawPoint({ x: ax + partW * i, y: ay + partH * i }); }}
Añadiendo un desplazamiento a la posición de los puntos utilizados para dibujar los segmentos y modificando su valor a lo largo del tiempo es posible mostrar polígonos animados, donde los objetos no estén colocados en posiciones estáticas sino que se muevan siguiendo la forma del polígono.
El desplazamiento se especificará como una variable numérica entre 0 y 1. Un valor de 0 será sinónimo de no desplazamiento, mientras con valor de 1 significará que el primer punto estaría en la posición en la que habría estado el segundo si no se hubiera aplicado desplazamiento.
El siguiente bloque de código muestra la función de dibujado de segmentos de polígonos adaptada para soportar desplazamiento. Es similar a la anterior, pero añadimos un nuevo parámetro offset
y y se lo sumamos al número de punto.
function drawPolygonSegment(ax, ay, bx, by, numPoints, offset){ // Calculamos el alto y ancho del segmento var w = bx - ax; var h = by - ay; // Calculamos el alto y ancho de las partes var numParts = numPoints - 1; var partW = w / numParts; var partH = h / numParts; // Para i entre 0 y `numPoints - 1` (no incluido) ... // El último punto no se dibuja porque coincide con // el primer punto del siguiente segmento for (var i = 0; i < numPoints - 1; i++) { // Dibujamos un punto que será A sumado i // veces el ancho y alto de la parte. drawPoint({ x: ax + partW * (i + offset), y: ay + partH * (i + offset) }); }}
Sólo quedaría añadir este parámetro también a la función de dibujar polígono y modificar su valor en cada frame.
A partir de un polígono regular, es fácil construir una estrella. Para ello se sigue el mismo procedimiento que en el caso del polígono, con una diferencia: Cada vértice del polígono será unido con el que esté a $n \geq 2$ posiciones después de él. A este $n$ lo denominaremos el paso de la estrella.
En la siguiente figura se muestra la construcción de una estrella de cinco puntas con paso 2:
El siguiente algoritmo nos permitiría construir una estrella de este tipo:
function drawStar(numVertices, centerX, centerY, radius, rotation, numPointsSegment, starStep, segmentOffset){ var vertices = calcPolygonVertices(numVertices, centerX, centerY, radius, rotation); // Una estrella tiene el doble de segmentos que vértices var numSegments = vertices.length * 2; var indexVertexA = 0; // Empezamos trazando desde el // vértice 0 for (var i = 0; i < numSegments; i++) { // El vértice destino del segmento es aquel que esté a // `starStep` posiciones del vértice de origen var indexVertexB = (indexVertexA + starStep) % vertices.length; // Dibujamos el segmento var vertexA = vertices[indexVertexA]; var vertexB = vertices[indexVertexB]; drawPolygonSegment(vertexA.ax, vertexA.ay, vertexB.ax, vertexB.ay, numPointsSegment, segmentOffset); // El vértice destino de este segmento será el vértice // inicio del siguiente indexVertexA = indexVertexB; }}
Con el algoritmo anterior no es posible dibujar algunos tipos de estrellas. Es el caso de la estrella de seis puntas o hexagrama, mostrada en la figura siguiente.
Al dibujar la estrella de seis puntas con un paso de dos, nos encontramos con que de después de tres pasos volvemos a al vértice inicial. Los segmentos de la estrella de seis puntas, no están todos conectados.
Cuando se dé este caso, para terminar la estrella deberemos repetir el proceso desde el vértice siguiente a aquel en el que empezamos.
El siguiente fragmento de código muestra cómo dibujar una estrella de forma que se tenga en cuenta el caso de que haya grupos de segmentos desconectados.
function drawStar(numVertices, centerX, centerY, radius, rotation, numPointsSegment, starStep, segmentOffset){ var vertices = calcPolygonVertices(numVertices, centerX, centerY, radius, rotation); // Una estrella tiene el doble de segmentos que // vértices var numSegments = vertices.length * 2; // Empezamos trazando desde el vértice 0 var indexStartVertex = 0; var indexVertexA = indexStartVertex; for (var i = 0; i < numSegments; i++) { // El vértice destino del segmento es aquel que esté // a `starStep` posiciones del vértice de origen var indexVertexB = (indexVertexA + starStep) % vertices.length; // Dibujamos el segmento var vertexA = vertices[indexVertexA]; var vertexB = vertices[indexVertexB]; drawPolygonSegment(vertexA.ax, vertexA.ay, vertexB.ax, vertexB.ay, numPointsSegment, segmentOffset); // El vértice destino de este segmento será el // vértice inicio del siguiente indexVertexA = indexVertexB; // Si volvemos al punto de inicio y todavía quedan // más pasos, estamos ante una estrella desconectada. // Deberemos repetir el proceso empezando a dibujar // por el punto siguiente. if (indexVertexA == indexStartVertex) { indexStartVertex = (indexStartVertex + 1) % vertices.length; indexVertexA = indexStartVertex; } }}
La siguiente demo muestra la construcción de una estrella haciendo uso de los algoritmos explicados.
Podemos modificar en cada frame los valores algunos parámetros de la función drawStar
para crear animaciones:
rotation
define la rotación global de la estrella. Con un valor de cero el primero vértice de la estrella está situado en la parte derecha de la circunferencia (0°). Modificando este valor periódicamente podemos conseguir que la estrella gire. segmentOffset
define el desplazamiento de los objetos utilizados para dibujar los segmentos de la estrella. Modificando periódicamente este valor entre cero y uno conseguiremos que los objetos que utilicemos para usemos para dibujar la estrella no estén situados estáticamente sino que sigan la forma de la estrella.radius
define el radio de la circunferencia sobre la que se inscribe la estrella. Animando el valor de esta variable podemos hacer que crezca o decrezca.centerX
y centerY
definen la posición de la estrella. Animando estos valores podemos hacer que se mueva por la pantalla.La siguiente demostración permite animar alguno de de estos valores:
Esta manera de actualizar ángulos, aunque es simple, tiene un pequeño defecto: si seguimos girando en el mismo sentido continuamente el ángulo resultante tendrá un valor muy grande.
Si bien, matemáticamente un ángulo muy grande equivalente a uno más pequeño de cero y 360°, hay varias razones por la que nos podría interesar limitar el valor del ángulo a este rango:
Si como parte de la interfaz de nuestro juego queremos mostrar el valor de un ángulo, sería poco conveniente mostrar un ángulo no normalizado, por ejemplo 3630°, en vez del ángulo normalizado equivalente (en el ejemplo, 30°).
La precisión de los ordenadores a la hora de trabajar con números decimales es limitada, y al llegar a valores muy grandes descartan las cifras menos significativas. Si incrementamos el valor de un ángulo una y otra vez sin normalizarlo llegará un momento en el que el valor sea tan grande que los incrementos que se hagan tendrán una precisión reducida o incluso dejarán de tener efecto.
Por ejemplo, sumar 10.000.000 + 1 en una variable float de 32 bits devuelve 10.000.000 porque la variable no tiene espacio suficiente para almacenar el número entero.
Teniendo en cuenta que en una variable float, de los 32 bits disponibles, 23 bits se utilizan para guardar la parte fraccionaria (referencia) y que para representar un ángulo con precisión capaz de distinguir 512 valores necesitamos 9 bits, empezaríamos a tener problemas tras girar nuestro objeto $2^{(23-9)}=2^{14}= 16.384$ vueltas, suponiendo que ha partido de un valor de cero. Si consideramos que esto puede suceder, nos interesará normalizar los ángulos para evitarlo.
En primer lugar estudiaremos el algoritmo de normalización en genérico, de manera que nos pueda servir para cualquier variable cíclica. Después la especializaremos para grados y radianes.
Entenderemos una variable cíclica como aquella en la que existe un rango normal de valores (definido por un mínimo y un máximo $[min,max)$) y en la un valor fuera del rango normal es funcionalmente equivalente a ese mismo valor sumado o restado la amplitud del rango ($max - min$) un número entero de veces.
Un ejemplo cotidiano de variable cíclica es el reloj de 12 horas: las 11 + 4 horas son las 15, cuyo valor normalizado es $15-12=3$ (las 3).
Un aspecto a destacar es que el máximo del rango de una variable cíclica, por definición, no es un valor normalizado. Por ejemplo, si nuestro rango es $[0^{\circ}, 360^{\circ})$, el valor normalizado de 360° es 0°.
Seguiremos utilizando el ejemplo del reloj de 12 horas. En la figura siguiente se ven representados sobre una línea el el rango de valores normal y un valor val
que intentaremos normalizar. Una hora normalizada tiene cualquier valor entre 1 y 13, con el 13 no incluido (1:00 es una hora normalizada y 12:59 también).
En primer lugar desplazaremos todos los valores de manera que el rango comience en cero.
El proceso seguir después, depende de si $val - min$ es positivo o negativo.
En el caso de que $val - min$ sea positivo, estará a la derecha del cero en nuestra representación. Calculamos el resto de la división de $val - min$ entre la amplitud ($max - min$).
$a\ %\ b$ denota el resto obtenido al hacer la división de $a$ entre $b$.
Con esto hemos conseguido ajustar el valor de $val$ entre 0 y 12. pero nuestro rango inicial era entre 1 y 13. Sumando el mínimo (1) obtenemos el valor deseado.
$$normalizar(val, min, max)\ |\ val \geq min \\ = min + (val - min)\ %\ (max - min)$$
En el caso de que $val - min$ sea positivo, estará a la izquierda del cero en nuestra representación, como podemos ver en la siguiente figura.
Podemos alternar el signo dando la vuelta a los operandos de la resta y operar como en el caso de que fuera positivo.
$$-(val - min)\
= -val -(-min) \
=min - val$$
El resultado es correcto, pero está invertido debido al cambio de signo. Podemos corregir esto restando a la amplitud $(max - min)$ el valor calculado.
Finalmente, igual que en el caso de los valores positivos, sumamos $min$ para obtener una hora entre 1 y 13.
$$normalizar(val, min, max)\ |\ val \lt min \\ = min + (max - min) \\ - ((min - val) %\ (max - min))$$
$min-min$ se anula y queda…
$$normalizar(val, min, max)\ |\ val \lt min \\ = max - ((min - val)\ %\ (max - min))$$
Juntando los dos casos anteriores, esta es la definición matemática de nuestra función de normalización:
$$normalizar(val, min, max)\ |\ val \lt min \\ = min + (max - min) \\ - ((min - val)\ %\ (max - min))$$
En código:
function normCyclic(val, min, max) { if (val >= min) { return min + (val - min) % (max - min); } else { return max - (min - val) % (max - min); }}
Con esta función podemos normalizar cualquier varible cíclica. Para terminar el ejemplo, la siguiente función normalizaría tiempo en formato de 12 horas.
function norm12Hours(val) { return normCyclic(val, 1, 13);}
Teniendo nuestra función de normalización, es trivial construir funciones que normalizen variables cíclicas de distinto tipo.
Esta función normaliza grados medidos en radianes dentro del rango $[-\pi,\pi)$. Podemos utilizarla para actualizar variables de giro sin miedo a que se produzca pérdida de precisión.
function normRad(val) { return normCyclic(val, -Math.PI, Math.PI);}
Para actualizar una variable de rotación realizando normalización utilizaríamos un código como este:
function update() { var rotSpeed = 3; // rad/s // Actualiza la rotación del objeto teniendo // sumándole la velocidad angular. this.rotation = normRad(this.rotation + rotSpeed);}
De forma similar podemos normalizar grados. Podemos utilizar el rango $[0^{\circ}, 360^{\circ})$, con lo que obtendríamos sólo números positivos, o $[-180^{\circ}, 180^{\circ})$ si queremos tener ángulos tanto positivos como negativos.
function normDeg360(val) { return normCyclic(val, 0, 360);}function normDeg180(val) { return normCyclic(val, -180, 180);}
Cuando definimos al principio de la definición del algoritmo las variables cíclicas especificamos una restricción:
Entenderemos una variable cíclica como aquella en la que existe un rango normal de valores (definido por un mínimo y un máximo $[min,max)$) y en la un valor fuera del rango normal es funcionalmente equivalente a ese mismo valor sumado o restado la amplitud del rango ($max - min$) un número entero de veces.
En una variable de rotación la condición de equivalencia es cierta. Por ejemplo, un elemento rotado 540° produce el mismo resultado que rotando 180°: el elemento aparece girado media vuelta.
Sin embargo, hay otras variables en la que hay una diferencia muy importante. Es el caso de las variables de velocidad angular y aceleración angular: que un objeto gire 540°/s no produce el mismo efecto que si gira a 180°/s. En el primer caso da una vuelta completa y media en un segundo, mientras en el segundo caso, en el mismo tiempo, sólo recorre media vuelta.
La normalización de ángulos es útil para evitar condiciones inesperadas en nuestro código, así como mostrar valores al usuario en un rango razonable, pero no debe usarse de forma compulsiva en toda variable angular.
]]>Para empezar con algo simple, consideremos que nuestro juego tiene dos dimensiones: altura y profundidad.
En física, a menudo se encuentran casos donde una magnitud (cantidad) y una dirección están intimamente relacionadas a la hora de describir un fenómeno. Un ejemplo clásico es la velocidad: es tan importante la magnitud de la velocidad de un coche (habitualmente medida en km/h) como en qué dirección se está moviendo.
Cuando se dan estas relaciones es habitual tratar magnitud y dirección de manera conjunta. Cuando una variable matemática representa estos dos conceptos simultáneamente, se le denomina vector y se representa colocando una flecha encima del nombre de la variable, por ejemplo, $\vec{v}$.
Hay dos formas habituales de expresar el valor de un vector. Una forma es indicar la magnitud y la dirección (ángulo) expresamente. Por ejemplo:
$$\vec{v} = 100_{60^{\circ}}\space km/h$$
Esta fórmula representaría la velocidad de un coche que se mueve a 100 km/h en dirección 60º. (medidos con respecto a un ángulo que consideremos origen, por ejemplo el Norte geográfico y en un determinado sentido). Cuando usemos ángulos en vectores debemos tener en cuenta siempre qué dirección consideraremos como referencia y en qué sentido haremos la medición. Por ejemplo, en este caso podríamos considerar que el ángulo se mide en grados desde el Norte geográfico hasta la posición a medir girando en sentido de las agujas del reloj.
En matemáticas, una convención habitual es considerar que 0º es el plano horizontal y los ángulos positivos están en el sentido contrario a las agujas del reloj, por ejemplo:
Otra forma de expresar un vector es descomponer magnitud y dirección en coordenadas. Por ejemplo:
$$\vec{v} = (30, 50)\space km/h$$
En este caso estaríamos representando la velocidad de un coche que cada hora avanza 30 km en el eje horizontal (x) y 50 km en el eje vertical (y). Los ejes horizontal y vertical se pueden representar también como vectores, habitualmente con los nombres $\vec{i}$ y $\vec{j}$ para los ejes $x$ e $y$ respectivamente. Por ejemplo, el mismo vector anterior se podría representar como:
$$\vec{v} = 30\vec{i} + 50\vec{j}\space km/h$$
Cada representación tiene sus ventajas e inconvenientes. En nuestros ejemplos utilizaremos la representación de coordenadas, con una variable por coordenada, por ejemplo: velX
, velY
. En esta representación es muy fácil sumar vectores.
Para incorporar física en nuestros objetos debemos añadirles propiedades que reflejen el estado de todas las variables físicas que sean de nuestro interés. En este caso necesitamos, como mínimo:
Cada objeto de nuestro juego deberá tener una función de actualización donde sus variables físicas sean actualizadas. Esta función será ejecutada de manera periódica, típicamente una vez por frame.
En nuestro ejemplo, la función de actualización se encargaría de actualizar la posición en función de la velocidad. Si la velocidad del objeto es 2 píxels por frame y la función de actualización se ejecuta cada frame, deberá sumarse 2 a la posición del objeto.
function update() { this.posX = this.posX + this.velX; this.posY = this.posY + this.velY;}
Lanzar un proyectil consiste en aplicarle una velocidad.
Sin ninguna fuerza que modifique la trayectoria del proyectil, éste se moverán línea recta.
Es posible que en este paso necesitemos convertir una velocidad expresada en magnitud y dirección a formato de componentes $x$ e $y$. Para ello podemos hacer uso de trigonometría básica:
function shoot(speedMagnitude, speedDirection) { this.velX += speedMagnitude * Math.cos(speedDirection); this.velY += speedMagnitude * Math.sin(speedDirection);}
En el mundo real, los objetos en movimiento están sometidos a fuerzas, que los aceleran en determinadas direcciones. Particularmente, la gravedad cada usa que los proyectiles caigan.
Para objetos en la Tierra de una masa pequeña comparada a la de un planeta, la aceleración causada por la gravedad es constante y conocida: 9,8 $m/s^2$.
Esto significa que cada segundo, la velocidad del objeto se incrementa en 9,8 $m/s$ (metros por segundo) en dirección al centro de la Tierra. Cuanto más tiempo lleva cayendo, más rápido cae.
Por supuesto este movimiento cesa cuando el objeto llega al suelo. (Realmente la gravedad sigue haciendo su trabajo, pero aparece una fuerza de tensión en sentido contrario ejercida por el suelo que causa que el objeto no se hunda.)
Debemos tener en cuenta esto en nuestros programas para que nuestros objetos no caigan al eternamente al vacío (salvo que sea eso exactamente lo que queramos).
Para tener en cuenta la gravedad modificaremos la velocidad en nuestra función de actualización. Por ejemplo:
function update() { //En este ejemplo asumimos que y=0 es el suelo y las // y's positivas están encima del suelo. // La gravedad modifica la velocidad del objeto, // siempre que no esté en el suelo. if (this.y > 0) { var gravity = 9.8; // píxels/frame² this.velY = this.velY - gravity; } // Movemos el objeto de acuerdo a su velocidad. this.posX = this.posX + this.velX; // Utilizamos la función máximo para que el objeto // no pueda caer a una posición subterránea. this.posY = Math.max(this.posY + this.velY, 0);}
En la siguiente demo se muestran una pequeña aplicación del tiro parabólico.
Muchos proyectiles, por su forma aerodinámica, giran sobre sí mismos conforme avanzan en su trayectoria. Es el caso de las flechas empleadas en la demostración anterior. Si no fuera por esta rotación, el movimiento sería algo extraño, como en la imagen siguiente:
En lugar de eso queremos algo así, como vimos en la demo:
Afortunadamente, este efecto es fácil de simular. Observa la siguiente ilustración de la posible trayectoria de una flecha.
Los círculos rosados señalan los puntos de la trayectoria que realmente aparecerían en nuestro juego. Aunque en teoría la trayectoria tiene infinitos puntos, nuestro juego tendrá que conformarse con representar sólo algunos. Esto es lo que hacemos cuando actualizamos la posición de nuestros objetos sólo 60 veces por segundo. Por supuesto, en la imagen este efecto está muy exagerado.
Si trazamos una línea entre dos puntos consecutivos obtendremos la inclinación que queremos para nuestro proyectil cuando se halle sobre uno de esos puntos de forma bastante precisa. A mayor sea la resolución (más cerca estén esos puntos), mayor será la precisión.
Habiendo visto esto, el procedimiento es muy fácil. Sólo necesitamos conocer cómo ha cambiado nuestra posición durante el frame actual, o dicho de otro modo, en qué dirección nos estamos moviendo. Esto nos lo da el vector velocidad.
Teniendo un vector que indica nuestra dirección, es muy fácil extraer su ángulo utilizando razones trigonométricas. El ángulo del vector velocidad será el ángulo de rotación de nuestro proyectil.
El ángulo de un triángulo representado arriba se determina mediante la función arcotangente (abreviada $atan$), que a su vez es la inversa de la función tangente (abreviada $tan$ o $tg$).
La función tangente de un ángulo se define como la división de la longitud del cateto opuesto a ese ángulo entre la longitud del cateto contiguo.
$$tan(\alpha) = \frac{cateto\ opuesto}{cateto\ contiguo}$$
La arcotangente nos da la información contraria: a partir de la división del cateto opuesto y el cateto contiguo nos da el ángulo.
$$atan\bigg( \frac{cateto\ opuesto}{cateto\ contiguo} \bigg) = \alpha$$
La función arcotangente viene implementada en todos los lenguajes de programación de uso común. Sin embargo, tiene un defecto: sólo devuelve ángulos entre -90° y 90°. Esto es debido a que al hacer la división de cateto opuesto entre cateto contiguo estamos perdiendo información. Por ejemplo, basta con que uno de ellos sea negativo para que el resultado sea también negativo, y a partir del resultado no podemos determinar el signo de los operandos.
Para obtener un ángulo entre 0° y 360° (o lo que es lo mismo entre -180° y 180°) debemos tener en cuenta el signo de los catetos del triángulo. Por ejemplo, si el cateto horizontal es negativo y el vertical es positivo, será necesario sumar 90° al ángulo devuelto por la arcotangente. Además con la función de arcotangente que tener un cuidado especial, ya que si el cateto contiguo es cero se producirá una excepción de división por cero.
Debido a todos estos casos especiales, y siendo un problema muy habitual necesitar calcular un ángulo a partir de sus catetos, la mayoría de lenguajes de programación implementan una función atan2
(definición en Wikipedia) que recibe en parámetros separados la longitud del cateto opuesto y la longitud del cateto contiguo, encargándose la misma función de ajustar el valor de retorno en función del signo de los argumentos, así como de los valores límite.
Su uso es muy sencillo. $y$ es la longitud del cateto opuesto (habitualmente altura) y $x$ la del cateto contiguo (habitualmente anchura). Cualquiera de ellos negativos si están al otro lado del origen de coordenadas:
$$atan2(y, x) = \alpha$$
A continuación se muestra una función de actualización que actualiza la rotación del proyectil durante su trayectoria utilizando el método explicado:
function update() { // En este ejemplo asumimos que y=0 es el suelo y las // y's positivas están encima del suelo. // La gravedad modifica la velocidad del objeto, // siempre que no esté en el suelo. if (this.y > 0) { var gravity = 9.8; // píxels/frame² this.velY = this.velY - gravity; } // Movemos el objeto de acuerdo a su velocidad. this.posX = this.posX + this.velX; // Utilizamos la función máximo para que el objeto // no pueda caer a una posición subterránea. this.posY = Math.max(this.posY + this.velY, 0); // Actualizamos la rotación del objeto this.angle = Math.atan2(this.velY, this.velX);}
]]>En esta gráfica podemos ver una circunferencia de radio 1 (llamada circunferencia unitaria por esta razón) centrada en $(x = 0, y = 0)$.
Dado cualquier punto de la circunferencia unitaria, podemos trazar una línea desde el centro de la circunferencia hasta él. La inclinación de esta línea respecto a la línea base nos da un ángulo que llamaremos $\alpha$.
Utilizando el valor de este ángulo, podemos utilizar dos funciones trigonométricas nos dan la posición exacta de un punto:
Si tenemos una circunferencia cuyo radio no es 1, multiplicaremos las coordenadas obtenidas por el radio. Por ejemplo: en una circunferencia de radio 1, el punto con ángulo 30º es $(0.87, 0.5)$. En una circunferencia de radio 2, ese punto estará en $(1.74, 1)$.
Las coordenadas devueltas por las funciones seno y coseno asumen que nuestra circunferencia tiene centro en $(0, 0)$. Podemos sumar o restar valores en estas coordenadas para colocar la circunferencia donde queramos.
Hay dos unidades populares para medir ángulos:
Las medidas en grados son más fáciles de comunicar numéricamente a personas: es más fácil recordar que un cuarto de circunferencia son 90 grados, que recordar que son 1,5707… radianes. Por otro lado, muchas fórmulas matemáticas son más sencillas si los ángulos están expresados en radianes.
Distintos lenguajes de programación pueden utilizar diferentes unidades para los ángulos, e incluso dentro del mismo lenguaje puede haber funcionalidades que trabajen en una u otra unidad. Es importante saber con qué unidades trabaja una determinada función antes de utilizarla.
En cuanto a las funciones trigonométricas habituales (seno, coseno…), la mayoría de lenguajes utilizan radianes como unidad. Convertir ángulos de una unidad a otra es fácil:
$$grados\ a\ radianes(grados) = grados\cdot\frac{2\pi}{360}\\
radianes\ a\ grados(radianes) = radianes \cdot \frac{360}{2\pi}$$
El primer paso que tenemos que tomar es decidir cuántos objetos colocar (num_objetos
). Si dividimos 360º o $2\pi$ rad obtendremos un ángulo que nos indicará la separación entre cada objeto, separacion
.
Multiplicaremos separacion
por cada número entero entre 0 y num_objetos
(no incluido). En cada caso obtendremos el ángulo en el que debemos colocar un objeto. Utilizando las funciones seno y coseno podemos transformar este ángulo en coordenadas $x$ e $y$.
var tau = 2 * Math.PI;function drawWidgetsAroundCircle(ctx, centerX, centerY, radius, numWidgets){ var ang_interval = tau / numWidgets; for (var i = 0; i < numWidgets; i++) { var x = centerX + Math.cos(ang_interval) * radius; var y = centerY + Math.sin(ang_interval) * radius; drawWidget(ctx, x, y); }}