memoscan
← all memos

Memo 0x49412b63…2b7210 on Ethereum

e", 6], ["dblue", 3]]); // always a visible frame const frameNative = caw >= 12 ? BLK : 1; const cells = []; const n = cols * rows; for (let i = 0; i < n; i++) { const cr = makeRng(String(hash) + ":" + i); // independent, reproducible per case const creature = buildCreature(cr, caw, cah, theme); const col = i % cols, row = (i / cols) | 0; cells.push({ i, col, row, ox: col * caw, oy: row * cah, creature }); } return { hash, cols, rows, caw, cah, cells, border, frameNative, theme }; } /* ===================================================================== 7. PAINT (reads spec + time only; seamless loop) ===================================================================== */ function phase(t, period, off) { let p = (t / period + off) % 1; if (p < 0) p += 1; return p; } const TAU = Math.PI * 2; function paintBack(fb, ox, oy, cr, t) { const b = cr.back; if (b.type === "plain") return; const put = (x, y) => { if (x >= 1 && x < cr.cw - 1 && y >= 1 && y < cr.ch - 1) fb.blk(ox + x, oy + y, b.col); }; if (b.type === "scan") { const shift = b.anim ? Math.floor(phase(t, b.period, b.off) * 3) : 0; for (let y = 1 + ((shift) % 3); y < cr.ch - 1; y += 3) for (let x = 1; x < cr.cw - 1; x += 2) put(x, y); } else if (b.type === "dots") { for (let y = 2; y < cr.ch - 1; y += 3) for (let x = 2; x < cr.cw - 1; x += 3) put(x, y); } else if (b.type === "bricks") { for (let y = 2; y < cr.ch - 1; y += 3) { for (let x = 1; x < cr.cw - 1; x++) put(x, y); } for (let y = 2; y < cr.ch - 1; y += 3) { const o = ((y / 3) | 0) % 2 ? 2 : 0; for (let x = 1 + o; x < cr.cw - 1; x += 4) for (let k = 1; k < 3 && y + k < cr.ch - 1; k++) put(x, y + k); } } else if (b.type === "stars") { for (const s of b.items) { const on = !s.tw || phase(t, b.period, s.ph) > 0.5; if (on) put(s.x, s.y); } } } function paintCreature(fb, ox, oy, cr, t) { const C = cr.colors, A = cr.anim, bb = cr.bb; paintBack(fb, ox, oy, cr, t); // ----- motion ----- let dy = 0, dx = 0, shadow = 0; const pm = phase(t, A.period, A.off); const s = Math.sin(pm * TAU); if (A.motion === "drift") { const d = A.dir || [0, -1]; dx = Math.round(A.amp * s * d[0]); dy = Math.round(A.amp * s * d[1]); } else if (A.motion === "float") { dy = Math.round(A.amp * s); shadow = 1; } // wiggle / march / wave / burst handled at their parts below // shared rhythmic hop: lifts every creature on the beat dy -= Math.round((cr.hopAmt || 1) * hopEnv(t)); const put = (x, y, c) => fb.blk(ox + x + dx, oy + y + dy, c); // shadow under a floating creature (width shrinks as it rises) if (shadow) { const fy = bb.y0 + bb.h + 1; const half = Math.max(1, Math.round(bb.w * 0.3) - (dy < 0 ? 1 : 0)); for (let x = -half; x <= half; x++) fb.blk(ox + Math.round((cr.cw - 1) / 2) + x, oy + fy, C.bg === NEON.bg ? "#0a0c1a" : shade(C.bg, 0.25)); } // ----- feet (march alternates) ----- if (cr.feet.length) { const marchP = phase(t, A.period, A.off); const up = Math.sin(marchP * TAU) > 0 ? 0 : 1; cr.feet.forEach((f, idx) => { const lift = A.motion === "march" ? ((idx % 2 === 0) === (up === 0) ? -1 : 0) : 0; put(f.x, f.y + lift, C.outline); }); } // ----- body: 1px outline only (hollow, like the sheets) ----- for (const p of cr.outPx) put(p[0], p[1], C.stroke); // ----- pixel explosion: single 1px pixels fly out from the centre, loop-safe ----- if (A.motion === "burst" && A.burst) { const cxp = (cr.cw - 1) / 2, cyp = bb.y0 + bb.h / 2; for (const pt of A.burst) { const p = phase(t, A.period, (A.off + pt.off) % 1); const r = p * pt.maxR; const x = Math.round(cxp + Math.cos(pt.ang) * r), y = Math.round(cyp + Math.sin(pt.ang) * r); if (x >= 1 && x < cr.cw - 1 && y >= 1 && y < cr.ch - 1) put(x, y, pt.col); // 1px, clipped inside the case } } // ----- arms (wave alternates) ----- if (cr.arms.length) { const wp = phase(t, A.period, A.off); cr.arms.forEach((a, idx) => { const lift = A.motion === "wave" ? Math.round(Math.sin((wp + idx * 0.5) * TAU)) : 0; put(a.x, a.y + lift, C.body); put(a.x, a.y + lift, C.body); }); } // ----- antennae (wiggle sways the tip) ----- for (const an of cr.antennae) { const wp = phase(t, A.period, A.off); const sway = A.motion === "wiggle" ? Math.round(Math.sin(wp * TAU)) : 0; for (let k = 1; k <= an.h; k++) put(an.x, an.baseY - k, C.outline); put(an.x + sway, an.baseY - an.h - 1, an.tip); } // ----- face features (with expression, features always stay visible) ----- const ep = phase(t, A.exprPeriod || MASTER_SECONDS, A.exprOff || 0); if (cr.face === "skull") { for (const e of cr.eyes) for (let ax = -1; ax <= 0; ax++) for (let ay = 0; ay <= 1; ay++) put(e.x + ax, e.y + ay, C.eye); // yellow eye holes put(Math.round((cr.cw - 1) / 2), cr.eyes[0].y + 2, C.stroke); // nose const mo = cr.mouth, open = hopEnv(t) > 0.5 ? 1 : 0; // jaw chatters on the beat for (let x = mo.x0; x <= mo.x1; x += 2) put(x, mo.y + open, C.stroke); // teeth marks } else if (cr.face === "alien") { const blink = A.eyeExpr === "blink" && ep > 0.9; const lookX = A.eyeExpr === "look" ? Math.round(Math.cos(ep * TAU)) : 0; for (const e of cr.eyes) { const dir = e.x < (cr.cw - 1) / 2 ? 1 : -1; if (blink) { for (let dx2 = -1; dx2 <= 1; dx2++) put(e.x + dx2, e.y, C.eye); } // lid line (still visible) else { const bx = e.x + lookX; for (let ay = -1; ay <= 1; ay++) { put(bx, e.y + ay, C.eye); put(bx - dir, e.y + ay, C.eye); } put(bx - dir, e.y - 1, C.eye); put(bx, e.y - 1, "#ffffff"); // glint } } const mo = cr.mouth, open = A.mouthExpr === "open" && Math.sin(ep * TAU) > 0.4 ? 1 : 0; for (let x = mo.x0; x <= mo.x1; x++) { put(x, mo.y, C.mouthCol); if (open) put(x, mo.y + 1, C.mouthCol); } } else { // ----- generic eyes with expression ----- const blink = A.eyeExpr === "blink" && ep > 0.9; const squint = A.eyeExpr === "squint" && ((ep % 0.5) > 0.32); const lookX = A.eyeExpr === "look" ? Math.round(Math.cos(ep * TAU)) : 0; const lookY = A.eyeExpr === "look" ? Math.round(Math.sin(ep * TAU) * 0.6) : 0; cr.eyes.forEach((e, idx) => { const wink = A.eyeExpr === "wink" && idx === cr.eyes.length - 1 && ep > 0.85; if (blink || squint || wink) { put(e.x - 1, e.y, C.eye); put(e.x, e.y, C.eye); } // closed dash (visible) else { put(e.x + lookX, e.y + lookY, C.eye); if (cr.bigEyes) put(e.x + lookX, e.y - 1 + lookY, "#ffffff"); } }); // ----- generic mouth with expression ----- const mo = cr.mouth, mid = (mo.x0 + mo.x1) / 2, halfW = Math.max(1, (mo.x1 - mo.x0) / 2); let cs = mo.smile ? 1 : -1, open = 0; if (A.mouthExpr === "smileshift") cs = Math.sin(ep * TAU); // smile <-> frown else if (A.mouthExpr === "talk") open = hopEnv(t) > 0.5 ? 1 : 0; // opens on the beat else if (A.mouthExpr === "open") open = Math.sin(ep * TAU) > 0.4 ? 1 : 0; for (let x = mo.x0; x <= mo.x1; x++) { const md = Math.abs(x - mid) / halfW; put(x, mo.y - Math.round((1 - md) * cs), C.mouthCol); if (open) put(x, mo.y + 1, C.mouthCol); // open mouth (second row) } } // bonus badge (static, top-left corner of the case) if (cr.bonus && BONUS[cr.bonus]) { const bc = BONUS[cr.bonus], ic = bc.icon; for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) if (ic[r][c] === "1") fb.blk(ox + 1 + c, oy + 1 + r, bc.col); } // rarity pips (static, top-right corner) — common shows none, keeping the art clean if (cr.rarity && cr.rarity !== "common") { const pips = cr.rarity === "rare" ? 1 : cr.rarity === "epic" ? 2 : 3; const rc = cr.rarity === "rare" ? "#00e5ff" : cr.rarity === "epic" ? "#ff17c6" : "#ffe600"; for (let p = 0; p < pips; p++) fb.blk(ox + cr.cw - 2 - p * 2, oy + 1, rc); } } /* interactive explosion: shatter a touched monster into 1px pixels, then respawn */ const FX_DUR = 0.55; // seconds function explode(grid, idx, now) { const cell = grid.cells[idx]; if (!cell || cell.fx) return; const cr = cell.creature; const cxp = (cr.cw - 1) / 2, cyp = cr.bb.y0 + cr.bb.h / 2; const parts = []; for (const p of cr.outPx) { const ang = Math.atan2(p[1] - cyp, p[0] - cxp) + (Math.random() - 0.5) * 0.7; const spd = 6 + Math.random() * 12; // art px / second parts.push({ x: p[0], y: p[1], vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd - 3, col: cr.colors.stroke }); } cell.fx = { start: now, parts }; return true; } /* match-style chain: exploding a monster also clears a contiguous same-colour run of 3+ on its row and/or its column, then each cell respawns at random. Returns how many cells were triggered. No score, just a satisfying pop. */ function explodeMatch(grid, idx, now) { const cell = grid.cells[idx]; if (!cell) return 0; const cols = grid.cols, rows = grid.rows; const color = cell.creature.colors.stroke; const r0 = (idx / cols) | 0, c0 = idx % cols; const same = (i) => grid.cells[i] && grid.cells[i].creature.colors.stroke === color; const hit = new Set([idx]); const runH = [idx]; for (let c = c0 - 1; c >= 0 && same(r0 * cols + c); c--) runH.unshift(r0 * cols + c); for (let c = c0 + 1; c < cols && same(r0 * cols + c); c++) runH.push(r0 * cols + c); if (runH.length >= 3) runH.forEach((i) => hit.add(i)); const runV = [idx]; for (let r = r0 - 1; r >= 0 && same(r * cols + c0); r--) runV.unshift(r * cols + c0); for (let r = r0 + 1; r < rows && same(r * cols + c0); r++) runV.push(r * cols + c0); if (runV.length >= 3) runV.forEach((i) => hit.add(i)); let out = []; for (const i of hit) if (explode(grid, i, now)) out.push(i); return out; } function paint(fb, grid, t, now) { if (now == null) now = t; fb.clear(NEON.bg); for (const cell of grid.cells) { if (cell.fx) { const e = now - cell.fx.start; if (e >= FX_DUR) { // explosion done -> respawn a fresh monster cell._resp = (cell._resp || 0) + 1; cell.creature = buildCreature(makeRng(String(grid.hash) + ":" + cell.i + ":r" + cell._resp + ":" + Math.floor(now * 1000)), grid.caw, grid.cah, grid.theme); if (Math.random() < 0.16) cell.creature.bonus = randomBonus(); // bonuses only emerge during play cell.fx = null; paintCreature(fb, cell.ox, cell.oy, cell.creature, t); } else { // draw flying 1px shards const g = 22; for (const pr of cell.fx.parts) { const px = pr.x + pr.vx * e, py = pr.y + pr.vy * e + 0.5 * g * e * e; if (px >= 1 && px < grid.caw - 1 && py >= 1 && py < grid.cah - 1) fb.blk(cell.ox + Math.round(px), cell.oy + Math.round(py), pr.col); } } } else { paintCreature(fb, cell.ox, cell.oy, cell.creature, t); } } // case grid: single 1px lines at every boundary (no doubling at shared edges) if (grid.border !== "none") { const col = NEON[grid.border]; const cwN = grid.caw * BLK, chN = grid.cah * BLK; for (let c = 0; c <= grid.cols; c++) { const x = Math.min(CW - 1, c * cwN); for (let y = 0; y < CH; y++) fb.set(x, y, col); } for (let r = 0; r <= grid.rows; r++) { const y = Math.min(CH - 1, r * chN); for (let x = 0; x < CW; x++) fb.set(x, y, col); } } // bonus cases blink a 2px-wide border in the bonus colour if (Math.floor(now * 3) % 2 === 0) { const cwN = grid.caw * BLK, chN = grid.cah * BLK; for (const cell of grid.cells) { const b = cell.creature.bonus; if (!b || !BONUS[b] || cell.fx) continue; const x0 = cell.ox * BLK, y0 = cell.oy * BLK, c = BONUS[b].col; for (let x = 0; x < cwN; x++) for (let d = 0; d < 2; d++) { fb.set(x0 + x, y0 + d, c); fb.set(x0 + x, Math.min(CH - 1, y0 + chN - 1 - d), c); } for (let y = 0; y < chN; y++) for (let d = 0; d < 2; d++) { fb.set(x0 + d, y0 + y, c); fb.set(Math.min(CW - 1, x0 + cwN - 1 - d), y0 + y, c); } } } } /* ===================================================================== 8. AUDIO (deterministic chiptune sequence, one MASTER-second loop) --------------------------------------------------------------------- Pure data: notes as MIDI numbers (or null for a rest). The harness turns this into sound with Web Audio. 16 steps across MASTER_SECONDS. ===================================================================== */ const SCALES = { minPent: [0, 3, 5, 7, 10], majPent: [0, 2, 4, 7, 9], dorian: [0, 2, 3, 5, 7, 9, 10], lydian: [0, 2, 4, 6, 7, 9, 11], }; const STYLES = ["acid", "stab", "rolling", "arp", "pluck"]; const SONG_BARS = 36; // 8 steps/bar at 0.25s = 2s bar (120 BPM) -> 72s const PROGS = [[0, 0, 4, 3], [0, 3, 4, 2], [0, 4, 2, 3], [0, 2, 4, 5], [0, 0, 3, 4], [0, 5, 3, 4]]; function buildAudio(hash) { const R = makeRng(String(hash) + "|audio"); const SPB = 8; // steps per bar, kick every 2 steps = 120 BPM four-on-the-floor const scaleName = R.pick(["minPent", "dorian", "minPent"]); // darker, driving scales const scale = SCALES[scaleName], SL = scale.length; const root = R.pick([43, 45, 47, 48, 50]); const style = R.weighted([["acid", 4], ["stab", 3], ["rolling", 3], ["arp", 3], ["pluck", 3]]); const prog = R.pick(PROGS); const leadWave = style === "stab" || style === "pluck" ? "square" : "sawtooth"; const bassWave = R.pick(["square", "triangle"]); const noteAt = (deg) => root + 12 + scale[((deg % SL) + SL) % SL] + 12 * Math.floor(deg / SL); // an acid pattern of scale-degree offsets, lightly mutated per bar for variety const basePat = []; for (let i = 0; i < SPB; i++) basePat.push(R.int(0, SL)); const lead = [], bass = [], perc = [], waves = [], vel = []; for (let b = 0; b < SONG_BARS; b++) { const phrase = (b / 4) | 0; const barDeg = prog[phrase % prog.length]; const intense = Math.min(3, phrase); // song structure -> energy dynamics + variation const mode = b < 2 ? "intro" : (phrase % 4 === 3 ? "break" : (b % 8 < 1 ? "build" : "main")); const pat = basePat.map((d) => (R.bool(0.18) ? R.int(0, SL) : d)); // per-bar mutation for (let s = 0; s < SPB; s++) { const beat = s % 2 === 0; // on-beat (quarter) // ---- drums: driving four-on-the-floor ---- let v = 0; if (mode !== "break" && beat) v |= 2; // kick every beat else if (mode === "break" && s === 0) v |= 2; // sparse kick in the break if (!beat) v |= 8; // offbeat open hat (the techno "tss") if ((mode === "main" && intense >= 2)) v |= 1; // driving 16th closed hats when intense if ((mode === "main" || mode === "build") && (s === 2 || s === 6)) v |= 4; // clap on 2 & 4 perc.push(v); // ---- bass: syncopated, driving ---- let bn = null; if (mode !== "intro") { if (!beat || intense >= 2) bn = root - 12 + scale[barDeg % SL]; } bass.push(bn == null ? null : Math.max(24, Math.min(60, bn))); // ---- lead ---- let ln = null, gv = 0.15; if (mode === "main" || mode === "build") { if (style === "stab") { if (!beat) ln = noteAt(barDeg + [0, 2, 4][s % 3]); } else if (style === "rolling") { ln = noteAt(barDeg + basePat[0] + (s % 4 === 0 ? SL : 0)); } else { ln = noteAt(barDeg + pat[s] + (R.bool(0.12) ? SL : 0)); } // acid / arp / pluck if (beat) gv = 0.2; // accent on beats for punch } lead.push(ln == null ? null : Math.max(24, Math.min(96, ln))); vel.push(gv); waves.push(leadWave); } } const STEPS = SONG_BARS * SPB; if (perc.filter((p) => p & 2).length < SONG_BARS) for (let b = 0; b < SONG_BARS; b++) { perc[b * SPB] |= 2; perc[b * SPB + 4] |= 2; } return { steps: STEPS, stepSeconds: MASTER_SECONDS / 16, loopSeconds: STEPS * (MASTER_SECONDS / 16), root, scaleName, style, leadWave, bassWave, waves, vel, lead, bass, perc, }; } /* ===================================================================== 9. EXPORTS ===================================================================== */ return { CW, CH, BLK, MASTER_SECONDS, NEON, makeHash, makeRng, FB, buildGrid, buildCreature, paint, paintCreature, buildAudio, explode, explodeMatch, BONUS, hexToRgb, }; })(); /* ===================== PXL BITS — vanilla harness ===================== Pure Canvas 2D + DOM + Web Audio. No libraries. The engine above (E) is deterministic from the token hash; this drives display, input, the contemplative demo, the 69s game, sound and sharing. */ /* ---- token hash (Art Blocks / Artsunami style) with a local fallback ---- */ var FX_ALPHABET = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; function randomToken() { var s = "oo"; for (var i = 0; i < 49; i++) s += FX_ALPHABET[(Math.random() * FX_ALPHABET.length) | 0]; return s; } var TOKEN_HASH = (typeof tokenData !== "undefined" && tokenData && tokenData.hash) ? tokenData.hash : (typeof fxhash !== "undefined" && fxhash) ? fxhash : (typeof $fx !== "undefined" && $fx && $fx.hash) ? $fx.hash : randomToken(); var CW = E.CW, CH = E.CH, HDR = 24; // header (title/score/timer) native height /* ---- canvas: use the platform's canvas if present, else create one ---- */ var canvas = (typeof window !== "undefined" && window.canvas && window.canvas.getContext) ? window.canvas : (typeof document !== "undefined" && document.querySelector && document.querySelector("canvas")) || (function () { var c = document.createElement("canvas"); (document.body || document.documentElement).appendChild(c); return c; })(); var ctx = canvas.getContext("2d"); try { canvas.style.position = "absolute"; canvas.style.left = "50%"; canvas.style.top = "50%"; canvas.style.transform = "translate(-50%,-50%)"; canvas.style.imageRendering = "pixelated"; } catch (e) {} var off = document.createElement("canvas"); off.width = CW; off.height = CH; var offctx = off.getContext("2d"); var imgData = offctx.createImageData(CW, CH); var fb = new E.FB(); var curHash = TOKEN_HASH; var grid = E.buildGrid(curHash); /* ---- colour cache ---- */ var RGB = {}; function rgbOf(h) { return RGB[h] || (RGB[h] = E.hexToRgb(h)); } var BG = E.hexToRgb(E.NEON.bg); /* ---- clock (monotonic seconds) ---- */ var clock = (typeof performance !== "undefined" && performance.now) ? function () { return performance.now() / 1000; } : function () { return Date.now() / 1000; }; var T0 = clock(); function nowSec() { return clock() - T0; } /* ---- game state ---- */ var score = 0, ROUND = 69, roundState = "demo", roundStart = 0; var lastInteraction = 0, lastAuto = 0, DEMO_RETURN = 18, AUTO_EVERY = 0.5; var multUntil = 0, flashText = "", flashUntil = 0, flashCol = "#ffffff"; var dispScale = 1, dispOX = 0, dispOY = 0; /* ---- per-colour identity (destruction note + timbre + base points) ---- */ var COLOR_META = { green: { note: 64, base: 5, wave: "triangle" }, cyan: { note: 67, base: 6, wave: "triangle" }, blue: { note: 69, base: 7, wave: "sawtooth" }, pink: { note: 72, base: 8, wave: "sawtooth" }, yellow: { note: 62, base: 6, wave: "square" }, orange: { note: 60, base: 7, wave: "square" }, red: { note: 57, base: 9, wave: "square" }, white: { note: 76, base: 10, wave: "square" } }; var META_BY_HEX = {}; for (var _nm in COLOR_META) if (E.NEON[_nm]) META_BY_HEX[String(E.NEON[_nm]).toLowerCase()] = COLOR_META[_nm]; function metaFor(hex) { return (hex && META_BY_HEX[String(hex).toLowerCase()]) || { note: 60, base: 6, wave: "square" }; } var RARITY_MULT = { common: 1, rare: 2, epic: 4, legendary: 8 }; function scoreForCells(idxs) { var s = 0; for (var k = 0; k < idxs.length; k++) { var c = grid.cells[idxs[k]].creature; s += metaFor(c.colors.stroke).base * (RARITY_MULT[c.rarity] || 1); } return s; } /* ---- 3x5 pixel font ---- */ var PXFONT = { "0": ["111", "101", "101", "101", "111"], "1": ["010", "110", "010", "010", "111"], "2": ["111", "001", "111", "100", "111"], "3": ["111", "001", "111", "001", "111"], "4": ["101", "101", "111", "001", "001"], "5": ["111", "100", "111", "001", "111"], "6": ["111", "100", "111", "101", "111"], "7": ["111", "001", "010", "010", "010"], "8": ["111", "101", "111", "101", "111"], "9": ["111", "101", "111", "001", "111"], "A": ["010", "101", "111", "101", "101"], "B": ["110", "101", "110", "101", "110"], "C": ["111", "100", "100", "100", "111"], "D": ["110", "101", "101", "101", "110"], "E": ["111", "100", "111", "100", "111"], "G": ["111", "100", "101", "101", "111"], "H": ["101", "101", "111", "101", "101"], "I": ["111", "010", "010", "010", "111"], "L": ["100", "100", "100", "100", "111"], "M": ["101", "111", "111", "101", "101"], "N": ["101", "111", "111", "111", "101"], "O": ["111", "101", "101", "101", "111"], "P": ["111", "101", "111", "100", "100"], "R": ["110", "101", "110", "101", "101"], "S": ["111", "100", "111", "001", "111"], "T": ["111", "010", "010", "010", "010"], "U": ["101", "101", "101", "101", "111"], "V": ["101", "101", "101", "101", "010"], "X": ["101", "101", "010", "101", "101"], ":": ["000", "010", "000", "010", "000"], " ": ["000", "000", "000", "000", "000"] }; function textW(str, ps) { return str.length * (4 * ps) - ps; } function drawText(cx, str, x, cy, ps, col, align, outlineCol, ob) { var total = textW(str, ps); var sx0 = Math.round(align === "left" ? x : align === "right" ? x - total : x - total / 2); var y0 = Math.round(cy - (5 * ps) / 2); var cells = [], sx = sx0; for (var ci = 0; ci < str.length; ci++) { var g = PXFONT[str[ci]] || PXFONT[" "]; for (var r = 0; r < 5; r++) for (var c = 0; c < 3; c++) if (g[r][c] === "1") cells.push([sx + c * ps, y0 + r * ps]); sx += 4 * ps; } if (outlineCol) { var o = ob || 2; cx.fillStyle = outlineCol; for (var p = 0; p < cells.length; p++) cx.fillRect(cells[p][0] - o, cells[p][1] - o, ps + 2 * o, ps + 2 * o); } cx.fillStyle = col; for (var q = 0; q < cells.length; q++) cx.fillRect(cells[q][0], cells[q][1], ps, ps); } /* ---- sizing: fill the frame at the game's portrait aspect ---- */ function fitSize() { var NW = CW, NH = CH + HDR; var W = (typeof innerWidth === "number" && innerWidth) || 0; var H = (typeof innerHeight === "number" && innerHeight) || 0; if (W < 2 || H < 2) { W = 540; H = 1080; } var s = Math.min(W / NW, H / NH); return { w: Math.max(1, Math.round(NW * s)), h: Math.max(1, Math.round(NH * s)) }; } function resize() { var sz = fitSize(); canvas.width = sz.w; canvas.height = sz.h; ctx.imageSmoothingEnabled = false; } resize(); if (typeof addEventListener !== "undefined") addEventListener("resize", resize); /* ============================ AUDIO (Web Audio) ============================ */ var AC = null, master = null, muted = false, audioStarted = false; var seq = null, nextStepTime = 0, stepIdx = 0, visT0 = 0, schedTimer = null; var LOOKAHEAD = 0.10, TICK_MS = 25; function midiToFreq(m) { return 440 * Math.pow(2, (m - 69) / 12); } function buildSeq() { seq = E.buildAudio(curHash); } function startAudio() {