// Penrose Tiling (P3 — Rhombus tiling)
//
// Generated via Robinson triangle decomposition.
// Two triangle types:
// thick = golden triangle (36-72-72°, apex angle 36°, ratio φ:φ:1)
// thin = golden gnomon (36-36-108°, apex angle 108°, ratio 1:1:φ)
//
// Subdivision rules (each type produces exactly 2 children):
// thick → 1 thick + 1 thin
// thin → 1 thick + 1 thin
//
// Seed: 10 thick triangles arranged sun-style around the center (10-fold symmetry).
// At depth 5+ the tiling exhibits the iconic aperiodic Penrose pattern.
export const PARAMS = {
depth: { value: 5, min: 2, max: 7, step: 1, label: "Subdivision Depth", folder: "Structure", rebuildOnChange: true },
showFills: { value: false, label: "Show Fills", folder: "Appearance" },
thickFill: { value: "#d97757", type: "color", label: "Thick Fill", folder: "Appearance" },
thinFill: { value: "#2a3f5f", type: "color", label: "Thin Fill", folder: "Appearance" },
bg: { value: "#0a0a0a", type: "color", label: "Background", folder: "Appearance" },
stroke: { value: "#c8b87a", type: "color", label: "Stroke", folder: "Appearance" },
strokeW: { value: 1.0, min: 0.2, max: 4, step: 0.1, label: "Stroke Width", folder: "Appearance" },
};
const PHI = (1 + Math.sqrt(5)) / 2; // golden ratio ≈ 1.618
// Parse hex color string
function hexToRgbStr(hex) {
const h = hex.replace('#', '');
const r = parseInt(h.substring(0, 2), 16);
const g = parseInt(h.substring(2, 4), 16);
const b = parseInt(h.substring(4, 6), 16);
return `rgb(${r},${g},${b})`;
}
// 2D point arithmetic
function add(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function lerp(a, b, t) { return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]; }
// ── Robinson triangle decomposition ────────────────────────────────────────────
//
// Each triangle is { type: 'thick'|'thin', A, B, C } with vertex A always at
// the apex (the unique-angle corner).
//
// thick (A-tile, acute golden, 36-72-72°):
// A = apex (36°), B and C = base corners (72° each)
// |AB| = |AC| = φ × |BC| — long equal sides meet at apex; short side opposite.
// Decomposition: P on AB with AP = |BC| (the short length), so AP/AB = 1/φ.
// The two children are:
// thin (apex P, 108°): {P, A, C} — sides PA=1, PC=1, AC=φ
// thick (apex C, 36°): {C, P, B} — sides CP=1, CB=1, PB=1/φ
//
// thin (O-tile, obtuse golden gnomon, 108-36-36°):
// A = apex (108°), B and C = base corners (36° each)
// |BC| = φ × |AB| — short equal sides meet at apex; long side opposite.
// Decomposition: Q on BC with BQ = |AB| (the short length), so BQ/BC = 1/φ.
// The two children are:
// thick (apex B, 36°): {B, A, Q} — sides BA=1, BQ=1, AQ=1/φ
// thin (apex Q, 108°): {Q, A, C} — sides QA=1/φ, QC=1/φ, AC=1
//
// References: Wikipedia "Penrose tiling § Robinson triangle decompositions"
function subdivide(tri) {
const { type, A, B, C } = tri;
if (type === 'thick') {
// P on long side AB at distance |BC| from A
const P = lerp(A, B, 1 / PHI);
return [
{ type: 'thin', A: P, B: A, C: C }, // apex at P (108°)
{ type: 'thick', A: C, B: P, C: B }, // apex at C (36°)
];
} else {
// Q on long base BC at distance |AB| from B
const Q = lerp(B, C, 1 / PHI);
return [
{ type: 'thick', A: B, B: A, C: Q }, // apex at B (36°)
{ type: 'thin', A: Q, B: A, C: C }, // apex at Q (108°)
];
}
}
// Build initial "sun" seed: 10 thick triangles fanned around the center.
// Each thick triangle has its apex at the center, base on the outer ring.
function buildSeed(cx, cy, radius) {
const triangles = [];
for (let i = 0; i < 10; i++) {
const a1 = (2 * Math.PI * i) / 10 - Math.PI / 2;
const a2 = (2 * Math.PI * (i + 1)) / 10 - Math.PI / 2;
const B = [cx + radius * Math.cos(a1), cy + radius * Math.sin(a1)];
const C = [cx + radius * Math.cos(a2), cy + radius * Math.sin(a2)];
// Alternate orientation so edge-pairs share edges correctly
if (i % 2 === 0) {
triangles.push({ type: 'thick', A: [cx, cy], B, C });
} else {
triangles.push({ type: 'thick', A: [cx, cy], B: C, C: B });
}
}
return triangles;
}
// Subdivide N times
function buildPenrose(cx, cy, radius, depth) {
let tris = buildSeed(cx, cy, radius);
for (let d = 0; d < depth; d++) {
const next = [];
for (const tri of tris) {
for (const child of subdivide(tri)) {
next.push(child);
}
}
tris = next;
}
return tris;
}
export function sketchSetup(ctx, w, h, tng) {
return {};
}
export function sketchDraw(ctx, w, h, params, tng) {
// Background
ctx.fillStyle = hexToRgbStr(params.bg);
ctx.fillRect(0, 0, w, h);
const cx = w / 2;
const cy = h / 2;
const radius = Math.max(w, h) * 0.85;
const tris = buildPenrose(cx, cy, radius, params.depth);
const thickFill = hexToRgbStr(params.thickFill);
const thinFill = hexToRgbStr(params.thinFill);
const strokeCol = hexToRgbStr(params.stroke);
const lw = params.strokeW;
const showFills = params.showFills;
if (showFills) {
for (const tri of tris) {
ctx.beginPath();
ctx.moveTo(tri.A[0], tri.A[1]);
ctx.lineTo(tri.B[0], tri.B[1]);
ctx.lineTo(tri.C[0], tri.C[1]);
ctx.closePath();
ctx.fillStyle = tri.type === 'thick' ? thickFill : thinFill;
ctx.fill();
if (lw > 0) {
ctx.strokeStyle = strokeCol;
ctx.lineWidth = lw;
ctx.stroke();
}
}
} else {
// Stroke-only: the iconic 10-fold symmetric line drawing
ctx.strokeStyle = strokeCol;
ctx.lineWidth = lw;
ctx.lineJoin = 'round';
for (const tri of tris) {
ctx.beginPath();
ctx.moveTo(tri.A[0], tri.A[1]);
ctx.lineTo(tri.B[0], tri.B[1]);
ctx.lineTo(tri.C[0], tri.C[1]);
ctx.closePath();
ctx.stroke();
}
}
}