← 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()