/* ============================================================ layering.mjs — the assembled piece: FILM/diffusion (back) + a QFT vacuum-carpet DECK (middle, 3 spaced plate sheets) + a BUBBLE-CHAMBER event (front), composited as one image (and the literal plexi stack). Produces variations into output/layering/. Usage: node tools/layering.mjs [size] ============================================================ */ import { writeFileSync, mkdirSync } from 'node:fs'; import { carpetSVG } from '../src/qft/carpet.js'; import { generateScene } from '../src/scene/scene.js'; import { renderSVG } from '../src/render/svgVector.js'; import { paramsFromSeed as bcParams } from '../src/scene/params.js'; import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js'; const SIZE = +(process.argv[2] || 1500); const OUT = 'output/layering'; mkdirSync(OUT, { recursive: true }); const u = SIZE / 1000; const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64'); // ---- FILM / diffusion: milky clouds (light, soft) the light diffuses through ---- function filmSVG(o = {}) { const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o; const t = tone.map(v => (v / 255).toFixed(3)); return ` `; } // ---- film GRAIN veil (fine, dark, low opacity) on top ---- function grainSVG(o = {}) { const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o; const t = tone.map(v => (v / 255).toFixed(3)); return ` `; } // ---- bubble-chamber event ---- function bcSVG(seed, over = {}, float = false) { const p = { ...FIXED, ...bcParams(seed) }; for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value; for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value; p.invert = true; p.showHeader = false; if (float) p.transparentPaper = true; // no paper rect → event floats as an object Object.assign(p, over); const scene = generateScene(p); const sh = scene.shock ? { x: scene.shock.x, y: scene.shock.y } : { x: 0, y: 0 }; return { svg: renderSVG(scene, p, SIZE), sh }; // sh = disk centre (normalized) for anchoring } // AGING layer: sparse scratches + dust speckle + foxing/stain blotches. Seeded. function agingSVG(o = {}) { const { seed = 5, scratches = 7, dust = 0.5, foxing = 0.5, tone = [60, 48, 34] } = o; const t = tone.map(v => (v / 255).toFixed(3)); let lcg = (seed * 9301 + 49297) % 233280; const rnd = () => (lcg = (lcg * 9301 + 49297) % 233280) / 233280; let lines = ''; for (let i = 0; i < scratches; i++) { const x = rnd() * SIZE, y0 = rnd() * SIZE * 0.4, len = (0.3 + rnd() * 0.6) * SIZE; const x2 = x + (rnd() - 0.5) * 40, y2 = y0 + len; const op = (0.05 + rnd() * 0.12).toFixed(3), wsc = (0.4 + rnd() * 0.8).toFixed(2); lines += ``; } return ` ${lines}`; } // build a 3-sheet carpet deck for a variation. // PROGRESSION back→front: thick lines (+ heavy blur in the composite) → fine, crisp. function deck(c) { const base = { mode: 'plate', rows: 46, horizon: c.horizon ?? 0.37, wFar: 0.58, wNear: 0.7, overlap: c.overlap ?? 1.7, mound: c.mound ?? 0.35, sat: c.sat ?? 0.58, lightNear: 0.33, lightFar: 0.56, blips: c.blips ?? 1.0 }; const lerp = (a, b, t) => a + (b - a) * t; // per-sheet stroke weights: back coarse, front fine const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }]; return [0, 1, 2].map(i => { const t = i / 2; // 0 back → 1 front return carpetSVG(SIZE, { ...base, salt: 'field' + i, hue: lerp(c.hueBack, c.hueFront, t), hue2: lerp(c.hueBack, c.hueFront, t) + 0.035, chaos: lerp((c.chaos ?? 0.6) * 0.8, c.chaos ?? 0.6, t), strokeNear: strokes[i].near, strokeFar: strokes[i].far }); }); } function compose(v) { const film = dataUri(filmSVG(v.film)); const sheets = deck(v.carpet).map(dataUri); const float = !!v.bcFloat; const { svg: bcRaw, sh } = bcSVG(v.bcSeed, v.bcOver, float); const bc = dataUri(bcRaw); const grain = dataUri(grainSVG(v.grain)); const aging = v.aging ? dataUri(agingSVG(v.aging)) : null; const base = v.base || 'rgb(226,219,199)'; const blend = float ? 'normal' : 'multiply'; // floating event = transparent paper, normal blend // BC placement. sunAt:[sx,sy] (frame coords -1..1) anchors on the DISK centre and // scales the whole event about it → the sun lands exactly there, never clipped. let bcEl; if (v.sunAt) { const s = v.bcScale ?? 1, scB = (SIZE / 2) * (1 - 0.02); const dpx = SIZE / 2 + sh.x * scB, dpy = SIZE / 2 + sh.y * scB; // disk px in BC canvas const fx = SIZE / 2 + v.sunAt[0] * (SIZE / 2), fy = SIZE / 2 + v.sunAt[1] * (SIZE / 2); bcEl = ``; } else { const s = v.bcScale ?? 1, dx = (v.bcDx || 0) * SIZE, dy = (v.bcDy || 0) * SIZE; const bcW = SIZE * s, bcX = (SIZE - bcW) / 2 + dx, bcY = (SIZE - bcW) / 2 + dy; bcEl = ``; } const dir = v.dir ? `${OUT}/${v.dir}` : OUT; if (v.dir) mkdirSync(dir, { recursive: true }); const svg = ` ${bcEl} ${aging ? `` : ''} `; writeFileSync(`${dir}/${v.name}.svg`, svg); console.log(` ${v.dir ? v.dir + '/' : ''}${v.name} (bc=${v.bcSeed})`); } // ============================================================ // Variations — film + carpet + event, ranging mood & "loudness". // Guiding thesis: calm sea, loud event. // ============================================================ const VARIATIONS = [ { name: '01_mono-calm-sea', base: 'rgb(228,221,201)', film: { seed: 3, density: 0.5, tone: [236, 228, 208] }, grain: { amount: 0.4 }, carpet: { hueBack: 0.58, hueFront: 0.50, chaos: 0.35, blips: 0.7, mound: 0.32 }, bcSeed: 'LAMBDA-2648', bcOver: { palette: 'mono' } }, { name: '02_magenta-over-teal', base: 'rgb(228,221,201)', film: { seed: 8, density: 0.5 }, grain: { amount: 0.4 }, carpet: { hueBack: 0.54, hueFront: 0.47, chaos: 0.45, blips: 1.0, mound: 0.35 }, bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.05 } }, { name: '03_quiet-vast', base: 'rgb(230,224,206)', film: { seed: 12, density: 0.42, tone: [238, 231, 212] }, grain: { amount: 0.32 }, carpet: { hueBack: 0.57, hueFront: 0.52, chaos: 0.22, blips: 0.5, mound: 0.28, overlap: 1.5 }, bcSeed: 'NUCLEON-2131', bcOver: { palette: 'mono', primaries: 7, burst: 0.4, cosmics: 2, deltaRate: 0.45 } }, { name: '04_kind-verdigris', base: 'rgb(226,221,205)', film: { seed: 21, density: 0.5 }, grain: { amount: 0.4 }, carpet: { hueBack: 0.45, hueFront: 0.40, chaos: 0.5, blips: 1.1, sat: 0.42, mound: 0.35 }, bcSeed: 'HYPERON-8444', bcOver: { palette: 'kind', saturation: 1.0 } }, { name: '05_ember-warm', base: 'rgb(230,222,202)', film: { seed: 30, density: 0.52, tone: [238, 226, 204] }, grain: { amount: 0.42 }, carpet: { hueBack: 0.10, hueFront: 0.07, chaos: 0.5, blips: 1.0, sat: 0.5, mound: 0.34 }, bcSeed: 'CASCADE-2755', bcOver: { palette: 'kindrise', saturation: 1.0 } }, { name: '06_seethe-bold', base: 'rgb(227,220,200)', film: { seed: 41, density: 0.55 }, grain: { amount: 0.46 }, carpet: { hueBack: 0.55, hueFront: 0.47, chaos: 0.8, blips: 1.4, mound: 0.4, overlap: 1.9 }, bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.08 } }, ]; console.log(`Layered piece — ${VARIATIONS.length} variations → ${OUT}/`); for (const v of VARIATIONS) compose(v); // ============================================================ // COMPOSITION study — fixed magenta/teal deck (the 02 look), only the // BC event's COMPOSITION changes: vertex placement (eventX/Y), disk height // (shockY), and placement in frame (bcScale/bcDy). Same palette / density / // arc / disk style throughout. Chasing "sunrise over a vast ocean". // ============================================================ const TEAL = { hueBack: 0.54, hueFront: 0.47, chaos: 0.42, blips: 1.0, mound: 0.35 }; const tealFilm = { seed: 8, density: 0.5 }, tealGrain = { amount: 0.4 }; // place the SUN (disk) and the event VERTEX together so tracks radiate from it const sun = (name, px, py) => ({ name, dir: 'composition', base: 'rgb(228,221,201)', film: tealFilm, grain: tealGrain, carpet: TEAL, bcSeed: 'MESON-5113', bcOver: { palette: 'magentarise', saturation: 1.05, shockX: px, shockY: py, eventX: px, eventY: py * 0.9 }, }); const COMPOSITION = [ sun('01_sunrise-on-horizon', 0, -0.26), // sun resting on the horizon, rays up sun('02_sun-low-in-sea', 0, 0.46), // low in the water — reflected/setting sun('03_high-sun-vast', 0, -0.5), // small high sun, vast sea below sun('04_offset-left', -0.46, -0.12), sun('05_offset-right-rise', 0.44, -0.06), sun('06_centered', 0, 0.0), sun('07_low-corner-sun', 0.42, 0.34), // rising in the lower-right sun('08_corner-dawn-left', -0.5, 0.28), // rising in the lower-left ]; console.log(`BC composition study — ${COMPOSITION.length} → ${OUT}/composition/`); for (const v of COMPOSITION) compose(v); { const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/composition/m.html`, `
${COMPOSITION.map(v => `
${cap(v)}
`).join('')}
`); } // VAST — the floating event (transparent paper) shrunk + placed into a huge sea, // so the sun is small and the ocean is vast. The event stays CENTRED in its own // canvas (never clipped); (sx,sy) is its target position in the FINAL frame // (-1..1), reached by translating the scaled image: bcD = s/2. const vast = (name, sx, sy, scale) => ({ name, dir: 'vast', base: 'rgb(229,222,203)', film: tealFilm, grain: tealGrain, carpet: TEAL, bcSeed: 'MESON-5113', bcFloat: true, bcScale: scale, bcDx: sx / 2, bcDy: sy / 2, bcOver: { palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: 0, eventY: 0 }, }); const VAST = [ vast('01_distant-dawn', 0, -0.26, 0.6), // sun on the horizon vast('02_small-sun-horizon', 0, -0.26, 0.5), vast('03_corner-rise-left', -0.42, 0.02, 0.55), vast('04_high-lonely', 0.14, -0.5, 0.42), // tiny sun high, immense sea vast('05_mid-sea-jewel', 0, -0.04, 0.7), vast('06_low-wide-sea', 0, 0.22, 0.62), ]; console.log(`VAST (floating sun) — ${VAST.length} → ${OUT}/vast/`); for (const v of VAST) compose(v); // ============================================================ // VAST02 — small sun (≈04 disk size) but LARGER bubble traces; ripple-forward // calm sea; now WITH film grain + an aging layer (scratches/dust/foxing). // Disk anchored via sunAt (never clips). shockSize small + bcScale big → small // disk, big traces. Each iteration varies the trace character. // ============================================================ const RIPPLE = { hueBack: 0.54, hueFront: 0.47, chaos: 0.32, blips: 0.7, mound: 0.3, overlap: 1.55 }; const v2 = (name, sunAt, bcOver, scale = 0.78) => ({ name, dir: 'vast02', base: 'rgb(229,222,203)', film: { seed: 8, density: 0.5 }, grain: { amount: 0.42 }, aging: { seed: (name.charCodeAt(3) * 7) % 97, scratches: 6, dust: 0.5, foxing: 0.55 }, agingOpacity: 0.6, carpet: RIPPLE, bcSeed: 'MESON-5113', bcFloat: true, bcScale: scale, sunAt, bcOver: { palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: 0, eventY: 0, shockSize: 0.16, ...bcOver }, }); const VAST02 = [ v2('01_dawn-sweeping-arcs', [0, -0.26], { sweepers: 6, primaries: 18, eloss: 0.34, pspread: 0.8 }), v2('02_high-lonely-long', [0.1, -0.5], { bfield: 0.7, eloss: 0.3, primaries: 16, cosmics: 7 }, 0.72), v2('03_corner-left-bigarcs', [-0.4, -0.04], { sweepers: 7, primaries: 17, bfield: 0.9 }), v2('04_low-wide-spirals', [0, 0.2], { deltaRate: 0.85, deltaTight: 0.5, primaries: 20, eloss: 0.4 }), v2('05_mid-jewel-dense', [0, -0.05], { primaries: 24, burst: 0.82, pspread: 0.9 }, 0.74), v2('06_right-rise-curl', [0.4, -0.06], { bfield: 1.7, primaries: 18, deltaRate: 0.7 }), v2('07_centered-long-reach', [0, 0.0], { eloss: 0.28, pspread: 0.95, primaries: 16, shockSize: 0.15 }), v2('08_distant-tiny-vast', [0.06, -0.42], { primaries: 18, sweepers: 5, eloss: 0.34 }, 0.6), v2('09_dawn-cosmic-streaks', [-0.14, -0.2], { cosmics: 9, primaries: 15, bfield: 0.8, eloss: 0.3 }), v2('10_horizon-symmetry', [0, -0.24], { sweepers: 4, primaries: 19, deltaRate: 0.7, shockSize: 0.17 }), ]; console.log(`VAST02 (small sun, big traces, grain+aging) — ${VAST02.length} → ${OUT}/vast02/`); for (const v of VAST02) compose(v); { const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/vast02/m.html`, `
${VAST02.map(v => `
${cap(v)}
`).join('')}
`); } // ============================================================ // VAST03 — an "archive of dawns": vary SUN COLOUR (diskHue/diskSat) + slight // SIZE, keep larger bubble traces, and bring back the ANNOTATING HAND // (grease-pencil ring/№/angle via `annotate`; a faint réseau on a couple). // Trace family stays magenta → a two-colour relationship against each sun. // ============================================================ const v3 = (name, sunAt, sun, traces, ann, scale = 0.78) => ({ name, dir: 'vast03', base: 'rgb(229,222,203)', film: { seed: 8, density: 0.5 }, grain: { amount: 0.4 }, aging: { seed: (name.charCodeAt(2) * 7) % 97, scratches: 6, dust: 0.5, foxing: 0.55 }, agingOpacity: 0.58, carpet: RIPPLE, bcSeed: 'MESON-5113', bcFloat: true, bcScale: scale, sunAt, bcOver: { palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: 0, eventY: 0, diskHue: sun.h, diskSat: sun.s, shockSize: sun.size, annotate: ann.a, reseau: ann.r || 0, ...traces, }, }); const VAST03 = [ v3('01_burnt-orange-studied', [0, -0.25], { h: 0.06, s: 0.82, size: 0.16 }, { sweepers: 6, primaries: 18, eloss: 0.34 }, { a: 0.9 }), v3('02_gold-sun-light-ring', [0.08, -0.42],{ h: 0.11, s: 0.85, size: 0.15 }, { primaries: 17, eloss: 0.3, cosmics: 6 }, { a: 0.6 }, 0.74), v3('03_amber-reseau', [0, -0.1], { h: 0.085, s: 0.9, size: 0.17 }, { primaries: 20, sweepers: 5 }, { a: 0.7, r: 0.32 }), v3('04_coral-red-spirals', [-0.1, 0.12], { h: 0.02, s: 0.85, size: 0.16 }, { deltaRate: 0.85, deltaTight: 0.5, primaries: 20 }, { a: 0.8 }), v3('05_rose-pink-dawn', [0.36, -0.06],{ h: 0.95, s: 0.7, size: 0.15 }, { bfield: 1.4, primaries: 18 }, { a: 0.7 }), v3('06_copper-long-reach', [0, 0.0], { h: 0.05, s: 0.72, size: 0.18 }, { eloss: 0.28, pspread: 0.95, primaries: 16 }, { a: 0.65 }), v3('07_pale-white-hot', [0.05, -0.5], { h: 0.1, s: 0.22, size: 0.14 }, { primaries: 16, sweepers: 5, eloss: 0.32 }, { a: 0.55 }, 0.7), v3('08_cool-teal-sun', [-0.4, -0.04],{ h: 0.5, s: 0.6, size: 0.16 }, { sweepers: 7, primaries: 17 }, { a: 0.7 }), v3('09_magenta-monochrome', [0, -0.24], { h: 0.9, s: 0.78, size: 0.17 }, { primaries: 19, deltaRate: 0.7, sweepers: 4 }, { a: 0.8, r: 0.28 }), v3('10_deep-ember-studied', [0.12, 0.18], { h: 0.0, s: 0.9, size: 0.19 }, { primaries: 22, burst: 0.82, eloss: 0.36 }, { a: 1.0 }, 0.76), ]; console.log(`VAST03 (archive of dawns: sun colour + annotation) — ${VAST03.length} → ${OUT}/vast03/`); for (const v of VAST03) compose(v); { const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/vast03/m.html`, `
${VAST03.map(v => `
${cap(v)}
`).join('')}
`); } // ============================================================ // VAST04 — refined annotation: ONLY "No 001" + an arrow (no ring, no scrawl), // THIN white pencil; the particle-collision VERTEX is OFFSET from the disk // (eventX/Y ≠ shock); the disk gets colour + PRESSURE variation (dark dense // core). Larger traces continue. Two graphite-pencil controls for comparison. // ============================================================ const v4 = (name, sunAt, sun, ev, traces, pencil = '#f6f2ea', scale = 0.78) => ({ name, dir: 'vast04', base: 'rgb(229,222,203)', film: { seed: 8, density: 0.5 }, grain: { amount: 0.4 }, aging: { seed: (name.charCodeAt(2) * 7) % 97, scratches: 5, dust: 0.45, foxing: 0.5 }, agingOpacity: 0.55, carpet: RIPPLE, bcSeed: 'MESON-5113', bcFloat: true, bcScale: scale, sunAt, bcOver: { palette: 'magentarise', saturation: 1.05, shockX: 0, shockY: 0, eventX: ev[0], eventY: ev[1], // collision offset from the disk diskHue: sun.h, diskSat: sun.s, shockSize: sun.size, diskPressure: sun.p, annotate: 0.5, annoRing: false, annoExtras: false, annoArrow: true, annoNum: true, annoLabel: 'No 001', annoWidth: 0.4, annotateInk: pencil, ...traces, }, }); const VAST04 = [ v4('01_burnt-orange-pressure', [0, -0.25], { h: 0.06, s: 0.82, size: 0.16, p: 0.85 }, [0.26, -0.14], { sweepers: 6, primaries: 18, eloss: 0.34 }), v4('02_gold-mid-pressure', [0.08, -0.42], { h: 0.11, s: 0.85, size: 0.15, p: 0.5 }, [-0.2, 0.16], { primaries: 17, eloss: 0.3, cosmics: 6 }, '#f6f2ea', 0.74), v4('03_amber-soft-core', [0, -0.1], { h: 0.085, s: 0.9, size: 0.17, p: 0.25 }, [0.28, 0.12], { primaries: 20, sweepers: 5 }), v4('04_coral-deep-core', [-0.1, 0.12], { h: 0.02, s: 0.85, size: 0.16, p: 0.8 }, [0.22, -0.18], { deltaRate: 0.85, deltaTight: 0.5, primaries: 20 }), v4('05_copper-pressure', [0.36, -0.06], { h: 0.05, s: 0.72, size: 0.16, p: 0.55 }, [-0.24, -0.1], { bfield: 1.4, primaries: 18 }), v4('06_deep-ember-densecore', [0, -0.24], { h: 0.0, s: 0.9, size: 0.18, p: 0.95 }, [0.2, 0.18], { primaries: 19, sweepers: 4 }), v4('07_rose-mid', [0.05, -0.5], { h: 0.95, s: 0.7, size: 0.15, p: 0.5 }, [-0.18, 0.2], { primaries: 16, eloss: 0.3, cosmics: 5 }, '#f6f2ea', 0.7), v4('08_magenta-pressure', [0, 0.0], { h: 0.9, s: 0.78, size: 0.17, p: 0.7 }, [0.3, -0.1], { eloss: 0.28, pspread: 0.95, primaries: 16 }), v4('09_graphite-on-orange', [-0.4, -0.04], { h: 0.06, s: 0.82, size: 0.16, p: 0.8 }, [0.24, 0.14], { sweepers: 7, primaries: 17 }, '#39312a'), v4('10_graphite-on-ember', [0.12, 0.18], { h: 0.0, s: 0.9, size: 0.19, p: 0.9 }, [-0.22, -0.16],{ primaries: 22, burst: 0.82 }, '#39312a', 0.76), ]; console.log(`VAST04 (No001 + arrow, white pencil, vertex offset, disk pressure) — ${VAST04.length} → ${OUT}/vast04/`); for (const v of VAST04) compose(v); { const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/vast04/m.html`, `
${VAST04.map(v => `
${cap(v)}
`).join('')}
`); } { const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/vast/m.html`, `
${VAST.map(v => `
${cap(v)}
`).join('')}
`); } const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' '); writeFileSync(`${OUT}/index.html`, `Layered piece · film + QFT carpet + bubble chamber

Layered piece — film · QFT vacuum carpet · bubble-chamber event

back→front: cream ground · milky film/diffusion · 3 spaced QFT carpet sheets (back two blurred = air-gap depth of field) · bubble-chamber event (multiply) · film grain. The literal plexi stack, previewed as one image.
${VARIATIONS.map(v => `
${cap(v)}${v.bcSeed} · ${v.bcOver?.palette || 'mono'}
`).join('\n')}
`); writeFileSync(`${OUT}/m.html`, `
${VARIATIONS.map(v => `
${cap(v)}
`).join('')}
`); console.log(`contact sheets -> ${OUT}/index.html , m.html`);