Fable Review
This commit is contained in:
58
tools/inkwash-export.mjs
Normal file
58
tools/inkwash-export.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
/* ============================================================
|
||||
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`);
|
||||
34
tools/milkweed.html
Normal file
34
tools/milkweed.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"><style>html,body{margin:0;background:#000}canvas{display:block}</style></head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script type="module">
|
||||
import { generateUmbel } from '../src/milkweed/umbel.js';
|
||||
import { renderWatercolor } from '../src/render/watercolor.js';
|
||||
|
||||
const q = new URLSearchParams(location.search);
|
||||
const params = {
|
||||
seed: q.get('seed') || 'ASCLEPIAS-0001',
|
||||
palette: 'milkweed',
|
||||
paperTone: 'cream', toneStrength: 1, paperBright: 1.06, glow: 0.5,
|
||||
saturation: 1, hueShift: 0, size: 1, density: 1, depth: 0, aging: 0,
|
||||
showHeader: true,
|
||||
};
|
||||
// pass through any query param (numbers/bools), incl. wc* dials and umbel* knobs
|
||||
for (const [k, v] of q) {
|
||||
if (k === 'size_px' || k === 'hq') continue;
|
||||
params[k] = (v === 'true') ? true : (v === 'false') ? false : (isNaN(+v) ? v : +v);
|
||||
}
|
||||
|
||||
const SIZE = +(q.get('size_px') || 1200);
|
||||
const c = document.getElementById('c');
|
||||
c.width = c.height = SIZE;
|
||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||
const scene = generateUmbel(params);
|
||||
renderWatercolor(ctx, SIZE, SIZE, scene, params, { preview: q.get('hq') !== '1' });
|
||||
document.title = 'done';
|
||||
window.__done = true;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,7 @@
|
||||
<script type="module">
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderCanvasPhoto } from '../src/render/canvasPhoto.js';
|
||||
import { renderWatercolor } from '../src/render/watercolor.js';
|
||||
import { GROUPS, TOGGLES, SELECTS, FIXED, PRESETS } from '../src/ui/controls.js';
|
||||
import { paramsFromSeed } from '../src/scene/params.js';
|
||||
|
||||
@@ -24,8 +25,9 @@ if (q.get('fromseed') === '1') {
|
||||
}
|
||||
if (q.get('seed')) params.seed = q.get('seed');
|
||||
for (const [k, v] of q) {
|
||||
if (k === 'seed' || k === 'preset' || k === 'size_px') continue;
|
||||
if (k in params) params[k] = (v === 'true') ? true : (v === 'false') ? false : (isNaN(+v) ? v : +v);
|
||||
if (k === 'seed' || k === 'preset' || k === 'size_px' || k === 'renderer' || k === 'hq') continue;
|
||||
// pass through known params plus any wc* watercolour dials (not in the base set)
|
||||
if (k in params || k.startsWith('wc')) params[k] = (v === 'true') ? true : (v === 'false') ? false : (isNaN(+v) ? v : +v);
|
||||
}
|
||||
|
||||
const SIZE = +(q.get('size_px') || 1200);
|
||||
@@ -33,7 +35,8 @@ const c = document.getElementById('c');
|
||||
c.width = c.height = SIZE;
|
||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||
const scene = generateScene(params);
|
||||
renderCanvasPhoto(ctx, SIZE, SIZE, scene, params, { preview: q.get('hq') !== '1' });
|
||||
const render = q.get('renderer') === 'watercolor' ? renderWatercolor : renderCanvasPhoto;
|
||||
render(ctx, SIZE, SIZE, scene, params, { preview: q.get('hq') !== '1' });
|
||||
document.title = 'done';
|
||||
window.__done = true;
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user