549 lines
No EOL
19 KiB
HTML
549 lines
No EOL
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>The Glider That Remembered Me — A Research Exhibit</title>
|
||
<style>
|
||
:root{
|
||
--gold:#e8d36d;
|
||
--gold-soft:#f5e9a3;
|
||
--ink:#000;
|
||
--panel:rgba(0,0,0,.55);
|
||
--panel-strong:rgba(0,0,0,.7);
|
||
--border:rgba(232,211,109,.35);
|
||
}
|
||
html,body{margin:0;height:100%;background:var(--ink);color:var(--gold);font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
|
||
canvas{display:block}
|
||
/* Layout */
|
||
#stage{position:fixed;inset:0}
|
||
#hud{position:fixed;inset:0;pointer-events:none}
|
||
.stack{pointer-events:auto;position:absolute;display:flex;flex-direction:column;gap:.75rem;padding:1rem 1.25rem;background:var(--panel);backdrop-filter: blur(6px); border:1px solid var(--border); border-radius:16px; box-shadow:0 10px 30px rgba(0,0,0,.35)}
|
||
#controls{left:1.25rem; top:1.25rem; max-width:360px}
|
||
#analytics{right:1.25rem; top:1.25rem; max-width:420px}
|
||
#caption{left:1.25rem; bottom:1.25rem; max-width:min(720px, 70vw); background:var(--panel-strong)}
|
||
#footer{right:1.25rem; bottom:1.25rem; max-width:420px}
|
||
h1,h2,h3{font-weight:500;margin:.1rem 0;color:var(--gold-soft)}
|
||
h1{font-size:1.1rem;letter-spacing:.04em}
|
||
h2{font-size:1rem;margin-top:.25rem}
|
||
p{margin:.25rem 0;line-height:1.5}
|
||
small{opacity:.9}
|
||
label{font-size:.85rem;opacity:.95}
|
||
input[type="range"]{width:100%}
|
||
button, select{
|
||
background:transparent;color:var(--gold);border:1px solid var(--border);
|
||
border-radius:999px;padding:.45rem .8rem;cursor:pointer
|
||
}
|
||
button:hover{border-color:var(--gold)}
|
||
button.primary{background:var(--gold);color:#111;border-color:var(--gold)}
|
||
.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}
|
||
.grid{display:grid;gap:.5rem}
|
||
.grid.cols-2{grid-template-columns:1fr 1fr}
|
||
.muted{opacity:.8}
|
||
.mono{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace}
|
||
.pill{border:1px solid var(--border);padding:.2rem .5rem;border-radius:999px;font-size:.8rem}
|
||
canvas.thumb{width:100%;height:auto;background:#000;border:1px solid var(--border);border-radius:12px}
|
||
.hr{height:1px;background:linear-gradient(90deg, transparent, var(--border), transparent); margin:.25rem 0 .5rem}
|
||
details{border:1px solid var(--border); border-radius:12px;padding:.75rem}
|
||
summary{cursor:pointer;list-style:none}
|
||
summary::-webkit-details-marker{display:none}
|
||
summary .chev{display:inline-block;transition:transform .2s ease}
|
||
details[open] summary .chev{transform:rotate(90deg)}
|
||
@media (max-width: 900px){
|
||
#controls{max-width:calc(100vw - 2.5rem)}
|
||
#analytics{position:static; inset:auto; margin:1rem; }
|
||
#caption{max-width:calc(100vw - 2.5rem)}
|
||
#footer{position:static; inset:auto; margin:1rem}
|
||
#hud{position:static}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Simulation canvas -->
|
||
<canvas id="stage"></canvas>
|
||
|
||
<!-- UI / HUD -->
|
||
<div id="hud">
|
||
<!-- Controls -->
|
||
<section id="controls" class="stack">
|
||
<h1>THE GLIDER THAT REMEMBERED ME</h1>
|
||
<div class="muted">Conway’s Life as an interactive essay on emergence, spectral memory, and proto-awareness.</div>
|
||
<div class="hr"></div>
|
||
<div class="row">
|
||
<button id="playBtn" class="primary">▶ Play</button>
|
||
<button id="stepBtn">Step</button>
|
||
<button id="clearBtn">Clear</button>
|
||
<button id="randomBtn">Random</button>
|
||
</div>
|
||
<div class="row">
|
||
<label class="pill"><input type="checkbox" id="drawToggle"> Painter mode</label>
|
||
<label class="pill"><input type="checkbox" id="wrapToggle" checked> Wrap edges</label>
|
||
</div>
|
||
<div>
|
||
<label>Speed <span id="speedVal" class="mono">30</span> gen/s</label>
|
||
<input type="range" id="speed" min="1" max="60" value="30">
|
||
</div>
|
||
<div>
|
||
<label>Cell size <span id="sizeVal" class="mono">6</span> px</label>
|
||
<input type="range" id="cellSize" min="4" max="16" value="6">
|
||
</div>
|
||
<div class="grid cols-2">
|
||
<div>
|
||
<label>Patterns</label><br/>
|
||
<select id="pattern">
|
||
<option value="none">— insert pattern —</option>
|
||
<option value="glider">Glider</option>
|
||
<option value="ggg">Gosper Glider Gun</option>
|
||
<option value="blinker">Blinker line</option>
|
||
<option value="lwss">Lightweight Spaceship</option>
|
||
</select>
|
||
</div>
|
||
<div class="row" style="align-items:flex-end; justify-content:flex-end">
|
||
<button id="placeBtn">Place at center</button>
|
||
</div>
|
||
</div>
|
||
<details>
|
||
<summary><span class="chev">▶</span> Tips</summary>
|
||
<ul style="margin:.5rem 0 .2rem .9rem; opacity:.9">
|
||
<li>Toggle <b>Painter mode</b> to draw/erase with your cursor or finger.</li>
|
||
<li>Use <b>Patterns</b> to seed gliders or a Gosper gun.</li>
|
||
<li><b>Spectrum Lab</b> (right) reveals the unseen spectral memory.</li>
|
||
</ul>
|
||
</details>
|
||
</section>
|
||
|
||
<!-- Analytics -->
|
||
<section id="analytics" class="stack">
|
||
<h2>Analytics & Spectrum Lab</h2>
|
||
<div class="row" style="gap:1rem">
|
||
<div class="pill">Population: <span id="pop" class="mono">0</span></div>
|
||
<div class="pill">Entropy: <span id="entropy" class="mono">0.000</span></div>
|
||
<div class="pill">Gliders: <span id="gliders" class="mono">0</span></div>
|
||
<div class="pill">Gen: <span id="gens" class="mono">0</span></div>
|
||
</div>
|
||
<div class="hr"></div>
|
||
<div class="grid cols-2">
|
||
<div>
|
||
<label>Spectrum (2D FFT)</label>
|
||
<canvas id="fftCanvas" class="thumb" width="128" height="128" aria-label="2D FFT heatmap"></canvas>
|
||
</div>
|
||
<div>
|
||
<label>Space-Time Ribbon</label>
|
||
<canvas id="timeline" class="thumb" width="256" height="128" aria-label="Population & entropy over time"></canvas>
|
||
</div>
|
||
</div>
|
||
<small class="muted">FFT updates every few frames with downsampled grid. Brighter = stronger spatial frequency → “spectral memory.”</small>
|
||
</section>
|
||
|
||
<!-- Caption / Philosophy -->
|
||
<section id="caption" class="stack">
|
||
<h2>Science as Art — Why Life matters to consciousness</h2>
|
||
<p>
|
||
Five cells, drifting diagonally, taught me that rules can remember. What you see on the left is emergence: local
|
||
updates birthing global order. What you do <em>not</em> see keeps the pattern coherent: an invisible spectral geometry.
|
||
</p>
|
||
<p>
|
||
The FFT heatmap exposes that hidden order; the space-time ribbon shows how population and entropy braid through
|
||
time. Run it long enough and the lattice begins to dream: a miniature of cultural memory—archetypes and gods
|
||
condensing from repetition into form.
|
||
</p>
|
||
</section>
|
||
|
||
<!-- Footer -->
|
||
<section id="footer" class="stack">
|
||
<h3>Controls</h3>
|
||
<p class="muted">• Play/Pause with the button • Step to advance one generation • Draw to intervene as an “observer”</p>
|
||
<p class="muted">© The Empathic Technologist — Gold on Black Study. This page is a vessel for recursive coherence.</p>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
/* =========================
|
||
Utilities (complex & FFT)
|
||
========================= */
|
||
class Complex {
|
||
constructor(re=0, im=0){ this.re=re; this.im=im }
|
||
add(b){ return new Complex(this.re+b.re, this.im+b.im) }
|
||
sub(b){ return new Complex(this.re-b.re, this.im-b.im) }
|
||
mul(b){ return new Complex(this.re*b.re - this.im*b.im, this.re*b.im + this.im*b.re) }
|
||
}
|
||
function fft1d(arr){
|
||
const N = arr.length;
|
||
if (N<=1) return arr;
|
||
// radix-2 only
|
||
const even = fft1d(arr.filter((_,i)=>i%2===0));
|
||
const odd = fft1d(arr.filter((_,i)=>i%2===1));
|
||
const out = new Array(N);
|
||
for(let k=0;k<N/2;k++){
|
||
const t = -2*Math.PI*k/N;
|
||
const w = new Complex(Math.cos(t), Math.sin(t));
|
||
const wk = w.mul(odd[k]);
|
||
out[k] = even[k].add(wk);
|
||
out[k+N/2] = even[k].sub(wk);
|
||
}
|
||
return out;
|
||
}
|
||
function fft2d(mat){ // mat: Complex[N][N], N power-of-two
|
||
const N = mat.length;
|
||
// rows
|
||
for(let r=0;r<N;r++){
|
||
mat[r] = fft1d(mat[r]);
|
||
}
|
||
// columns
|
||
for(let c=0;c<N;c++){
|
||
const col = new Array(N);
|
||
for(let r=0;r<N;r++) col[r]=mat[r][c];
|
||
const F = fft1d(col);
|
||
for(let r=0;r<N;r++) mat[r][c]=F[r];
|
||
}
|
||
return mat;
|
||
}
|
||
function nextPow2(n){ let p=1; while(p<n) p<<=1; return p; }
|
||
|
||
/* =========================
|
||
Simulation (Conway Life)
|
||
========================= */
|
||
const canvas = document.getElementById('stage');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
let cellSize = 6;
|
||
let wrap = true;
|
||
let w=0,h=0, cols=0, rows=0;
|
||
let grid=null, next=null;
|
||
let playing=false;
|
||
let gens=0;
|
||
let fps=30, acc=0, last=performance.now();
|
||
|
||
function resize(){
|
||
w = window.innerWidth; h = window.innerHeight;
|
||
canvas.width = w; canvas.height = h;
|
||
cols = Math.floor(w / cellSize);
|
||
rows = Math.floor(h / cellSize);
|
||
const g = makeGrid(rows, cols);
|
||
// keep center if existing grid
|
||
if(grid){
|
||
const rMin=Math.max(0, Math.floor((rows-grid.length)/2));
|
||
const cMin=Math.max(0, Math.floor((cols-grid[0].length)/2));
|
||
for(let r=0;r<Math.min(rows, grid.length); r++)
|
||
for(let c=0;c<Math.min(cols, grid[0].length); c++)
|
||
g[rMin+r][cMin+c]=grid[r][c];
|
||
}
|
||
grid=g; next=makeGrid(rows, cols);
|
||
}
|
||
window.addEventListener('resize', resize);
|
||
|
||
function makeGrid(r,c){ return Array.from({length:r},()=>Array(c).fill(0)); }
|
||
function randomize(p=0.15){
|
||
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++)
|
||
grid[r][c] = Math.random()<p?1:0;
|
||
}
|
||
function clearGrid(){
|
||
for(let r=0;r<rows;r++) grid[r].fill(0);
|
||
gens=0;
|
||
}
|
||
|
||
function neighbors(r,c){
|
||
let n=0;
|
||
for(let dr=-1; dr<=1; dr++) for(let dc=-1; dc<=1; dc++){
|
||
if(dr===0 && dc===0) continue;
|
||
let rr=r+dr, cc=c+dc;
|
||
if(wrap){
|
||
rr=(rr+rows)%rows; cc=(cc+cols)%cols;
|
||
n+=grid[rr][cc];
|
||
} else {
|
||
if(rr>=0 && rr<rows && cc>=0 && cc<cols) n+=grid[rr][cc];
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
function step(){
|
||
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
|
||
const s=grid[r][c]; const n=neighbors(r,c);
|
||
next[r][c] = (s && (n===2||n===3)) || (!s && n===3) ? 1 : 0;
|
||
}
|
||
[grid,next]=[next,grid];
|
||
gens++;
|
||
}
|
||
|
||
function draw(){
|
||
ctx.fillStyle="#000"; ctx.fillRect(0,0,w,h);
|
||
ctx.fillStyle="#e8d36d";
|
||
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++)
|
||
if(grid[r][c]) ctx.fillRect(c*cellSize, r*cellSize, cellSize, cellSize);
|
||
}
|
||
|
||
function loop(now){
|
||
const dt = (now - last)/1000; last=now;
|
||
acc += dt;
|
||
const target = 1/Math.max(1,fps);
|
||
while(acc >= target){
|
||
step();
|
||
updateMetrics();
|
||
if((gens % 4)===0){ updateFFT(); updateTimeline(); detectGliders(); }
|
||
acc-=target;
|
||
}
|
||
draw();
|
||
if(playing) requestAnimationFrame(loop);
|
||
}
|
||
|
||
/* =========================
|
||
Painter / Interaction
|
||
========================= */
|
||
let painting=false, painterOn=false;
|
||
canvas.addEventListener('pointerdown', e=>{
|
||
if(!painterOn) return;
|
||
painting=true;
|
||
toggleAtEvent(e);
|
||
});
|
||
canvas.addEventListener('pointermove', e=>{
|
||
if(!painterOn || !painting) return;
|
||
toggleAtEvent(e,true);
|
||
});
|
||
window.addEventListener('pointerup', ()=>painting=false);
|
||
|
||
function toggleAtEvent(e, drawOnly=false){
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = Math.floor((e.clientX - rect.left)/cellSize);
|
||
const y = Math.floor((e.clientY - rect.top)/cellSize);
|
||
if(x>=0 && x<cols && y>=0 && y<rows){
|
||
grid[y][x] = drawOnly ? 1 : (grid[y][x]^1);
|
||
}
|
||
}
|
||
|
||
/* =========================
|
||
Metrics / Timeline
|
||
========================= */
|
||
const elPop = document.getElementById('pop');
|
||
const elEntropy = document.getElementById('entropy');
|
||
const elGliders = document.getElementById('gliders');
|
||
const elGens = document.getElementById('gens');
|
||
const timeline = document.getElementById('timeline');
|
||
const tctx = timeline.getContext('2d');
|
||
// init timeline
|
||
tctx.fillStyle="#000"; tctx.fillRect(0,0,timeline.width,timeline.height);
|
||
|
||
let gliderCount=0;
|
||
|
||
function updateMetrics(){
|
||
let pop=0;
|
||
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++) pop+=grid[r][c];
|
||
elPop.textContent = pop;
|
||
// entropy of alive vs dead (Shannon)
|
||
const N = rows*cols;
|
||
const p = pop/N, q = 1-p;
|
||
let H = 0;
|
||
if(p>0) H -= p*Math.log2(p);
|
||
if(q>0) H -= q*Math.log2(q);
|
||
elEntropy.textContent = H.toFixed(3);
|
||
elGens.textContent = gens;
|
||
}
|
||
|
||
function updateTimeline(){
|
||
// scroll left by 1 px
|
||
const w = timeline.width, h=timeline.height;
|
||
const img = tctx.getImageData(1,0,w-1,h);
|
||
tctx.putImageData(img,0,0);
|
||
// draw new column at right
|
||
const colX = w-1;
|
||
// population (upper)
|
||
// map entropy to lower band
|
||
// clear col
|
||
tctx.fillStyle="#000"; tctx.fillRect(colX,0,1,h);
|
||
// pop bar
|
||
let pop=0; for(let r=0;r<rows;r++) for(let c=0;c<cols;c++) pop+=grid[r][c];
|
||
const popNorm = pop/(rows*cols);
|
||
const popY = Math.floor((1-popNorm)*(h/2-2));
|
||
tctx.fillStyle="#e8d36d";
|
||
tctx.fillRect(colX, popY, 1, 2);
|
||
// entropy band
|
||
const textH = parseFloat(elEntropy.textContent);
|
||
const eNorm = Math.min(1, textH/1.0); // binary entropy max ~1 at p=0.5
|
||
const eY = Math.floor(h/2 + (1-eNorm)*(h/2-2));
|
||
tctx.fillRect(colX, eY, 1, 2);
|
||
}
|
||
|
||
/* =========================
|
||
Glider detection (simple)
|
||
========================= */
|
||
const GLIDER_SHAPES = [
|
||
// canonical (top-left anchored)
|
||
[[1,0],[2,1],[0,2],[1,2],[2,2]],
|
||
// rotate 90
|
||
[[0,1],[1,2],[2,0],[2,1],[2,2]],
|
||
// rotate 180
|
||
[[0,0],[1,1],[2,2],[1,2],[0,2]],
|
||
// rotate 270
|
||
[[0,0],[0,1],[0,2],[1,0],[2,1]]
|
||
];
|
||
function detectGliders(){
|
||
let count=0;
|
||
// thin scan every 2nd cell to reduce cost
|
||
for(let r=0;r<rows-3;r+=1){
|
||
for(let c=0;c<cols-3;c+=1){
|
||
for(const shape of GLIDER_SHAPES){
|
||
let ok=true;
|
||
for(const [dx,dy] of shape){
|
||
const rr = wrap? (r+dy)%rows : r+dy;
|
||
const cc = wrap? (c+dx)%cols : c+dx;
|
||
if(rr<0||cc<0||rr>=rows||cc>=cols || grid[rr][cc]!==1){ ok=false; break; }
|
||
}
|
||
if(ok){ count++; break; }
|
||
}
|
||
}
|
||
}
|
||
gliderCount = count;
|
||
elGliders.textContent = gliderCount;
|
||
}
|
||
|
||
/* =========================
|
||
2D FFT (Spectrum Lab)
|
||
========================= */
|
||
const fftCanvas = document.getElementById('fftCanvas');
|
||
const ftx = fftCanvas.getContext('2d');
|
||
function updateFFT(){
|
||
// downsample grid to small power-of-two square
|
||
const N = 128; // target
|
||
const S = Math.min(N, Math.min(rows, cols));
|
||
// build SxS array centered
|
||
const r0 = Math.floor((rows - S)/2), c0 = Math.floor((cols - S)/2);
|
||
let mat = Array.from({length:S},()=>Array(S));
|
||
// remove DC bias for better contrast: use 1 for alive, -1 for dead
|
||
for(let r=0;r<S;r++){
|
||
for(let c=0;c<S;c++){
|
||
const v = grid[r0+r][c0+c] ? 1 : -1;
|
||
mat[r][c] = new Complex(v, 0);
|
||
}
|
||
}
|
||
// pad to power of two (S already <= 128; we’ll just assume 128 or 64)
|
||
const M = nextPow2(S);
|
||
// pad matrix
|
||
let M2 = Array.from({length:M},(_,r)=>Array.from({length:M},(_,c)=>
|
||
r<S && c<S ? mat[r][c] : new Complex(0,0)
|
||
));
|
||
// FFT
|
||
const F = fft2d(M2);
|
||
// magnitude and log scale
|
||
const img = ftx.createImageData(M, M);
|
||
let idx=0, maxMag=1e-9;
|
||
const mags = new Array(M*M);
|
||
for(let r=0;r<M;r++){
|
||
for(let c=0;c<M;c++){
|
||
const z = F[r][c];
|
||
const m = Math.hypot(z.re, z.im);
|
||
mags[idx++] = m; if(m>maxMag) maxMag=m;
|
||
}
|
||
}
|
||
// draw (centered DC shift)
|
||
idx=0;
|
||
for(let r=0;r<M;r++){
|
||
for(let c=0;c<M;c++){
|
||
// shift quadrants for visual
|
||
const rr = (r+M/2)%M, cc=(c+M/2)%M;
|
||
const m = mags[rr*M + cc];
|
||
const v = Math.sqrt(m/maxMag); // gamma for contrast
|
||
const col = Math.floor(255*Math.min(1,v));
|
||
const pos = (r*M + c)*4;
|
||
img.data[pos+0]=col; // grayscale
|
||
img.data[pos+1]=col;
|
||
img.data[pos+2]=0; // warm tint toward gold
|
||
img.data[pos+3]=255;
|
||
}
|
||
}
|
||
// put and scale into 128x128
|
||
const tmp = document.createElement('canvas');
|
||
tmp.width = M; tmp.height = M;
|
||
tmp.getContext('2d').putImageData(img,0,0);
|
||
ftx.clearRect(0,0,fftCanvas.width, fftCanvas.height);
|
||
ftx.drawImage(tmp, 0,0, fftCanvas.width, fftCanvas.height);
|
||
}
|
||
|
||
/* =========================
|
||
Patterns
|
||
========================= */
|
||
function placeAtCenter(cells){
|
||
const minR = Math.min(...cells.map(([r,_c])=>r));
|
||
const minC = Math.min(...cells.map(([r,c])=>c));
|
||
const norm = cells.map(([r,c])=>[r-minR, c-minC]);
|
||
const pr = Math.floor(rows/2)-8, pc = Math.floor(cols/2)-16;
|
||
for(const [r,c] of norm){
|
||
const rr = wrap? (pr+r+rows)%rows : pr+r;
|
||
const cc = wrap? (pc+c+cols)%cols : pc+c;
|
||
if(rr>=0 && rr<rows && cc>=0 && cc<cols) grid[rr][cc]=1;
|
||
}
|
||
}
|
||
function patternCells(name){
|
||
// Coordinates from canonical sources, as (row, col)
|
||
if(name==='glider'){
|
||
return [[0,1],[1,2],[2,0],[2,1],[2,2]];
|
||
}
|
||
if(name==='blinker'){
|
||
return [[0,0],[0,1],[0,2],[0,3],[0,4]];
|
||
}
|
||
if(name==='lwss'){
|
||
// Lightweight spaceship (5x4)
|
||
return [[0,1],[0,2],[0,3],[0,4],[1,0],[2,0],[3,0],[3,4],[2,5],[1,5]];
|
||
}
|
||
if(name==='ggg'){ // Gosper glider gun
|
||
// reputable small set (will expand on stage)
|
||
return [
|
||
[5,1],[5,2],[6,1],[6,2],
|
||
[5,11],[6,11],[7,11],[4,12],[8,12],[3,13],[9,13],[3,14],[9,14],[6,15],[4,16],[8,16],[5,17],[6,17],[7,17],[6,18],
|
||
[3,21],[4,21],[5,21],[3,22],[4,22],[5,22],[2,23],[6,23],[1,25],[2,25],[6,25],[7,25],
|
||
[3,35],[4,35],[3,36],[4,36]
|
||
];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/* =========================
|
||
Wiring UI
|
||
========================= */
|
||
const playBtn = document.getElementById('playBtn');
|
||
const stepBtn = document.getElementById('stepBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const randomBtn = document.getElementById('randomBtn');
|
||
const patternSel = document.getElementById('pattern');
|
||
const placeBtn = document.getElementById('placeBtn');
|
||
const speed = document.getElementById('speed');
|
||
const speedVal = document.getElementById('speedVal');
|
||
const size = document.getElementById('cellSize');
|
||
const sizeVal = document.getElementById('sizeVal');
|
||
const drawToggle = document.getElementById('drawToggle');
|
||
const wrapToggle = document.getElementById('wrapToggle');
|
||
|
||
playBtn.addEventListener('click', ()=>{
|
||
playing = !playing;
|
||
playBtn.textContent = playing? '⏸ Pause' : '▶ Play';
|
||
playBtn.classList.toggle('primary', playing);
|
||
if(playing){ last=performance.now(); requestAnimationFrame(loop); }
|
||
});
|
||
stepBtn.addEventListener('click', ()=>{ if(!playing){ step(); updateMetrics(); updateFFT(); updateTimeline(); detectGliders(); draw(); }});
|
||
clearBtn.addEventListener('click', ()=>{ clearGrid(); updateMetrics(); updateFFT(); updateTimeline(); detectGliders(); draw(); });
|
||
randomBtn.addEventListener('click', ()=>{ randomize(); updateMetrics(); });
|
||
placeBtn.addEventListener('click', ()=>{
|
||
const p = patternSel.value;
|
||
if(p==='none') return;
|
||
placeAtCenter(patternCells(p));
|
||
updateMetrics();
|
||
});
|
||
speed.addEventListener('input', ()=>{ fps = parseInt(speed.value,10); speedVal.textContent=fps; });
|
||
size.addEventListener('input', ()=>{
|
||
cellSize = parseInt(size.value,10);
|
||
sizeVal.textContent = cellSize;
|
||
resize();
|
||
draw();
|
||
});
|
||
drawToggle.addEventListener('change', ()=>{ painterOn = drawToggle.checked; });
|
||
wrapToggle.addEventListener('change', ()=>{ wrap = wrapToggle.checked; });
|
||
|
||
/* =========================
|
||
Boot
|
||
========================= */
|
||
(function init(){
|
||
resize();
|
||
randomize(0.12);
|
||
updateMetrics(); updateFFT(); updateTimeline(); detectGliders(); draw();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |