← back to Penrose Designs
penrose tiling: 20-design generator + layered PSD build
67552e3e41244fb75c77d101d6d22191323dce12 · 2026-07-23 09:24:43 -0700 · Steve
Files touched
A .gitignoreA generate.py
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 →