Rendering concentric hexes on Canvas

860 Views Asked by At

I've written a loop in JavaScript that will render rings of concentric hexagons around a central hexagon on the HTML canvas.

I start with the innermost ring, draw the hex at 3 o'clock, then continue around in a circle until all hexes are rendered. Then I move on to the next ring and repeat.

When you draw hexagons this way (instead of tiling them using solely x and y offsets) any hexagon that is not divisible by 60 is not the same distance to the center hex as those that are divisible by 60 (because these hexes comprise the flat edges, not the vertices, of the larger hex).

The problem I'm having is these hexes (those not divisible by 60 degrees) are rendering in a slightly off position. I'm not sure if it is a floating point math problem, the problem with my algorithm, the problem with my rusty trig, or just plain stupidity. I'm betting 3 out of 4. To cut to the chase, look at the line if (alpha % 60 !== 0) in the code below.

As a point of information, I decided to draw the grid this way because I needed an easy way to map the coordinates of each hex into a data structure, with each hex being identified by its ring # and ID# within that ring. If there is a better way to do it I'm all ears, however, I'd still like to know why my rendering is off.

https://www.dropbox.com/s/2moe9ysrrw865vh/Hexes.jpg?dl=0

Here is my very amateur code, so bear with me.

  <script type="text/javascript">
        window.addEventListener('load', eventWindowLoaded, false);
        function eventWindowLoaded() {
            canvasApp();
        }

    function canvasApp(){
        var xOrigin;
        var yOrigin;
        var scaleFactor = 30;

        var theCanvas = document.getElementById("canvas");
        var context;

        if (canvas.getContext) {
            context = theCanvas.getContext("2d");
            window.addEventListener('resize', resizeCanvas, false);
            window.addEventListener('orientationchange', resizeCanvas, false);
            resizeCanvas();
        }
        drawScreen();

        function resizeCanvas() {
            var imgData = context.getImageData(0,0, theCanvas.width, theCanvas.height);
            theCanvas.width = window.innerWidth;
            theCanvas.height = window.innerHeight;
            context.putImageData(imgData,0,0);
            xOrigin = theCanvas.width / 2;
            yOrigin = theCanvas.height / 2;
        }

        function drawScreen() {
            var rings = 3;
            var alpha = 0;
            var modifier = 1;

            context.clearRect(0, 0, theCanvas.width, theCanvas.height);
            drawHex(0,0);

            for (var i = 1; i<=rings; i++) {
                for (var j = 1; j<=i*6; j++) {
                     if (alpha % 60 !== 0) {
                         var h = modifier * scaleFactor / Math.cos(dtr(360 / (6 * i)));
                         drawHex(h * (Math.cos(dtr(alpha))), h * Math.sin(dtr(alpha)));
                    }
                    else {
                        drawHex(2 * scaleFactor * i * Math.cos(dtr(alpha)), 2 * scaleFactor * i * Math.sin(dtr(alpha)));
                    }
                    alpha += 360 / (i*6);
                }
                modifier+=2;
            }
        }

        function drawHex(xOff, yOff) {
            context.fillStyle = '#aaaaaa';
            context.strokeStyle = 'black';
            context.lineWidth = 2;
            context.lineCap = 'square';
            context.beginPath();
            context.moveTo(xOrigin+xOff-scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
            context.lineTo(xOrigin+xOff,yOrigin+yOff-scaleFactor/Math.cos(dtr(30)));
            context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
            context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
            context.lineTo(xOrigin+xOff,yOrigin+yOff+scaleFactor/Math.cos(dtr(30)));
            context.lineTo(xOrigin+xOff-scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
            context.closePath();
            context.stroke();
        }

        function dtr(ang) {
            return ang * Math.PI / 180;
        }

        function rtd(ang) {
            return ang * 180 / Math.PI;
        }
    }
    </script>
2

There are 2 best solutions below

2
On BEST ANSWER

Man it took me longer than I'd like to admit to find the pattern for the hexagonal circles. I'm too tired right now to explain since I think I'll need to make some assisting illustrations in order to explain it.

In short, each "circle" of hexagonal shapes is itself hexagonal. The number of hexagonal shapes along one edge is the same as the number of the steps from the center.

var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
c.width = 500;
c.height = 500;


var hexRadius = 20;
var innerCircleRadius = hexRadius/2*Math.sqrt(3);
var TO_RADIANS = Math.PI/180;

function drawHex(x,y) {
    var r = hexRadius;
    ctx.beginPath();
    ctx.moveTo(x,y-r);
    for (var i = 0; i<=6; i++) {
         ctx.lineTo(x+Math.cos((i*60-90)*TO_RADIANS)*r,y+Math.sin((i*60-90)*TO_RADIANS)*r);
    }
    ctx.closePath();
    ctx.stroke();
}

drawHexCircle(250,250,4);

function drawHexCircle(x,y,circles) {
    var rc = innerCircleRadius;
    drawHex(250,250); //center
    
    for (var i = 1; i<=circles; i++) {
        for (var j = 0; j<6; j++) {
            var currentX = x+Math.cos((j*60)*TO_RADIANS)*rc*2*i;
            var currentY = y+Math.sin((j*60)*TO_RADIANS)*rc*2*i;
                drawHex(currentX,currentY);
            for (var k = 1; k<i; k++) {
                var newX = currentX + Math.cos((j*60+120)*TO_RADIANS)*rc*2*k;
                var newY = currentY + Math.sin((j*60+120)*TO_RADIANS)*rc*2*k;
                drawHex(newX,newY);
            }
        }
    }
    
}
canvas {
    border: 1px solid black;
}
<canvas id="canvas"></canvas>

2
On

I think you're trying to use radial coordinates for something that isn't a circle. As you noted correctly, the (centers of) the vertex hexagons are indeed laid out in a circle and you can use basic radial positioning to lay them out. However, the non-vertex ones are not laid out on an arc of that circle, but on a chord of it (the line connecting two vertex hexagons). So your algorithm, which tries to use a constant h (radius) value for these hexagons, will not lay them out correctly.

You can try interpolating the non-vertex hexagons from the vertex hexagons: the position of of the Kth (out of N) non-vertex hexagon H between vertex hexagons VH1 and VH2 is:

Pos(H) = Pos(VH1) + (K / (N + 1)) * (Pos(VH2)-Pos(VH1))

e.g. in a ring with 4 hexagons per edge (i.e. 2 non-vertex hexagons), look at the line of hexagons between the 3 o'clock and the 5 o'clock: the 3 o'clock is at 0% along that line, the one after that is at 1/3 of the way, the next is at 2/3 of the way, and the 5 o'clock is at 100% of the way. Alternatively you can think of each hexagon along that line as "advancing" by a predetermined vector in the direction between the two vertices until you reach the end of the line.

So basically your algorithm could go through the 6 primary vertex hexagons, each time interpolating the hexagons from the current vertex hexagon to the next. Thus you should probably have three nested loops: one for rings, one for angles on a ring (always six steps), and one for interpolating hexagons along a given angle (number of steps according to ring number).