256 lines
14 KiB
JavaScript
256 lines
14 KiB
JavaScript
|
|
/* ============================================================
|
|||
|
|
refined-backlit2.mjs — SUN-ABOVE SCALE STUDIES.
|
|||
|
|
One frame (plate Nº 217, exposure 19:04, MESON-5113), colours
|
|||
|
|
and composition held constant; each render plays with the
|
|||
|
|
RELATIVE SCALE of the marks:
|
|||
|
|
· traces — bubble size / density of the particle ink
|
|||
|
|
· waves — row count (few = broad swells, many = fine ripple)
|
|||
|
|
· depth — overlap: wave amplitude relative to row gap
|
|||
|
|
· line — stroke weight of the wave rows
|
|||
|
|
No hand. Same sun (hollow heart), same sea hue, same aging.
|
|||
|
|
|
|||
|
|
Usage: node tools/refined-backlit2.mjs [size]
|
|||
|
|
→ fable/REFINED_Backlit2/
|
|||
|
|
============================================================ */
|
|||
|
|
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';
|
|||
|
|
import { splitTracksAtWaterline } from '../src/compose/waterline.js';
|
|||
|
|
import { makeRng } from '../src/rng.js';
|
|||
|
|
|
|||
|
|
const SIZE = +(process.argv[2] || 1500);
|
|||
|
|
const OUT = 'fable/REFINED_Backlit2';
|
|||
|
|
mkdirSync(OUT, { recursive: true });
|
|||
|
|
const u = SIZE / 1000;
|
|||
|
|
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
|||
|
|
|
|||
|
|
/* ---- the world constants: identical to One Sun Setting ---- */
|
|||
|
|
const SEED = 'MESON-5113';
|
|||
|
|
const BASE = 'rgb(227,220,200)';
|
|||
|
|
const HORIZON = 0.37;
|
|||
|
|
const YH = HORIZON * 2 - 1;
|
|||
|
|
const FILM = { seed: 41, density: 0.6 };
|
|||
|
|
|
|||
|
|
/* ---- film / grain / aging (unchanged from refined-backlit) ---- */
|
|||
|
|
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 `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
|||
|
|
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
|||
|
|
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
|||
|
|
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
|||
|
|
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
|
|||
|
|
}
|
|||
|
|
function grainSVG(o = {}) {
|
|||
|
|
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
|||
|
|
const t = tone.map(v => (v / 255).toFixed(3));
|
|||
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
|||
|
|
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
|||
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
|||
|
|
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
|||
|
|
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
|
|||
|
|
}
|
|||
|
|
function agingSVG(o = {}) {
|
|||
|
|
const { seed = 5, scratches = 6, dust = 0.5, foxing = 0.55, tone = [60, 48, 34] } = o;
|
|||
|
|
const t = tone.map(v => (v / 255).toFixed(3));
|
|||
|
|
const rng = makeRng(SEED, 'aging' + seed);
|
|||
|
|
let lines = '';
|
|||
|
|
for (let i = 0; i < scratches; i++) {
|
|||
|
|
const x = rng() * SIZE, y0 = rng() * SIZE * 0.4, len = (0.3 + rng() * 0.6) * SIZE;
|
|||
|
|
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
|||
|
|
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
|||
|
|
}
|
|||
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
|||
|
|
<defs>
|
|||
|
|
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
|||
|
|
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
|||
|
|
</defs>
|
|||
|
|
<rect width="${SIZE}" height="${SIZE}" filter="url(#fox)"/>
|
|||
|
|
<rect width="${SIZE}" height="${SIZE}" filter="url(#dust)"/>
|
|||
|
|
${lines}</svg>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ---- the sea deck, scale-parameterised ----
|
|||
|
|
rows: wave count · overlap: amplitude vs row gap (depth) ·
|
|||
|
|
strokeMul: line weight multiplier on the per-sheet stroke table */
|
|||
|
|
function deck(sea, warpFn) {
|
|||
|
|
const rows = sea.rows ?? 46, overlap = sea.overlap ?? 1.9, sm = sea.strokeMul ?? 1;
|
|||
|
|
const base = { mode: 'plate', rows, horizon: HORIZON, wFar: 0.58, wNear: 0.7,
|
|||
|
|
overlap, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips: 1.4, warpFn };
|
|||
|
|
const lerp = (a, b, t) => a + (b - a) * t;
|
|||
|
|
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;
|
|||
|
|
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
|
|||
|
|
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
|||
|
|
chaos: lerp(0.8 * 0.8, 0.8, t),
|
|||
|
|
strokeNear: strokes[i].near * sm, strokeFar: strokes[i].far * sm });
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ---- iron filings (identical logic, geometry follows the sea's scale) ---- */
|
|||
|
|
function buildAttraction(below, shock, sunSubmerged) {
|
|||
|
|
const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98;
|
|||
|
|
const pts = [];
|
|||
|
|
for (const t of below) for (let i = 0; i < t.pts.length; i += 5)
|
|||
|
|
pts.push({ x: S(t.pts[i].x), y: S(t.pts[i].y), w: 1 });
|
|||
|
|
if (sunSubmerged) pts.push({ x: S(shock.x), y: S(shock.y), w: 12 });
|
|||
|
|
const G = 72, cell = SIZE / G, sig = 0.045 * SIZE;
|
|||
|
|
const fy = new Float32Array(G * G);
|
|||
|
|
for (let gy = 0; gy < G; gy++) for (let gx = 0; gx < G; gx++) {
|
|||
|
|
const x = (gx + 0.5) * cell, y = (gy + 0.5) * cell;
|
|||
|
|
let dy = 0, wsum = 0;
|
|||
|
|
for (const p of pts) {
|
|||
|
|
const dx0 = p.x - x, dy0 = p.y - y;
|
|||
|
|
const k = p.w * Math.exp(-(dx0 * dx0 + dy0 * dy0) / (2 * sig * sig));
|
|||
|
|
dy += k * Math.max(-sig, Math.min(sig, dy0));
|
|||
|
|
wsum += k;
|
|||
|
|
}
|
|||
|
|
fy[gy * G + gx] = wsum > 0.02 ? (dy / (wsum + 1.2)) / sig : 0;
|
|||
|
|
}
|
|||
|
|
return (x, y) => {
|
|||
|
|
const fx0 = Math.min(G - 1.001, Math.max(0, x / cell - 0.5));
|
|||
|
|
const fy0 = Math.min(G - 1.001, Math.max(0, y / cell - 0.5));
|
|||
|
|
const ix = Math.floor(fx0), iy = Math.floor(fy0), ax = fx0 - ix, ay = fy0 - iy;
|
|||
|
|
const v00 = fy[iy * G + ix], v10 = fy[iy * G + ix + 1];
|
|||
|
|
const v01 = fy[(iy + 1) * G + ix], v11 = fy[(iy + 1) * G + ix + 1];
|
|||
|
|
return (v00 * (1 - ax) + v10 * ax) * (1 - ay) + (v01 * (1 - ax) + v11 * ax) * ay;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function warpFor(sample, pullPx, sea) {
|
|||
|
|
const rows = sea.rows ?? 46, overlap = sea.overlap ?? 1.9;
|
|||
|
|
const hY = HORIZON * SIZE, bottom = 0.99 * SIZE;
|
|||
|
|
return (t, d) => {
|
|||
|
|
const half = (0.58 + 0.12 * d) * SIZE;
|
|||
|
|
const x = SIZE / 2 - half + t * 2 * half;
|
|||
|
|
const y = hY + (bottom - hY) * Math.pow(d, 1.7);
|
|||
|
|
const localGap = (bottom - hY) * 1.7 * Math.pow(Math.max(d, 0.02), 0.7) / (rows - 1);
|
|||
|
|
const amp = Math.max(2 * u, overlap * localGap);
|
|||
|
|
const n = sample(x, y) * pullPx / amp;
|
|||
|
|
return Math.max(-0.9, Math.min(0.9, n));
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ---- the event: sun-above config, colours fixed ---- */
|
|||
|
|
function bcAssembleParams(over = {}) {
|
|||
|
|
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;
|
|||
|
|
Object.assign(p, {
|
|||
|
|
invert: true, transparentPaper: true, vign: 0,
|
|||
|
|
showHeader: false, showBoundary: false, showFiducials: false,
|
|||
|
|
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
|||
|
|
palette: 'magbeta', saturation: 1.0,
|
|||
|
|
diskBubbles: false, diskPressure: 0.9, diskSoften: 0.8,
|
|||
|
|
shockStriations: 1.0, diskHollow: 0.85,
|
|||
|
|
depth: 0.3, aging: 0.2,
|
|||
|
|
eventX: -0.14, eventY: -0.37,
|
|||
|
|
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
|||
|
|
bgEvents: 2, shockX: -0.55, shockY: -0.72, shockSize: 0.17, shockIntensity: 0.8,
|
|||
|
|
}, over);
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderVariant(v) {
|
|||
|
|
const p = bcAssembleParams(v.traces || {});
|
|||
|
|
const scene = generateScene(p);
|
|||
|
|
const { above, below } = splitTracksAtWaterline(scene.tracks, YH, { kink: 0.016, compress: 0.95 });
|
|||
|
|
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
|||
|
|
const sea = v.sea || {};
|
|||
|
|
|
|||
|
|
const wamp = 0.003;
|
|||
|
|
const belowWavy = below.map(t => ({
|
|||
|
|
...t,
|
|||
|
|
pts: t.pts.map(pt => ({ ...pt, y: pt.y + wamp * Math.sin(pt.x * 34 + pt.y * 18) })),
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const trackSVG = (tracks) => renderSVG({ ...bare, tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE);
|
|||
|
|
const diskSVG = () => renderSVG({ ...bare, tracks: [], shock: scene.shock }, { ...p, emit: ['disk'] }, SIZE);
|
|||
|
|
|
|||
|
|
const aboveImg = dataUri(trackSVG(above));
|
|||
|
|
const belowImg = dataUri(trackSVG(belowWavy));
|
|||
|
|
const diskImg = dataUri(diskSVG());
|
|||
|
|
|
|||
|
|
const sample = buildAttraction(belowWavy, scene.shock, false);
|
|||
|
|
const sheets = deck(sea, warpFor(sample, 7 * u, sea)).map(dataUri);
|
|||
|
|
|
|||
|
|
const film = dataUri(filmSVG(FILM));
|
|||
|
|
const grain = dataUri(grainSVG({ amount: 0.42 }));
|
|||
|
|
const aging = dataUri(agingSVG({ seed: 5, scratches: 6, dust: 0.5, foxing: 0.55 }));
|
|||
|
|
|
|||
|
|
const hpx = (HORIZON * SIZE).toFixed(1);
|
|||
|
|
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
|
|||
|
|
|
|||
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
|||
|
|
<defs>
|
|||
|
|
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
|||
|
|
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
|||
|
|
<filter id="subT" x="-6%" y="-6%" width="112%" height="112%">
|
|||
|
|
<feColorMatrix type="matrix" values="0.78 0 0 0 0.010 0 0.94 0 0 0.022 0 0 0.96 0 0.022 0 0 0 0.85 0"/>
|
|||
|
|
<feGaussianBlur stdDeviation="${(1.0 * u).toFixed(2)}"/>
|
|||
|
|
</filter>
|
|||
|
|
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
|||
|
|
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
|||
|
|
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
|||
|
|
</filter>
|
|||
|
|
<clipPath id="skyc"><rect x="0" y="0" width="${SIZE}" height="${hpx}"/></clipPath>
|
|||
|
|
<clipPath id="seac"><rect x="0" y="${hpx}" width="${SIZE}" height="${(SIZE - HORIZON * SIZE).toFixed(1)}"/></clipPath>
|
|||
|
|
</defs>
|
|||
|
|
<rect width="${SIZE}" height="${SIZE}" fill="${BASE}"/>
|
|||
|
|
${IMG(film, 'opacity="0.6"')}
|
|||
|
|
<g clip-path="url(#skyc)">${IMG(diskImg)}</g>
|
|||
|
|
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
|
|||
|
|
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)"')}</g>
|
|||
|
|
${IMG(belowImg, 'filter="url(#subT)"')}
|
|||
|
|
<rect x="0" y="${hpx}" width="${SIZE}" height="${SIZE}" fill="rgb(96,138,128)" opacity="0.07"/>
|
|||
|
|
${IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`)}
|
|||
|
|
${IMG(sheets[2], 'opacity="0.95"')}
|
|||
|
|
${IMG(aboveImg)}
|
|||
|
|
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
|||
|
|
${IMG(grain, `opacity="0.45" style="mix-blend-mode:multiply"`)}
|
|||
|
|
</svg>`;
|
|||
|
|
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
|||
|
|
console.log(` ${v.name} ${v.note}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
THE STUDIES — single axes first, then two deliberate chords.
|
|||
|
|
============================================================ */
|
|||
|
|
const VARIANTS = [
|
|||
|
|
{ name: '00_reference', note: 'baseline (traces 1.0 · rows 46 · overlap 1.9 · line ×1)',
|
|||
|
|
traces: {}, sea: {} },
|
|||
|
|
{ name: '01_traces-bold', note: 'evidence enlarged (bubble size 1.6, density 0.9)',
|
|||
|
|
traces: { size: 1.6, density: 0.9 } },
|
|||
|
|
{ name: '02_traces-fine', note: 'evidence miniaturised (size 0.62, density 1.3)',
|
|||
|
|
traces: { size: 0.62, density: 1.3 } },
|
|||
|
|
{ name: '03_sea-broad', note: 'few broad swells (rows 26)',
|
|||
|
|
sea: { rows: 26 } },
|
|||
|
|
{ name: '04_sea-fine', note: 'fine ripple (rows 78, line ×0.75)',
|
|||
|
|
sea: { rows: 78, strokeMul: 0.75 } },
|
|||
|
|
{ name: '05_sea-deep', note: 'deep undulation (overlap 3.1)',
|
|||
|
|
sea: { overlap: 3.1 } },
|
|||
|
|
{ name: '06_sea-etched', note: 'heavy wave line (line ×2.3)',
|
|||
|
|
sea: { strokeMul: 2.3 } },
|
|||
|
|
{ name: '07_giant-evidence', note: 'large ink over fine calm water (size 1.7 · rows 78 · line ×0.7 · overlap 1.6)',
|
|||
|
|
traces: { size: 1.7, density: 0.85 }, sea: { rows: 78, strokeMul: 0.7, overlap: 1.6 } },
|
|||
|
|
{ name: '08_woodcut', note: 'small ink in a heavy carved sea (size 0.7 · rows 24 · line ×2.4 · overlap 2.6)',
|
|||
|
|
traces: { size: 0.7, density: 1.3 }, sea: { rows: 24, strokeMul: 2.4, overlap: 2.6 } },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
console.log(`SUN-ABOVE SCALE STUDIES — ${VARIANTS.length} renders → ${OUT}/`);
|
|||
|
|
VARIANTS.forEach(renderVariant);
|
|||
|
|
|
|||
|
|
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit2 · sun-above scale studies</title>
|
|||
|
|
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
|||
|
|
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
|||
|
|
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
|
|||
|
|
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
|||
|
|
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
|||
|
|
<h1>Sun-above — scale studies (colours & composition held)</h1>
|
|||
|
|
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name.replace(/^\d+_/, '')} — ${v.note}</figcaption></figure>`).join('')}</div>
|
|||
|
|
</body></html>`);
|
|||
|
|
console.log(`gallery -> ${OUT}/index.html`);
|