{"slug":"penrose","number":"26","title":"Penrose tiling","era":"Penrose · 1974","section":"historical","provenance":"documented","reference":{"label":"R. Penrose, \"The role of aesthetics in pure and applied mathematical research\", Bulletin of the IMA 10, 1974; Robinson-triangle subdivision","url":"https://en.wikipedia.org/wiki/Penrose_tiling"},"summary":"Robinson triangles, aperiodic, drawn once.","description":"A pentagon of acute golden triangles, subdivided by Robinson's rule. Two rhombs, no period. Fat and thin take the two tweak colours. Depth rebuilds the mesh. Nothing moves and there is nothing to pan to, so the tiling is drawn once and again on resize.","apis":["Canvas 2D"],"tags":["tiling","geometry"],"hasGlsl":false,"interaction":null,"url":"https://3d-retro.com/experiments/penrose","example_url":"https://3d-retro.com/examples/penrose.html","api_url":"https://3d-retro.com/api/v1/experiments/penrose","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"Robinson-triangle Penrose tiling, drawn once.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/penrose\" />\n  <title>Penrose tiling · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #0b0c0e; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #0b0c0e; }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    color: \"#6ee7b7\",\n    accent: \"#d2a85c\",\n    depth: 6\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [110, 231, 183];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n  function css(rgb, a) {\n    return \"rgba(\" + rgb[0] + \",\" + rgb[1] + \",\" + rgb[2] + \",\" + a + \")\";\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    fallback.hidden = false;\n    fallback.textContent = \"Canvas 2D is not available in this browser.\";\n    return;\n  }\n\n  const PHI = (1 + Math.sqrt(5)) / 2;\n  const INV = 1 / PHI;\n  function lerp(a, b, t) {\n    return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];\n  }\n\n  function subdivide(tris) {\n    const out = [];\n    for (let i = 0; i < tris.length; i++) {\n      const t = tris[i];\n      const a = t.a, b = t.b, c = t.c;\n      if (t.k === \"A\") {\n        const p = lerp(a, b, INV);\n        out.push({ k: \"A\", a: c, b: p, c: b });\n        out.push({ k: \"O\", a: p, b: c, c: a });\n      } else {\n        const q = lerp(b, a, INV);\n        out.push({ k: \"O\", a: q, b: c, c: a });\n        out.push({ k: \"A\", a: q, b: c, c: b });\n      }\n    }\n    return out;\n  }\n\n  function build(depth) {\n    const seed = [];\n    for (let i = 0; i < 5; i++) {\n      const a0 = (i * 72 - 90) * Math.PI / 180;\n      const a1 = ((i + 1) * 72 - 90) * Math.PI / 180;\n      const b = [Math.cos(a0), Math.sin(a0)];\n      const c = [Math.cos(a1), Math.sin(a1)];\n      seed.push({ k: \"A\", a: [0, 0], b: b, c: c });\n    }\n    let t = seed;\n    const d = Math.max(3, Math.min(7, depth | 0));\n    for (let i = 0; i < d; i++) t = subdivide(t);\n    return t;\n  }\n\n  const tiles = build(PARAMS.depth);\n\n  // Nothing here moves: the tiling is drawn once and again on resize, so\n  // prefers-reduced-motion needs no special case.\n  function draw(w, h) {\n    ctx.fillStyle = \"#0b0c0e\";\n    ctx.fillRect(0, 0, w, h);\n    const fat = hexToRgb(PARAMS.color);\n    const thin = hexToRgb(PARAMS.accent);\n    const s = Math.min(w, h) * 0.46;\n    const cx = w * 0.5;\n    const cy = h * 0.5;\n    ctx.lineJoin = \"round\";\n    for (let i = 0; i < tiles.length; i++) {\n      const t = tiles[i];\n      ctx.beginPath();\n      ctx.moveTo(cx + t.a[0] * s, cy + t.a[1] * s);\n      ctx.lineTo(cx + t.b[0] * s, cy + t.b[1] * s);\n      ctx.lineTo(cx + t.c[0] * s, cy + t.c[1] * s);\n      ctx.closePath();\n      ctx.fillStyle = t.k === \"A\" ? css(fat, 0.55) : css(thin, 0.55);\n      ctx.fill();\n      ctx.strokeStyle = css(fat, 0.35);\n      ctx.lineWidth = 0.8;\n      ctx.stroke();\n    }\n  }\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n  }\n\n  function frame() {\n    resize();\n    draw(canvas.width, canvas.height);\n  }\n\n  window.addEventListener(\"resize\", frame);\n  frame();\n})();\n</script>\n</body>\n</html>\n"}