← back to Penrose Designs
generate.py
108 lines
#!/usr/bin/env python3
# Penrose P3 (rhombus) tiling — 20 luxe wallcovering colorways/styles -> PNGs -> layered PSD
# Deflation via Robinson half-rhombus triangles (Preshing recipe). $0 local. No figurative content.
import cmath, math
from PIL import Image, ImageDraw
PHI = (1 + 5 ** 0.5) / 2
def subdivide(tris):
out = []
for color, A, B, C in tris:
if color == 0: # thin (acute) Robinson triangle
P = A + (B - A) / PHI
out += [(0, C, P, B), (1, P, C, A)]
else: # fat (obtuse) Robinson triangle
Q = B + (A - B) / PHI
R = B + (C - B) / PHI
out += [(1, R, C, A), (1, Q, R, B), (0, R, Q, A)]
return out
def wheel():
tris = []
for i in range(10):
B = cmath.rect(1, (2*i - 1) * math.pi / 10)
C = cmath.rect(1, (2*i + 1) * math.pi / 10)
if i % 2 == 0: B, C = C, B
tris.append((0, 0j, B, C))
return tris
def build(depth):
t = wheel()
for _ in range(depth):
t = subdivide(t)
return t
# 20 luxe colorways: (name, bg, fat_fill, thin_fill, edge) — hex
PALETTES = [
("Oatmeal & Ink", "#EDE7DA","#D8CCB4","#BCA98A","#2B2622"),
("Celadon & Gold", "#E8EFE7","#BFD8C6","#9CBBA6","#B8912E"),
("Navy & Silver", "#111726","#20304F","#2C4568","#C9D3E0"),
("Terracotta & Cream","#F3E7DC","#D98B5F","#C46B3E","#3A2216"),
("Alabaster & Greige","#F4F1EA","#DED7C7","#C3B8A2","#6E6455"),
("Ink & Brass", "#161511","#2A2620","#3C362B","#C9A24B"),
("Blush & Bronze", "#F6E9E4","#E7C3B7","#D29C8B","#7A4A38"),
("Forest & Ivory", "#EEF0E9","#4C6B4A","#6B8A63","#EFE9D8"),
("Slate & Copper", "#20242A","#333A44","#47515E","#C0724A"),
("Sand & Charcoal", "#E9E2D2","#CFC3A9","#B0A488","#33312C"),
("Plum & Champagne", "#EDE4EA","#8A5E80","#A87F9C","#D8C08A"),
("Teal & Sand", "#E7EEEC","#2E6E6A","#4C8C88","#E4D8BE"),
("Onyx & Pearl", "#0F0F12","#1C1C22","#2A2A33","#E8E6E0"),
("Rose Quartz & Grey","#F1E9EC","#D9B9C2","#BFA0A9","#5B5257"),
("Olive & Linen", "#EFEBDF","#8A8654","#A6A26C","#3E3B2A"),
("Cobalt & Cream", "#EEF1F6","#2647A6","#4B69C0","#EDE7D6"),
("Mocha & Gold", "#EBE3D8","#6E5238","#8A6B4A","#C6A martes")[:4]+("#C6A24B",) if False else ("Mocha & Gold","#EBE3D8","#6E5238","#8A6B4A","#C6A24B"),
("Graphite & Mint", "#22262A","#31373D","#454D54","#9FD8C4"),
("Merlot & Blush", "#EEE2E2","#6E2438","#96384F","#E8C4C0"),
("Storm & Ivory", "#E9EAEC","#5A6470","#7B8794","#F2EEE4"),
]
def hx(h): h=h.lstrip('#'); return tuple(int(h[i:i+2],16) for i in (0,2,4))
def render(tris, pal, size, style, rot):
name,bg,fat,thin,edge = pal
BG,FAT,THIN,EDGE = hx(bg),hx(fat),hx(thin),hx(edge)
img = Image.new("RGB",(size,size),BG); d=ImageDraw.Draw(img)
r = cmath.rect(1, rot)
R=0
for _,A,B,C in tris:
for z in (A,B,C): R=max(R,abs(z))
sc = size*0.82/max(R,1e-9); cx=cy=size/2 # overfill -> edge-to-edge, corners covered
def P(z):
z=z*r; return (cx+z.real*sc, cy+z.imag*sc)
ew = max(1,int(size/900))
for color,A,B,C in tris:
poly=[P(A),P(B),P(C)]
if style=="twotone":
d.polygon(poly, fill=(FAT if color else THIN))
elif style=="fat-on-ground":
d.polygon(poly, fill=(FAT if color else BG))
elif style=="outline":
pass
elif style=="tonal":
f = FAT if color else THIN
d.polygon(poly, fill=f)
# rhombus side seams (A-B, A-C) — skip B-C internal diagonal
d.line([P(A),P(B)], fill=EDGE, width=ew)
d.line([P(A),P(C)], fill=EDGE, width=ew)
return img, name
def main():
SIZE=2000
styles=["twotone","tonal","fat-on-ground","twotone"]
depths=[6,6,7,6]
layers=[]
for i,pal in enumerate(PALETTES):
depth=depths[i%len(depths)]; style=styles[i%len(styles)]
rot=(i*0.31) # rotate each so aperiodic character reads differently
tris=build(depth)
img,name=render(tris,pal,SIZE,style,rot)
fn=f"out/penrose_{i+1:02d}.png"; img.save(fn)
layers.append((fn,f"{i+1:02d} {name}"))
print(f"[{i+1:02d}/20] {name:20s} depth{depth} {style:14s} tris={len(tris)}")
# write a manifest for the PSD layer-naming step
with open("out/layers.txt","w") as f:
for fn,lbl in layers: f.write(fn+"\t"+lbl+"\n")
print("PNGs done ->", len(layers))
if __name__=="__main__": main()