382 lines
22 KiB
JavaScript
382 lines
22 KiB
JavaScript
/* ============================================================
|
||
refined-backlit.mjs — ONE SUN SETTING: five plates of a single
|
||
seed's world (seethe-bold's sea, MESON-5113) across which time
|
||
advances. The trigger event is byte-identical in every frame —
|
||
a single millisecond, embalmed — while everything around it
|
||
ages: the sun walks its setting diagonal L→R and sinks, the
|
||
chamber keeps running (background events accumulate), the
|
||
grain grows as the light dies, and THE WATER REMEMBERS.
|
||
|
||
The constitution (2026-07-05/06, from the maker):
|
||
· THE WORLD IS SOVEREIGN IN THE INSTANT — the sea never reacts
|
||
to the event within a frame; submerged evidence is refracted,
|
||
tinted, dimmed, and the waves pass in front of it.
|
||
· …BUT THE WATER REMEMBERS ACROSS EXPOSURES — frame by frame,
|
||
the wave rows bend around where the evidence has been sitting
|
||
(feDisplacementMap driven by the submerged ink, scale growing
|
||
with time). Influence arrives only through time.
|
||
· THE SKY IS THE MARGIN WHERE THE HAND LIVES — and the hand has
|
||
a JOB: it tracks the descent. A pencil ghost-ring marks where
|
||
the sun was in the PREVIOUS exposure; an altitude reading is
|
||
logged each frame; when the sun sinks, the ring stops — the
|
||
hand loses its subject and starts guessing (h −0·54?).
|
||
· β-LAW COLOUR, magenta-centred — transparent births, the family
|
||
holds hot magenta, violet is only the last word.
|
||
· EMBER HEART · EVEN DEMOCRACY · AGED DOCUMENT (foxing, dust,
|
||
scratches; grain ramps up as dusk falls).
|
||
|
||
Usage: node tools/refined-backlit.mjs [size]
|
||
→ fable/REFINED_Backlit/
|
||
============================================================ */
|
||
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 { chinagraphText } from '../src/scene/chinagraph.js';
|
||
import { makeRng, gauss } from '../src/rng.js';
|
||
|
||
const SIZE = +(process.argv[2] || 1500);
|
||
const HAND = process.argv[3] !== undefined ? +process.argv[3] : 0; // scanner's-hand presence: 0 silent … 1 full (2026-07-16: DISABLED for now — still not feeling it; ledger experiment kept behind the flag)
|
||
const OUT = process.argv[4] || 'fable/REFINED_Backlit';
|
||
const LEDGER = process.argv[5] === 'ledger'; // EXPERIMENT 2026-07-16: embalmed marks — written once, persist on every later exposure
|
||
mkdirSync(OUT, { recursive: true });
|
||
const u = SIZE / 1000;
|
||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||
|
||
/* ---- the world constants: seethe-bold's own config ---- */
|
||
const SEED = 'MESON-5113';
|
||
const BASE = 'rgb(227,220,200)';
|
||
const HORIZON = 0.37;
|
||
const YH = HORIZON * 2 - 1; // -0.26 scene
|
||
const FILM = { seed: 41, density: 0.6 };
|
||
const GRAPHITE = '#39312a';
|
||
|
||
/* ---- film / grain / aging (the document's flesh) ---- */
|
||
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>`;
|
||
}
|
||
function deck(c, warpFn) {
|
||
const base = { mode: 'plate', rows: 46, horizon: HORIZON, wFar: 0.58, wNear: 0.7,
|
||
overlap: 1.9, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips: c.blips, 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(c.chaos * 0.8, c.chaos, t),
|
||
strokeNear: strokes[i].near, strokeFar: strokes[i].far });
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
IRON FILINGS — the field attends to the evidence.
|
||
A coarse attraction field is built from the submerged ink
|
||
(track points + the sunken sun); every carpet row point is
|
||
pulled TOWARD the nearest evidence, like filings onto a
|
||
magnet. Two time-scales: a faint instantaneous pull in every
|
||
frame (the field can never fully ignore the evidence) plus a
|
||
memory ramp that deepens across the exposures.
|
||
============================================================ */
|
||
function buildAttraction(below, shock, sunSubmerged) {
|
||
const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98; // scene → px (renderSVG's mapping)
|
||
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; // → roughly -0.5..0.5, fades to 0 away from ink
|
||
}
|
||
return (x, y) => { // bilinear sample, px in → pull fraction out
|
||
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;
|
||
};
|
||
}
|
||
// reconstruct carpetSVG's own row geometry so the pull is expressed in the
|
||
// hook's normalized units (returned value is multiplied by the row's amp)
|
||
function warpFor(sample, pullPx) {
|
||
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) / 45;
|
||
const amp = Math.max(2 * u, 1.9 * localGap);
|
||
const n = sample(x, y) * pullPx / amp;
|
||
return Math.max(-0.9, Math.min(0.9, n)); // pinch, don't shatter
|
||
};
|
||
}
|
||
|
||
/* ---- the scanner's marks (beyond glyphs) ---- */
|
||
function pencilRing(cx, cy, r, rng, ink, w) {
|
||
const n = 44, start = rng() * Math.PI * 2, turn = Math.PI * 2 * (1.04 + rng() * 0.1);
|
||
const dx = gauss(rng) * r * 0.05, dy = gauss(rng) * r * 0.05;
|
||
let d = '';
|
||
for (let i = 0; i <= n; i++) {
|
||
const a = start + turn * (i / n), rr = r * (1 + gauss(rng) * 0.045);
|
||
d += `${i ? 'L' : 'M'} ${(cx + Math.cos(a) * rr + dx * i / n).toFixed(1)} ${(cy + Math.sin(a) * rr + dy * i / n).toFixed(1)} `;
|
||
}
|
||
return `<path d="${d}" fill="none" stroke="${ink}" stroke-opacity="0.6" stroke-width="${w.toFixed(2)}" stroke-linecap="round"/>`;
|
||
}
|
||
function pencilArc(cx, cy, r, a0, a1, rng, ink, w) {
|
||
const n = 18; let d = '';
|
||
for (let i = 0; i <= n; i++) {
|
||
const a = a0 + (a1 - a0) * (i / n);
|
||
d += `${i ? 'L' : 'M'} ${(cx + Math.cos(a) * r + gauss(rng) * 1.4).toFixed(1)} ${(cy + Math.sin(a) * r + gauss(rng) * 1.4).toFixed(1)} `;
|
||
}
|
||
return `<path d="${d}" fill="none" stroke="${ink}" stroke-opacity="0.7" stroke-width="${w.toFixed(2)}" stroke-linecap="round"/>`;
|
||
}
|
||
|
||
/* ---- the event ---- */
|
||
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,
|
||
shockIntensity: 0.9, shockStriations: 1.0,
|
||
diskHollow: +(process.env.HOLLOW ?? 0.85), // open the sun's heart: centre fades to paper, ember gathers in a ring
|
||
depth: 0.3, aging: 0.25,
|
||
}, over);
|
||
return p;
|
||
}
|
||
|
||
const fx = (sx) => ((sx + 1) / 2), fy = (sy) => ((sy + 1) / 2); // scene → frame fraction
|
||
|
||
function renderPlate(v, i) {
|
||
const p = bcAssembleParams(v.bcOver);
|
||
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 };
|
||
|
||
// RECIPROCITY (asymmetric): the submerged trails pick up a subtle waviness
|
||
// along the field's rows — coerced, but far less than they coerce.
|
||
const wamp = v.wave;
|
||
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());
|
||
|
||
// IRON FILINGS: every wave row is pulled toward the submerged evidence.
|
||
const sunSubmerged = scene.shock && scene.shock.y > YH - scene.shock.r * 0.4;
|
||
const sample = buildAttraction(belowWavy, scene.shock, sunSubmerged);
|
||
const sheets = deck(v.sea, warpFor(sample, v.pull * u)).map(dataUri);
|
||
|
||
const film = dataUri(filmSVG(FILM));
|
||
const grain = dataUri(grainSVG({ amount: v.grain }));
|
||
const aging = dataUri(agingSVG({ seed: 5 + i, scratches: 6, dust: 0.5, foxing: 0.55 }));
|
||
|
||
const hpx = (HORIZON * SIZE).toFixed(1);
|
||
const m = v.muffle;
|
||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
|
||
|
||
/* ---- the hand: tracking the descent ---- */
|
||
const hRng = makeRng(SEED, 'hand:' + v.name);
|
||
const px = (f) => f * SIZE, py = (f) => f * SIZE;
|
||
let hand = '';
|
||
if (HAND > 0 && LEDGER) {
|
||
/* EMBALMED MARKS — one physical plate, five exposures. Each mark is
|
||
written ONCE, at its own exposure (rng seeded per mark, not per
|
||
plate), so its strokes are byte-identical on every later plate.
|
||
The ledger accumulates in the corner; the rings march down the
|
||
diagonal and stop where the water takes their subject. */
|
||
const rngFor = (tag) => makeRng(SEED, 'mark:' + tag);
|
||
const LX = 0.735; // ledger lives top-right: the north-west sky belongs to the sun's history
|
||
hand += chinagraphText('Nº 217', { x: px(LX), y: py(0.06), h: 20 * u, rng: rngFor('label'), ink: GRAPHITE, width: 2.6 * u });
|
||
for (let k = 0; k <= i; k++) {
|
||
const line = PLATES[k].time + ' h ' + alt(k) + (k === PLATES.length - 1 ? '?' : '');
|
||
hand += chinagraphText(line, { x: px(LX), y: py(0.102 + k * 0.031), h: 15 * u, rng: rngFor('line' + k), ink: GRAPHITE, width: 2.0 * u });
|
||
if (k < i && SUNPATH[k][1] < YH - 0.02)
|
||
hand += `<g clip-path="url(#skyc)">${pencilRing(px(fx(SUNPATH[k][0])), py(fy(SUNPATH[k][1])), 0.05 * SIZE, rngFor('ring' + k), GRAPHITE, 2.2 * u)}</g>`;
|
||
}
|
||
if (i >= 2) { // the θ measurement, taken at the third exposure, persists
|
||
const a = PLATES[2].arc;
|
||
hand += pencilArc(px(a.at[0]), py(a.at[1]), a.r * SIZE, a.a0, a.a1, rngFor('arc'), GRAPHITE, 2.2 * u);
|
||
hand += chinagraphText(a.text, { x: px(a.textAt[0]), y: py(a.textAt[1]), h: 17 * u, rng: rngFor('arctext'), ink: GRAPHITE, width: 2.2 * u });
|
||
}
|
||
} else if (HAND > 0) {
|
||
if (v.label) hand += chinagraphText(v.label, { x: px(0.065), y: py(0.085), h: 26 * u, rng: hRng, ink: GRAPHITE, width: 3.0 * u });
|
||
hand += chinagraphText(v.time, { x: px(0.065), y: py(0.125), h: 21 * u, rng: hRng, ink: GRAPHITE, width: 2.6 * u });
|
||
// ghost-ring where the sun was LAST exposure (only while that spot is in the sky)
|
||
if (v.ghost) {
|
||
const [gx, gy, gr] = v.ghost;
|
||
hand += `<g clip-path="url(#skyc)">${pencilRing(px(gx), py(gy), gr * SIZE, hRng, GRAPHITE, 2.4 * u)}</g>`;
|
||
}
|
||
// altitude reading, logged beside the sun's vertical
|
||
if (v.alt) hand += chinagraphText(v.alt.text, { x: px(v.alt.at[0]), y: py(v.alt.at[1]), h: 19 * u, rng: hRng, ink: GRAPHITE, width: 2.4 * u });
|
||
// plate 3: the measured angle lives ON the vertex, arc + reading
|
||
if (v.arc) {
|
||
const [ax, ay] = v.arc.at;
|
||
hand += pencilArc(px(ax), py(ay), v.arc.r * SIZE, v.arc.a0, v.arc.a1, hRng, GRAPHITE, 2.2 * u);
|
||
hand += chinagraphText(v.arc.text, { x: px(v.arc.textAt[0]), y: py(v.arc.textAt[1]), h: 19 * u, rng: hRng, ink: GRAPHITE, width: 2.4 * u });
|
||
}
|
||
}
|
||
if (HAND > 0 && HAND < 1) hand = `<g opacity="${HAND}">${hand}</g>`; // the hand recedes into the wax
|
||
|
||
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 - 0.55 * m).toFixed(2)} 0"/>
|
||
<feGaussianBlur stdDeviation="${((0.6 + 3.2 * m) * 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)}
|
||
${hand}
|
||
${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} sun(${v.bcOver.shockX},${v.bcOver.shockY}) bg=${v.bcOver.bgEvents} pull=${v.pull}`);
|
||
}
|
||
|
||
/* ============================================================
|
||
ONE SUN SETTING — five exposures of plate Nº 217.
|
||
The trigger is identical in every frame (embalmed); the sun
|
||
walks its setting diagonal; the chamber keeps running; the
|
||
grain grows; the water remembers.
|
||
============================================================ */
|
||
const EVENT = { eventX: -0.14, eventY: -0.37 };
|
||
const TRACES = { sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8 };
|
||
const SUNPATH = [[-0.55, -0.72], [-0.37, -0.53], [0.02, YH], [0.22, 0.15], [0.4, 0.27]];
|
||
const alt = (i) => {
|
||
const h = YH - SUNPATH[i][1];
|
||
const s = Math.abs(h).toFixed(2).replace('0.', '0·');
|
||
return (h > 0.005 ? '+' : h < -0.005 ? '-' : '') + s;
|
||
};
|
||
|
||
const PLATES = [
|
||
{ name: '01_sun-above', time: '19:04', label: 'Nº 217',
|
||
muffle: 0, pull: 7, wave: 0.003, grain: 0.42,
|
||
sea: { chaos: 0.8, blips: 1.4 },
|
||
alt: { text: 'h ' + alt(0), at: [fx(SUNPATH[0][0]) + 0.1, fy(SUNPATH[0][1]) - 0.02] },
|
||
bcOver: { ...EVENT, ...TRACES, bgEvents: 2, aging: 0.2, shockX: SUNPATH[0][0], shockY: SUNPATH[0][1], shockSize: 0.17, shockIntensity: 0.8 } },
|
||
|
||
{ name: '02_sun-touching', time: '19:26',
|
||
muffle: 0.18, pull: 13, wave: 0.0045, grain: 0.46,
|
||
sea: { chaos: 0.84, blips: 1.2 },
|
||
ghost: [fx(SUNPATH[0][0]), fy(SUNPATH[0][1]), 0.055],
|
||
alt: { text: 'h ' + alt(1), at: [fx(SUNPATH[1][0]) + 0.11, fy(SUNPATH[1][1]) - 0.02] },
|
||
bcOver: { ...EVENT, ...TRACES, bgEvents: 4, aging: 0.3, shockX: SUNPATH[1][0], shockY: SUNPATH[1][1], shockSize: 0.19, shockIntensity: 0.85 } },
|
||
|
||
{ name: '03_sun-half', time: '19:47',
|
||
muffle: 0.4, pull: 20, wave: 0.006, grain: 0.5,
|
||
sea: { chaos: 0.88, blips: 1.0 },
|
||
ghost: [fx(SUNPATH[1][0]), fy(SUNPATH[1][1]), 0.06],
|
||
alt: { text: 'h 0·00', at: [fx(SUNPATH[2][0]) + 0.12, fy(YH) - 0.035] },
|
||
arc: { at: [fx(EVENT.eventX), fy(EVENT.eventY)], r: 0.055, a0: 2.4, a1: 3.4, text: 'θ 38°',
|
||
textAt: [fx(EVENT.eventX) - 0.185, fy(EVENT.eventY) + 0.045] },
|
||
bcOver: { ...EVENT, ...TRACES, bgEvents: 6, aging: 0.45, shockX: SUNPATH[2][0], shockY: SUNPATH[2][1], shockSize: 0.21 } },
|
||
|
||
{ name: '04_sun-drowned', time: '20:08', label: 'Nº 217',
|
||
muffle: 0.62, pull: 28, wave: 0.008, grain: 0.55,
|
||
sea: { chaos: 0.92, blips: 0.85 },
|
||
// no ghost-ring: the previous position is at the waterline — the hand
|
||
// cannot ring what the water has taken; only the reading continues
|
||
alt: { text: 'h ' + alt(3), at: [fx(SUNPATH[3][0]) + 0.1, fy(YH) - 0.03] },
|
||
bcOver: { ...EVENT, ...TRACES, bgEvents: 8, aging: 0.6, shockX: SUNPATH[3][0], shockY: SUNPATH[3][1], shockSize: 0.24 } },
|
||
|
||
{ name: '05_sun-gone', time: '20:31',
|
||
muffle: 0.85, pull: 36, wave: 0.01, grain: 0.6,
|
||
sea: { chaos: 0.96, blips: 0.7 },
|
||
alt: { text: 'h ' + alt(4) + '?', at: [fx(SUNPATH[4][0]) + 0.06, fy(YH) - 0.03] },
|
||
bcOver: { ...EVENT, ...TRACES, bgEvents: 10, aging: 0.75, shockX: SUNPATH[4][0], shockY: SUNPATH[4][1], shockSize: 0.27, shockStriations: 1.2 } },
|
||
];
|
||
|
||
console.log(`ONE SUN SETTING — ${PLATES.length} plates → ${OUT}/`);
|
||
PLATES.forEach(renderPlate);
|
||
|
||
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
|
||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit · one sun setting</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(5,1fr);gap:10px;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}
|
||
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:22px}
|
||
.notes{color:#999;background:#161210;padding:12px 16px;border-left:3px solid #d9b48a;margin:14px 0;max-width:960px}</style></head><body>
|
||
<h1>One sun setting — plate Nº 217, five exposures</h1>
|
||
<div class="notes">The trigger event is identical in every frame — a single millisecond, embalmed — while everything around it ages: the sun walks its setting diagonal and sinks, the chamber keeps running (history accumulates), the grain grows as the light dies, and the water remembers: the wave rows slowly bend around the submerged evidence. The sea never reacts in the instant; influence arrives only through time. The hand tracks the descent — ghost-rings where the sun was, altitude readings, and when the sun is gone, a guess.</div>
|
||
<div class="row">${PLATES.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)} · ${v.time}</figcaption></figure>`).join('')}</div>
|
||
<div class="grid">${PLATES.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)} · ${v.time}</figcaption></figure>`).join('')}</div>
|
||
</body></html>`);
|
||
console.log(`gallery -> ${OUT}/index.html`);
|