SOURCE / PINNED RELEASE
Made of little things.
Conway's Game of Life
- Release
- 230955df1900…
- Author-recorded commit
- c08196c2b050…
- License
- Check the original source
- Author’s source reference
- nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-644c45e22c7
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Listing description: byte-identical to napplet.json `description`
(pinned by tests/guidance.test.mjs; frozen with the player at Phase 9). -->
<meta name="description" content="Conway's Game of Life on a wrapping 120 by 90 board: paint cells, drop classic patterns like the Gosper glider gun, and watch it evolve at your chosen speed." />
<title>Conway's Game of Life</title>
<script type="module" crossorigin>var $e=Object.defineProperty;var Me=(e,t,n)=>t in e?$e(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var L=(e,t,n)=>Me(e,typeof t!="symbol"?t+"":t,n);function Re(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function v(e){const n=Re()[e];if(!n)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return n}var _={getItem(e){return v("storage").getItem(e)},setItem(e,t){return v("storage").setItem(e,t)},removeItem(e){return v("storage").removeItem(e)},keys(){return v("storage").keys()},instance:{getItem(e){return v("storage").instance.getItem(e)},setItem(e,t){return v("storage").instance.setItem(e,t)},removeItem(e){return v("storage").instance.removeItem(e)},keys(){return v("storage").instance.keys()}}},q={get(){return v("config").get()},subscribe(e){return v("config").subscribe(e)},openSettings(e){v("config").openSettings(e)},registerSchema(e,t){return v("config").registerSchema(e,t)},onSchemaError(e){return v("config").onSchemaError(e)},get schema(){return v("config").schema}};function Le(e,t){return!!(e!=null&&e[t])}function j(e){return Le(window.napplet,e)}const Pe="http://json-schema.org/draft-07/schema#",Ae=1,Ie="object",Ce=!1,_e={textSize:{type:"integer",title:"Text size",description:"Base text size in pixels. Changes apply while the napplet is running.",minimum:10,maximum:20,default:13,"x-napplet-section":"Appearance","x-napplet-order":1},controlSize:{type:"integer",title:"Control height",description:"Choose compact controls or larger touch targets, in pixels.",minimum:24,maximum:44,default:28,"x-napplet-section":"Appearance","x-napplet-order":2},selectText:{type:"boolean",title:"Allow text selection",description:"Allow selecting interface text. Text inputs always remain selectable.",default:!1,"x-napplet-section":"Interaction"}},De={$schema:Pe,$version:Ae,type:Ie,additionalProperties:Ce,properties:_e};j("config")&&(async()=>{q.schema||await q.registerSchema(De);const e=q.subscribe(t=>{const n=document.documentElement.style,o=typeof t.textSize=="number"?t.textSize:13,s=typeof t.controlSize=="number"?t.controlSize:28;n.fontSize=`${o}px`,n.setProperty("--control-h",`${s}px`),n.setProperty("--napplet-text-selection",t.selectText===!0?"text":"none")});window.addEventListener("pagehide",()=>e.close(),{once:!0})})().catch(()=>{});function Te(e,t,n){const o=new Map,s={lastCell:null};function c(r,i){const a=e.getBoundingClientRect();if(a.width===0||a.height===0)return null;const f=Math.min(t.w-1,Math.max(0,Math.floor((r-a.left)/a.width*t.w))),g=Math.min(t.h-1,Math.max(0,Math.floor((i-a.top)/a.height*t.h)));return{x:f,y:g}}function p(r,i,a){return t.get(r,i)===a?!1:(t.set(r,i,a),!0)}function d(r,i){let a=r.lastX,f=r.lastY;const g=Math.abs(i.x-a),w=Math.abs(i.y-f),S=a<i.x?1:-1,b=f<i.y?1:-1;let E=g-w,I=!1;for(;p(a,f,r.value)&&(I=!0),!(a===i.x&&f===i.y);){const G=2*E;G>-w&&(E-=w,a+=S),G<g&&(E+=g,f+=b)}return r.lastX=i.x,r.lastY=i.y,I}e.addEventListener("pointerdown",r=>{var g;const i=c(r.clientX,r.clientY);if(!i||(s.lastCell=i,(g=n.interceptPointerDown)!=null&&g.call(n,i,r.button)))return;const a=r.button===2?0:1;o.set(r.pointerId,{value:a,lastX:i.x,lastY:i.y});try{e.setPointerCapture(r.pointerId)}catch{}p(i.x,i.y,a)&&(n.requestRedraw(),n.onPaint())}),e.addEventListener("pointermove",r=>{var f;const i=o.get(r.pointerId),a=c(r.clientX,r.clientY);if(!i){(f=n.onHover)==null||f.call(n,a);return}a&&d(i,a)&&(n.requestRedraw(),n.onPaint())}),e.addEventListener("pointerleave",()=>{var r;return(r=n.onHover)==null?void 0:r.call(n,null)});function h(r){o.delete(r.pointerId)}return e.addEventListener("pointerup",h),e.addEventListener("pointercancel",h),e.addEventListener("contextmenu",r=>{r.preventDefault()}),s}const te=3,k=2,K=3,Ne=120,Ue=90,ie=new Map;function Ye(e,t){ie.set(e,t)}function J(e){const t=e.replace(/^x\s*=\s*\d+\s*,\s*y\s*=\s*\d+\s*,\s*/,""),n=[];let o=[],s=0,c=0;for(const r of t){if(r==="!")break;if(r>="0"&&r<="9"){s=s*10+(r.charCodeAt(0)-48);continue}const i=s===0?1:s;if(s=0,r==="b")for(let a=0;a<i;a++)o.push(0);else if(r==="o")for(let a=0;a<i;a++)o.push(1);else if(r==="$"){o.length>c&&(c=o.length),n.push(o);for(let a=1;a<i;a++)n.push([]);o=[]}}o.length>c&&(c=o.length),(o.length>0||n.length===0)&&n.push(o);const p=c,d=n.length,h=new Uint8Array(p*d);for(let r=0;r<d;r++){const i=n[r];for(let a=0;a<i.length;a++)h[r*p+a]=i[a]}return{w:p,h:d,cells:h}}function Be(e,t,n){if(e.length!==t*n)throw new RangeError(`encodeRle: grid length ${e.length} does not match ${t}x${n}`);let o=`x=${t}, y=${n}, `;for(let s=0;s<n;s++){const c=s*t;let p=e[c],d=0;for(let h=0;h<=t;h++){const r=h<t?e[c+h]:-1;if(r===p){d++;continue}d!==1&&(o+=d),o+=p===1?"o":"b",p=r,d=1}o+="$"}return o+"!"}class Ge{constructor(t=Ne,n=Ue){L(this,"w");L(this,"h");L(this,"grid");L(this,"next");L(this,"alive",0);L(this,"generation",0);if(!Number.isInteger(t)||!Number.isInteger(n)||t<1||n<1)throw new RangeError(`grid must be a positive integer grid, got ${t}x${n}`);this.w=t,this.h=n,this.grid=new Uint8Array(t*n),this.next=new Uint8Array(t*n)}get(t,n){return this.grid[this.wrapY(n)*this.w+this.wrapX(t)]}set(t,n,o){const s=this.wrapY(n)*this.w+this.wrapX(t),c=o?1:0;this.grid[s]!==c&&(this.grid[s]=c,this.alive+=c===1?1:-1)}clear(){this.grid.fill(0),this.alive=0,this.generation=0}random(t=.15){let n=0;for(let o=0;o<this.grid.length;o++){const s=Math.random()<t?1:0;this.grid[o]=s,n+=s}this.alive=n}aliveCount(){return this.alive}countNeighbors(t,n){const o=this.w,s=this.wrapX(t-1),c=this.wrapX(t),p=this.wrapX(t+1);let d=0;for(const h of[this.wrapY(n-1),this.wrapY(n),this.wrapY(n+1)]){const r=h*o;d+=this.grid[r+s]+this.grid[r+c]+this.grid[r+p]}return d-=this.grid[this.wrapY(n)*o+c],d}step(){const t=this.w,n=this.h,o=this.grid,s=this.next;let c=0;for(let p=0;p<n;p++){const d=p===0?n-1:p-1,h=p===n-1?0:p+1,r=d*t,i=p*t,a=h*t;for(let f=0;f<t;f++){const g=f===0?t-1:f-1,w=f===t-1?0:f+1,S=o[r+g]+o[r+f]+o[r+w]+o[i+g]+o[i+w]+o[a+g]+o[a+f]+o[a+w],b=o[i+f];let E;b===1?E=S>=k&&S<=K?1:0:E=S===te?1:0,s[i+f]=E,c+=E}}this.grid=s,this.next=o,this.alive=c,this.generation++}stable(){const t=this.w,n=this.h,o=this.grid;for(let s=0;s<n;s++){const c=s===0?n-1:s-1,p=s===n-1?0:s+1,d=c*t,h=s*t,r=p*t;for(let i=0;i<t;i++){const a=i===0?t-1:i-1,f=i===t-1?0:i+1,g=o[d+a]+o[d+i]+o[d+f]+o[h+a]+o[h+f]+o[r+a]+o[r+i]+o[r+f],w=o[h+i];if((w===1?g>=k&&g<=K?1:0:g===te?1:0)!==w)return!1}}return!0}patternAt(t,n,o){const s=ie.get(t);if(s===void 0)throw new RangeError(`unknown pattern: ${t}`);return this.stampRle(s,n,o)}stampRle(t,n,o){const{w:s,h:c,cells:p}=J(t);let d=0;for(let h=0;h<c;h++)for(let r=0;r<s;r++)if(p[h*s+r]===1){const i=this.wrapY(o+h)*this.w+this.wrapX(n+r);this.grid[i]!==1&&(this.grid[i]=1,this.alive++),d++}return d}wrapX(t){return(t%this.w+this.w)%this.w}wrapY(t){return(t%this.h+this.h)%this.h}}const se="gosper-glider-gun",Oe="24bo11b$22bobo11b$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o14b$2o8bo3bob2o4bobo11b$10bo5bo7bo11b$11bo3bo20b$12b2o22b!",Xe=3,He=3,O=[{id:se,name:"Gosper glider gun",rle:Oe,w:36,h:9,pop:36,period:null},{id:"glider",name:"Glider",rle:"bob$2bo$3o!",w:3,h:3,pop:5,period:null},{id:"lwss",name:"Lightweight spaceship",rle:"bo2bo$o4b$o3bo$4o!",w:5,h:4,pop:9,period:null},{id:"pulsar",name:"Pulsar",rle:"2b3o3b3o2b2$o4bobo4bo$o4bobo4bo$o4bobo4bo$2b3o3b3o2b2$2b3o3b3o2b$o4bobo4bo$o4bobo4bo$o4bobo4bo2$2b3o3b3o!",w:13,h:13,pop:48,period:3},{id:"pentadecathlon",name:"Pentadecathlon",rle:"2bo4bo2b$2ob4ob2o$2bo4bo!",w:10,h:3,pop:12,period:15},{id:"block",name:"Block",rle:"2o$2o!",w:2,h:2,pop:4,period:1},{id:"beacon",name:"Beacon",rle:"2o$2o$2b2o$2b2o!",w:4,h:4,pop:8,period:2},{id:"beehive",name:"Beehive",rle:"b2o$o2bo$b2o!",w:4,h:3,pop:6,period:1},{id:"r-pentomino",name:"R-pentomino",rle:"b2o$2o$bo!",w:3,h:3,pop:5,period:null}],ae={grid:"#1e1e3c",dead:"#0a0a28",alive:"#ffffd7",aboutToDie:"#c8c8e1"};function Ve(e,t,n,o=ae,s=null){const c=t.w,p=t.h,d=t.grid;e.fillStyle=o.grid,e.fillRect(0,0,c*n,p*n);const h=n-1;for(let r=0;r<p;r++){const i=r===0?p-1:r-1,a=r===p-1?0:r+1,f=i*c,g=r*c,w=a*c,S=r*n;for(let b=0;b<c;b++){const E=b===0?c-1:b-1,I=b===c-1?0:b+1,G=g+b;let z;if(d[G]===1){const ee=d[f+E]+d[f+b]+d[f+I]+d[g+E]+d[g+I]+d[w+E]+d[w+b]+d[w+I];z=ee>=k&&ee<=K?o.alive:o.aboutToDie}else z=o.dead;e.fillStyle=z,e.fillRect(b*n,S,h,h)}}if(s){e.save(),e.globalAlpha=.5,e.fillStyle=o.alive;for(let r=0;r<s.h;r++)for(let i=0;i<s.w;i++){if(s.cells[r*s.w+i]!==1)continue;const a=((s.x+i)%c+c)%c,f=((s.y+r)%p+p)%p;e.fillRect(a*n,f*n,h,h)}e.restore()}}function ze(e,t,n){return Math.max(4,Math.floor(Math.min(e/n.w,t/n.h)))}const le="game-of-life:board",ce="game-of-life:speed",ue="game-of-life:generation",de=1,pe=30;function Y(){return j("storage")}async function qe(e,t,n){if(!Y())return null;try{const[o,s,c]=await Promise.all([_.getItem(le),_.getItem(ce),_.getItem(ue)]);if(o===null&&s===null&&c===null||typeof o!="string")return null;const p=J(o);if(p.w!==e||p.h!==t)return null;const d=Math.round(Number(s)),h=s===null||!Number.isFinite(d)?n:Math.min(pe,Math.max(de,d)),r=Math.max(0,Math.trunc(Number(c))||0);return{boardRle:o,speed:h,generation:r}}catch{return null}}async function Fe(e,t,n){if(!Y())return!1;try{return await _.setItem(le,e),await _.setItem(ce,String(t)),await _.setItem(ue,String(n)),!0}catch{return!1}}const Q=10,ke=5,l={board:m("#board"),canvas:m("#cells"),generation:m("#generation"),population:m("#population"),status:m("#status"),playPause:m("#play-pause"),step:m("#step"),clear:m("#clear"),random:m("#random"),speed:m("#speed"),speedValue:m("#speed-value"),saveStatus:m("#save-status"),placeStatus:m("#place-status"),patterns:m("#patterns"),patternMenu:m("#pattern-menu"),patternList:m("#pattern-list"),menuClose:m("#menu-close"),welcome:m("#welcome"),welcomeStart:m("#welcome-start")};function m(e){const t=document.querySelector(e);if(!t)throw new Error(`Missing required element: ${e}`);return t}const u=new Ge;for(const e of O)Ye(e.id,e.rle);u.patternAt(se,Xe,He);let P=!1,D=Q;const Ke=.15;let x=!1,y=!0,$=8;const fe=We(l.canvas);function We(e){const t=e.getContext("2d");if(!t)throw new Error("Canvas 2D context unavailable");return t}function he(){const e=l.board.clientWidth,t=l.board.clientHeight;if(e===0||t===0)return;$=ze(e,t,u);const n=Math.max(1,window.devicePixelRatio||1);l.canvas.width=u.w*$*n,l.canvas.height=u.h*$*n;const o=Math.min(1,e/(u.w*$),t/(u.h*$));l.canvas.style.width=`${Math.floor(u.w*$*o)}px`,l.canvas.style.height=`${Math.floor(u.h*$*o)}px`,fe.setTransform(n,0,0,n,0,0),y=!0}new ResizeObserver(he).observe(l.board);he();const ge=new Map(O.map(e=>[e.id,e]));let N=null,M=null,T=null;function U(e){const t=e===null?void 0:ge.get(e);if(e!==null&&!t)throw new RangeError(`unknown pattern: ${e}`);N=t?t.id:null,M=t?J(t.rle):null,T=null,t?(l.placeStatus.textContent=`Placing ${t.name} — tap the board · Esc cancels`,l.placeStatus.hidden=!1):l.placeStatus.hidden=!0,y=!0}Te(l.canvas,u,{requestRedraw:()=>{y=!0},onPaint:()=>{x=u.stable(),R(),A()},interceptPointerDown:(e,t)=>{if(!N)return!1;const n=N;return t===2?U(null):(U(null),xe(n,e.x,e.y)),!0},onHover:e=>{T=e,M&&(y=!0)}});const me="cue-gen",we="cue-pop";let ne=-1,oe=-1;function re(e,t){e.classList.remove(t),e.offsetWidth,e.classList.add(t)}l.generation.addEventListener("animationend",()=>l.generation.classList.remove(me));l.population.addEventListener("animationend",()=>l.population.classList.remove(we));function R(){l.generation.textContent=String(u.generation),l.population.textContent=String(u.aliveCount()),u.generation!==ne&&(re(l.generation,me),ne=u.generation);const e=u.aliveCount();e!==oe&&(re(l.population,we),oe=e),l.status.hidden=!x}let W=performance.now(),C=0;function be(e){const t=Math.min(e-W,250);if(W=e,P){C+=t;const n=1e3/D;let o=0;for(;C>=n&&o<ke;)u.step(),C-=n,o++;C>n&&(C=0),o>0&&(x=u.stable(),R(),y=!0,A())}if(y){let n=null;M&&T&&(n={cells:M.cells,w:M.w,h:M.h,x:T.x-Math.floor(M.w/2),y:T.y-Math.floor(M.h/2)}),Ve(fe,u,$,ae,n),y=!1}requestAnimationFrame(be)}R();requestAnimationFrame(be);const je="⏵",Je="⏸";function ve(){l.playPause.textContent=P?Je:je,l.playPause.setAttribute("aria-label",P?"Pause":"Play")}function X(e){P=e,W=performance.now(),C=0,ve()}function Ee(){u.step(),x=u.stable(),R(),y=!0,A()}function ye(){u.clear(),x=u.stable(),R(),y=!0,A()}function Qe(){u.random(Ke),x=u.stable(),R(),y=!0,A()}function Ze(){const e=Math.round(Number(l.speed.value));D=Number.isFinite(e)?Math.min(pe,Math.max(de,e)):Q,l.speedValue.textContent=`${D}/s`,A()}l.playPause.addEventListener("click",()=>X(!P));l.step.addEventListener("click",Ee);l.clear.addEventListener("click",ye);l.random.addEventListener("click",Qe);l.speed.addEventListener("input",Ze);function xe(e,t,n){const o=ge.get(e);if(!o)throw new RangeError(`unknown pattern: ${e}`);const s=u.patternAt(e,t-Math.floor(o.w/2),n-Math.floor(o.h/2));return x=u.stable(),R(),y=!0,A(),s}function B(e){H=e,l.patternMenu.hidden=!e,l.patterns.setAttribute("aria-expanded",String(e)),e&&l.menuClose.focus()}let H=!1,Z=!0;function et(e){Z=e,l.welcome.hidden=!e}for(const e of O){const t=document.createElement("button");t.type="button",t.className="menu-item",t.dataset.pattern=e.id,t.textContent=e.name,l.patternList.appendChild(t)}l.patterns.addEventListener("click",()=>B(!H));l.menuClose.addEventListener("click",()=>B(!1));l.welcomeStart.addEventListener("click",()=>{et(!1),X(!0)});l.welcomeStart.focus();l.patternList.addEventListener("click",e=>{const t=e.target instanceof HTMLElement?e.target.closest("button[data-pattern]"):null;t!=null&&t.dataset.pattern&&(U(t.dataset.pattern),B(!1))});document.addEventListener("keydown",e=>{if(e.key==="Escape"){if(N){e.preventDefault(),U(null);return}if(H){e.preventDefault(),B(!1);return}}const t=e.target instanceof HTMLElement?e.target.tagName:"";t==="BUTTON"||t==="INPUT"||t==="SELECT"||t==="TEXTAREA"||e.ctrlKey||e.metaKey||e.altKey||Z||(e.key===" "?(e.preventDefault(),X(!P)):e.key==="ArrowRight"?(e.preventDefault(),Ee()):(e.key==="c"||e.key==="C")&&(e.preventDefault(),ye()))});ve();function Se(){l.saveStatus.hidden=!1}async function V(){const e=await Fe(Be(u.grid,u.w,u.h),D,u.generation);return!e&&Y()&&Se(),e}const tt=1e3;let F;function A(){!Y()||F!==void 0||(F=window.setTimeout(()=>{F=void 0,V()},tt))}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&V()});window.addEventListener("pagehide",()=>void V());Y()?qe(u.w,u.h,Q).then(e=>{e&&(u.clear(),u.stampRle(e.boardRle,0,0),u.generation=e.generation,D=e.speed,l.speed.value=String(e.speed),l.speedValue.textContent=`${e.speed}/s`,x=u.stable(),R(),y=!0)}):Se();window.game_of_life={engine:u,isRunning:()=>P,setRunning:e=>X(e),genPerSec:()=>D,isStable:()=>x,patternIds:()=>O.map(e=>e.id),stampPattern:(e,t,n)=>xe(e,t,n),menuIsOpen:()=>H,setMenuOpen:e=>B(e),pendingPattern:()=>N,setPendingPattern:e=>U(e),placeHintVisible:()=>!l.placeStatus.hidden,welcomeVisible:()=>Z,domains:{storage:j("storage")},saveNow:()=>V(),saveStatusVisible:()=>!l.saveStatus.hidden};
</script>
<style>:root{--scene-bg: #0a0a28;--scene-grid: #1e1e3c;--cell-alive: #ffffd7;--cell-dying: #c8c8e1;--fg: var(--cell-alive);--muted: color-mix(in srgb, var(--cell-alive) 55%, var(--scene-bg));--bg: var(--scene-bg);--border: color-mix(in srgb, var(--cell-dying) 18%, transparent);--space-1: 4px;--space-2: 8px;--napplet-text-selection: none;color-scheme:dark;font:13px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:var(--napplet-text-selection, none);user-select:var(--napplet-text-selection, none)}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg);overflow:hidden}.app{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:var(--space-1);padding:var(--space-2)}.hud{display:flex;align-items:baseline;gap:var(--space-2);min-height:18px;font-size:12px;color:var(--muted)}.hud .num{font-variant-numeric:tabular-nums;color:var(--fg)}.hud{flex-wrap:wrap}.hud-status{margin-left:auto;font-style:italic;color:var(--cell-dying)}.hud-status[hidden]{display:none}.hud-place{color:var(--cell-alive)}@keyframes gen-tick{0%{color:#fff;text-shadow:0 0 6px color-mix(in srgb,var(--cell-alive) 70%,transparent)}to{color:var(--fg);text-shadow:none}}@keyframes pop-pulse{0%{color:var(--cell-alive);text-shadow:0 0 8px color-mix(in srgb,var(--cell-alive) 60%,transparent)}to{color:var(--fg);text-shadow:none}}.num.cue-gen{animation:gen-tick .3s ease-out}.num.cue-pop{animation:pop-pulse .6s ease-out}@media(prefers-reduced-motion:reduce){.num.cue-gen,.num.cue-pop{animation-duration:1ms}}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1)}.ctl{font:inherit;color:var(--fg);background:color-mix(in srgb,var(--cell-dying) 12%,var(--scene-bg));border:1px solid var(--border);border-radius:6px;min-height:30px;min-width:34px;padding:2px 10px;cursor:pointer}.ctl:hover{background:color-mix(in srgb,var(--cell-dying) 22%,var(--scene-bg))}.ctl:focus-visible{outline:2px solid var(--cell-alive);outline-offset:1px}.ctl-speed{display:flex;align-items:center;gap:var(--space-1);flex:1 1 150px;min-width:120px}.ctl-speed input[type=range]{flex:1;accent-color:var(--cell-alive);min-height:30px}#speed-value{font-variant-numeric:tabular-nums;color:var(--muted);min-width:3.5ch}.board{position:relative;display:flex;align-items:center;justify-content:center;min-height:0;background:var(--scene-grid);border:1px solid var(--border);border-radius:6px}#cells{display:block;touch-action:none;cursor:crosshair}.menu{position:absolute;top:var(--space-2);left:50%;transform:translate(-50%);display:flex;flex-direction:column;gap:var(--space-1);max-width:min(280px,92%);max-height:calc(100% - var(--space-2) * 2);padding:var(--space-2);background:color-mix(in srgb,var(--scene-bg) 94%,var(--cell-dying));border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 18px #00000080}.menu[hidden]{display:none}.menu-title{font-size:12px;color:var(--muted)}.menu-list{display:flex;flex-direction:column;gap:var(--space-1);overflow-y:auto;overscroll-behavior:contain}.menu-item{font:inherit;text-align:left;color:var(--fg);background:color-mix(in srgb,var(--cell-dying) 12%,var(--scene-bg));border:1px solid var(--border);border-radius:6px;min-height:30px;padding:2px 10px;cursor:pointer}.menu-item:hover{background:color-mix(in srgb,var(--cell-dying) 22%,var(--scene-bg))}.menu-item:focus-visible{outline:2px solid var(--cell-alive);outline-offset:1px}.welcome{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;padding:var(--space-2);background:color-mix(in srgb,var(--scene-bg) 78%,transparent)}.welcome[hidden]{display:none}.welcome-card{display:flex;flex-direction:column;gap:var(--space-1);width:min(300px,92%);max-height:calc(100% - var(--space-2) * 2);padding:var(--space-2);background:color-mix(in srgb,var(--scene-bg) 94%,var(--cell-dying));border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 18px #00000080}.welcome-title{font-size:13px;color:var(--fg)}.welcome-list{display:flex;flex-direction:column;gap:var(--space-1);margin:0;padding:0;list-style:none;overflow-y:auto;overscroll-behavior:contain;font-size:12px;color:var(--muted)}#welcome-start{align-self:center;min-width:90px}
</style>
<meta name="napplet-type" content="my-napplet">
<meta name="napplet-requires" content="storage">
<meta name="napplet-config-schema" content="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"textSize":{"type":"integer","title":"Text size","description":"Base text size in pixels. Changes apply while the napplet is running.","minimum":10,"maximum":20,"default":13,"x-napplet-section":"Appearance","x-napplet-order":1},"controlSize":{"type":"integer","title":"Control height","description":"Choose compact controls or larger touch targets, in pixels.","minimum":24,"maximum":44,"default":28,"x-napplet-section":"Appearance","x-napplet-order":2},"selectText":{"type":"boolean","title":"Allow text selection","description":"Allow selecting interface text. Text inputs always remain selectable.","default":false,"x-napplet-section":"Interaction"}}}">
</head>
<body>
<!--
A napplet is an applet framed by a runtime: the runtime already shows this
napplet's name, so there is no title header here. The board is the frame
from the first pixel; the HUD is one quiet status line (spec §8 copy).
-->
<main id="app" class="app">
<div class="hud" aria-label="Simulation status">
<span class="hud-item">gen <span id="generation" class="num">0</span></span>
<span class="hud-item">pop <span id="population" class="num">0</span></span>
<output id="status" class="hud-status" aria-live="polite" hidden>Stable</output>
<output id="save-status" class="hud-status hud-save" aria-live="polite" hidden
>Saving unavailable — play stays in this session.</output
>
<!-- Armed-placement hint (spec §6 rework ruling 2026-10-30): text set
by main.ts while a pattern is armed; hidden = not armed. -->
<output id="place-status" class="hud-status hud-place" aria-live="polite" hidden></output>
</div>
<!-- Static control chrome (spec §5): outside any state machine; painting
stays legal while any control state is active. -->
<div class="controls" aria-label="Simulation controls">
<!-- Boots PAUSED behind the welcome gate (spec §5 welcome rule), so
the initial glyph is Play; syncRunButton() keeps it in step. -->
<button id="play-pause" class="ctl" type="button" aria-label="Play">⏵</button>
<button id="step" class="ctl" type="button" title="Step one generation" aria-label="Step one generation">⏭</button>
<button id="clear" class="ctl ctl-word" type="button">Clear</button>
<button id="random" class="ctl ctl-word" type="button">Random</button>
<button id="patterns" class="ctl ctl-word" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="pattern-menu" title="Pattern library">Patterns</button>
<label class="ctl-speed">
<input id="speed" type="range" min="1" max="30" step="1" value="10" aria-label="Speed in generations per second" />
<output id="speed-value" class="num" for="speed">10/s</output>
</label>
</div>
<div id="board" class="board">
<canvas id="cells" aria-label="Game of Life board"></canvas>
<!-- Pattern library overlay (Phase 5, spec §6): static chrome riding
outside any state machine — open/close never touches running,
generation, or paint legality. Items built from PATTERNS. -->
<div id="pattern-menu" class="menu" hidden>
<div class="menu-title">Patterns</div>
<div id="pattern-list" class="menu-list"></div>
<button id="menu-close" class="ctl ctl-word" type="button">Back</button>
</div>
<!-- Welcome gate (spec §5 welcome rule, player ruling 2026-10-30):
full-frame primer overlay at boot; Start dismisses it AND enters
the sim through the same setRunning path as the play/pause
button. Fixed positioning rides the FRAME, not the board box:
at the 200x160 floor the board row collapses under the control
bar, and a board-anchored card would squash to nothing. Markup
lives inside .board; the scrim blocks all controls until Start.
Copy pinned by the browser suite (spec §8). -->
<div id="welcome" class="welcome" aria-label="How to play">
<div class="welcome-card">
<div class="welcome-title">How to play</div>
<ul class="welcome-list">
<li>Left click/drag to paint cells</li>
<li>Right click/drag to erase</li>
<li>Space play / pause, → step, C clear</li>
<li>Patterns: pick one, then tap the board</li>
<li>Slider sets speed, 1–30 per second</li>
</ul>
<button id="welcome-start" class="ctl ctl-word" type="button">Start</button>
</div>
</div>
</div>
</main>
</body>
</html>
