Skip to content

Instantly share code, notes, and snippets.

@bbengfort
Created September 26, 2024 18:02
Show Gist options
  • Save bbengfort/19ce3cd70c1fc7181866cdcbc4e4590b to your computer and use it in GitHub Desktop.
Save bbengfort/19ce3cd70c1fc7181866cdcbc4e4590b to your computer and use it in GitHub Desktop.
A utility to draw Rotational colored hexagonal tiles. Adapted from https://variable-scope.com/posts/hexagon-tilings-with-python
#!/usr/bin/env python
import math
import random
import argparse
from PIL import Image
from aggdraw import Draw, Brush
COLORS = {
# dark to light
"space_cadet": (25, 46, 91),
"lapis_lazuli": (29, 101, 166),
"air_superiority": (114, 162, 192),
"sky_blue": (128, 218, 236),
"water": (205, 247, 247),
# dark to light
"dark_slate": (47, 72, 88),
"philippine_gray": (140, 140, 140),
"metallic_silver": (161, 169, 180),
"platinum": (227, 227, 225),
"ghost_white": (248, 248, 255),
# just white
"white": (255, 255, 255),
}
ALL_COLORS = list(COLORS.values())
class HexagonGenerator(object):
"""
Generates hexagon paths for the specified size
"""
def __init__(self, edge_length):
self.edge = edge_length
@property
def pattern_size(self):
return self.width, self.height*2
@property
def width(self):
return self.edge * 3
@property
def height(self):
return math.sin(math.pi / 3) * self.edge
def rows(self, height):
"""
Returns the number of rows required to fill the canvas height.
"""
return int(math.ceil(height/self.height))
def __call__(self, row, col):
x = (col + 0.5 * (row % 2)) * self.width
y = row * self.height
for angle in range(0, 360, 60):
x += math.cos(math.radians(angle)) * self.edge
y += math.sin(math.radians(angle)) * self.edge
yield x
yield y
def create_canvas(pattern_size, repetitions):
"""
Returns an Image that fits the given number of pattern repetitions.
"""
width = int(repetitions*pattern_size[0])
height = int(round(width / pattern_size[1]) * pattern_size[1])
return Image.new('RGB', (width, height), 'white')
def tiled_canvas(width, height):
if width is None and height is None:
return None
if width is None:
width = height
if height is None:
height = width
return Image.new('RGB', (width, height), 'white')
def random_color():
"""
Returns a random RGB color from the color options.
"""
return random.choice(ALL_COLORS)
def main(args):
# Generate hexagon tiles
hexagon_generator = HexagonGenerator(args.size)
tiles = create_canvas(hexagon_generator.pattern_size, args.repetitions)
draw = Draw(tiles)
for row in range(hexagon_generator.rows(tiles.size[1])):
colors = [random_color() for _ in range(args.repetitions)]
for col in range(args.repetitions+1):
color = colors[col % args.repetitions]
draw.polygon(list(hexagon_generator(row, col)), Brush(color))
for col, color in enumerate(colors):
draw.polygon(list(hexagon_generator(-1, col)), Brush(color))
draw.flush()
# Perform tiling, if necessary
image = tiled_canvas(args.width, args.height)
if image is not None:
tw, th = tiles.size
iw, ih = image.size
for i in range(0, iw, tw):
for j in range(0, ih, th):
image.paste(tiles, (i, j))
else:
image = tiles
# Show the results to the user
if args.out:
image.save(args.out)
print(f"image saved to {args.out}")
else:
image.show()
if __name__ == "__main__":
opts = {
('-s', '--size'): {
'type': int, 'default': 40, 'metavar': 'PX',
'help': 'size in pixels of the hexagon edge length',
},
('-r', '--repetitions'): {
'type': int, 'default': 2, 'metavar': 'N',
'help': 'specify the number of repetitions or rows of the hexagons', # noqa
},
('-o', '--out'): {
'default': None, 'metavar': 'PATH',
'help': 'write the image out to disk instead of showing it',
},
('-W', '--width'): {
'default': None, 'type': int, 'metavar': 'PX',
'help': 'the width of the final canvas to export',
},
('-H', '--height'): {
'default': None, 'type': int, 'metavar': 'PX',
'help': 'the height of the final canvas to export',
},
}
parser = argparse.ArgumentParser(
description="generate rotational colored hexagonal tiles",
epilog="from the blog post https://variable-scope.com/posts/hexagon-tilings-with-python" # noqa
)
for pargs, kwargs in opts.items():
if isinstance(pargs, str):
pargs = (pargs,)
parser.add_argument(*pargs, **kwargs)
args = parser.parse_args()
main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment