Drawing Hexmaps

The other day, Thomas Guest talked about drawing chessboards, and ended with a challenge. I wanted to answer a different challenge, however. What if, instead of drawing on a rectangular grid, we wanted to draw on a hexagonal grid? The following is my slapdash answer. For real-world use I'd make nice classes and pass more parameters to the methods, but to demonstrate the math I'm just going to use global constants and functions.

Like Thomas' article, I will show solutions using several different tools, in this case Apple's Core Graphics, PyGame, Python Imaging Library (PIL), and SVG. All of these solutions will use the same constants and math:

# Constants used by each solution
from math import sin, cos, pi, sqrt
THETA = pi / 3.0 # Angle from one point to the next
HEXES_HIGH = 8 # How many rows of hexes
HEXES_WIDE = 5 # How many hexes in a row
RADIUS = 30 # Size of a hex
HALF_RADIUS = RADIUS / 2.0
HALF_HEX_HEIGHT = sqrt(RADIUS ** 2 - HALF_RADIUS ** 2)
IMAGE_WIDTH = int(RADIUS * (HEXES_WIDE * 3 + .5))
IMAGE_HEIGHT = int(HALF_HEX_HEIGHT * (HEXES_HIGH + 1))
# Functions (generators) used by each solution
def hex_points(x,y):
    '''Given x and y of the origin, return the six points around the origin of RADIUS distance'''
    for i in range(6):
        yield cos(THETA * i) * RADIUS + x, sin(THETA * i) * RADIUS + y
def hex_centres():
    for x in range(HEXES_WIDE):
        for y in range(HEXES_HIGH):
            yield (x * 3 + 1) * RADIUS + RADIUS * 1.5 * (y % 2), (y + 1) * HALF_HEX_HEIGHT

Now, given the above, what does the code look like to draw the hexes? Because each library handles colours slightly differently, we will need a generator for colours (and we will need more than just black and white as the chessboard used, because each hex borders on six others). I haven't given a lot of thought to optimal colouring schemes: each colour generator simply produces red, yellow, blue, and green in a cycle. Here is the image produced by the Core Graphics solution, followed by the code:

Quartz hexes

def quartz_colours():
    while True:
        yield 1,0,0,1 # red
        yield 1,1,0,1 # yellow
        yield 0,0,1,1 # blue
        yield 0,1,0,1 # green
def quartz_hex():
    '''Requires a Mac with OS 10.4 or better and the Developer Tools installed'''
    import CoreGraphics as cg
    colours = quartz_colours()
    cs = cg.CGColorSpaceCreateDeviceRGB()
    c = cg.CGBitmapContextCreateWithColor(IMAGE_WIDTH, IMAGE_HEIGHT, cs, (0,0,0,.2))
    c.saveGState()
    c.setRGBStrokeColor(0,0,0,0)
    c.setLineWidth(0)
    for x,y in hex_centres():
        c.beginPath()
        c.setRGBFillColor(*colours.next())
        points = list(hex_points(x,y))
        c.moveToPoint(*points[-1])
        [c.addLineToPoint(*pt) for pt in points]
        c.drawPath(cg.kCGPathFill)
    c.restoreGState()
    c.writeToFile("quartz_hexes.png", cg.kCGImageFormatPNG)

Now for some cross-platform examples. Here is the image generated by PyGame, followed by that code:

PyGame hexes

def pygame_colours():
    while True:
        yield 255, 0, 0 # red
        yield 255, 255, 0 # yellow
        yield 0, 0, 255 # blue
        yield 0, 255, 0 # green
def pygame_hex():
    '''Requires PyGame 1.8 or better to save as PNG'''
    import pygame
    pygame.init()
    screen = pygame.display.set_mode((IMAGE_WIDTH, IMAGE_HEIGHT))
    colours = pygame_colours()
    for x,y in hex_centres():
        pygame.draw.polygon(screen, colours.next(), list(hex_points(x,y)))
    pygame.image.save(screen, 'pygame_hexes.png')

When you run the PyGame script, it will actually pop up a window very briefly, draw into the window, save the result, and close the window. I also didn't get the PyGame script to add transparency for the background, although I think it could be added fairly easily. Now, for the web, here is a solution in SVG, with the image captured by screenshot in Safari, followed by the Python code, and the resulting SVG code:

SVG hexes

def svg_colours():
    while True:
        yield 'rgb(255, 0, 0)'
        yield 'rgb(255, 255, 0)'
        yield 'rgb(0, 0, 255)'
        yield 'rgb(0, 255, 0)'
def svg_hex():
    out = open('svg_hexes.svg', 'w')
    print >> out, '''<?xml version="1.0" standalone="no"?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    <svg width="%spx" height="%spx" version="1.1" xmlns="http://www.w3.org/2000/svg">''' % (IMAGE_WIDTH, IMAGE_HEIGHT)
    colours = svg_colours()
    for pt in hex_centres():
        print >> out, '<polygon fill="%s" stroke-width="0" points="%s" />' % (colours.next(),  ' '.join(["%s,%s" % (x,y) for (x,y) in hex_points(*pt)]))
    print >> out, '</svg>'
    out.close()

And here is the SVG created by the above script: SVG Hexes

Finally, one library which overlaps with the ones used by the Chessboard example: Python Imaging Library.

PIL hexes

pil_colours = pygame_colours  # same format works, so we'll re-use it
def pil_hex():
    import Image, ImageDraw
    image = Image.new("RGBA", (IMAGE_WIDTH,IMAGE_HEIGHT), (0,0,0,0))
    colours = pil_colours()
    draw = ImageDraw.Draw(image)
    for x,y in hex_centres():
        draw.polygon(list(hex_points(x,y)), fill=colours.next())
    image.save('pil_hexes.png', 'PNG')

That's it for my examples. Thomas ended with a challenge for displaying chess, and for describing the position. To describe the position, I would use a standard chess notation, such as described here. For my challenge, what other formats would be useful to create hex maps in? POVRay? Flash? Any other examples out there?


Tags:

[] Posted on 2008-03-20 by Dethe Elza

Previous: The 419 Economy Next: MetaProgramming at VanPyZ