59 lines
3.1 KiB
JavaScript
59 lines
3.1 KiB
JavaScript
|
|
/* ============================================================
|
|||
|
|
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`);
|