<!-- Penrose kite-and-dart tiling demo — inflate to subdivide tiles -->
<div class="controls">
<button id="btn-inflate" type="button">Inflate (subdivide)</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="info" id="info">Step 0 — seed (10 kites)</span>
</div>
<canvas id="canvas" width="480" height="420"></canvas>
<p class="hint">Blue = kite, orange = dart. Each inflation step subdivides every tile into smaller copies following the golden ratio — tile count grows by ~2.618× each step. The pattern is aperiodic: it never repeats, even over an infinite plane.</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fa; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 10px; background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,.08); max-width: 100%; }
.info { font-size: .88rem; color: #555; margin-left: .3rem; }
.hint { font-size: .82rem; color: #666; margin: .5rem 0 0; line-height: 1.5; }
/* Penrose P2 tiling via inflation (deflation of kites and darts) */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const PHI = (1 + Math.sqrt(5)) / 2; /* golden ratio — the key constant in Penrose tilings */
/* tile type constants */
const KITE = 0, DART = 1;
/* fill and stroke colors per tile type */
const COLOR = {
[KITE]: { fill: '#d4e6f7', stroke: '#2a6496' },
[DART]: { fill: '#fde8cc', stroke: '#c07030' }
};
/* each tile: { type, A, B, C } where A/B/C are 2D points */
/* A = apex, B and C = base vertices for kite; adjusted for dart */
let tiles = [];
function seed() {
/* build a 5-fold-symmetric sun of 10 kites as the starting configuration */
tiles = [];
const cx = 240, cy = 220, R = 170;
/* place one kite per 36-degree sector of the sun */
for (let i = 0; i < 5; i++) {
const a0 = (i * 2 * Math.PI) / 5 - Math.PI / 2;
const a1 = a0 + (2 * Math.PI) / 5;
const p0 = { x: cx, y: cy };
const p1 = { x: cx + R * Math.cos(a0), y: cy + R * Math.sin(a0) };
const p2 = { x: cx + R * Math.cos(a1), y: cy + R * Math.sin(a1) };
/* two reflected kites fill one 72-degree wedge of the sun */
tiles.push({ type: KITE, A: p0, B: p1, C: p2 });
tiles.push({ type: KITE, A: p0, B: p2, C: p1 });
}
}
function midPhi(a, b) {
/* point 1/phi of the way from a to b — the golden-ratio subdivision point */
return { x: a.x + (b.x - a.x) / PHI, y: a.y + (b.y - a.y) / PHI };
}
function inflate() {
/* subdivision rules: each kite becomes 2 kites + 1 dart; each dart becomes 1 kite + 1 dart */
const next = [];
for (const t of tiles) {
const { type, A, B, C } = t;
if (type === KITE) {
/* kite -> 2 kites + 1 dart (classic P2 inflation rule) */
const P = midPhi(A, C);
const Q = midPhi(B, C);
next.push({ type: DART, A: Q, B: P, C: A });
next.push({ type: KITE, A: Q, B: C, C: P });
next.push({ type: KITE, A: Q, B: A, C: B });
} else {
/* dart -> 1 kite + 1 dart (classic P2 inflation rule) */
const P = midPhi(C, A);
const Q = midPhi(C, B);
next.push({ type: DART, A: B, B: Q, C: A });
next.push({ type: KITE, A: P, B: A, C: Q });
}
}
tiles = next;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const t of tiles) {
const { A, B, C } = t;
const col = COLOR[t.type];
ctx.beginPath();
ctx.moveTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.lineTo(C.x, C.y);
ctx.closePath();
ctx.fillStyle = col.fill;
ctx.fill();
ctx.strokeStyle = col.stroke;
ctx.lineWidth = 0.7;
ctx.stroke();
}
}
let step = 0;
function updateInfo() {
document.getElementById('info').textContent =
i18nFmt('Step %step% — %n% tiles', { step: step, n: tiles.length });
}
document.getElementById('btn-inflate').addEventListener('click', () => {
if (step >= 7) return; /* cap at 7 inflations to avoid performance issues */
inflate();
step++;
draw();
updateInfo();
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
seed();
draw();
document.getElementById('info').textContent = 'Step 0 — seed (10 kites)';
});
seed();
draw();
<!-- Penrose kite-and-dart tiling demo — inflate to subdivide tiles -->
<div class="controls">
<button id="btn-inflate" type="button">Inflate (subdivide)</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="info" id="info">Step 0 — seed (10 kites)</span>
</div>
<canvas id="canvas" width="480" height="420"></canvas>
<p class="hint">Blue = kite, orange = dart. Each inflation step subdivides every tile into smaller copies following the golden ratio — tile count grows by ~2.618× each step. The pattern is aperiodic: it never repeats, even over an infinite plane.</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fa; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 10px; background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,.08); max-width: 100%; }
.info { font-size: .88rem; color: #555; margin-left: .3rem; }
.hint { font-size: .82rem; color: #666; margin: .5rem 0 0; line-height: 1.5; }
/* Penrose P2 tiling via inflation (deflation of kites and darts) */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const PHI = (1 + Math.sqrt(5)) / 2; /* golden ratio — the key constant in Penrose tilings */
/* tile type constants */
const KITE = 0, DART = 1;
/* fill and stroke colors per tile type */
const COLOR = {
[KITE]: { fill: '#d4e6f7', stroke: '#2a6496' },
[DART]: { fill: '#fde8cc', stroke: '#c07030' }
};
/* each tile: { type, A, B, C } where A/B/C are 2D points */
/* A = apex, B and C = base vertices for kite; adjusted for dart */
let tiles = [];
function seed() {
/* build a 5-fold-symmetric sun of 10 kites as the starting configuration */
tiles = [];
const cx = 240, cy = 220, R = 170;
/* place one kite per 36-degree sector of the sun */
for (let i = 0; i < 5; i++) {
const a0 = (i * 2 * Math.PI) / 5 - Math.PI / 2;
const a1 = a0 + (2 * Math.PI) / 5;
const p0 = { x: cx, y: cy };
const p1 = { x: cx + R * Math.cos(a0), y: cy + R * Math.sin(a0) };
const p2 = { x: cx + R * Math.cos(a1), y: cy + R * Math.sin(a1) };
/* two reflected kites fill one 72-degree wedge of the sun */
tiles.push({ type: KITE, A: p0, B: p1, C: p2 });
tiles.push({ type: KITE, A: p0, B: p2, C: p1 });
}
}
function midPhi(a, b) {
/* point 1/phi of the way from a to b — the golden-ratio subdivision point */
return { x: a.x + (b.x - a.x) / PHI, y: a.y + (b.y - a.y) / PHI };
}
function inflate() {
/* subdivision rules: each kite becomes 2 kites + 1 dart; each dart becomes 1 kite + 1 dart */
const next = [];
for (const t of tiles) {
const { type, A, B, C } = t;
if (type === KITE) {
/* kite -> 2 kites + 1 dart (classic P2 inflation rule) */
const P = midPhi(A, C);
const Q = midPhi(B, C);
next.push({ type: DART, A: Q, B: P, C: A });
next.push({ type: KITE, A: Q, B: C, C: P });
next.push({ type: KITE, A: Q, B: A, C: B });
} else {
/* dart -> 1 kite + 1 dart (classic P2 inflation rule) */
const P = midPhi(C, A);
const Q = midPhi(C, B);
next.push({ type: DART, A: B, B: Q, C: A });
next.push({ type: KITE, A: P, B: A, C: Q });
}
}
tiles = next;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const t of tiles) {
const { A, B, C } = t;
const col = COLOR[t.type];
ctx.beginPath();
ctx.moveTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.lineTo(C.x, C.y);
ctx.closePath();
ctx.fillStyle = col.fill;
ctx.fill();
ctx.strokeStyle = col.stroke;
ctx.lineWidth = 0.7;
ctx.stroke();
}
}
let step = 0;
function updateInfo() {
document.getElementById('info').textContent =
i18nFmt('Step %step% — %n% tiles', { step: step, n: tiles.length });
}
document.getElementById('btn-inflate').addEventListener('click', () => {
if (step >= 7) return; /* cap at 7 inflations to avoid performance issues */
inflate();
step++;
draw();
updateInfo();
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
seed();
draw();
document.getElementById('info').textContent = 'Step 0 — seed (10 kites)';
});
seed();
draw();
Notice that no matter how many steps you apply, the same local patches reappear — but never in a globally periodic grid. The golden ratio is woven into the proportions: the ratio of kites to darts in any large tiling converges to exactly φ.
Comments
Loading comments...