Generar una imagen SVG con forma de celdas hexagonales en JavaScript

Cómo crear una imagen en JavaScript añadiendo celdas hexagonales siguiendo la guía de un espiral.

algoritmo-panal-hexagonal-spiral-svg-javascript

Hace ya un tiempo, en mi trabajo tenía entre manos un proyecto en el que debía mostrar de forma visual pods de varios clústeres de Kubernetes. Buscando ideas me topé con una aplicación de observabilidad de New Relic. Lo que llamó mi atención fue cómo representaban los pods usando hexágonos. Esa imagen me hizo pensar en la posibilidad de generar un algoritmo en forma de espiral que dibujara un panal (¡como el de las abejas!) en SVG. Así nació la idea de crear este ejemplo, un pequeño experimento que une programación, gráficos, matemáticas y un poco de lógica.

¿Qué vamos a crear?

Aquí te dejo el resultado de la visualización de la imagen SVG. En la pestaña de código puedes ver todo el algoritmo en JavaScript. Además al final del código puedes ver una función donde se define el número de celdas hexagonales que se dibujarán:

Funciones básicas del código

A continuación explicamos los conceptos clave de la función.

  • Coordenadas axiales y la base del hexágono
    Usamos coordenadas axiales para trabajar de forma natural con rejillas hexagonales. Esto facilita la determinación de vecinos y distancias. Por ejemplo, definimos las direcciones para hexágonos con punta hacia arriba de la siguiente manera:

    // Índice: 0: Este, 1: Noreste, 2: Noroeste, 3: Oeste, 4: Suroeste, 5: Sureste.
    const directions = [
    { q: 1, r: 0 },
    { q: 1, r: -1 },
    { q: 0, r: -1 },
    { q: -1, r: 0 },
    { q: -1, r: 1 },
    { q: 0, r: 1 }
    ];
  • Generación en espiral y rotación de anillos
    El algoritmo comienza en el centro y añade celdas en anillos concéntricos. Cada anillo se recorre y rota para que la primera celda esté lo más cerca posible de una posición ideal (en este caso, hacia el suroeste), manteniendo la continuidad del patrón. Aquí un extracto del bucle principal:

    while (cells.length < N) {
    let ringCells = [];
    let start = axialAdd({ q: 0, r: 0 }, axialScale(directions[4], radius));
    let hex = start;
    for (let side = 0; side < 6; side++) {
    for (let step = 0; step < radius; step++) {
    ringCells.push(hex);
    hex = axialNeighbor(hex, side);
    }
    }
    // Aquí se rota el anillo para alinear la celda de inicio
    for (let cell of ringCells) {
    if (cells.length < N) {
    cells.push(cell);
    } else {
    break;
    }
    }
    radius++;
    }
  • Funciones auxiliares para operaciones geométricas
    Para mantener el código limpio y modular, se utilizan funciones auxiliares que realizan operaciones como sumar coordenadas o convertir de coordenadas axiales a píxeles. Por ejemplo, la función que encuentra el vecino de un hexágono es la siguiente:

    function axialNeighbor(hex, directionIndex) {
    const dir = directions[directionIndex];
    return { q: hex.q + dir.q, r: hex.r + dir.r };
    }

Del código al lienzo: dibujando el panal en SVG

El siguiente paso es traducir la lista de celdas generadas en un dibujo SVG. Cada celda se representa como un polígono con 6 vértices, calculados usando funciones trigonométricas. Además, se coloca un número en cada hexágono, siguiendo el orden en que se generaron. Esto ayuda a visualizar claramente la espiral y a comprobar que el algoritmo está funcionando como se esperaba.

El código se encarga también de calcular la caja de contorno de todos los hexágonos, ajustando dinámicamente el viewBox del SVG para que se adapte perfectamente a la figura. Es un proceso que, aunque técnico, resulta muy intuitivo una vez que lo ves en acción.

// --- Crear el SVG y los elementos hexagonales ---
const svgns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgns, "svg");
// Variables para calcular la caja de contorno de todos los hexágonos.
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// Para cada celda, calcular su centro, dibujar el hexágono y añadir el número.
cells.forEach((cell, index) => {
const center = axialToPixel(cell, hexSize);
let points = [];
// Calcular los 6 vértices del hexágono (punta arriba, iniciando en -30°).
for (let i = 0; i < 6; i++) {
const angleDeg = 60 * i - 30;
const angleRad = (Math.PI / 180) * angleDeg;
const x = center.x + hexSize * Math.cos(angleRad);
const y = center.y + hexSize * Math.sin(angleRad);
points.push(`${x},${y}`);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
// Crear el elemento <polygon> para el hexágono.
const polygon = document.createElementNS(svgns, "polygon");
polygon.setAttribute("points", points.join(" "));
polygon.setAttribute("fill", "#fff"); // Color uniforme sin variaciones
polygon.setAttribute("stroke", "#000");
polygon.setAttribute("stroke-width", "1");
svg.appendChild(polygon);
// Crear el elemento <text> para mostrar el número (orden espiral).
const text = document.createElementNS(svgns, "text");
text.setAttribute("x", center.x);
text.setAttribute("y", center.y);
text.setAttribute("text-anchor", "middle");
text.setAttribute("dominant-baseline", "central");
text.setAttribute("font-size", "12");
text.setAttribute("fill", "#000");
text.textContent = index + 1; // Numeración iniciando en 1
svg.appendChild(text);
});
// --- Configurar viewBox y dimensiones del SVG ---
const margin = hexSize;
minX -= margin;
minY -= margin;
maxX += margin;
maxY += margin;
const width = maxX - minX;
const height = maxY - minY;
svg.setAttribute("viewBox", `${minX} ${minY} ${width} ${height}`);
svg.setAttribute("width", width);
svg.setAttribute("height", height);

Y la conversión de coordenadas axiales a píxeles para posicionar los hexágonos en el SVG se realiza con:

function axialToPixel(hex, size) {
const x = size * Math.sqrt(3) * (hex.q + hex.r / 2);
const y = size * 1.5 * hex.r;
return { x, y };
}

Conclusión

En resumen, este código es un ejemplo práctico de cómo se pueden combinar conceptos geométricos y gráficos en la web para crear visualizaciones interesantes. Espero que te sirva de inspiración para tus propios proyectos y que encuentres útiles las técnicas presentadas aquí.