Penrose tiling with faked rendering

A forum where anything goes. Introduce yourselves to other members of the forums, discuss how your name evolves when written out in the Game of Life, or just tell us how you found it. This is the forum for "non-academic" content.
Post Reply
User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Penrose tiling with faked rendering

Post by pcallahan » 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.

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)
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:
layer1.png
layer1.png (881.87 KiB) Viewed 2242 times
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.
rect2284.png
rect2284.png (722.34 KiB) Viewed 2242 times
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.

User avatar
otismo
Posts: 1201
Joined: August 18th, 2010, 1:41 pm
Location: Florida
Contact:

Re: Penrose tiling with faked rendering

Post by otismo » July 24th, 2022, 8:45 pm

open in MS Paint and click to fill with color
open in MS Paint and click to fill with color
rect2284-colorised.png (798.91 KiB) Viewed 2226 times
@ pcallahan

Make a PDF Coloring Book !

Thank You !
"One picture is worth 1000 words; but one thousand words, carefully crafted, can paint an infinite number of pictures."
- autonomic writing
forFUN : http://viropet.com
Art Gallery : http://cgolart.com
Video WebSite : http://conway.life

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » July 24th, 2022, 9:12 pm

otismo wrote:
July 24th, 2022, 8:45 pm
open in MS Paint and click to fill with color
For a second, I thought you had modified the script. That wouldn't be too hard, but you'd have to identify the different tiles, maybe by distance from the center (in fact, they have id strings based on how they were generated, like: dart_4-_k2k1k2k1k3). You'd have to shift the tiles in the script, but that's even easier than I originally thought because the shift can be applied to the whole kite group or dart group. Background color also needs to be set or you can put something in back of them to show up a color in the gaps.

Added note: I decided to update the script after all to make the fake 3D effect without going into Inkscape. Instead of just having one background color to fill in gaps, I repeat the darts in the original position in a darker color and paint over them. Here's the Python:

Code: Select all

import sys
import math
import numpy

ROTATION = 36 *  math.pi / 180
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]])

# To give tiling a layered appearance (distance is relative to side length)
SHIFT_DISTANCE = 0.12
SHIFT_ANGLE = 43 * math.pi / 180

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)

# darts in same position as kites for shadows; no stroke 
print('    <g style="fill:#384657;fill-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_bg_%s"/>''' % (to_components(mat) + (rules,)))
print('    </g>')

# darts in shifted positions
shift = SHIFT_DISTANCE * RATIO ** depth
print('''    <g style="fill:#596e8a;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        transform="translate(%f,%f)">''' % (shift * math.cos(SHIFT_ANGLE), shift * math.sin(SHIFT_ANGLE)))
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>')

# kites
print('    <g style="fill:#87a9d4;fill-opacity:1;stroke:#909090;stroke-width:0.02;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(SVG_TAIL)
And here is an example taking the kite decagon to depth 6.
layer1.png
layer1.png (1.34 MiB) Viewed 2209 times

hotdogPi
Posts: 1587
Joined: August 12th, 2020, 8:22 pm

Re: Penrose tiling with faked rendering

Post by hotdogPi » July 25th, 2022, 6:41 am

I've played HyperRogue enough that I expected the tiles on the edge to be tiny (and to have many more of them).
User:HotdogPi/My discoveries

Periods discovered: 5-16,⑱,⑳G,㉑G,㉒㉔㉕,㉗-㉛,㉜SG,㉞㉟㊱㊳㊵㊷㊹㊺㊽㊿,54G,55G,56,57G,60,62-66,68,70,73,74S,75,76S,80,84,88,90,96
100,02S,06,08,10,12,14G,16,17G,20,26G,28,38,47,48,54,56,72,74,80,92,96S
217,486,576

S: SKOP
G: gun

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » July 25th, 2022, 10:14 am

hotdogPi wrote:
July 25th, 2022, 6:41 am
I've played HyperRogue enough that I expected the tiles on the edge to be tiny (and to have many more of them).
I imagine it wouldn't be hard to lay it out as a hyperbolic geometry by adjusting the scale based on distance from center. The result of the deflation process is just a set of linear coordinate transformations. You would have to adjust the shape of the polygons too.

Or maybe it would work better to apply the deflation transformations to all the vertices first and then apply a rule to those for hyperbolic geometry.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » July 31st, 2022, 1:29 pm

Aside: I wonder if anyone has tried to make an exhaustive list of Penrose tiles in architecture. I knew the San Francisco transit center has a Penrose tile design. Now I see Pomona College (southern California) has a rhomb tiling in their physics/mathematics/astronomy building: https://searockstaffordcm.com/pomona-college/

I updated my code to assign more colors to tiles using the last three deflation rules applied. It picks a color randomly from a palette (hardcoded for kites and darts but you can easily change it) the first time it sees the rules and reuses it for others.

Code: Select all

import sys
import math
import numpy
import random

ROTATION = 36 *  math.pi / 180
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]])

# To give tiling a layered appearance (distance is relative to side length)
SHIFT_DISTANCE = 0.12
SHIFT_ANGLE = 43 * math.pi / 180

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)

# darts in same position as kites for shadows; no stroke 
print('    <g style="fill:#384657;fill-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_bg_%s"/>''' % (to_components(mat) + (rules,)))
print('    </g>')

dart_colors = ['4b5d75', '526680', '596e8a', '607795', '69809e']
kite_colors = ['ff7f77', 'f7d0ae', 'e4e2af', 'd2d2d2', 'eeceb8', 'ade2e5', 'ebe0a9', '44feff', 'dfd0c5', 'fa85f5']

palette = {}

# darts in shifted positions
shift = SHIFT_DISTANCE * RATIO ** depth
print('''    <g transform="translate(%f,%f)">''' % (shift * math.cos(SHIFT_ANGLE), shift * math.sin(SHIFT_ANGLE)))
for name, mat, rules in darts:
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.5,-0.36 0.31,-0.95 z"
        id="dart_%s"/>''' % (to_components(mat) + (palette.setdefault(rules[-6:], random.choice(dart_colors)), rules,)))
print('    </g>')

# kites
print('    <g>')
for name, mat, rules in kites:
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.81,-0.59 0.31,-0.95 z"
        id="kite_%s"/>''' % (to_components(mat) + (palette.setdefault(rules[-6:], random.choice(kite_colors)), rules,)))
print('    </g>')

print(SVG_TAIL)
Here's an example.
layer1a.png
layer1a.png (1.11 MiB) Viewed 2099 times
With a less garish palette:

Code: Select all

dart_colors = ['526680', '596e8a', '607795', '69809e']
kite_colors = ['84bbcf', '8dc0d3', '97c5d7', 'a0cbda', 'a9d0de', 'b3d5e1', 'bcdae5', 'c6dfe9', 'cfe5ec', 'd9eaf0']
layer1.png
layer1.png (1.08 MiB) Viewed 2098 times
Update: This is more visually interesting in my opinion. I can assign the colors based on rotation angle (I knew there was some reason to pull those out of the transformation) and use it to create a very primitive diffuse lighting effect as if the tilings are tilted towards their shared vertices. Updated code and example. (Note: I first posted this with the wrong color palette and I have now fixed this.)

Code: Select all

import sys
import math
import numpy
import random

ROTATION = 36 *  math.pi / 180
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]])

# To give tiling a layered appearance (distance is relative to side length)
SHIFT_DISTANCE = 0.12
SHIFT_ANGLE = 43 * math.pi / 180

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)

# darts in same position as kites for shadows; no stroke 
print('    <g style="fill:#384657;fill-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_bg_%s"/>''' % (to_components(mat) + (rules,)))
print('    </g>')

dart_colors = ['7693b8', '6d87a9', '647c9b', '5b718e', '536680', '4f627a', '576c88', '607795', '6982a3', '718db0']
kite_colors = ['a9d0de', 'afd8e7', 'b7e2f1', 'bfebfb', 'c6f5ff', 'cbfaff', 'c3f0ff', 'bbe7f6', 'b4deec', 'acd4e2']

palette = {}

# darts in shifted positions
shift = SHIFT_DISTANCE * RATIO ** depth
print('''    <g transform="translate(%f,%f)">''' % (shift * math.cos(SHIFT_ANGLE), shift * math.sin(SHIFT_ANGLE)))
for name, mat, rules in darts:
      components = to_components(mat)
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.5,-0.36 0.31,-0.95 z"
        id="dart_%s"/>''' % (components + (dart_colors[(components[2] // 36) % 10], rules,)))
print('    </g>')

# kites
print('    <g>')
for name, mat, rules in kites:
      components = to_components(mat)
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.81,-0.59 0.31,-0.95 z"
        id="kite_%s"/>''' % (components + (kite_colors[(components[2] // 36) % 10], rules,)))
print('    </g>')

print(SVG_TAIL)
The idea is to suggest that the kites form peaks and the darts form valleys.
layer1.png
layer1.png (1.11 MiB) Viewed 2094 times
One more note: you can eliminate the shift in dart tiles (SHIFT_DISTANCE = 0) and there is still a 3D effect. I think it's a little cleaner this way, but I have hit my attachment limit.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » August 7th, 2022, 9:53 pm

It occurred to me during a bike ride just now that it would be nice to have a 3-coloring of kite and dart tilings if one existed. A 4-coloring exists by the 4-color theorem, and I suspect an attractive-looking symmetric 5-coloring might not be hard to come up with.

Anyway, there is a 3-coloring according to https://core.ac.uk/download/pdf/82256902.pdf That was published in 2001. I'm surprised the result is so recent. I wonder if there's an implementation somewhere. The paper gets bogged down in a case analysis and the author does not believe this is avoidable.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » August 27th, 2022, 12:31 am

Continuing with the Penrose theme, I was thinking about the fivefold symmetry of passionflowers and how to use that in a tiling. After way too much work, I changed some photos into Penrose kite and dart tiles that fit together according the the rules and produce connected vines and blossoms:
pfkite2.png
pfkite2.png (1.58 MiB) Viewed 1945 times
pfdart2.png
pfdart2.png (1.46 MiB) Viewed 1945 times
An interesting thing about passionflowers is that they have three large stigmas that break the symmetry. One thing I was able to do with these tiles was preserve the 3-stigma property where 2 kites join to a dart. In other places there are anywhere from 0 to 4 stigmas, so it's not perfectly realistic, but it was nice to get it right sometimes.

Here's an example tiling:
vinetiling.jpg
vinetiling.jpg (3.06 MiB) Viewed 1945 times

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » August 28th, 2022, 1:42 am

Here's a larger one that I generated in software instead of trying to lay out by hand. Note that if you open the image in a new tab you can zoom in and see a lot more detail.
penrosepf.jpg
penrosepf.jpg (3.88 MiB) Viewed 1906 times

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » September 5th, 2022, 3:52 pm

Continuing with Penrose kite and dart tilings, here's one that generates braids.
penrosebraidsmall.jpg
penrosebraidsmall.jpg (2.22 MiB) Viewed 1835 times

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » September 12th, 2022, 1:29 am

Another variation on a Penrose kite and dart braid. This time I switched things so braids cross on darts and twist around on kites. This makes sense because there is more area to work with in the kite.

I also tried to even the spacing but I could still do better. I'm doing this by eye and I wonder if I ought to use algebra instead.
penrose0911.jpg
penrose0911.jpg (1.7 MiB) Viewed 1779 times

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » September 13th, 2022, 3:28 pm

The one below is more decorative. Another thing that might look good is to find the connected strands and give them distinct colors such that no two intersecting strands have the same color unless they're self-intersecting (which happens a lot). It's doable automatically, but I'd probably try it by hand first.

This one would make an attractive window grating if I happened to have a decagonal window.
penrosekitedart3.jpg
penrosekitedart3.jpg (1.65 MiB) Viewed 1742 times

User avatar
Macbi
Posts: 903
Joined: March 29th, 2009, 4:58 am

Re: Penrose tiling with faked rendering

Post by Macbi » September 15th, 2022, 3:50 am

I saw this related cool paper: https://langorigami.com/wp-content/uplo ... ntasia.pdf
John H. Conway [...] pointed out that one could create a surface in R^3 composed of equilateral triangles from a kite-dart Penrose tiling, in which each of the kite and dart quadrilaterals in the plane is replaced by a pair of equilateral triangles that are joined along one edge into a folded quadrilateral.

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » September 15th, 2022, 10:44 am

Macbi wrote:
September 15th, 2022, 3:50 am
I saw this related cool pape
That's interesting. I'll have to try it if I find a method less labor intensive than origami. I wonder if I could cut out interlocking shapes on a Cricut that just had to be folded along one line (but I would need a few layers and glue so it's still pretty time-consuming).

I remembered reading that Penrose rhombs tiles could be projected from a 3D surface made of congruent rhombuses. I searched again and it's a Wieringa roof. In fact, there's a demo from Adam Goucher: https://demonstrations.wolfram.com/Penr ... ingaRoofs/

I hadn't seen that before today. There article I read was about a glass sculpture using this method: https://coombscriddle.wordpress.com/tag ... a-roofing/

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » July 8th, 2023, 7:59 pm

One of the things I found dissatisfying about Penrose tilings was the fact that despite the aperiodicity, all the layouts I was able to make were 5-fold symmetric deflations of kites and darts. I was tempted to write a backtracking search for laying out random tilings and then it hit me there is a much easier way. To get asymmetric tile patches, I need only zoom in on a tiling at some high level of deflation, much how this is done when generating Mandelbrot fractals. I updated my Python script with this approach:

Code: Select all

import sys
import math
import numpy
import random

ROTATION = 36 *  math.pi / 180
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]])
SCALE_INV = numpy.linalg.inv(numpy.matrix([[RATIO, 0, 0], [0, RATIO, 0], [0, 0, 1]]))

# To give tiling a layered appearance (distance is relative to side length)
SHIFT_DISTANCE = 0.12
SHIFT_ANGLE = 43 * math.pi / 180

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)

TILE_CENTER = {'dart': translate(0.25, -0.18), 'kite': translate(0.405, -0.295)}

def distance(tile):
  shifted = tile[1] * TILE_CENTER[tile[0]]
  return math.sqrt(shifted[0, 2] ** 2 + shifted[1, 2] ** 2)

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(10)">'''

SVG_TAIL = '''</g></svg>'''

start_tile = sys.argv[1]
depth = int(sys.argv[2])
patch_center = (0, 0)
tile_limit = 100000
if len(sys.argv) > 5:
  patch_center = (float(sys.argv[3]), float(sys.argv[4]))
  tile_limit = int(sys.argv[5])

# set up starting tiles
rotation = translate(-patch_center[0], -patch_center[1])
tiles = []
for i in range(5):
  tiles.append((start_tile, rotation, '%d+_' % i))
  tiles.append((start_tile, rotation * FLIP_Y, '%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'))

  # keep only closest tiles but scale the size back up 
  new_tiles.sort(key = distance)
  tiles = [(tile[0], SCALE_INV * tile[1], tile[2]) for tile in new_tiles[:2 * tile_limit]]

# 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)

# darts in same position as kites for shadows; no stroke 
print('    <g style="fill:#384657;fill-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_bg_%s"/>''' % (to_components(mat) + (rules,)))
print('    </g>')

dart_colors = ['7693b8', '6d87a9', '647c9b', '5b718e', '536680', '4f627a', '576c88', '607795', '6982a3', '718db0']
kite_colors = ['a9d0de', 'afd8e7', 'b7e2f1', 'bfebfb', 'c6f5ff', 'cbfaff', 'c3f0ff', 'bbe7f6', 'b4deec', 'acd4e2']

palette = {}

# darts in shifted positions
shift = SHIFT_DISTANCE * RATIO ** depth
print('''    <g transform="translate(%f,%f)">''' % (shift * math.cos(SHIFT_ANGLE), shift * math.sin(SHIFT_ANGLE)))
for name, mat, rules in darts:
      components = to_components(mat)
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.5,-0.36 0.31,-0.95 z"
        id="dart_%s"/>''' % (components + (dart_colors[(components[2] // 36) % 10], rules,)))
print('    </g>')

# kites
print('    <g>')
for name, mat, rules in kites:
      components = to_components(mat)
      print('''      <path transform="translate(%f,%f) rotate(%d) scale(%f)"
        style="fill:#%s;fill-opacity:1;stroke:#909090;stroke-width:0.02;stroke-linejoin:miter;stroke-opacity:1"
        d="M 0,0 1,0 0.81,-0.59 0.31,-0.95 z"
        id="kite_%s"/>''' % (components + (kite_colors[(components[2] // 36) % 10], rules,)))
print('    </g>')

print(SVG_TAIL)
Note that if I simply said "Apply the deflation rules 20 times and then show me a small patch." then I would be generating an enormous number of tiles and throwing almost all of them away. So instead what I do is to translate the origin before doing any deflation, and then with each step throw away all but the closest 1000 tiles to the origin. (Or I keep all of them if there are less than 1000 as in the early steps.)

In fact, I can go out more than 20 deflation steps with this approach, since there are only 2000 half-tiles to deflate at each iteration. I am not sure if I would eventually run into numerical instability when taking it very far out. I haven't so far. The deflations are linear transformations, after all, so they are pretty stable. I haven't tried pushing the limits. It will just end with some examples. The arguments are starting tile, number of deflations, x shift, y shift, number of tiles to keep. At a glance the patches don't look all that different, but you can see that they are asymmetric and have distinct layouts.

Code: Select all

python3 penrose.py kite 10 -0.24 0.85 1000
penrose1.png
penrose1.png (860.59 KiB) Viewed 1320 times

Code: Select all

python3 penrose.py kite 20 0.24 0.85 1000
penrose2.png
penrose2.png (859.25 KiB) Viewed 1320 times

Code: Select all

python3 penrose.py kite 8 0.24 0.-17 1000
penrose3.png
penrose3.png (855.71 KiB) Viewed 1320 times
Update: I was surprised I could run a command like this and still get a result with tiles placed properly:

Code: Select all

python3 penrose.py kite 1000 0.25 -0.13 500
This applies 1000 deflations and would be infeasible to to without pruning the tiles. However, The reason I think it works is that the tiles at deflation n+k for some small k (somewhere between 5 and 10) are almost all the descendants of the tile containing the origin, and possibly some bordering tiles depending on how close to an edge or vertex the origin lies. So most of my concerns about error amplification aren't really applicable. The placement of a tile only depends on one near the origin a few deflations back. Moreover, the pruning step favors the tile closest to the origin even if it has drifted from the position implied by the initial placement. So the result is that one should get a reasonable-looking tile patch no matter what. However, it is not necessarily the actual tile patch that would be obtained with infinite precision arithmetic.

User avatar
otismo
Posts: 1201
Joined: August 18th, 2010, 1:41 pm
Location: Florida
Contact:

Re: Penrose tiling with faked rendering

Post by otismo » August 10th, 2023, 4:57 pm

@ P. C. :

Looks like you are well on your way here.

KUDOS !

"I also tried to even the spacing but I could still do better. I'm doing this by eye and I wonder if I ought to use algebra instead."

I think you have the "Precision Eyeballs" !

Cheers !
"One picture is worth 1000 words; but one thousand words, carefully crafted, can paint an infinite number of pictures."
- autonomic writing
forFUN : http://viropet.com
Art Gallery : http://cgolart.com
Video WebSite : http://conway.life

User avatar
pcallahan
Posts: 845
Joined: April 26th, 2013, 1:04 pm

Re: Penrose tiling with faked rendering

Post by pcallahan » December 26th, 2023, 3:00 am

Here's something very frivolous. I finally found a simple way to combine Penrose tiles with AI art without wrecking the tiling, at least not too much. If the section that is a Penrose tiling (in this case a perspective-adjusted circle) is by itself on a transparent image, it can be be used as a control net feature in stable diffusion, and the AI art will add the other details around them.

Some ideas for the entrance to my new house. In reality, I don't even have a dog.
big-fluffy-dog-standing-in-back-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric (1).png
big-fluffy-dog-standing-in-back-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric (1).png (1.78 MiB) Viewed 832 times
big-fluffy-dog-standing-in-back-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric (2).png
big-fluffy-dog-standing-in-back-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric (2).png (1.75 MiB) Viewed 832 times
big-fluffy-dog-standing-in-front-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric.png
big-fluffy-dog-standing-in-front-ofround-penrose-tiled-patio-of-black-and-white-stones-in-geometric.png (1.76 MiB) Viewed 832 times

Post Reply