1/* Game of Life: square cells on a canvas that glide from one layout to the next. */ 2(function (root) { 3 const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x)); 4 const lerp = (a, b, t) => a + (b - a) * t; 5 const ease = t => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); 6 const reduced = root.matchMedia('(prefers-reduced-motion: reduce)'); 7 8 // Largest pitch (cell plus gap) that fits n square cells in w by h, and the column count that gets it. 9 function pack(n, w, h, max = Infinity) { 10 let best = { cols: 1, p: Math.min(w, h, max) }; 11 for (let cols = 1; cols <= n; cols++) { 12 if (w / cols < best.p) break; // more columns only shrink the cells from here on 13 const p = Math.min(w / cols, h / Math.ceil(n / cols), max); 14 if (p > best.p || cols === 1) best = { cols, p }; 15 } 16 return best; 17 } 18 19 // A scene is a function (width, height) => { cells: [...], labels: [...] } in CSS pixels. 20 // Cell: { k, x, y, s, c: [r, g, b], a?, d? (delay in ms), from?: { x, y, s }, group?, info? }. 21 // `group` ties cells together for hover emphasis; `info` is whatever the page shows on hover. 22 // Label: { k, text, x, y, font, color, align? }. 23 function createCells(canvas, { duration = 900 } = {}) { 24 const ctx = canvas.getContext('2d'); 25 const cells = new Map(); 26 const labels = new Map(); 27 let W = 0, H = 0, dpr = 1, t0 = 0, running = false, scene = null, instant = false; 28 let hover = null; // { group?, key?, stroke? }: dims other groups and outlines one cell 29 30 function measure() { 31 const r = canvas.getBoundingClientRect(); 32 dpr = Math.min(root.devicePixelRatio || 1, 2); 33 W = r.width; 34 H = r.height; 35 const w = Math.max(1, Math.round(W * dpr)), h = Math.max(1, Math.round(H * dpr)); 36 if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; } 37 } 38 39 function begin(item, to, d) { 40 item.from = instant ? to : Object.fromEntries(Object.keys(to).map(key => [key, item[key]])); 41 item.to = to; 42 item.d = d; 43 item.dead = false; 44 } 45 46 function show(next, animate = true) { 47 scene = next; 48 measure(); 49 if (W < 1 || H < 1) return; 50 instant = !animate || reduced.matches; 51 const out = scene(W, H); 52 53 const seen = new Set(); 54 for (const t of out.cells) { 55 seen.add(t.k); 56 const to = { x: t.x, y: t.y, s: t.s, a: t.a ?? 1, r: t.c[0], g: t.c[1], b: t.c[2] }; 57 let c = cells.get(t.k); 58 if (!c) { 59 const f = t.from || { x: t.x + t.s / 2, y: t.y + t.s / 2, s: 0 }; 60 c = { ...to, x: f.x, y: f.y, s: f.s, k: t.k }; 61 cells.set(t.k, c); 62 } 63 c.group = t.group; 64 c.info = t.info; 65 begin(c, to, t.d || 0); 66 } 67 for (const [k, c] of cells) { 68 if (seen.has(k)) continue; 69 begin(c, { x: c.x + c.s / 4, y: c.y + c.s / 4, s: c.s / 2, a: 0 }, 0); 70 c.dead = true; 71 } 72 73 const seenLabels = new Set(); 74 for (const t of out.labels || []) { 75 seenLabels.add(t.k); 76 let l = labels.get(t.k); 77 if (!l) { l = { x: t.x, y: t.y, a: 0 }; labels.set(t.k, l); } 78 Object.assign(l, { text: t.text, font: t.font, color: t.color, align: t.align || 'left' }); 79 begin(l, { x: t.x, y: t.y, a: 1 }, 0); 80 } 81 for (const [k, l] of labels) { 82 if (seenLabels.has(k)) continue; 83 begin(l, { a: 0 }, 0); 84 l.dead = true; 85 } 86 87 t0 = performance.now(); 88 if (instant) tick(Infinity); 89 else if (!running) { running = true; requestAnimationFrame(frame); } 90 } 91 92 function tick(T) { 93 let busy = false; 94 for (const map of [cells, labels]) { 95 for (const [k, item] of map) { 96 const p = clamp((T - item.d) / duration, 0, 1), e = ease(p); 97 for (const key in item.to) item[key] = lerp(item.from[key], item.to[key], e); 98 if (p < 1) busy = true; 99 else if (item.dead) map.delete(k); 100 } 101 } 102 draw(); 103 return busy; 104 } 105 106 function frame(now) { 107 if (tick(now - t0)) requestAnimationFrame(frame); 108 else running = false;
109 } 110 111 function draw() { 112 const px = v => Math.round(v * dpr) / dpr; 113 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 114 ctx.clearRect(0, 0, W, H); 115 let fill = ''; 116 const dim = hover && hover.group !== undefined; 117 for (const c of cells.values()) { 118 if (c.a < 0.02 || c.s < 0.3) continue; 119 const f = `rgb(${c.r | 0},${c.g | 0},${c.b | 0})`; 120 if (f !== fill) { ctx.fillStyle = f; fill = f; } 121 ctx.globalAlpha = dim && c.group !== hover.group ? c.a * 0.25 : c.a; 122 const s = Math.max(1 / dpr, px(c.s)); 123 ctx.fillRect(px(c.x), px(c.y), s, s); 124 } 125 const lifted = hover && hover.key !== undefined && cells.get(hover.key); 126 if (lifted && lifted.a > 0.02) { 127 const s = Math.max(1 / dpr, px(lifted.s)); 128 ctx.globalAlpha = 1; 129 ctx.strokeStyle = hover.stroke || '#000'; 130 ctx.lineWidth = 1.5; 131 ctx.strokeRect(px(lifted.x) - 1, px(lifted.y) - 1, s + 2, s + 2); 132 } 133 ctx.textBaseline = 'middle'; 134 for (const l of labels.values()) { 135 if (l.a < 0.02) continue; 136 ctx.globalAlpha = l.a; 137 ctx.font = l.font; 138 ctx.fillStyle = l.color; 139 ctx.textAlign = l.align; 140 ctx.fillText(l.text, l.x, l.y); 141 } 142 ctx.globalAlpha = 1; 143 } 144 145 // The visible cell under a point, in CSS pixels. The target includes the gap around 146 // each square so the pointer never falls between two of them. 147 function hit(x, y) { 148 for (const c of cells.values()) { 149 if (c.dead || !c.to || c.to.a <= 0.05) continue; 150 const pad = Math.max(1, c.to.s * 0.2); 151 if (x >= c.to.x - pad && x <= c.to.x + c.to.s + pad && y >= c.to.y - pad && y <= c.to.y + c.to.s + pad) return c; 152 } 153 return null; 154 } 155 156 function setHover(next) { 157 hover = next; 158 if (!running) draw(); 159 } 160 161 new ResizeObserver(() => { if (scene) show(scene, false); }).observe(canvas); 162 163 return { show, hit, setHover }; 164 } 165 166 root.Cells = { createCells, pack }; 167})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.