memoscan
← all memos

Memo 0x224e90ee…456f24 on Ethereum

[((i - offset + 1) % n + n) % n]; ctx.fillStyle = css(paper); ctx.fillRect(e[0]*cs, e[1]*cs, cs, cs); ctx.fillStyle = css(ink); drawGlyph(ctx, applyCharSet(model, e[0], e[1], e[2]), e[0]*cs, e[1]*cs, cs, 0); }; if(model.mode === 'breathe'){ // grow to full, then shrink back to seed (palindrome -> clean loop) const k = t < 0.5 ? Math.floor(t * 2 * P) : Math.floor((1 - t) * 2 * P); const limit = Math.min(k, P); for(let i = 0; i < limit; i++) drawCell(i); } else { // exhale: a ring expands outward, clearing behind it. order[] is BFS-from- // center so an index window == a ring at a radius. It wraps, so a new ring // enters the center as the old exits the edge -> perfectly loop-clean. const head = Math.floor(t * P); const w = Math.max(3, Math.floor(P * 0.30)); for(let i = 0; i < P; i++){ if((((head - i) % P) + P) % P < w) drawCell(i); } } } /* ---- Rain: per-column vertical streams, deterministic glyph per cell, head bright, trail dims through the palette. Each column's head cycles N rows per loop (at speed 1) or 2N (at speed 2), so every column closes the loop seamlessly. */ const RAIN_GLYPHS = ['.',':','X','O','I','L','M','BALL','DIAG_FWD','DIAG_BACK']; function buildRain(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const cols = new Array(N); const glyphs = new Array(N*N); for(let x = 0; x < N; x++){ cols[x] = { phase: Math.floor(r() * N), speed: r() < 0.7 ? 1 : 2, trail: Math.max(3, 3 + Math.floor(r() * Math.min(N - 2, 8))) }; for(let y = 0; y < N; y++) glyphs[y*N+x] = pick(RAIN_GLYPHS, r); } return { kind:'rain', seed: seedStr, N, palette, cols, glyphs, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawRain(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); for(let x = 0; x < N; x++){ const col = model.cols[x]; const head = ((Math.floor(t * col.speed * N) + col.phase) % N + N) % N; for(let i = 0; i < col.trail; i++){ const y = (head - i + N) % N; const ci = Math.max(0, n - 1 - Math.floor(i / col.trail * n)); ctx.fillStyle = css(pal[ci]); drawGlyph(ctx, applyCharSet(model, x, y, model.glyphs[y*N+x]), x*cs, y*cs, cs, 0); } } } /* ---- Cellular: one row evolves via a Wolfram rule, becomes the next row. The resulting fractal triangle is rendered as field; color sweeps over it. */ /* the seven rules are sorted numerically and assigned letter labels A..G so the published trait is opaque (no math citation to argue about). The internal "rule" integer is what the renderer actually uses. */ const CA_RULES = [22, 30, 73, 90, 105, 110, 150]; // published by their real Wolfram rule number (accurate + recognizable) const CA_LABELS = ['Rule 22','Rule 30','Rule 73','Rule 90','Rule 105','Rule 110','Rule 150']; function buildCellular(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const ruleIdx = Math.floor(r() * CA_RULES.length); const rule = CA_RULES[ruleIdx]; const pattern = CA_LABELS[ruleIdx]; let row = new Uint8Array(N); if(r() < 0.6){ row[Math.floor(N/2)] = 1; } else { for(let i = 0; i < N; i++) row[i] = r() < 0.5 ? 1 : 0; } const cellGlyphs = new Array(N*N); const field = new Int16Array(N*N); for(let y = 0; y < N; y++){ for(let x = 0; x < N; x++){ const i = y*N + x; cellGlyphs[i] = row[x] ? 'BLOCK' : '.'; field[i] = x + y; } const next = new Uint8Array(N); for(let x = 0; x < N; x++){ const l = row[(x - 1 + N) % N], c = row[x], rr = row[(x + 1) % N]; const idx = (l << 2) | (c << 1) | rr; next[x] = (rule >> idx) & 1; } row = next; } const laps = Math.max(2, Math.round(10 / numColors)); return { kind:'field', seed: seedStr, N, palette, field, cellGlyphs, rule, pattern, laps, dir: 1, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } /* ---- Fireworks: 2-4 staggered bursts per loop. Each burst rises then explodes as an 8-point ring. Loop-clean because each burst t-window has fixed t_start and duration; outside the window nothing renders. */ function buildFireworks(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const count = 2 + Math.floor(r() * 3); const dur = (1 / count) * 0.95; const bursts = []; for(let i = 0; i < count; i++){ bursts.push({ xc: 2 + Math.floor(r() * (N - 4)), yp: 2 + Math.floor(r() * (N/2)), t0: (i / count + r() * 0.04) % 1, dur, color: i % numColors, }); } return { kind:'fireworks', seed: seedStr, N, palette, bursts, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawFireworks(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); for(const b of model.bursts){ const p = (t - b.t0 + 1) % 1; if(p > b.dur) continue; const phase = p / b.dur; if(phase < 0.3){ const yNow = Math.floor(N - 1 - (phase / 0.3) * (N - 1 - b.yp)); ctx.fillStyle = css(pal[b.color % n]); drawGlyph(ctx, '.', b.xc * cs, yNow * cs, cs, 0); } else { const burstPhase = (phase - 0.3) / 0.7; const maxR = Math.max(2, Math.floor(Math.min(N/3, 10))); const radius = Math.max(1, Math.floor(burstPhase * maxR)); for(let a = 0; a < 8; a++){ const ang = a / 8 * Math.PI * 2; const x = b.xc + Math.round(Math.cos(ang) * radius); const y = b.yp + Math.round(Math.sin(ang) * radius); if(x < 0 || y < 0 || x >= N || y >= N) continue; const ci = (b.color + a) % n; ctx.fillStyle = css(pal[ci]); drawGlyph(ctx, applyCharSet(model, x, y, 'X_DIAG'), x * cs, y * cs, cs, 0); } } } } /* ---- Snake: a colored worm traces a deterministic path that wraps the grid. Head bright, tail dims through the palette. Path is pre-computed so the traversal is perfectly periodic. */ function buildSnake(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const trail = Math.max(4, Math.floor(N * 0.6)); const path = []; const seen = new Uint8Array(N*N); let x = Math.floor(r() * N), y = Math.floor(r() * N); const target = N * N; while(path.length < target){ if(!seen[y*N + x]){ path.push([x, y]); seen[y*N + x] = 1; } const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; const ord = dirs.map(d => [d, r()]).sort((a, b) => a[1] - b[1]).map(p => p[0]); let moved = false; for(const [dx, dy] of ord){ const nx = (x + dx + N) % N, ny = (y + dy + N) % N; if(!seen[ny*N + nx]){ x = nx; y = ny; moved = true; break; } } if(!moved){ let next = -1; for(let i = 0; i < N*N; i++) if(!seen[i]){ next = i; break; } if(next < 0) break; x = next % N; y = Math.floor(next / N); } } return { kind:'snake', seed: seedStr, N, palette, path, trail, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawSnake(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const path = model.path, P = path.length, T = model.trail; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); const head = Math.floor(t * P); for(let i = 0; i < T; i++){ const idx = ((head - i) % P + P) % P; const cell = path[idx]; const ci = Math.max(0, n - 1 - Math.floor(i / T * n)); ctx.fillStyle = css(pal[ci]); drawGlyph(ctx, applyCharSet(model, cell[0], cell[1], i === 0 ? 'BLOCK' : 'BALL'), cell[0] * cs, cell[1] * cs, cs, 0); } } /* ---- Julia: classic complex-plane fractal. Each cell maps to a point in [-1.5, 1.5] x [-1.5, 1.5]; we iterate z = z² + c and color by escape time. c morphs around a small circle in the complex plane over the loop, so the shape continuously breathes and returns exactly to its start at t = 1. Loop-clean because c(t) is periodic and the iteration is pure math. */ const JULIA_GLYPHS = ['.', ':', 'DIAG_FWD', 'X', 'X_DIAG', 'CHECKER', 'BLOCK']; // ten distinct fractal forms. Internally these are complex-plane c values that // drive the iteration math, but the published trait is just an opaque letter // (A through J) so the name doesn't invite math debates. const JULIA_SEEDS = [ [-0.7, 0.27015], [-0.8, 0.156], [0.285, 0.01], [-0.4, 0.6], [0.355, 0.355], [-0.835, -0.2321], [-0.7269, 0.1889], [-0.835, 0.232], [-0.74543, 0.11301], [0.37, 0.1], ]; const JULIA_LABELS = ['A','B','C','D','E','F','G','H','I','J']; function buildJulia(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const seedIdx = Math.floor(r() * JULIA_SEEDS.length); const seed = JULIA_SEEDS[seedIdx]; const form = JULIA_LABELS[seedIdx]; const c0r = seed[0], c0i = seed[1]; const morphR = 0.02 + r() * 0.04; // gentle morph: stays in interesting territory const maxIter = 24 + Math.floor(r() * 16); // 24..40 return { kind:'julia', seed: seedStr, N, palette, c0r, c0i, morphR, maxIter, form, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawJulia(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const { c0r, c0i, morphR, maxIter } = model; const ang = t * Math.PI * 2; const cr = c0r + morphR * Math.cos(ang); const ci = c0i + morphR * Math.sin(ang); const scale = 3 / N; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); const gLen = JULIA_GLYPHS.length; for(let y = 0; y < N; y++){ const zy0 = -1.5 + y * scale; for(let x = 0; x < N; x++){ const zx0 = -1.5 + x * scale; let zx = zx0, zy = zy0, i = 0; while(i < maxIter && zx*zx + zy*zy < 4){ const t2 = zx*zx - zy*zy + cr; zy = 2*zx*zy + ci; zx = t2; i++; } if(i >= maxIter) continue; // inside the set: leave black const colorIdx = i % n; const g = JULIA_GLYPHS[Math.min(gLen - 1, Math.floor(i / maxIter * gLen))]; ctx.fillStyle = css(pal[colorIdx]); drawGlyph(ctx, applyCharSet(model, x, y, g), x*cs, y*cs, cs, 0); } } } const TAU = Math.PI * 2; /* ---- Plasma: layered sine fields morph + drift; loop-clean because every sine phase advances an integer number of cycles over t in [0,1). ---- */ function buildPlasma(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const fx1 = 2 + Math.floor(r() * 4), fy1 = 2 + Math.floor(r() * 4); const fd = 2 + Math.floor(r() * 4), fr = 4 + Math.floor(r() * 5); const drift = (r() < 0.5 ? 1 : -1) * (1 + Math.floor(r() * 2)); return { kind:'plasma', seed: seedStr, N, palette, fx1, fy1, fd, fr, drift, palMode: rollPal(r), eye: makeEye(N, r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawPlasma(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const { fx1, fy1, fd, fr, drift } = model, cx = (N-1)/2, cy = (N-1)/2; for(let y = 0; y < N; y++){ const yPx = y * cs; for(let x = 0; x < N; x++){ const v = Math.sin(x/N*Math.PI*fx1 + t*TAU) + Math.sin(y/N*Math.PI*fy1 + t*TAU) + Math.sin((x+y)/N*Math.PI*fd + t*TAU*2) + Math.sin(Math.hypot(x-cx,y-cy)/N*Math.PI*fr - drift*t*TAU); const lvl = Math.floor((v + 4) / 8 * n); ctx.fillStyle = css(pal[((lvl)%n+n)%n]); ctx.fillRect(x*cs, yPx, cs, cs); ctx.fillStyle = css(pal[((lvl+1)%n+n)%n]); drawGlyph(ctx, applyCharSet(model, x, y, 'BLOCK'), x*cs, yPx, cs, 0); } } } /* ---- Spiral: rotating arms; loop-clean (integer rotations per loop). ---- */ function buildSpiral(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const arms = 2 + Math.floor(r() * 5), twist = 3 + Math.floor(r() * 6); const rot = 1 + Math.floor(r() * 2), dir = r() < 0.5 ? 1 : -1; return { kind:'spiral', seed: seedStr, N, palette, arms, twist, rot, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawSpiral(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const { arms, twist, rot, dir } = model, cx = (N-1)/2, cy = (N-1)/2; for(let y = 0; y < N; y++){ const yPx = y * cs; for(let x = 0; x < N; x++){ const ang = Math.atan2(y-cy, x-cx), rad = Math.hypot(x-cx, y-cy); const lvl = Math.floor((ang*arms/TAU + rad*twist/N - dir*t*rot) * n); ctx.fillStyle = css(pal[((lvl)%n+n)%n]); ctx.fillRect(x*cs, yPx, cs, cs); ctx.fillStyle = css(pal[((lvl+1)%n+n)%n]); drawGlyph(ctx, applyCharSet(model, x, y, 'BLOCK'), x*cs, yPx, cs, 0); } } } /* ---- Starfield: parallax warp; loop-clean (depth wraps mod 1 per loop). ---- */ function buildStarfield(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const count = 20 + Math.round(N * N * 0.06); const speed = 1 + Math.floor(r() * 2); const stars = []; for(let i = 0; i < count; i++) stars.push({ ang: r() * TAU, depth: r() }); return { kind:'starfield', seed: seedStr, N, palette, stars, speed, palMode: rollPal(r), eye: makeEye(N, r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawStarfield(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const { stars, speed } = model, cx = (N-1)/2, cy = (N-1)/2, maxR = N * 0.62; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); for(const s of stars){ const rr = ((s.depth + t*speed) % 1 + 1) % 1; const sr = rr * rr * maxR; const px = Math.round(cx + Math.cos(s.ang)*sr), py = Math.round(cy + Math.sin(s.ang)*sr); if(px < 0 || py < 0 || px >= N || py >= N) continue; ctx.fillStyle = css(pal[Math.min(n-1, Math.floor(rr * n))]); drawGlyph(ctx, applyCharSet(model, px, py, 'BALL'), px*cs, py*cs, cs, 0); } } /* ---- Lissajous: a harmonic curve traces + morphs; loop-clean (phase TAU). ---- */ function buildLissajous(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); let a = 2 + Math.floor(r() * 3), b = 2 + Math.floor(r() * 3); if(a === b) b = (b % 3) + 2; const dir = r() < 0.5 ? 1 : -1; return { kind:'lissajous', eyesBehind: true, seed: seedStr, N, palette, a, b, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawLissajous(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const { a, b, dir } = model, cx = (N-1)/2, cy = (N-1)/2, A = (N-1)/2*0.92, B = (N-1)/2*0.92; const steps = N * 28; let ppx = null, ppy = null; for(let k = 0; k <= steps; k++){ // <= closes the loop seam const phi = (k % steps) / steps * TAU; const px = Math.round(cx + A*Math.sin(a*phi + dir*t*TAU)), py = Math.round(cy + B*Math.sin(b*phi)); if(px < 0 || py < 0 || px >= N || py >= N){ ppx = ppy = null; continue; } ctx.fillStyle = css(pal[Math.floor(phi/TAU*n)%n]); drawGlyph(ctx, applyCharSet(model, px, py, 'BALL'), px*cs, py*cs, cs, 0); // bridge diagonal hops so the curve stays solidly connected for any char // set (Dots especially): fill the corner cell between diagonal steps. if(ppx !== null && Math.abs(px - ppx) === 1 && Math.abs(py - ppy) === 1){ drawGlyph(ctx, applyCharSet(model, px, ppy, 'BALL'), px*cs, ppy*cs, cs, 0); } ppx = px; ppy = py; } } /* ---- Joystick: a crypto wink. An abstracted 8-bit "joystick" — the two signature eyes form the base, a glyph shaft + rounded tip extend out, and color radiates from the tip like the Flower's bloom. Reads as a wide-eyed little character, not anatomy. Loop-clean (radial color cycle). ---- */ function segDistJ(px, py, x0, y0, x1, y1){ const dx = x1 - x0, dy = y1 - y0, L2 = dx*dx + dy*dy || 1; let u = ((px - x0) * dx + (py - y0) * dy) / L2; u = Math.max(0, Math.min(1, u)); return Math.hypot(px - (x0 + u*dx), py - (y0 + u*dy)); } function buildJoystick(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const HEADINGS = [[0,1],[0,1],[0,-1]]; // vertical; biased eyes-on-top (reads best as a face) const [hx, hy] = HEADINGS[Math.floor(r() * HEADINGS.length)]; const px = -hy, py = hx; // perpendicular const ballR = N * (0.11 + r() * 0.05); const shaftW = N * (0.07 + r() * 0.04); const tipR = shaftW * (1.45 + r() * 0.5); // bulbous tip — keeps the radial bloom prominent const shaftLen = N * (0.34 + r() * 0.16); const gap = ballR * (0.95 + r() * 0.3); // center the whole silhouette (back-of-eyes .. tip) on the grid so nothing clips const baseX = N/2 - hx * ((shaftLen + tipR) / 2), baseY = N/2 - hy * ((shaftLen + tipR) / 2); const balls = [[baseX + px*gap, baseY + py*gap], [baseX - px*gap, baseY - py*gap]]; const sx0 = baseX + hx*ballR, sy0 = baseY + hy*ballR; const tipX = baseX + hx*(ballR + shaftLen), tipY = baseY + hy*(ballR + shaftLen); const N2 = N*N, shaftMask = new Uint8Array(N2), dist = new Int16Array(N2); const scale = 0.7 + r() * 0.5; for(let y=0;y<N;y++) for(let x=0;x<N;x++){ const i = y*N+x, fx = x+0.5, fy = y+0.5; if(segDistJ(fx, fy, sx0, sy0, tipX, tipY) <= shaftW || Math.hypot(fx-tipX, fy-tipY) <= tipR){ shaftMask[i] = 1; dist[i] = Math.round(Math.hypot(fx-tipX, fy-tipY) * scale); } } const cellGlyphs = Array.from({length:N2}, () => FIELD_POOL[Math.floor(r()*FIELD_POOL.length)]); return { kind:'joystick', seed: seedStr, N, palette, shaftMask, dist, cellGlyphs, laps: Math.max(2, Math.round(10 / numColors)), dir: r() < 0.5 ? 1 : -1, balls, ballR, eye: makeEye(N, r), eyesBehind: true, palMode: rollPal(r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawBallEyes(ctx, model, t, cs, balls, R){ const e = model.eye, N = model.N; const sclera = (e.light != null && e.light !== 1) ? e.light : 1; const pupilCol = (e.light === 0) ? 1 : 0; // Void => light pupil const blink = e.blink ? (((t * (e.blink + 1)) % 1) > 0.94) : false; const ang = t * TAU, frantic = (e.gaze || 0) > 4 ? 1 : 0.4; const ox = Math.cos(ang) * R * 0.28 * frantic, oy = Math.sin(ang*1.3) * R * 0.28 * frantic; for(const [bx, by] of balls){ for(let dy=-Math.ceil(R); dy<=Math.ceil(R); dy++) for(let dx=-Math.ceil(R); dx<=Math.ceil(R); dx++){ if(dx*dx+dy*dy > R*R) continue; const x = Math.round(bx+dx), y = Math.round(by+dy); if(x<0||y<0||x>=N||y>=N) continue; ctx.fillStyle = css(sclera); ctx.fillRect(x*cs, y*cs, cs, cs); } if(blink) continue; const pr = Math.max(1, R*0.42); for(let dy=-Math.ceil(pr); dy<=Math.ceil(pr); dy++) for(let dx=-Math.ceil(pr); dx<=Math.ceil(pr); dx++){ if(dx*dx+dy*dy > pr*pr) continue; const x = Math.round(bx+ox+dx), y = Math.round(by+oy+dy); if(x<0||y<0||x>=N||y>=N) continue; ctx.fillStyle = css(pupilCol); ctx.fillRect(x*cs, y*cs, cs, cs); } } } function drawJoystick(ctx, model, t, cs){ const N = model.N, n = model.palette.length; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); const off = Math.floor(t * model.laps * n) * model.dir; // radial bloom from the tip for(let y=0;y<N;y++) for(let x=0;x<N;x++){ const i = y*N+x; if(!model.shaftMask[i]) continue; const ci = ((model.dist[i] + off) % n + n) % n; ctx.fillStyle = css(model.palette[ci]); drawGlyph(ctx, applyCharSet(model, x, y, 'BLOCK'), x*cs, y*cs, cs, 0); } drawBallEyes(ctx, model, t, cs, model.balls, model.ballR); } /* ---- Frog: ultra-rare crypto wink. A centered, symmetric little frog — round body + bulging signature eyes on top + a wide grin + tiny feet — with color radiating from the center like the Flower. Styled, not literal. Loop-clean. ---- */ function buildFrog(seedStr){ const r = makeRng(seedStr); let N = pickN(r); if(N < 24) N = 24; // frogs need room to read // green-biased palette: a dark->light green ramp (+ occasional accent) so every // frog reads froggy while still varying piece to piece (Pepto/Colodore shift too). const palette = [r() < 0.5 ? 9 : 11, 5, 13]; // shadow (brown/dk grey), green, light green if(r() < 0.45) palette.push(3); // cyan highlight sometimes if(r() < 0.22) palette.push(r() < 0.5 ? 8 : 14); // rare warm/cool accent spot const cx = N/2, cy = N*0.54; const rx = N*0.34, ry = N*0.30; // body ellipse const eyeR = N * (0.12 + r()*0.03); const eyeGap = N*0.18, eyeY = cy - ry*0.80; // eyes bulge up off the top const eyes = [[cx - eyeGap, eyeY], [cx + eyeGap, eyeY]]; const footR = N*0.10, footY = cy + ry*0.92, footX = N*0.20; const N2 = N*N, mask = new Uint8Array(N2), dist = new Int16Array(N2); const scale = 0.6 + r()*0.4; for(let y=0;y<N;y++) for(let x=0;x<N;x++){ const i=y*N+x, fx=x+0.5, fy=y+0.5; const inBody = ((fx-cx)/rx)**2 + ((fy-cy)/ry)**2 <= 1; const inFoot = Math.hypot(fx-(cx-footX), fy-footY) <= footR || Math.hypot(fx-(cx+footX), fy-footY) <= footR; if(inBody || inFoot){ mask[i]=1; dist[i]=Math.round(Math.hypot(fx-cx, fy-cy)*scale); } } // wide grin (gentle upturned smile) + two nostrils above it const mouthCells=[], mY = cy + ry*0.34, mW = rx*0.6; for(let x=Math.round(cx-mW); x<=Math.round(cx+mW); x++){ const dxn = Math.abs((x-cx)/mW); mouthCells.push([x, Math.round(mY - dxn*dxn*N*0.07)]); // ends curve up => smile } const nostrils = [[Math.round(cx - N*0.05), Math.round(cy + ry*0.06)], [Math.round(cx + N*0.05), Math.round(cy + ry*0.06)]]; const cellGlyphs = Array.from({length:N2}, () => FIELD_POOL[Math.floor(r()*FIELD_POOL.length)]); return { kind:'frog', seed:seedStr, N, palette, mask, dist, cellGlyphs, laps: Math.max(2, Math.round(10/palette.length)), dir: r()<0.5?1:-1, eyes, eyeR, mouthCells, nostrils, eye: makeEye(N, r), eyesBehind:true, palMode: rollPal(r), marquee:null, hasMarquee:false, static:false, fx: rollFx(r) }; } function drawFrog(ctx, model, t, cs){ const N = model.N, n = model.palette.length; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); const off = Math.floor(t * model.laps * n) * model.dir; for(let y=0;y<N;y++) for(let x=0;x<N;x++){ const i=y*N+x; if(!model.mask[i]) continue; const ci = ((model.dist[i] + off) % n + n) % n; ctx.fillStyle = css(model.palette[ci]); drawGlyph(ctx, applyCharSet(model, x, y, 'BLOCK'), x*cs, y*cs, cs, 0); } ctx.fillStyle = css(0); for(const [x,y] of model.mouthCells) if(x>=0&&y>=0&&x<N&&y<N) ctx.fillRect(x*cs, y*cs, cs, cs); for(const [x,y] of model.nostrils) if(x>=0&&y>=0&&x<N&&y<N) ctx.fillRect(x*cs, y*cs, cs, cs); drawBallEyes(ctx, model, t, cs, model.eyes, model.eyeR); } /* ====== public bootstrap ====== */ const LOOP_MS = 3400; function buildFromSpec(spec, salt){ const sfx = salt ? '-' + salt : ''; const seed = (spec.seed || spec.id || 'token') + sfx; let m; switch(spec.