Penrose tiling with faked rendering
Posted: July 24th, 2022, 5:15 pm
Long story, but I wanted to put a Penrose tiling on a homemade greeting card I was making on a Cricut, and spent so much time writing a Python script to do it (stuck for a couple hours before I realized I was multiplying matrices in reverse order) that I thought I would try to get more mileage out the script. Note that there are many web pages out there that will make Penrose tilings for you, but I didn't like any of them enough to use.
I have mostly avoided Penrose tilings (because Roger Penrose is a big meanie who hates AI -- yes I am very a mature) but they are pretty cool and rather nice looking (and I don't think he can sue you for using them anymore). So here's my Python script after a lot of tweaking. It uses numpy for the matrices, the deflation rules from the Wikipedia page, and outputs SVG. I intentionally convert the matrices back into rotations and translations for readability in the resulting SVG. I had an earlier version that just output the matrices as SVG transforms. You can run it with a command like python "penrose.py kite 5 > penrose.svg" and it will produce (in this case) a tiling that starts with 5 kites arranged in a decagon. You can also start with 5 darts. (Did I mention it's a kite and dart tiling?) That will give you flat output like this: This is fine, but I kept wishing I could snazzy it up with some lighting effects. I am sure I could do that if I imported it into something like Unity 3D. It suddenly hit me though, because this groups the kites and darts, I can use inkscape and shift offset kites from darts a little. Then with strategic color choices, I get something like a diffuse lighting effect. This wasn't too much extra work and I like it. This is so simple I guess I could add it to the Python script. D'oh! I may do that, but the one above just makes flat output.
I have mostly avoided Penrose tilings (because Roger Penrose is a big meanie who hates AI -- yes I am very a mature) but they are pretty cool and rather nice looking (and I don't think he can sue you for using them anymore). So here's my Python script after a lot of tweaking. It uses numpy for the matrices, the deflation rules from the Wikipedia page, and outputs SVG. I intentionally convert the matrices back into rotations and translations for readability in the resulting SVG. I had an earlier version that just output the matrices as SVG transforms.
Code: Select all
import sys
import math
import numpy
ROTATION = 36 * 2 * math.pi / 360
RATIO = 2/ (math.sqrt(5) + 1)
FLIP_Y = numpy.matrix([[1, 0, 0], [0, -1, 0], [0, 0, 1]])
ROTATE = numpy.matrix([
[math.cos(ROTATION), -math.sin(ROTATION), 0],
[math.sin(ROTATION), math.cos(ROTATION), 0],
[0, 0, 1]])
SCALE = numpy.matrix([[RATIO, 0, 0], [0, RATIO, 0], [0, 0, 1]])
def translate(x, y):
return numpy.matrix([[1, 0, x], [0, 1, y], [0, 0, 1]])
def to_components(transformation):
xmult = transformation[0, 0]
ymult = transformation[1, 0]
theta = int(round(math.atan2(ymult, xmult) * 180 / math.pi))
scale = math.sqrt(xmult * xmult + ymult * ymult)
return(transformation[0, 2], transformation[1, 2], theta, scale)
HALF_KITE_TO_HALF_KITE_1 = translate(1, 0) * ROTATE ** 7 * SCALE
HALF_KITE_TO_HALF_KITE_2 = translate(1, 0) * ROTATE ** 5 * FLIP_Y * SCALE
HALF_KITE_TO_HALF_DART = ROTATE ** 9 * FLIP_Y * SCALE
HALF_DART_TO_HALF_KITE = SCALE
HALF_DART_TO_HALF_DART = translate(1, 0) * ROTATE ** 6 * SCALE
SVG_HEAD = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="420mm"
height="297mm"
viewBox="0 0 420 297"
version="1.1"
id="svg8">
<g id="layer1" transform="scale(250)">'''
SVG_TAIL = '''</g></svg>'''
start_tile = sys.argv[1]
depth = int(sys.argv[2])
# set up starting tiles
rotation = numpy.identity(3)
tiles = []
for i in range(5):
tiles.append((start_tile, rotation, '%d+_' % i))
tiles.append((start_tile, FLIP_Y * rotation, '%d-_' % i))
rotation = rotation * ROTATE * ROTATE
# apply deflation rules
for rep in range(depth):
new_tiles = []
for kind, transformation, rules in tiles:
if kind == 'kite':
new_tiles.append(('kite', transformation * HALF_KITE_TO_HALF_KITE_1, rules + 'k1'))
new_tiles.append(('kite', transformation * HALF_KITE_TO_HALF_KITE_2, rules + 'k2'))
new_tiles.append(('dart', transformation * HALF_KITE_TO_HALF_DART, rules + 'k3'))
else:
new_tiles.append(('kite', transformation * HALF_DART_TO_HALF_KITE, rules + 'd1'))
new_tiles.append(('dart', transformation * HALF_DART_TO_HALF_DART, rules + 'd2'))
tiles = new_tiles
# eliminate flipped tile halves so we draw only whole non-flipped tiles
not_flipped = [tile for tile in tiles if numpy.linalg.det(tile[1]) > 0]
kites = [tile for tile in not_flipped if tile[0] == 'kite']
darts = [tile for tile in not_flipped if tile[0] == 'dart']
print(SVG_HEAD)
print(' <g style="fill:#404040;fill-opacity:1;stroke:#909090;stroke-width:0.05;stroke-linejoin:miter;stroke-opacity:1">')
for name, mat, rules in kites:
print(''' <path transform="translate(%f,%f) rotate(%d) scale(%f)"
d="M 0,0 1,0 0.81,-0.59 0.31,-0.95 z"
id="kite_%s"/>''' % (to_components(mat) + (rules,)))
print(' </g>')
print(' <g style="fill:#ffffff;fill-opacity:1;stroke:#909090;stroke-width:0.05;stroke-linejoin:miter;stroke-opacity:1">')
for name, mat, rules in darts:
print(''' <path transform="translate(%f,%f) rotate(%d) scale(%f)"
d="M 0,0 1,0 0.5,-0.36 0.31,-0.95 z"
id="dart_%s"/>''' % (to_components(mat) + (rules,)))
print(' </g>')
print(SVG_TAIL)