/* NERVOUS renderer bundle, v1.1.0 — on-chain SSTORE2 storage */
(function(global){
"use strict";
/* ====== src/glyphfont.js ====== */
/* glyphfont.js — authentic Commodore 64 glyphs, 8x8.
These are the EXACT bytes from the C64 character ROM (characters.901225-01),
uppercase/graphics set. Each glyph is 8 rows of 8 bits (bit 7 = leftmost
pixel), i.e. 8 bytes per glyph. Not a redraw, the real letterforms and the
real PETSCII pattern tiles, so the look is genuine C64.
Drawn with fillRect so it scales crisply to any cell size and inherits the
current ctx.fillStyle. The graphics tiles are the pattern-makers (dither,
diagonals, quadrants, lines, suits); the letters cover OXPEQ + MALLRIOT. */
const GLYPH_W = 8;
const GLYPH_H = 8;
const GLYPHS = {
/* full uppercase alphabet, genuine C64 ROM 901225-01 bytes */
'A': [24,60,102,126,102,102,102,0],
'B': [124,102,102,124,102,102,124,0],
'C': [60,102,96,96,96,102,60,0],
'D': [120,108,102,102,102,108,120,0],
'E': [126,96,96,120,96,96,126,0],
'F': [126,96,96,120,96,96,96,0],
'G': [60,102,96,96,110,102,60,0],
'H': [102,102,102,126,102,102,102,0],
'I': [60,24,24,24,24,24,60,0],
'J': [30,12,12,12,12,108,56,0],
'K': [102,108,120,112,120,108,102,0],
'L': [96,96,96,96,96,96,126,0],
'M': [99,119,127,107,99,99,99,0],
'N': [102,118,126,126,110,102,102,0],
'O': [60,102,102,102,102,102,60,0],
'P': [124,102,102,124,96,96,96,0],
'Q': [60,102,102,102,102,60,14,0],
'R': [124,102,102,124,120,108,102,0],
'S': [60,102,96,60,6,102,60,0],
'T': [126,24,24,24,24,24,24,0],
'U': [102,102,102,102,102,102,60,0],
'V': [102,102,102,102,102,60,24,0],
'W': [99,99,99,107,127,119,99,0],
'X': [102,102,60,24,60,102,102,0],
'Y': [102,102,102,60,24,24,24,0],
'Z': [126,6,12,24,48,96,126,0],
/* full lowercase alphabet, genuine C64 set-2 forms */
'a': [0,0,60,6,62,102,62,0],
'b': [0,96,96,124,102,102,124,0],
'c': [0,0,60,96,96,96,60,0],
'd': [0,6,6,62,102,102,62,0],
'e': [0,0,60,102,126,96,60,0],
'f': [0,14,24,60,24,24,24,0],
'g': [0,0,62,102,102,62,6,124],
'h': [0,96,96,124,102,102,102,0],
'i': [0,24,0,56,24,24,60,0],
'j': [0,6,0,6,6,6,6,60],
'k': [0,96,96,108,120,108,102,0],
'l': [0,56,24,24,24,24,60,0],
'm': [0,0,102,127,127,107,99,0],
'n': [0,0,124,102,102,102,102,0],
'o': [0,0,60,102,102,102,60,0],
'p': [0,0,124,102,102,124,96,96],
'q': [0,0,62,102,102,62,6,6],
'r': [0,0,124,102,96,96,96,0],
's': [0,0,62,96,60,6,124,0],
't': [0,24,126,24,24,24,14,0],
'u': [0,0,102,102,102,102,62,0],
'v': [0,0,102,102,102,60,24,0],
'w': [0,0,99,107,127,62,54,0],
'x': [0,0,102,60,24,60,102,0],
'y': [0,0,102,102,102,62,6,124],
'z': [0,0,126,12,24,48,126,0],
/* full digits 0-9, authentic C64 ROM bytes */
'0': [60,102,110,118,102,102,60,0],
'1': [24,56,24,24,24,24,126,0],
'2': [60,102,6,12,48,96,126,0],
'3': [60,102,6,28,6,102,60,0],
'4': [6,14,30,102,127,6,6,0],
'5': [126,96,124,6,6,102,60,0],
'6': [60,96,96,124,102,102,60,0],
'7': [126,102,12,24,24,24,24,0],
'8': [60,102,102,60,102,102,60,0],
'9': [60,102,102,62,6,102,60,0],
/* a few extra punctuation marks for marketing copy */
',': [0,0,0,0,0,24,24,48],
'?': [60,102,6,12,24,0,24,0],
'-': [0,0,0,126,0,0,0,0],
'+': [0,24,24,126,24,24,0,0],
'=': [0,0,126,0,0,126,0,0],
'/': [3,7,14,28,56,112,224,192],
':': [0,0,24,24,0,24,24,0],
';': [0,0,24,24,0,24,24,48],
"'": [24,24,24,0,0,0,0,0],
'"': [102,102,102,0,0,0,0,0],
/* number-row symbols, genuine C64 forms. '^' has no C64 caret; the key in
that position is the up-arrow, so we map '^' to the authentic up-arrow. */
'!': [24,24,24,24,0,0,24,0],
'@': [60,102,110,110,96,98,60,0],
'#': [102,102,255,102,255,102,102,0],
'$': [24,62,96,60,6,124,24,0],
'%': [98,102,12,24,48,102,70,0],
'^': [0,24,60,126,24,24,24,24], // C64 up-arrow
'&': [60,102,60,56,103,102,63,0],
'*': [0,102,60,255,60,102,0,0],
'(': [12,24,48,48,48,24,12,0],
')': [48,24,12,12,12,24,48,0],
'.': [0,0,0,0,0,24,24,0],
/* PETSCII graphic / pattern tiles (the real pattern vocabulary) */
'CHECKER': [204,204,51,51,204,204,51,51], // dither / hatch
'DIAG_BACK':[192,224,112,56,28,14,7,3], // \
'DIAG_FWD': [3,7,14,28,56,112,224,192], // /
'DIAG_QUAD':[240,240,240,240,15,15,15,15], // ▚ opposed quadrants
'TRI_LOW': [255,127,63,31,15,7,3,1], // lower-left solid triangle
'TRI_UP': [255,254,252,248,240,224,192,128], // upper-left solid triangle
'HBAR': [0,0,0,255,255,0,0,0], // ─
'VBAR': [24,24,24,24,24,24,24,24], // │
'CROSS': [24,24,24,255,255,24,24,24], // ┼
'X_DIAG': [195,231,126,60,60,126,231,195], // ✕
'HALF_L': [240,240,240,240,240,240,240,240], // ▌ left half
'HALF_B': [0,0,0,0,255,255,255,255], // ▄ bottom half
'QUAD_TL': [240,240,240,240,0,0,0,0], // ▘ top-left quadrant
'QUAD_BL': [0,0,0,0,240,240,240,240], // ▖ bottom-left quadrant
'BLOCK': [255,255,255,255,255,255,255,255], // full block (reverse space)
'SPADE': [8,28,62,127,127,28,62,0],
'HEART': [54,127,127,127,62,28,8,0],
'DIAMOND': [8,28,62,127,62,28,8,0],
'CLUB': [24,24,102,102,24,24,60,0],
'BALL': [0,60,126,126,126,126,60,0],
// C64-style pi: a top bar with two descending legs, drawn in the 8x8 ROM grid.
'PI': [0,0,126,102,102,102,102,0],
};
/* draw glyph `ch` centered in a `cell`-px square at pixel origin (px, py),
using the current ctx.fillStyle. `pad` (0..0.4) shrinks it within the cell.
Returns true if the glyph exists. */
function drawGlyph(ctx, ch, px, py, cell, pad = 0, flipH = false, flipV = false){
const g = GLYPHS[ch];
if(!g) return false;
const inner = cell * (1 - pad * 2);
const ps = Math.max(1, Math.floor(Math.min(inner / GLYPH_W, inner / GLYPH_H)));
const gw = GLYPH_W * ps, gh = GLYPH_H * ps;
const ox = Math.round(px + (cell - gw) / 2);
const oy = Math.round(py + (cell - gh) / 2);
for(let r = 0; r < GLYPH_H; r++){
const sr = flipV ? GLYPH_H - 1 - r : r; // mirror rows
const bits = g[sr];
for(let c = 0; c < GLYPH_W; c++){
const sc = flipH ? GLYPH_W - 1 - c : c; // mirror columns
if(bits & (1 << (GLYPH_W - 1 - sc))) ctx.fillRect(ox + c * ps, oy + r * ps, ps, ps);
}
}
return true;
}
const GLYPH_KEYS = Object.keys(GLYPHS);
/* ====== src/c64palette.js ====== */
/* c64palette.js — the genuine Commodore 64 (VIC-II) 16-color hardware palette,
"Pepto" values, the widely-accepted accurate set. This is THE palette for the
reworked art (native-C64 default). Index order matches the C64's color codes. */
const C64 = [
[0x00, 0x00, 0x00], // 0 black
[0xFF, 0xFF, 0xFF], // 1 white
[0x68, 0x37, 0x2B], // 2 red
[0x70, 0xA4, 0xB2], // 3 cyan
[0x6F, 0x3D, 0x86], // 4 purple
[0x58, 0x8D, 0x43], // 5 green
[0x35, 0x28, 0x79], // 6 blue
[0xB8, 0xC7, 0x6F], // 7 yellow
[0x6F, 0x4F, 0x25], // 8 orange
[0x43, 0x39, 0x00], // 9 brown
[0x9A, 0x67, 0x59], // 10 light red
[0x44, 0x44, 0x44], // 11 dark grey
[0x6C, 0x6C, 0x6C], // 12 grey
[0x9A, 0xD2, 0x84], // 13 light green
[0x6C, 0x5E, 0xB5], // 14 light blue
[0x95, 0x95, 0x95], // 15 light grey
];
/* Colodore palette — the other widely-used C64 set, calibrated to the 1084
monitor (warmer / softer than Pepto). Toggle at render time via setPaletteMode. */
const COLODORE = [
[0x00, 0x00, 0x00], [0xFF, 0xFF, 0xFF], [0x81, 0x33, 0x38], [0x75, 0xCE, 0xC8],
[0x8E, 0x3C, 0x97], [0x56, 0xAC, 0x4D], [0x2E, 0x2C, 0x9B], [0xED, 0xF1, 0x71],
[0x8E, 0x50, 0x29], [0x55, 0x38, 0x00], [0xC4, 0x6C, 0x71], [0x4A, 0x4A, 0x4A],
[0x7B, 0x7B, 0x7B], [0xA9, 0xFF, 0x9F], [0x70, 0x6D, 0xEB], [0xB2, 0xB2, 0xB2],
];
/* darker codes good for backgrounds, brighter codes good for foreground/ink,
so band pairings always have contrast */
const DARKS = [0, 2, 4, 6, 8, 9, 11];
const BRIGHTS = [1, 3, 5, 7, 10, 12, 13, 14, 15];
let CURRENT = C64;
function setPaletteMode(mode){ CURRENT = (mode === 'colodore') ? COLODORE : C64; }
const css = (i) => {
const c = CURRENT[i] || CURRENT[0];
return `rgb(${c[0]},${c[1]},${c[2]})`;
};
/* nearest palette index to an arbitrary RGB (used to quantize uploaded images
into the C64 palette for "honorary" pieces). Uses the active palette mode. */
function nearestIndex(r, g, b){
let best = 0, bd = Infinity;
for(let i = 0; i < CURRENT.length; i++){
const c = CURRENT[i];
const dr = c[0] - r, dg = c[1] - g, db = c[2] - b;
const d = dr * dr + dg * dg + db * db;
if(d < bd){ bd = d; best = i; }
}
return best;
}
/* a slightly brighter relative for the "nervous" shimmer of a given ink color */
const BRIGHTER = {
2: 10, 6: 14, 5: 13, 4: 14, 8: 7, 9: 8, 11: 12, 12: 15, 3: 1, 7: 1,
10: 1, 13: 1, 14: 1, 15: 1, 1: 1, 0: 11,
};
/* ====== src/post-fx.js ====== */
/* post-fx.js — whole-canvas post-process passes ported from
mallriot's glitch-o-matic 5000. Run after eyes/border/tears so the
effects apply to the final composite (true VHS/CRT vibe).
Effects (each applied only if its intensity is > 0):
1. rgbShift — R/B horizontal channel shift
2. crtEffect — barrel distortion + scanlines + vignette
Takes effect intensities as explicit parameters so we don't depend on
any module-level state object (keeps the IIFE bundle self-contained). */
function applyPostFx(ctx, opts = {}){
const rgb = (opts.rgbShift | 0) || 0;
const crt = (opts.crtEffect | 0) || 0;
if(rgb === 0 && crt === 0) return;
if(rgb > 0) applyRgbShift(ctx, rgb);
if(crt > 0) applyCrtEffect(ctx, crt);
}
/* ---------- C64 monitor look (baked into canvas; mirrors the live CSS) ----------
composite: horizontal color bleed (px radius), the "on a TV" merge of dithers.
sat/con/bri: saturation / contrast / brightness multipliers (like CSS filters).
scan: scanline darkening (0..1) on alternate rows. Used at export time so a
saved GIF matches the live CSS monitor overlay. */
function applyMonitor(ctx, { composite = 0, sat = 1, con = 1, bri = 1, scan = 0 } = {}){
const W = ctx.canvas.width, H = ctx.canvas.height;
const img = ctx.getImageData(0, 0, W, H);
const d = img.data;
if(composite > 0){
const src = new Uint8ClampedArray(d);
const r = composite | 0, div = r * 2 + 1;
for(let y = 0; y < H; y++){
const rb = y * W;
for(let x = 0; x < W; x++){
let R = 0, G = 0, B = 0;
for(let k = -r; k <= r; k++){ const xx = x + k < 0 ? 0 : x + k >= W ? W - 1 : x + k; const i = (rb + xx) * 4; R += src[i]; G += src[i+1]; B += src[i+2]; }
const di = (rb + x) * 4; d[di] = R / div; d[di+1] = G / div; d[di+2] = B / div;
}
}
}
if(sat !== 1 || con !== 1 || bri !== 1 || scan > 0){
for(let y = 0; y < H; y++){
const sl = (scan > 0 && (y % 2 === 0)) ? (1 - scan) : 1;
for(let x = 0; x < W; x++){
const i = (y * W + x) * 4;
let R = d[i], G = d[i+1], B = d[i+2];
if(sat !== 1){ const g = 0.299*R + 0.587*G + 0.114*B; R = g + (R-g)*sat; G = g + (G-g)*sat; B = g + (B-g)*sat; }
if(con !== 1){ R = (R-128)*con + 128; G = (G-128)*con + 128; B = (B-128)*con + 128; }
if(bri !== 1){ R *= bri; G *= bri; B *= bri; }
if(sl !== 1){ R *= sl; G *= sl; B *= sl; }
d[i] = R < 0 ? 0 : R > 255 ? 255 : R; d[i+1] = G < 0 ? 0 : G > 255 ? 255 : G; d[i+2] = B < 0 ? 0 : B > 255 ? 255 : B;
}
}
}
ctx.putImageData(img, 0, 0);
}
/* ---------- rgb shift / channel separation (R right, B left) ---------- */
function applyRgbShift(ctx, shift){
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const img = ctx.getImageData(0, 0, W, H);
const data = img.data;
const src = new Uint8ClampedArray(data);
for(let y = 0; y < H; y++){
const rowBase = y * W;
for(let x = 0; x < W; x++){
const dIdx = (rowBase + x) * 4;
const rX = Math.max(0, Math.min(W - 1, x + shift));
const bX = Math.max(0, Math.min(W - 1, x - shift));
data[dIdx] = src[(rowBase + rX) * 4];
data[dIdx + 2] = src[(rowBase + bX) * 4 + 2];
}
}
ctx.putImageData(img, 0, 0);
}
/* ---------- noise: per-pixel random brightness offset ---------- */
function applyNoise(ctx, intensity){
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const img = ctx.getImageData(0, 0, W, H);
const data = img.data;
const prob = intensity / 100;
for(let i = 0; i < data.length; i += 4){
if(Math.random() < prob){
const n = (Math.random() - 0.5) * 100;
data[i] = Math.max(0, Math.min(255, data[i] + n));
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + n));
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + n));
}
}
ctx.putImageData(img, 0, 0);
}
/* ---------- CRT: barrel distortion + scanlines + vignette ----------
The (x,y) → (sx,sy) source map for the barrel warp depends only on the
canvas size + intensity, never on the frame. Same for the vignette
strength per pixel. We cache both, keyed by W:H:intensity, so the per-
frame cost drops to a tight memory-bound loop. Critical for marketplace
iframes where browsers throttle synchronous canvas work. */
const _crtCache = new Map();
function getCrtCache(W, H, intensity){
const key = W + ':' + H + ':' + intensity;
let e = _crtCache.get(key);
if(e) return e;
const f = intensity / 100;
const cx = W / 2, cy = H / 2;
const curvature = 0.15 * f;
const vignetteStrength = 0.4 * f;
const Wm1 = W - 1, Hm1 = H - 1;
const n = W * H;
const sxArr = new Float32Array(n);
const syArr = new Float32Array(n);
const vigArr = new Float32Array(n);
for(let y = 0; y < H; y++){
const dy = (y - cy) / cy;
const rowBase = y * W;
for(let x = 0; x < W; x++){
const dx = (x - cx) / cx;
const rSq = dx * dx + dy * dy;
const distortion = 1 + curvature * rSq;
let sx = cx + (x - cx) * distortion;
let sy = cy + (y - cy) * distortion;
if(sx < 0) sx = 0; else if(sx > Wm1) sx = Wm1;
if(sy < 0) sy = 0; else if(sy > Hm1) sy = Hm1;
const i = rowBase + x;
sxArr[i] = sx;
syArr[i] = sy;
vigArr[i] = 1 - rSq * vignetteStrength;
}
}
e = { sxArr, syArr, vigArr };
_crtCache.set(key, e);
return e;
}
function applyCrtEffect(ctx, intensity){
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const img = ctx.getImageData(0, 0, W, H);
const data = img.data;
const src = new Uint8ClampedArray(data);
const { sxArr, syArr, vigArr } = getCrtCache(W, H, intensity);
const scanlineOpacity = 0.3 * (intensity / 100);
const dim2 = 1 - scanlineOpacity;
const Wm1 = W - 1, Hm1 = H - 1;
for(let y = 0; y < H; y++){
const rowBase = y * W;
const rowScan = (y % 2 === 0) ? dim2 : 1;
for(let x = 0; x < W; x++){
const i = rowBase + x;
const sx = sxArr[i];
const sy = syArr[i];
const x0 = sx | 0, y0 = sy | 0;
const x1 = x0 < Wm1 ? x0 + 1 : x0;
const y1 = y0 < Hm1 ? y0 + 1 : y0;
const fx = sx - x0, fy = sy - y0;
const ifx = 1 - fx, ify = 1 - fy;
const w00 = ifx * ify;
const w10 = fx * ify;
const w01 = ifx * fy;
const w11 = fx * fy;
const i00 = (y0 * W + x0) * 4;
const i10 = (y0 * W + x1) * 4;
const i01 = (y1 * W + x0) * 4;
const i11 = (y1 * W + x1) * 4;
const dim = rowScan * vigArr[i];
let R = (src[i00] * w00 + src[i10] * w10 + src[i01] * w01 + src[i11] * w11) * dim;
let G = (src[i00 + 1] * w00 + src[i10 + 1] * w10 + src[i01 + 1] * w01 + src[i11 + 1] * w11) * dim;
let B = (src[i00 + 2] * w00 + src[i10 + 2] * w10 + src[i01 + 2] * w01 + src[i11 + 2] * w11) * dim;
const dIdx = i * 4;
data[dIdx] = R < 0 ? 0 : R > 255 ? 255 : R;
data[dIdx + 1] = G < 0 ? 0 : G > 255 ? 255 : G;
data[dIdx + 2] = B < 0 ? 0 : B > 255 ? 255 : B;
}
}
ctx.putImageData(img, 0, 0);
}
/* ====== src/c64render.js ====== */
/* c64render.js — C64 art core (cell grid of glyph tiles).
A token is a horizontal stack of bands. Each band has a C64 ink/paper pair, a
small glyph alphabet, a motif, and its OWN motion mode so a single token mixes
left/right, up/down, diagonal, and chasing motions instead of one repetitive
scroll. Some bands also get a bright "chase" sweep on top.
Everything is driven by a normalized loop phase t in [0,1) with whole-cell
steps, and every per-band shift is an integer multiple of the grid width N, so
at t=1 every offset wraps to 0 and the loop is perfect by construction.
Eyes (the face) sit upper-right and blink periodically. CRT bend + chroma
split are reused from post-fx.js. Does NOT use the old band path. */
/* weighted motif pool: the letter X is by far the most common tile; suits
(spade/heart/diamond/club) left out for now. [glyphKey, weight] */
const MOTIF_WEIGHTED = [
['X', 10], // most common
['CHECKER', 2], ['DIAG_FWD', 2], ['DIAG_BACK', 2],
['X_DIAG', 1], ['DIAG_QUAD', 1], ['HBAR', 1], ['VBAR', 1], ['CROSS', 1],
['TRI_LOW', 1], ['TRI_UP', 1], ['HALF_L', 1], ['HALF_B', 1],
['QUAD_TL', 1], ['QUAD_BL', 1], ['BLOCK', 1], ['BALL', 1],
];
const MOTIF_TOTAL = MOTIF_WEIGHTED.reduce((s, [, w]) => s + w, 0);
function pickGlyph(r){
let n = r() * MOTIF_TOTAL;
for(const [k, w] of MOTIF_WEIGHTED){ n -= w; if(n <= 0) return k; }
return MOTIF_WEIGHTED[0][0];
}
const MOTIONS = ['scrollH', 'scrollV', 'diag', 'diagB', 'altRows', 'altCols'];
const GRID_CHOICES = [16, 24, 32, 48, 64]; // 64 = native 8px glyph cells (floor)
const COLODORE_PCT = 0.08; // ~8% use the warm "Colodore" monitor palette
const CRT_PCT = 0.12; // ~12% get CRT bend baked in (~123 of 1024)
const CHROMA_PCT = 0.12; // ~12% get chroma split baked in (~123 of 1024)
// the two rolls are INDEPENDENT, so both at 12% means about 1.5% of tokens
// land with both effects (~15 of 1024) — the super-rare crossover tier.
/* rollFx: independently roll CRT bend intensity and chroma split intensity
for any token. Returns { crt, chroma } where each is either 0 (no effect)
or a non-zero intensity in the same range the old Glitch family used.
Order of r() calls is fixed so the rolls are deterministic from the seed. */
function rollFx(r){
const crtOn = r() < CRT_PCT;
const crtAmt = crtOn ? 45 + Math.floor(r() * 40) : 0; // 45-85 when on
const chromaOn = r() < CHROMA_PCT;
const chromaAmt = chromaOn ? 3 + Math.floor(r() * 8) : 0; // 3-10 when on
return { crt: crtAmt, chroma: chromaAmt };
}
// color count 2..12 on a gentle bell (triangular, avg of 2 uniforms) so 2 and 12
// are rarest but still appear (~0.5% each) and the middle counts dominate
function bellColors(r){ const t = (r() + r()) / 2; return Math.max(2, Math.min(12, Math.round(2 + t * 10))); }
function rollPal(r){ return r() < COLODORE_PCT ? 'colodore' : null; }
function makeRng(seedStr){
let h = 2166136261;
const s = String(seedStr);
for(let i = 0; i < s.length; i++){ h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
let a = h >>> 0;
return () => {
a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const pick = (arr, r) => arr[Math.floor(r() * arr.length)];
// density (grid N) is weighted toward 32: a bell curve so 16 and 64 are rare and
// 32 is the most common. Weights line up with GRID_CHOICES [16,24,32,48,64].
// Consumes exactly one r() call (same as pick) so downstream rolls are unchanged.
const GRID_WEIGHTS = [6, 22, 44, 22, 6]; // -> ~6% / 22% / 44% / 22% / 6%
const GRID_WEIGHT_TOTAL = GRID_WEIGHTS.reduce((a, b) => a + b, 0);
function pickN(r){
let n = r() * GRID_WEIGHT_TOTAL;
for(let i = 0; i < GRID_CHOICES.length; i++){ n -= GRID_WEIGHTS[i]; if(n <= 0) return GRID_CHOICES[i]; }
return GRID_CHOICES[GRID_CHOICES.length - 1];
}
/* ---- Char Set: what every cell is "made of". A global trait applied to ALL
families, overriding the per-family fill glyph. Rarity decreasing, with two
ultra-rares (Garble, Pi) at ~0.5% each. Off cells ('.'/':') are preserved so
cellular/fractal patterns survive; maze keeps one diagonal as the tile and
blanks the other so the maze still reads. Rolled on an independent rng
sub-stream (':charset') so it doesn't shift any other trait. */
const CHARSET_GARBLE = ['A','B','X','Z','#','@','%','&','*','?','=','+','BLOCK','BALL',
'HEART','DIAMOND','SPADE','CLUB','DIAG_FWD','DIAG_BACK','CHECKER','CROSS','X_DIAG','HBAR','VBAR','PI'];
const CHARSET_TIERS = [
{ name:'Blocks', glyph:'BLOCK', w:400 },
{ name:'X', glyph:'X', w:240 },
{ name:'Dots', glyph:'BALL', w:150 },
{ name:'Diagonals', glyph:'DIAG_FWD', w:100 },
{ name:'Crosses', glyph:'CROSS', w:60 },
{ name:'Spades', glyph:'SPADE', w:12 }, // suits split 4 ways,
{ name:'Clubs', glyph:'CLUB', w:12 }, // totaling the old Suits share;
{ name:'Hearts', glyph:'HEART', w:5.5 }, // hearts + diamonds rarer
{ name:'Diamonds', glyph:'DIAMOND', w:5.5 },
{ name:'Garble', glyph:null, w:5 }, // random glyph per cell
{ name:'Pi', glyph:'PI', w:5 },
];
const CHARSET_TOTAL = CHARSET_TIERS.reduce((s, t) => s + t.w, 0);
function pickCharSet(r){
let n = r() * CHARSET_TOTAL;
for(const t of CHARSET_TIERS){ n -= t.w; if(n <= 0) return t; }
return CHARSET_TIERS[0];
}
/* attach a char set to a finished model (idempotent). Uses its own rng stream. */
function rollCharSetForModel(model){
if(!model || model.charSet) return model;
const r = makeRng((model.seed || 'token') + ':charset');
const tier = pickCharSet(r);
model.charSet = tier.name;
if(tier.name === 'Garble'){ model.charSetGlyph = null; model.charSetSalt = (Math.floor(r() * 0xffffffff)) >>> 0; }
else model.charSetGlyph = tier.glyph;
return model;
}
function csGlyph(model, x, y){
if(model.charSet === 'Garble'){
let h = ((x * 73856093) ^ (y * 19349663) ^ (model.charSetSalt || 0)) >>> 0;
h = (h ^ (h >>> 13)) >>> 0;
return CHARSET_GARBLE[h % CHARSET_GARBLE.length];
}
return model.charSetGlyph;
}
/* swap a cell's fill glyph for the char-set glyph. keeps '.'/':'(off) cells, and
for maze keeps one diagonal as the tile + blanks the other. */
function applyCharSet(model, x, y, orig){
if(!model.charSet) return orig;
if(orig === '.' || orig === ':') return orig;
return csGlyph(model, x, y);
}
/* divisors of N (>=4 preferred) so motifs are longer / less obviously tiled */
function motifLength(N, r){
const all = [];
for(let d = 1; d <= N; d++) if(N % d === 0) all.push(d);
const longish = all.filter(d => d >= 4);
return pick(longish.length ? longish : all, r);
}
function buildC64Token(seedStr){
const r = makeRng(seedStr);
const N = pickN(r);
const bandCount = 3 + Math.floor(r() * 4); // 3..6
const weights = Array.from({ length: bandCount }, () => 0.4 + r());
const wsum = weights.reduce((a, b) => a + b, 0);
let rows = weights.map(w => Math.max(1, Math.round(w / wsum * N)));
let diff = N - rows.reduce((a, b) => a + b, 0);
for(let i = 0; diff !== 0; i = (i + 1) % bandCount){
if(diff > 0){ rows[i]++; diff--; } else if(rows[i] > 1){ rows[i]--; diff++; }
}
const bands = [];
let y = 0;
for(let b = 0; b < bandCount; b++){
const bg = pick(DARKS, r);
let fg = pick(BRIGHTS, r);
if(fg === bg) fg = 1;
// a small alphabet (2-3 glyphs) arranged into a longer motif = less tiling
const alpha = Array.from({ length: 2 + Math.floor(r() * 2) }, () => pickGlyph(r));
const L = motifLength(N, r);
const motif = Array.from({ length: L }, () => pick(alpha, r));
const motion = pick(MOTIONS, r);
const dir = r() < 0.5 ? 1 : -1;
const speedMul = r() < 0.7 ? 1 : 2; // mostly slow (1 lap)
con
↓ show full (24,000 bytes)