memoscan
← all memos

Memo 0x30a69be8…a15d2c on Ethereum

st chase = r() < 0.35; // ~1/3 of bands get a chase sweep bands.push({ y0: y, y1: y + rows[b], bg, fg, fgBright: BRIGHTER[fg] ?? 1, motif, motion, dir, speed: N * speedMul, chase, chaseAx: r() < 0.5 ? 1 : 0, chaseAy: r() < 0.5 ? 1 : 0, chaseSpeed: N * (r() < 0.5 ? 1 : 2), chaseThick: 1 + Math.floor(r() * 2), }); y += rows[b]; } clampBandColors(bands); return { seed: seedStr, N, bands, colorCount: countBandColors(bands), eye: makeEye(N, r), marquee: null, hasMarquee: false, palMode: rollPal(r), static: false, fx: rollFx(r) }; } /* eye geometry + per-token gaze behavior (the old shiftRate / blinkRate, now seeded per token so every face darts and blinks differently). All loop-clean: the dart sequence wraps at t=1 and blink windows live strictly inside (0,1). */ const TEARS_PCT = 0.30; // ultra-rare "pattern" sclera fills the eye with symbol glyphs, one symbol per // row (a random combo down the eye). Genuine C64 ROM chars. const EYE_PATTERN_SYMS = ['#', 'V', '%', '&', '@', '*']; const EYE_PATTERN_PCT = 0.02; // ~2% of tokens (ultra rare) const EYE_HIGH_COLOR = 10; // C64 light red (pink) = the "High AF" eye const EYE_COLOR_PCT = 0.10; // ~10% of the collection have a non-white eye const EYE_HIGH_OF_COLORED = 0.10; // of colored eyes, ~10% are High AF (~1% overall) // every other C64 color, randomly mixed. black (0) = "Void", which gets a white // pupil instead of black so the dot still reads against the black sclera. const EYE_COLOR_POOL = [0, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; // ultra-rare animated ("strobe") eyes: the color cycles every few frames. non- // void eyes strobe the sclera/field; void eyes strobe the pupil. probabilities // (a rate, so counts scale with supply) bumped ~50% over the prior pass. At the // 2048 supply the expected counts are roughly ~21 solid, ~21 grid, ~27 pattern, // ~12 void. void is clamped at 0.90 so a couple of static voids still exist. const EYE_ANIM_PROB = { solid: 0.02175, grid: 0.02175, pattern: 0.648, void: 0.90 }; const EYE_ANIM_COLORS = [2, 3, 4, 5, 7, 8, 10, 13, 14]; // vivid C64 strobe cycle function makeEye(N, rng){ const r = rng || Math.random; const w = Math.max(3, Math.round(0.18 * N)); // a touch bigger so a pupil fits const h = Math.max(2, Math.round(0.13 * N)); const gap = Math.max(1, Math.round(0.06 * N)); const top = Math.max(1, Math.round(0.16 * N)); const left = Math.min(N - (w * 2 + gap) - 1, Math.round(0.48 * N)); const mid = Math.floor(w / 2); const looks = [mid, mid, 0, w - 1]; // center-biased gaze targets const steps = 3 + Math.floor(r() * 5); // 3..7 darts per loop (shift rate) const dartSeq = Array.from({ length: steps }, () => looks[Math.floor(r() * looks.length)]); dartSeq[0] = mid; // rest at center at loop start/end const blinkCount = [0, 1, 1, 1, 2, 2, 3][Math.floor(r() * 7)]; // blink rate const blinks = []; for(let i = 0; i < blinkCount; i++) blinks.push([0.05 + r() * 0.8, 0.045]); // tears: ~30% of tokens cry. one cell-sized drop falls below each eye, // advancing one row per loop-tick and wrapping at t=1 (loop-clean). const tears = r() < TEARS_PCT; // sclera fill, now a deliberate seeded trait (was an accidental density // artifact): solid block / grid of squares / ultra-rare symbol pattern. const fr = r(); let fill, fillRows = null; if(fr < EYE_PATTERN_PCT){ fill = 'pattern'; fillRows = Array.from({ length: h }, () => EYE_PATTERN_SYMS[Math.floor(r() * EYE_PATTERN_SYMS.length)]); } else if(fr < EYE_PATTERN_PCT + (1 - EYE_PATTERN_PCT) / 2){ fill = 'solid'; } else { fill = 'grid'; } // eye color: ~90% white. ~10% get a non-white sclera; of those, ~10% are the // pink "High AF" eye (~1% overall) and the rest are any other C64 color. let light = 1; // white sclera (default) if(r() < EYE_COLOR_PCT){ light = (r() < EYE_HIGH_OF_COLORED) ? EYE_HIGH_COLOR : EYE_COLOR_POOL[Math.floor(r() * EYE_COLOR_POOL.length)]; } const pupil = (light === 0) ? 1 : 0; // void (black) eye gets a white dot // ultra-rare strobe: non-void eyes animate the sclera, void eyes the pupil let anim = null; if(light === 0){ if(r() < EYE_ANIM_PROB.void) anim = 'pupil'; } else if(r() < (EYE_ANIM_PROB[fill] || 0)){ anim = 'sclera'; } return { w, h, gap, top, left, light, dark: 0, pupil, dartSeq, blinks, tears, tearColor: 14, gaze: steps, blink: blinkCount, fill, fillRows, anim }; } /* ---- generic "field" renderer ---- Per-cell color cycles along a scalar field (distance, axis, angle, ...) with a temporal offset, over a fixed field of random symbols. One renderer covers flowers (radial), tunnels (square), stripes (axis), rays (angle), checker, pulse and diagonals. Loop-clean: offset advances whole color-cycles per loop. */ const FIELD_POOL = ['X', '.', 'DIAG_FWD', 'DIAG_BACK', 'X_DIAG', 'CHECKER', 'CROSS', 'HBAR', 'VBAR', 'DIAG_QUAD', 'TRI_LOW', 'TRI_UP', 'HALF_L', 'HALF_B', 'QUAD_TL', 'QUAD_BL', 'BLOCK']; function pickColors(r, count){ const all = Array.from({ length: 16 }, (_, i) => i); for(let i = all.length - 1; i > 0; i--){ const j = (r() * (i + 1)) | 0; [all[i], all[j]] = [all[j], all[i]]; } return all.slice(0, count); } const FIELD_FNS = { radial: (x, y, c0) => Math.round(Math.hypot(x - c0, y - c0)), tunnel: (x, y, c0) => Math.max(Math.abs(x - c0), Math.abs(y - c0)), hbars: (x, y) => y, vbars: (x, y) => x, diag: (x, y) => x + y, checker: (x, y, c0, sc) => Math.floor(x / sc) + Math.floor(y / sc), rays: (x, y, c0, sc, sectors) => Math.floor(((Math.atan2(y - c0, x - c0) + Math.PI) / (2 * Math.PI)) * sectors), pulse: () => 0, }; function buildField(seedStr, type){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const c0 = (N - 1) / 2; const sc = 1 + Math.floor(r() * 3); // checker scale const sectors = numColors * (2 + Math.floor(r() * 3)); // rays: multiple of colors const fn = FIELD_FNS[type] || FIELD_FNS.radial; const field = new Int16Array(N * N); for(let y = 0; y < N; y++) for(let x = 0; x < N; x++) field[y * N + x] = fn(x, y, c0, sc, sectors); const cellGlyphs = Array.from({ length: N * N }, () => FIELD_POOL[Math.floor(r() * FIELD_POOL.length)]); const laps = Math.max(2, Math.round(12 / numColors)); const dir = r() < 0.5 ? 1 : -1; return { kind: 'field', seed: seedStr, N, palette, field, cellGlyphs, laps, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } /* ---- Flowers: a real petalled bloom, not concentric rings. The color bands follow a rose curve (radius modulated by cos(petals*angle)) so they bulge into petals at the tips and pinch into seams between them, around a randomized center point. Petal count + center are internal (NOT traits). ---- */ function buildFlower(seedStr){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const cx = (N - 1) / 2 + (r() - 0.5) * N * 0.3; // randomized center point const cy = (N - 1) / 2 + (r() - 0.5) * N * 0.3; const petals = 5 + Math.floor(r() * 4); // 5..8 petals (internal) const amp = 0.55 + r() * 0.25; // petal depth const phase = r() * Math.PI * 2; // flower rotation const scale = 0.5 + r() * 0.4; // band density const field = new Int16Array(N * N); for(let y = 0; y < N; y++) for(let x = 0; x < N; x++){ const dx = x - cx, dy = y - cy; const ang = Math.atan2(dy, dx) + phase; const rad = Math.hypot(dx, dy); const petalR = 1 + amp * Math.cos(petals * ang); // >0 since amp<=0.8 field[y * N + x] = Math.round(rad / petalR * scale); } const cellGlyphs = Array.from({ length: N * N }, () => FIELD_POOL[Math.floor(r() * FIELD_POOL.length)]); const laps = Math.max(2, Math.round(12 / numColors)); const dir = r() < 0.5 ? 1 : -1; return { kind: 'field', seed: seedStr, N, palette, field, cellGlyphs, laps, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } /* mirror / kaleidoscope: a coherent MOVING pattern (chevrons / bars / waves / rings) folded across symmetry axes, so the motion itself reflects. The pattern is a level field evaluated on the FOLDED coordinate, then color-cycled by the loop phase (drawField), so bands sweep and meet symmetrically at the axes. Directional glyphs are flipped per fold so the texture reflects too. modes: 'h' (vertical axis), 'v' (horizontal axis), 'd' (diagonal), 'quad' (4-fold). */ const MIRROR_BASES = ['diag', 'bars', 'rings', 'wave']; const MIRROR_GLYPH = { diag: 'DIAG_FWD', bars: 'VBAR', rings: 'BLOCK', wave: 'HBAR' }; function buildMirror(seedStr, mode){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const base = pick(MIRROR_BASES, r); const glyph = MIRROR_GLYPH[base]; const freq = 1 + Math.floor(r() * 3), amp = 1 + Math.floor(r() * 3); const cx = (N - 1) / 2, cy = (N - 1) / 2; const lvl = (sx, sy) => { switch(base){ case 'bars': return Math.round(sx); case 'rings': return Math.round(Math.hypot(sx, sy)); case 'wave': return Math.round(sy + amp * Math.sin(sx * freq * 0.4)); default: return Math.round(sx + sy); // diag -> chevrons when folded } }; const field = new Int16Array(N * N); const cellGlyphs = new Array(N * N); const fh = new Uint8Array(N * N), fv = new Uint8Array(N * N); for(let y = 0; y < N; y++) for(let x = 0; x < N; x++){ let sx, sy, h = 0, v = 0; if(mode === 'h'){ sx = Math.abs(x - cx); sy = y; h = x < cx ? 1 : 0; } else if(mode === 'v'){ sx = x; sy = Math.abs(y - cy); v = y < cy ? 1 : 0; } else if(mode === 'quad'){ sx = Math.abs(x - cx); sy = Math.abs(y - cy); h = x < cx ? 1 : 0; v = y < cy ? 1 : 0; } else { if(x >= y){ sx = x; sy = y; } else { sx = y; sy = x; } } // 'd' diagonal fold const i = y * N + x; field[i] = lvl(sx, sy); cellGlyphs[i] = glyph; fh[i] = h; fv[i] = v; } const laps = Math.max(2, Math.round(12 / numColors)); const dir = r() < 0.5 ? 1 : -1; return { kind: 'field', seed: seedStr, N, palette, field, cellGlyphs, cellFlipH: fh, cellFlipV: fv, base, mirrorMode: mode, laps, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawField(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length; const offset = (model.dir || 1) * Math.floor(t * n * (model.laps || 1)); const f = model.field, g = model.cellGlyphs, fh = model.cellFlipH, fv = model.cellFlipV; for(let y = 0; y < N; y++){ const yPx = y * cs; for(let x = 0; x < N; x++){ const i = y * N + x; const lvl = f[i]; const paper = pal[((lvl - offset) % n + n) % n]; const ink = pal[((lvl - offset + 1) % n + n) % n]; ctx.fillStyle = css(paper); ctx.fillRect(x * cs, yPx, cs, cs); ctx.fillStyle = css(ink); drawGlyph(ctx, applyCharSet(model, x, y, g[i]), x * cs, yPx, cs, 0, fh ? !!fh[i] : false, fv ? !!fv[i] : false); } } } /* ---- band-based idea builders ---- */ function splitHeights(N, count, r){ const w = Array.from({ length: count }, () => 0.5 + r()); const s = w.reduce((a, b) => a + b, 0); const rows = w.map(x => Math.max(1, Math.round(x / s * N))); let diff = N - rows.reduce((a, b) => a + b, 0); for(let i = 0; diff !== 0; i = (i + 1) % count){ if(diff > 0){ rows[i]++; diff--; } else if(rows[i] > 1){ rows[i]--; diff++; } } return rows; } function buildWeave(seed){ const r = makeRng(seed); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const T = 2 + Math.floor(r() * 3); // thread width 2..4 cells const bandThreads = 1 + Math.floor(r() * 2); // threads per color stripe const woff = 1 + Math.floor(r() * Math.max(1, numColors - 1)); // warp vs weft stripe offset const field = new Int16Array(N * N); const cellGlyphs = new Array(N * N).fill('BLOCK'); for(let y = 0; y < N; y++) for(let x = 0; x < N; x++){ const tx = Math.floor(x / T), ty = Math.floor(y / T); const over = ((tx + ty) & 1) === 0; // plain weave: alternate thread on top const warp = Math.floor(tx / bandThreads) % numColors; // vertical thread colour const weft = (Math.floor(ty / bandThreads) + woff) % numColors; // horizontal thread colour field[y * N + x] = over ? warp : weft; } const laps = Math.max(2, Math.round(12 / numColors)); const dir = r() < 0.5 ? 1 : -1; return { kind:'field', seed, N, palette, field, cellGlyphs, laps, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function bandAt(model, y){ const b = model.bands; for(let i = 0; i < b.length; i++) if(y >= b[i].y0 && y < b[i].y1) return b[i]; return b[b.length - 1]; } /* count the unique color indices actually used across every band in a token (bg / fg / fgBright, plus chase color = 1 if any band has chase on). Band-engine tokens don't carry a model.palette, so this is how the traits panel surfaces "colors" for Bands / Glitch / Weave / Single tokens. */ function countBandColors(bands){ const s = new Set(); for(const b of bands){ s.add(b.bg); s.add(b.fg); if(b.fgBright != null) s.add(b.fgBright); if(b.chase) s.add(1); // chase sweep is hardcoded white } return s.size; } /* quietly cap band-engine tokens to the published 2-12 colors range. Collapses fgBright shimmer onto its fg first (least visible change), then if still over, remaps a band's fg to the previous band's fg. Internal constraint; not surfaced as a public trait. */ const BAND_COLOR_MAX = 12; function clampBandColors(bands){ while(countBandColors(bands) > BAND_COLOR_MAX){ let collapsed = false; for(const b of bands){ if(b.fgBright !== b.fg){ b.fgBright = b.fg; collapsed = true; break; } } if(!collapsed) break; } while(countBandColors(bands) > BAND_COLOR_MAX){ let collapsed = false; for(let i = 1; i < bands.length; i++){ if(bands[i].fg !== bands[0].fg){ bands[i].fg = bands[0].fg; collapsed = true; break; } } if(!collapsed) break; } // final safety net: collapse extra bgs onto bands[0].bg if still over while(countBandColors(bands) > BAND_COLOR_MAX){ let collapsed = false; for(let i = 1; i < bands.length; i++){ if(bands[i].bg !== bands[0].bg){ bands[i].bg = bands[0].bg; collapsed = true; break; } } if(!collapsed) break; } } /* per-cell motif index for a band's motion mode (off = whole-cell phase offset) */ function motifIndex(band, x, y, off){ const L = band.motif.length; let v; switch(band.motion){ case 'scrollV': v = y - band.dir * off; break; case 'diag': v = x + y - band.dir * off; break; // along the x+y axis case 'diagB': v = x - y - band.dir * off; break; // opposite diagonal (TR<->BL) case 'altRows': v = x - (y % 2 ? -1 : 1) * off; break; case 'altCols': v = y - (x % 2 ? -1 : 1) * off; break; case 'scrollH': default: v = x - band.dir * off; break; } return ((v % L) + L) % L; } /* draw one frame. t in [0,1). opts: { crt:0-100, chroma:0-100, shimmer:bool } */ function drawC64Frame(ctx, model, t, opts = {}){ setPaletteMode(opts.palette || model.palMode || 'pepto'); // opts override > per-token > default const N = model.N; const D = ctx.canvas.width; const cs = Math.floor(D / N); if(model.kind === 'field'){ drawField(ctx, model, t, cs); } else if(model.kind === 'bloom'){ drawBloom(ctx, model, t, cs); } else if(model.kind === 'rain'){ drawRain(ctx, model, t, cs); } else if(model.kind === 'fireworks'){ drawFireworks(ctx, model, t, cs); } else if(model.kind === 'snake'){ drawSnake(ctx, model, t, cs); } else if(model.kind === 'julia'){ drawJulia(ctx, model, t, cs); } else if(model.kind === 'plasma'){ drawPlasma(ctx, model, t, cs); } else if(model.kind === 'spiral'){ drawSpiral(ctx, model, t, cs); } else if(model.kind === 'starfield'){ drawStarfield(ctx, model, t, cs); } else if(model.kind === 'lissajous'){ // eyes BEHIND the curve so the harmonic line weaves in front of the face // (the one family with a 3D/depth feel). Clear, draw eyes, then the curve. ctx.fillStyle = css(0); ctx.fillRect(0, 0, N * cs, N * cs); if(!opts.eyesOff) drawEyes(ctx, model, t, cs); drawLissajous(ctx, model, t, cs); } else if(model.kind === 'joystick'){ drawJoystick(ctx, model, t, cs); } else if(model.kind === 'frog'){ drawFrog(ctx, model, t, cs); } else if(model.kind === 'honorary'){ if(typeof drawHonorary === 'function') drawHonorary(ctx, model, t, cs); // marketing-only; absent in the lean on-chain bundle } else { const stat = !!model.static; const shimmer = opts.shimmer !== false && !stat; for(let y = 0; y < N; y++){ const band = bandAt(model, y); const off = stat ? 0 : Math.floor(t * band.speed); const chaseOff = (band.chase && !stat) ? Math.floor(t * band.chaseSpeed) : 0; const yPx = y * cs; for(let x = 0; x < N; x++){ ctx.fillStyle = css(band.bg); ctx.fillRect(x * cs, yPx, cs, cs); let inkIdx = band.fg; // periodic, loop-safe "nervous" shimmer if(shimmer){ const phase = t + ((x * 5 + y * 9) % 16) / 16; if(Math.sin(phase * Math.PI * 2) > 0.72) inkIdx = band.fgBright; } // chase sweep: a bright diagonal/line stripe running across the band if(band.chase){ const cv = ((band.chaseAx * x + band.chaseAy * y - chaseOff) % N + N) % N; if(cv < band.chaseThick) inkIdx = 1; // white sweep } ctx.fillStyle = css(inkIdx); drawGlyph(ctx, applyCharSet(model, x, y, band.motif[motifIndex(band, x, y, off)]), x * cs, yPx, cs, 0); } } } if(!opts.eyesOff && !model.eyesBehind && model.eye) drawEyes(ctx, model, t, cs); const bakedCrt = model.fx?.crt ?? model.crt ?? 0; const bakedChroma = model.fx?.chroma ?? model.chroma ?? 0; const crt = (opts.crt != null ? opts.crt : bakedCrt) | 0; const chroma = (opts.chroma != null ? opts.chroma : bakedChroma) | 0; if(crt > 0 || chroma > 0){ applyPostFx(ctx, { rgbShift: chroma, crtEffect: crt }); } } function drawEyes(ctx, model, t, cs){ const e = model.eye; const tt = t % 1; // per-token blink windows + dart sequence (the nervous identity, always alive) const blinks = e.blinks || []; const seq = (e.dartSeq && e.dartSeq.length) ? e.dartSeq : [Math.floor(e.w / 2)]; const blink = blinks.some(([s, d]) => tt >= s && tt < s + d); const cell = (cx, cy, ch, colorIdx) => { ctx.fillStyle = css(colorIdx); drawGlyph(ctx, ch, cx * cs, cy * cs, cs, 0); }; const pupilCol = seq[Math.floor(tt * seq.length) % seq.length]; const pupilRow = Math.min(e.h - 1, Math.floor(e.h / 2)); const fill = e.fill || 'solid'; // ultra-rare strobe: cycle a vivid color several times per loop (loop-clean, // wraps to the start color at t=1). sclera-strobe recolors the field, pupil- // strobe recolors the dot. const strobe = EYE_ANIM_COLORS[Math.floor(tt * EYE_ANIM_COLORS.length * 12) % EYE_ANIM_COLORS.length]; const scleraCol = (e.anim === 'sclera') ? strobe : e.light; const pupilColor = (e.anim === 'pupil') ? strobe : (e.pupil != null ? e.pupil : e.dark); const drawOne = (exCell) => { if(blink){ // solid eyelid line, regardless of fill style const yMid = e.top + Math.floor(e.h / 2); ctx.fillStyle = css(e.dark); for(let i = 0; i < e.w; i++) ctx.fillRect((exCell + i) * cs, yMid * cs, cs, cs); return; } // sclera for(let row = 0; row < e.h; row++){ for(let i = 0; i < e.w; i++){ const cx = exCell + i, cy = e.top + row; if(fill === 'pattern'){ // ultra-rare: a symbol per row (random combo down the eye) cell(cx, cy, (e.fillRows && e.fillRows[row]) || 'BLOCK', scleraCol); } else if(fill === 'grid'){ // deliberate grid: white square inset by a gutter so gaps always read const g = Math.max(1, Math.floor(cs * 0.14)); ctx.fillStyle = css(scleraCol); ctx.fillRect(cx * cs + g, cy * cs + g, cs - 2 * g, cs - 2 * g); } else { // solid: full cell, no gaps at any density ctx.fillStyle = css(scleraCol); ctx.fillRect(cx * cs, cy * cs, cs, cs); } } } // ball pupil sitting on the sclera (strobe / white on void / else black) cell(exCell + pupilCol, e.top + pupilRow, 'BALL', pupilColor); }; drawOne(e.left); drawOne(e.left + e.w + e.gap); // tears: a cell-sized drop below each eye, advancing one row per loop and // wrapping at t=1 so the loop stays seamless. Drawn after the eyes so the // drop reads on top of any field underneath. if(e.tears){ const N = model.N; const fallStart = e.top + e.h; const fallDist = N - fallStart; if(fallDist > 0){ const row = fallStart + Math.floor(tt * fallDist); const cx1 = e.left + Math.floor(e.w / 2); const cx2 = e.left + e.w + e.gap + Math.floor(e.w / 2); ctx.fillStyle = css(e.tearColor); drawGlyph(ctx, 'BALL', cx1 * cs, row * cs, cs, 0); drawGlyph(ctx, 'BALL', cx2 * cs, row * cs, cs, 0); } } } /* ---- Bloom: tree-of-life expansion from 4 center cells. modes: 'breathe' — expand to fill, contract back to seed, returns at t=1 (smooth loop) 'exhale' — expand to fill, snap reset to seed at loop end Both are cell-step and deterministic. */ const BLOOM_LEAVES = ['HEART','DIAMOND','SPADE','CLUB','BALL']; const BLOOM_BRANCH = ['DIAG_FWD','DIAG_BACK','BLOCK','X']; function buildBloom(seedStr, mode){ const r = makeRng(seedStr); const N = pickN(r); const numColors = bellColors(r); const palette = pickColors(r, numColors); const cx = Math.floor(N/2), cy = Math.floor(N/2); const seedCells = [[cx-1,cy-1],[cx,cy-1],[cx-1,cy],[cx,cy]]; const visited = new Uint8Array(N*N); const order = []; const frontier = []; for(const [x,y] of seedCells){ visited[y*N+x] = 1; order.push([x,y,'BLOCK']); frontier.push([x,y]); } const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; while(frontier.length){ const i = Math.floor(r() * frontier.length); const [fx,fy] = frontier.splice(i, 1)[0]; for(const [dx,dy] of dirs){ if(r() < 0.28) continue; // pruning rate keeps branches organic const nx = fx + dx, ny = fy + dy; if(nx<0||ny<0||nx>=N||ny>=N) continue; if(visited[ny*N+nx]) continue; visited[ny*N+nx] = 1; const g = (r() < 0.22) ? pick(BLOOM_LEAVES, r) : pick(BLOOM_BRANCH, r); order.push([nx,ny,g]); frontier.push([nx,ny]); } } const laps = Math.max(2, Math.round(12 / numColors)); const dir = r() < 0.5 ? 1 : -1; return { kind:'bloom', mode: mode||'breathe', seed: seedStr, N, palette, order, laps, dir, palMode: rollPal(r), eye: makeEye(N, r), marquee: null, hasMarquee: false, static: false, fx: rollFx(r) }; } function drawBloom(ctx, model, t, cs){ const N = model.N, pal = model.palette, n = pal.length, order = model.order, P = order.length; ctx.fillStyle = css(0); ctx.fillRect(0, 0, N*cs, N*cs); // void const offset = (model.dir || 1) * Math.floor(t * n * (model.laps || 1)); const drawCell = (i) => { const e = order[i]; const paper = pal[((i - offset) % n + n) % n]; const ink = pal