Colocar objetos siguiendo formas geométricas, parte 2: polígonos y estrellas

Hace tiempo vimos como colocar objetos en un videojuego siguiendo la forma de una circunferencia. En este artículo vamos a continuar con la serie, explicando cómo colocar objetos para dibujar segmentos (líneas), polígonos y estrellas.

Colocar objetos en un segmento

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.

Un segmento dividido en cuatro 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
    });
  }
}

Polígonos regulares

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 pentágono se construye dividiendo una circunferencia en cinco arcos iguales y uniendo los puntos de corte

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
    });
  }
}
Demostración en vivo
Demostración en vivo

Hacer los polígonos animables

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.

Estrellas

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:

Una estrella de cinco puntas dibujada a partir de un polígono

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;
  }
}

Estrellas no conectadas

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.

Estrella de seis puntas

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;
    }
  }
}

Demostración

La siguiente demo muestra la construcción de una estrella haciendo uso de los algoritmos explicados.

Demostración en vivo
Demostración en vivo

Animación

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:

Demostración en vivo
Demostración en vivo