Files
bubblechambersimart/tools/inkwash-export.mjs

59 lines
3.1 KiB
JavaScript
Raw Normal View History

2026-07-04 09:47:50 -04:00
/* ============================================================
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`);