/* ============================================================ inkwash-export.mjs — bubble-chamber geometry → inkwash strokes. Exploratory bridge: take a generated scene's tracks and emit a strokes.js that the forked inkwash play.html replays through its own brush engine + fluid sim → a watercolor/ink rendering of a particle event. Each track becomes one pen stroke; β drives the pen pressure (slow/dense end of range = heavier ink). NOTE (2026-06-24): the wet look now lives NATIVELY in the project as `src/render/watercolor.js` (renderer=watercolor), on the same scene model — no external fork needed. This bridge is kept as a record / for driving the external play.html, but the live watercolour path is the renderer. Usage: node tools/inkwash-export.mjs [seed] [out.js] [maxTracks] [deltaFrac] ============================================================ */ import { writeFileSync } from 'node:fs'; import { generateScene } from '../src/scene/scene.js'; import { paramsFromSeed } from '../src/scene/params.js'; const SEED = process.argv[2] || 'KAON-0088'; const OUT = process.argv[3] || '/tmp/inkwash/strokes.js'; const MAXTRACKS = +(process.argv[4] || 26); const DELTAFRAC = +(process.argv[5] ?? 0.08); // fraction of δ-ray curls to keep const FIT = 0.62; // event half-extent in uv (×0.5) const p = paramsFromSeed(SEED); const scene = generateScene(p); // frame coords (−1.2..1.2, y down) → inkwash uv (0..1, y up) const toUv = (x, y) => [0.5 + x * 0.5 * FIT, 0.5 - y * 0.5 * FIT]; // the heavy structural tracks first; δ-rays are tiny curls — keep only a few const heavy = scene.tracks.filter(t => t.kind !== 'delta'); const deltas = scene.tracks.filter(t => t.kind === 'delta'); const keepDeltas = deltas.filter((_, i) => i % Math.max(1, Math.round(1 / Math.max(DELTAFRAC, 1e-3))) === 0); // longest/most-energetic first so the cap keeps the spine of the event const ranked = heavy.sort((a, b) => b.pts.length - a.pts.length).slice(0, MAXTRACKS); const chosen = [...ranked, ...keepDeltas]; const strokes = []; for (const tr of chosen) { const pts = tr.pts.map(pt => { const [u, v] = toUv(pt.x, pt.y); // β low (slow, end of range) ⇒ denser ionisation ⇒ heavier ink const pr = Math.min(0.95, Math.max(0.18, 0.30 + 0.65 * (1 - (pt.beta ?? 0.6)))); return [+u.toFixed(4), +v.toFixed(4), +pr.toFixed(3)]; }).filter(([u, v]) => u > -0.2 && u < 1.2 && v > -0.2 && v < 1.2); if (pts.length >= 2) strokes.push({ mode: 'pen', pts }); } const opts = { stepUv: 0.008, gapFrames: 6, settleFrames: 120, fixEnd: true }; const totalPts = strokes.reduce((s, k) => s + k.pts.length, 0); writeFileSync(OUT, `/* generated from bubble-chamber seed ${SEED} — ${strokes.length} strokes, ${totalPts} pts */\n` + `window.INKWASH_OPTS = ${JSON.stringify(opts)};\n` + `window.INKWASH_STROKES = ${JSON.stringify(strokes)};\n`); console.log(`inkwash-export: seed ${SEED} → ${OUT}`); console.log(` ${chosen.length} tracks (${ranked.length} structural + ${keepDeltas.length} δ) → ${strokes.length} strokes, ${totalPts} pts`);