Files
bubblechambersimart/tools/inkwash-export.mjs
2026-07-04 09:47:50 -04:00

59 lines
3.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
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`);