[object Object]

← back to Penrose Designs

penrose tiling: 20-design generator + layered PSD build

67552e3e41244fb75c77d101d6d22191323dce12 · 2026-07-23 09:24:43 -0700 · Steve

Files touched

Diff

commit 67552e3e41244fb75c77d101d6d22191323dce12
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 09:24:43 2026 -0700

    penrose tiling: 20-design generator + layered PSD build
---
 .gitignore  |   6 ++++
 generate.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 113 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..28fee3a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+out/
+*.psd
+contact-sheet.png
+node_modules/
+.DS_Store
+*.log
diff --git a/generate.py b/generate.py
new file mode 100644
index 0000000..32caa98
--- /dev/null
+++ b/generate.py
@@ -0,0 +1,107 @@
+#!/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()

(oldest)  ·  back to Penrose Designs  ·  penrose: add Photoshop Penrose Tiling Generator (recolorable 1715b9b →