Chotto.app
Login
Sign Up
ES
App Name
kuyawa / conway
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"> <defs> <!-- Background gradient --> <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#14141e"/> <stop offset="100%" stop-color="#0a0a12"/> </linearGradient> <!-- Cell glow --> <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"> <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur"/> <feColorMatrix in="blur" type="matrix" values="1 0 0 0 0 0 0.7 0 0 0 0 0 0.1 0 0 0 0 0 0.7 0" result="colorBlur"/> <feMerge> <feMergeNode in="colorBlur"/> <feMergeNode in="SourceGraphic"/> </feMerge> </filter> <!-- Subtle inner light --> <radialGradient id="innerLight" cx="0.35" cy="0.3" r="0.7"> <stop offset="0%" stop-color="rgba(232,160,32,0.04)"/> <stop offset="100%" stop-color="rgba(0,0,0,0)"/> </radialGradient> <!-- Inactive cell gradient --> <linearGradient id="deadCell" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="#1e1e2e"/> <stop offset="100%" stop-color="#18182a"/> </linearGradient> <!-- Active cell gradient --> <linearGradient id="liveCell" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="#f0b030"/> <stop offset="100%" stop-color="#d89018"/> </linearGradient> <!-- Icon mask for rounded rect --> <clipPath id="iconClip"> <rect x="8" y="8" width="496" height="496" rx="108" ry="108"/> </clipPath> </defs> <!-- Shadow base --> <rect x="12" y="14" width="496" height="496" rx="108" ry="108" fill="#000" opacity="0.35"/> <!-- Main background --> <rect x="8" y="8" width="496" height="496" rx="108" ry="108" fill="url(#bg)"/> <!-- Inner ambient light --> <rect x="8" y="8" width="496" height="496" rx="108" ry="108" fill="url(#innerLight)"/> <!-- Subtle border --> <rect x="8" y="8" width="496" height="496" rx="108" ry="108" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/> <!-- Grid area: centered, 3x3 with spacing --> <!-- Grid: 3 cells, each ~76px, gap 16px, total = 76*3 + 16*2 = 260px --> <!-- Offset: (512 - 260) / 2 = 126 --> <g clip-path="url(#iconClip)"> <!-- Dead cells (background grid) --> <!-- Row 0 --> <rect x="126" y="126" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.7"/> <rect x="218" y="126" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.0"/> <rect x="310" y="126" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.7"/> <!-- Row 1 --> <rect x="126" y="218" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.7"/> <rect x="218" y="218" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.7"/> <rect x="310" y="218" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.0"/> <!-- Row 2 --> <rect x="126" y="310" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.0"/> <rect x="218" y="310" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.0"/> <rect x="310" y="310" width="76" height="76" rx="12" fill="url(#deadCell)" opacity="0.0"/> <!-- Live cells with glow --> <!-- Pattern: . X . / . . X / X X X (glider) --> <g filter="url(#glow)"> <!-- Row 0, col 1 --> <rect x="218" y="126" width="76" height="76" rx="12" fill="url(#liveCell)"/> <!-- Row 1, col 2 --> <rect x="310" y="218" width="76" height="76" rx="12" fill="url(#liveCell)"/> <!-- Row 2, col 0 --> <rect x="126" y="310" width="76" height="76" rx="12" fill="url(#liveCell)"/> <!-- Row 2, col 1 --> <rect x="218" y="310" width="76" height="76" rx="12" fill="url(#liveCell)"/> <!-- Row 2, col 2 --> <rect x="310" y="310" width="76" height="76" rx="12" fill="url(#liveCell)"/> </g> <!-- Highlight sheen on live cells --> <rect x="220" y="128" width="72" height="36" rx="10" fill="rgba(255,255,255,0.12)"/> <rect x="312" y="220" width="72" height="36" rx="10" fill="rgba(255,255,255,0.12)"/> <rect x="128" y="312" width="72" height="36" rx="10" fill="rgba(255,255,255,0.12)"/> <rect x="220" y="312" width="72" height="36" rx="10" fill="rgba(255,255,255,0.12)"/> <rect x="312" y="312" width="72" height="36" rx="10" fill="rgba(255,255,255,0.12)"/> <!-- Ghost trail cells (faded previous positions to imply motion) --> <rect x="126" y="218" width="76" height="76" rx="12" fill="#e8a020" opacity="0.06"/> <rect x="218" y="218" width="76" height="76" rx="12" fill="#e8a020" opacity="0.04"/> </g> <!-- Top edge highlight --> <rect x="8" y="8" width="496" height="248" rx="108" ry="108" fill="none" stroke="rgba(255,255,255,0.03)" stroke-width="1" clip-path="inset(0 0 50% 0)"/> </svg>
HTML Content
<!DOCTYPE html> <html lang="en" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Conway's Game of Life</title> <style> :root { --ts: 0.35s; } [data-theme="dark"] { --bg: #08080d; --bg2: #0f0f17; --fg: #e4e2dd; --muted: #5e5e72; --accent: #e8a020; --accent-dim: rgba(232,160,32,0.1); --accent-glow: rgba(232,160,32,0.35); --cell: #e8a020; --cell-glow: rgba(232,160,32,0.25); --gridline: rgba(255,255,255,0.035); --card: #14141e; --border: rgba(255,255,255,0.07); --btn: #1a1a28; --btn-h: #24243a; --btn-a: #2e2e48; --cvs-bg: #0b0b12; --shadow: rgba(0,0,0,0.6); --orb1: rgba(232,160,32,0.06); --orb2: rgba(200,80,40,0.04); } [data-theme="light"] { --bg: #f2f0eb; --bg2: #e8e5de; --fg: #1a1a28; --muted: #8a8a9e; --accent: #0c8c7f; --accent-dim: rgba(12,140,127,0.08); --accent-glow: rgba(12,140,127,0.25); --cell: #0c8c7f; --cell-glow: rgba(12,140,127,0.15); --gridline: rgba(0,0,0,0.055); --card: #ffffff; --border: rgba(0,0,0,0.09); --btn: #ffffff; --btn-h: #f0ede7; --btn-a: #e5e1da; --cvs-bg: #faf8f4; --shadow: rgba(0,0,0,0.08); --orb1: rgba(12,140,127,0.05); --orb2: rgba(12,180,160,0.04); } * { margin:0; padding:0; box-sizing:border-box; } body { font-family: 'Segoe UI', -apple-system, sans-serif; background: var(--bg); color: var(--fg); height: 100vh; display: flex; flex-direction: column; overflow: hidden; transition: background var(--ts), color var(--ts); } /* === HEADER === */ header { display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; background: var(--bg2); border-bottom: 1px solid var(--border); transition: background var(--ts), border-color var(--ts); flex-shrink: 0; z-index: 10; } .logo { display:flex; align-items:center; gap:10px; } .logo-grid { width: 26px; height: 26px; display: grid; grid-template-columns: repeat(3,1fr); gap: 2px; } .logo-grid i { width: 100%; height: 100%; border-radius: 2px; background: var(--muted); transition: background var(--ts); font-style: normal; } .logo-grid i.on { background: var(--accent); box-shadow: 0 0 6px var(--accent-glow); } .logo h1 { font-size: 17px; font-weight: 700; letter-spacing: -0.4px; } .logo h1 em { font-style:normal; color:var(--accent); transition:color var(--ts); } .stats { display:flex; gap:24px; } .stat { display:flex; flex-direction:column; align-items:center; gap:1px; } .stat-lbl { font-size: 9px; text-transform:uppercase; letter-spacing:1.2px; color: var(--muted); transition: color var(--ts); } .stat-val { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--accent); transition: color var(--ts); line-height: 1.1; } .hdr-right { display:flex; gap:10px; align-items:center; } /* Theme toggle switch */ .theme-sw { width: 46px; height: 26px; border-radius: 13px; background: var(--btn); border: 1px solid var(--border); cursor: pointer; position: relative; transition: all var(--ts); flex-shrink: 0; } .theme-sw::before { content: ''; position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); top: 7px; left: 7px; transition: all var(--ts); } .theme-sw::after { content: ''; position: absolute; width: 20px; height: 20px; border-radius: 50%; background: var(--accent); top: 2px; left: 2px; transition: transform var(--ts), background var(--ts); box-shadow: 0 1px 4px var(--shadow); } [data-theme="light"] .theme-sw::after { transform: translateX(20px); } [data-theme="light"] .theme-sw::before { background: var(--fg); opacity:0.3; } /* === CANVAS AREA === */ .canvas-area { flex: 1; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; padding: 12px; } /* Ambient orbs */ .orb { position: absolute; border-radius: 50%; filter: blur(80px); pointer-events: none; transition: background var(--ts); } .orb-1 { width: 400px; height: 400px; background: var(--orb1); top: -100px; left: -80px; animation: orbFloat1 12s ease-in-out infinite; } .orb-2 { width: 350px; height: 350px; background: var(--orb2); bottom: -80px; right: -60px; animation: orbFloat2 15s ease-in-out infinite; } .orb-3 { width: 200px; height: 200px; background: var(--orb1); top: 50%; left: 60%; animation: orbFloat3 10s ease-in-out infinite; } @keyframes orbFloat1 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(40px,30px) scale(1.15); } } @keyframes orbFloat2 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(-30px,-40px) scale(1.1); } } @keyframes orbFloat3 { 0%,100% { transform: translate(0,0) scale(0.8); opacity:0.5; } 50% { transform: translate(-20px,20px) scale(1.1); opacity:1; } } canvas { border-radius: 10px; box-shadow: 0 4px 40px var(--shadow), 0 0 0 1px var(--border); cursor: crosshair; transition: box-shadow var(--ts); position: relative; z-index: 1; max-width: 100%; max-height: 100%; } /* === CONTROLS === */ .controls { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; background: var(--bg2); border-top: 1px solid var(--border); transition: background var(--ts), border-color var(--ts); flex-wrap: wrap; flex-shrink: 0; z-index: 10; } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--btn); color: var(--fg); font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.12s; user-select: none; white-space: nowrap; font-family: inherit; outline: none; } .btn:hover { background:var(--btn-h); border-color:color-mix(in srgb, var(--accent) 50%, var(--border)); } .btn:active { background:var(--btn-a); transform:scale(0.96); } .btn:focus-visible { box-shadow: 0 0 0 2px var(--accent); } .btn.primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 700; } .btn.primary:hover { filter:brightness(1.1); border-color:var(--accent); } .btn svg { width:15px; height:15px; fill:currentColor; flex-shrink:0; } .sep { width:1px; height:26px; background:var(--border); margin:0 2px; flex-shrink:0; } .spd { display:flex; align-items:center; gap:8px; } .spd label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.6px; transition:color var(--ts); } input[type="range"] { -webkit-appearance:none; appearance:none; width:90px; height:4px; border-radius:2px; background:var(--border); outline:none; transition: background var(--ts); } input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; width:14px; height:14px; border-radius:50%; background:var(--accent); cursor:pointer; box-shadow:0 0 8px var(--accent-glow); } input[type="range"]::-moz-range-thumb { width:14px; height:14px; border-radius:50%; background:var(--accent); cursor:pointer; border:none; box-shadow:0 0 8px var(--accent-glow); } select { padding:8px 10px; border-radius:8px; border:1px solid var(--border); background:var(--btn); color:var(--fg); font-size:13px; cursor:pointer; transition:all 0.12s; outline:none; font-family:inherit; } select:hover, select:focus { border-color:var(--accent); } select:focus-visible { box-shadow:0 0 0 2px var(--accent); } /* === TOAST === */ .toast { position:fixed; bottom:70px; left:50%; transform:translateX(-50%) translateY(16px); background:var(--card); color:var(--fg); padding:9px 20px; border-radius:8px; border:1px solid var(--border); box-shadow:0 8px 30px var(--shadow); font-size:13px; font-weight:500; opacity:0; pointer-events:none; transition:all 0.25s cubic-bezier(.4,0,.2,1); z-index:100; } .toast.show { opacity:1; transform:translateX(-50%) translateY(0); } /* === KEYBOARD HINT === */ .hint { position:fixed; bottom:62px; right:16px; font-size:10px; color:var(--muted); letter-spacing:0.3px; opacity:0.5; transition:color var(--ts); z-index:10; text-align:right; line-height:1.6; } .hint kbd { display:inline-block; padding:1px 5px; border-radius:3px; border:1px solid var(--border); background:var(--btn); font-family:inherit; font-size:10px; } /* === RESPONSIVE === */ @media (max-width:700px) { header { padding:8px 12px; } .stats { gap:16px; } .stat-val { font-size:16px; } .stat-lbl { font-size:8px; } .canvas-area { padding:6px; } .controls { padding:8px 10px; gap:4px; } .btn { padding:7px 10px; font-size:12px; } .spd label { display:none; } input[type="range"] { width:60px; } .sep { display:none; } .hint { display:none; } } @media (max-width:440px) { .logo h1 { font-size:14px; } .stat-val { font-size:14px; } .btn .txt { display:none; } .btn svg { width:18px; height:18px; } .btn { padding:8px 9px; } select { font-size:12px; padding:7px 8px; } } @media (prefers-reduced-motion:reduce) { *, *::before, *::after { transition:none!important; animation:none!important; } } </style> </head> <body> <header> <div class="logo"> <div class="logo-grid" aria-hidden="true"> <i></i><i class="on"></i><i></i> <i></i><i></i><i class="on"></i> <i class="on"></i><i class="on"></i><i class="on"></i> </div> <h1>Game of <em>Life</em></h1> </div> <div class="stats"> <div class="stat"> <span class="stat-lbl">Generation</span> <span class="stat-val" id="sGen">0</span> </div> <div class="stat"> <span class="stat-lbl">Population</span> <span class="stat-val" id="sPop">0</span> </div> </div> <div class="hdr-right"> <div class="theme-sw" id="thmSw" role="button" tabindex="0" aria-label="Toggle theme"></div> </div> </header> <main class="canvas-area"> <div class="orb orb-1" aria-hidden="true"></div> <div class="orb orb-2" aria-hidden="true"></div> <div class="orb orb-3" aria-hidden="true"></div> <canvas id="cvs"></canvas> </main> <nav class="controls" aria-label="Game controls"> <button class="btn primary" id="bPlay" aria-label="Play or Pause"> <svg id="icPlay" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21"/></svg> <svg id="icPause" viewBox="0 0 24 24" style="display:none"><rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/></svg> <span class="txt">Play</span> </button> <button class="btn" id="bStep" aria-label="Step one generation"> <svg viewBox="0 0 24 24"><polygon points="4,3 14,12 4,21"/><rect x="16" y="3" width="3" height="18"/></svg> <span class="txt">Step</span> </button> <div class="sep"></div> <button class="btn" id="bClear" aria-label="Clear grid"> <svg viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14zM10 11v6M14 11v6"/></svg> <span class="txt">Clear</span> </button> <button class="btn" id="bRand" aria-label="Randomize grid"> <svg viewBox="0 0 24 24"><path d="M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22M2 6h1.4c1.3 0 2.5.6 3.3 1.7l.6.9M22 18h-5.9c-1.3 0-2.5-.6-3.3-1.7l-.6-.9"/></svg> <span class="txt">Random</span> </button> <div class="sep"></div> <div class="spd"> <label for="spdR">Speed</label> <input type="range" id="spdR" min="1" max="60" value="12" aria-label="Speed"> </div> <div class="sep"></div> <select id="patSel" aria-label="Insert pattern"> <option value="">Patterns</option> <option value="glider">Glider</option> <option value="lwss">Spaceship</option> <option value="pulsar">Pulsar</option> <option value="penta">Pentadecathlon</option> <option value="gosper">Gosper Gun</option> <option value="acorn">Acorn</option> <option value="rpent">R-Pentomino</option> <option value="diehard">Diehard</option> </select> </nav> <div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="hint" aria-hidden="true"> <kbd>Space</kbd> play/pause <kbd>N</kbd> step <kbd>R</kbd> random <kbd>C</kbd> clear <kbd>G</kbd> grid </div> <script> /* ============================================= Conway's Game of Life — Pure JS Engine ============================================= */ const cvs = document.getElementById('cvs'); const ctx = cvs.getContext('2d'); // --- State --- let cellSz = 10, cols = 0, rows = 0; let grid, nxt; let running = false, gen = 0, pop = 0; let speed = 12, lastT = 0, accum = 0, rafId = null; let drawing = false, drawVal = 1, showGrid = true; // --- DOM --- const sGen = document.getElementById('sGen'); const sPop = document.getElementById('sPop'); const bPlay = document.getElementById('bPlay'); const icPlay = document.getElementById('icPlay'); const icPause = document.getElementById('icPause'); const bStep = document.getElementById('bStep'); const bClear = document.getElementById('bClear'); const bRand = document.getElementById('bRand'); const spdR = document.getElementById('spdR'); const patSel = document.getElementById('patSel'); const thmSw = document.getElementById('thmSw'); const toastEl = document.getElementById('toast'); // --- Colors (read from CSS vars) --- let C = {}; function readColors() { const s = getComputedStyle(document.documentElement); C.bg = s.getPropertyValue('--cvs-bg').trim(); C.cell = s.getPropertyValue('--cell').trim(); C.glow = s.getPropertyValue('--cell-glow').trim(); C.grid = s.getPropertyValue('--gridline').trim(); } // --- Theme --- thmSw.addEventListener('click', () => { const h = document.documentElement; h.setAttribute('data-theme', h.dataset.theme === 'dark' ? 'light' : 'dark'); requestAnimationFrame(() => { readColors(); render(); }); }); thmSw.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); thmSw.click(); } }); // --- Grid init --- function initGrid() { const wrap = document.querySelector('.canvas-area'); const mw = wrap.clientWidth - 24; const mh = wrap.clientHeight - 24; // Target ~100 cols on desktop, fewer on mobile let target = Math.min(120, Math.max(25, Math.floor(mw / 9))); cellSz = Math.max(4, Math.floor(mw / target)); cellSz = Math.min(cellSz, Math.floor(mh / Math.max(15, Math.floor(target * 0.55)))); cellSz = Math.max(4, cellSz); cols = Math.max(10, Math.min(250, Math.floor(mw / cellSz))); rows = Math.max(10, Math.min(180, Math.floor(mh / cellSz))); cvs.width = cols * cellSz; cvs.height = rows * cellSz; const sz = cols * rows; grid = new Uint8Array(sz); nxt = new Uint8Array(sz); gen = 0; pop = 0; stats(); } const ix = (x, y) => y * cols + x; function getCell(x, y) { return grid[((y % rows + rows) % rows) * cols + ((x % cols + cols) % cols)]; } function setCell(x, y, v) { if (x >= 0 && x < cols && y >= 0 && y < rows) grid[ix(x, y)] = v; } // --- Rules --- function neighbors(x, y) { let n = 0; for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) if (dx || dy) n += getCell(x + dx, y + dy); return n; } function step() { let p = 0; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const i = ix(x, y), nb = neighbors(x, y), alive = grid[i]; nxt[i] = alive ? (nb === 2 || nb === 3 ? 1 : 0) : (nb === 3 ? 1 : 0); p += nxt[i]; } } const t = grid; grid = nxt; nxt = t; gen++; pop = p; stats(); } function countPop() { let p = 0; for (let i = 0; i < grid.length; i++) p += grid[i]; pop = p; stats(); } function stats() { sGen.textContent = gen.toLocaleString(); sPop.textContent = pop.toLocaleString(); } // --- Render --- function render() { const { bg, cell, glow, grid: gc } = C; ctx.fillStyle = bg; ctx.fillRect(0, 0, cvs.width, cvs.height); // Grid lines if (showGrid && cellSz >= 6) { ctx.strokeStyle = gc; ctx.lineWidth = 0.5; ctx.beginPath(); for (let x = 0; x <= cols; x++) { const px = x * cellSz + 0.5; ctx.moveTo(px, 0); ctx.lineTo(px, cvs.height); } for (let y = 0; y <= rows; y++) { const py = y * cellSz + 0.5; ctx.moveTo(0, py); ctx.lineTo(cvs.width, py); } ctx.stroke(); } // Cells if (cellSz >= 8) { ctx.shadowColor = glow; ctx.shadowBlur = cellSz >= 14 ? 10 : 5; ctx.fillStyle = cell; const pad = Math.max(1, Math.floor(cellSz * 0.08)); const sz = cellSz - pad * 2; const r = Math.max(1, Math.min(4, Math.floor(sz * 0.18))); for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { if (grid[ix(x, y)]) { ctx.beginPath(); if (ctx.roundRect) { ctx.roundRect(x * cellSz + pad, y * cellSz + pad, sz, sz, r); } else { ctx.rect(x * cellSz + pad, y * cellSz + pad, sz, sz); } ctx.fill(); } } } ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; } else { ctx.fillStyle = cell; for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) if (grid[ix(x, y)]) ctx.fillRect(x * cellSz, y * cellSz, cellSz - 1, cellSz - 1); } } // --- Loop --- function loop(ts) { if (!running) return; if (!lastT) lastT = ts; const dt = ts - lastT; lastT = ts; accum += dt; const iv = 1000 / speed; let steps = 0; while (accum >= iv && steps < 4) { step(); accum -= iv; steps++; } if (accum > iv * 4) accum = 0; render(); rafId = requestAnimationFrame(loop); } function play() { if (running) return; running = true; lastT = 0; accum = 0; icPlay.style.display = 'none'; icPause.style.display = ''; bPlay.querySelector('.txt').textContent = 'Pause'; rafId = requestAnimationFrame(loop); } function pause() { running = false; if (rafId) { cancelAnimationFrame(rafId); rafId = null; } icPlay.style.display = ''; icPause.style.display = 'none'; bPlay.querySelector('.txt').textContent = 'Play'; } // --- Drawing --- function gPos(e) { const r = cvs.getBoundingClientRect(); const sx = cvs.width / r.width, sy = cvs.height / r.height; let cx, cy; if (e.touches) { cx = e.touches[0].clientX; cy = e.touches[0].clientY; } else { cx = e.clientX; cy = e.clientY; } return { x: Math.floor((cx - r.left) * sx / cellSz), y: Math.floor((cy - r.top) * sy / cellSz) }; } function onDown(e) { e.preventDefault(); const { x, y } = gPos(e); if (x >= 0 && x < cols && y >= 0 && y < rows) { drawing = true; drawVal = grid[ix(x, y)] ? 0 : 1; setCell(x, y, drawVal); countPop(); render(); } } function onMove(e) { if (!drawing) return; e.preventDefault(); const { x, y } = gPos(e); if (x >= 0 && x < cols && y >= 0 && y < rows) { setCell(x, y, drawVal); countPop(); render(); } } function onUp() { drawing = false; } cvs.addEventListener('mousedown', onDown); cvs.addEventListener('mousemove', onMove); cvs.addEventListener('mouseup', onUp); cvs.addEventListener('mouseleave', onUp); cvs.addEventListener('touchstart', onDown, { passive: false }); cvs.addEventListener('touchmove', onMove, { passive: false }); cvs.addEventListener('touchend', onUp); // --- Buttons --- bPlay.addEventListener('click', running ? pause : play); bStep.addEventListener('click', () => { if (running) pause(); step(); render(); }); bClear.addEventListener('click', () => { pause(); grid.fill(0); gen = 0; pop = 0; stats(); render(); toast('Grid cleared'); }); bRand.addEventListener('click', () => { const d = 0.2 + Math.random() * 0.12; for (let i = 0; i < grid.length; i++) grid[i] = Math.random() < d ? 1 : 0; gen = 0; countPop(); render(); toast('Random pattern generated'); }); spdR.addEventListener('input', e => { speed = parseInt(e.target.value, 10); }); // --- Patterns --- const PAT = { glider: { n:'Glider', c:[[0,0],[1,0],[2,0],[2,-1],[1,-2]] }, lwss: { n:'Spaceship', c:[[0,0],[3,0],[4,1],[4,2],[4,3],[3,3],[2,3],[1,3],[0,2]] }, pulsar: { n:'Pulsar', c:(() => { const q=[[2,0],[3,0],[4,0],[0,2],[0,3],[0,4],[2,5],[3,5],[4,5],[5,2],[5,3],[5,4]]; const s=new Set(); q.forEach(([x,y])=>{[[x,y],[-x,y],[x,-y],[-x,-y]].forEach(p=>s.add(p[0]+','+p[1]));}); return [...s].map(v=>v.split(',').map(Number)); })() }, penta: { n:'Pentadecathlon', c:[[0,0],[1,0],[-1,0],[0,1],[0,-1],[-2,2],[2,2],[-2,-2],[2,-2],[-3,0],[3,0]] }, gosper: { n:'Gosper Gun', c:[ [0,4],[0,5],[1,4],[1,5], [10,4],[10,5],[10,6],[11,3],[11,7],[12,2],[12,8],[13,2],[13,8], [14,5],[15,3],[15,7],[16,4],[16,5],[16,6],[17,5], [20,2],[20,3],[20,4],[21,2],[21,3],[21,4],[22,1],[22,5], [24,0],[24,1],[24,5],[24,6], [34,2],[34,3],[35,2],[35,3] ]}, acorn: { n:'Acorn', c:[[0,0],[1,0],[1,-2],[3,-1],[4,0],[5,0],[6,0]] }, rpent: { n:'R-Pentomino', c:[[1,0],[2,0],[0,1],[1,1],[1,-1]] }, diehard: { n:'Diehard', c:[[0,0],[1,0],[1,1],[5,1],[6,1],[7,1],[6,-1]] } }; patSel.addEventListener('change', e => { const k = e.target.value; if (!k || !PAT[k]) return; const p = PAT[k].c; let mnx=1e9,mxx=-1e9,mny=1e9,mxy=-1e9; p.forEach(([x,y])=>{ mnx=Math.min(mnx,x); mxx=Math.max(mxx,x); mny=Math.min(mny,y); mxy=Math.max(mxy,y); }); const ox = Math.floor(cols/2) - Math.floor((mxx-mnx+1)/2) - mnx; const oy = Math.floor(rows/2) - Math.floor((mxy-mny+1)/2) - mny; p.forEach(([x,y]) => setCell(x+ox, y+oy, 1)); countPop(); render(); toast(PAT[k].n + ' placed'); patSel.value = ''; }); // --- Toast --- let tTO = null; function toast(msg) { toastEl.textContent = msg; toastEl.classList.add('show'); if (tTO) clearTimeout(tTO); tTO = setTimeout(() => toastEl.classList.remove('show'), 2000); } // --- Keyboard --- document.addEventListener('keydown', e => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; switch (e.key) { case ' ': e.preventDefault(); running ? pause() : play(); break; case 'n': case 'N': if (running) pause(); step(); render(); break; case 'c': case 'C': bClear.click(); break; case 'r': case 'R': bRand.click(); break; case 'g': case 'G': showGrid = !showGrid; render(); toast(showGrid ? 'Grid visible' : 'Grid hidden'); break; } }); // --- Resize --- let rTO = null; window.addEventListener('resize', () => { if (rTO) clearTimeout(rTO); rTO = setTimeout(() => { const og = grid, oc = cols, or2 = rows; initGrid(); if (og) { const mc = Math.min(oc, cols), mr = Math.min(or2, rows); for (let y = 0; y < mr; y++) for (let x = 0; x < mc; x++) grid[ix(x, y)] = og[y * oc + x]; countPop(); } render(); }, 200); }); // --- Boot --- readColors(); initGrid(); // Seed with a random pattern const d = 0.22; for (let i = 0; i < grid.length; i++) grid[i] = Math.random() < d ? 1 : 0; countPop(); render(); // Auto-play after a moment setTimeout(play, 600); </script> </body> </html>
Cancel