Element Liberation

This commit is contained in:
2026-06-10 18:23:47 +01:00
parent 6438d2c4d2
commit e7fb2e9a68
125 changed files with 9990 additions and 19 deletions

View File

@@ -70,12 +70,26 @@ function glowSVG(W, baseRGB, strength, cx = 0.5, cy = 0.44) {
<stop offset="62%" stop-color="rgb(${lc})" stop-opacity="0"/></radialGradient></defs>
<rect width="${W}" height="${W}" fill="url(#glow)"/></svg>`;
}
function vignetteSVG(W, strength) {
// liberated vignette: off-centre radial (cx/cy/radius) OR a directional linear
// darkening (mode:'linear' + angle). No longer a predictable centred ring.
function vignetteSVG(W, o = {}) {
const s = (typeof o === 'number') ? o : (o.strength ?? 0.4);
const mode = o.mode || 'radial';
let grad;
if (mode === 'linear') {
const ang = o.angle ?? 90, start = o.start ?? 0.5;
grad = `<linearGradient id="vig" gradientTransform="rotate(${ang} 0.5 0.5)">
<stop offset="0%" stop-color="#000" stop-opacity="0"/>
<stop offset="${(start * 100).toFixed(0)}%" stop-color="#000" stop-opacity="0"/>
<stop offset="100%" stop-color="#000" stop-opacity="${(s * 0.85).toFixed(3)}"/></linearGradient>`;
} else {
const cx = ((o.cx ?? 0.5) * 100).toFixed(1), cy = ((o.cy ?? 0.48) * 100).toFixed(1), r = ((o.radius ?? 0.75) * 100).toFixed(1);
grad = `<radialGradient id="vig" cx="${cx}%" cy="${cy}%" r="${r}%">
<stop offset="${o.inner != null ? (o.inner * 100).toFixed(0) : 50}%" stop-color="#000" stop-opacity="0"/>
<stop offset="100%" stop-color="#000" stop-opacity="${(s * 0.72).toFixed(3)}"/></radialGradient>`;
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${W}" viewBox="0 0 ${W} ${W}">
<defs><radialGradient id="vig" cx="50%" cy="48%" r="75%">
<stop offset="52%" stop-color="#000" stop-opacity="0"/>
<stop offset="100%" stop-color="#000" stop-opacity="${(0.72 * strength).toFixed(3)}"/></radialGradient></defs>
<rect width="${W}" height="${W}" fill="url(#vig)"/></svg>`;
<defs>${grad}</defs><rect width="${W}" height="${W}" fill="url(#vig)"/></svg>`;
}
// ---------- bubble-chamber sub-renders (centered, transparent, single group) ----------
@@ -117,9 +131,14 @@ function bubbleLayer(W, seed, b) {
// ---------- field decks ----------
function seaSheets(W, f) {
const n = f.layers ?? 3;
// luminosity: waves can sit darker OR LIGHTER than the ground. `light` sets the
// near-row lightness; far rows drift +0.18 (atmospheric). >ground = light-on-dark.
const lite = f.color?.light;
const lightNear = lite != null ? lite : 0.33;
const lightFar = lite != null ? Math.min(0.97, lite + 0.18) : 0.56;
const base = {
mode: 'plate', rows: f.lines ?? 46, horizon: f.horizonY ?? 0.36, wFar: 0.58, wNear: 0.7,
overlap: f.overlap ?? 1.7, mound: f.mound ?? 0.3, sat: (f.color?.sat) ?? 0.6, lightNear: 0.33, lightFar: 0.56, blips: f.blips ?? 0.7,
overlap: f.overlap ?? 1.7, mound: f.mound ?? 0.3, sat: (f.color?.sat) ?? 0.6, lightNear, lightFar, blips: f.blips ?? 0.7,
};
const hueBack = f.color?.hueBack ?? 0.54, hueFront = f.color?.hueFront ?? 0.47;
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
@@ -163,8 +182,10 @@ function gridSheets(W, f) {
dist: f.dist ?? 3.0, zShift: f.zShift ?? 0, scale: f.scale ?? 1, originX: f.originX ?? 0, originY: f.originY ?? 0.34,
rippleAmp: f.ripple?.amp ?? 0, rippleFreqI: f.ripple?.freqI ?? 0.5, rippleFreqK: f.ripple?.freqK ?? 0.35, ripplePhase: f.ripple?.phase ?? 0, ...color,
};
const back = perspFloorSVG(W, { ...pb, salt: 'fA', stroke: 2.0, strokeFar: 0.7 });
const front = perspFloorSVG(W, { ...pb, salt: 'fB', stroke: 1.0, strokeFar: 0.35, ripplePhase: pb.ripplePhase + (pb.rippleAmp ? 0.8 : 0) });
const r = f.resonance || {};
const res = { resAmp: r.amp ?? 0, resQ: r.q ?? 14, resAxis: r.axis ?? 'ties', resHue: r.hue ?? 0.06, resSat: r.sat ?? 0.7, resLight: r.light ?? 0.45 };
const back = perspFloorSVG(W, { ...pb, salt: 'fA', stroke: 2.0, strokeFar: 0.7 }); // grid only
const front = perspFloorSVG(W, { ...pb, salt: 'fB', stroke: 1.0, strokeFar: 0.35, ripplePhase: pb.ripplePhase + (pb.rippleAmp ? 0.8 : 0), ...res }); // grid + resonance
return [{ href: uri(back), blur: blur[0] ?? 1.1, opacity: op }, { href: uri(front), blur: blur[1] ?? 0, opacity: op + 0.16 }];
}
@@ -191,7 +212,7 @@ export function buildGroupPieces(group, comp, W) {
push(0.6, { href: uri(glowSVG(W, rgb, b.glow.strength, gcx, gcy)), opacity: 1 });
}
if (b.film && b.film.opacity !== 0) push(1, { href: uri(filmSVG(W, b.film)), opacity: b.film.opacity ?? 0.6 });
if (b.vignette && b.vignette.strength) push(90, { href: uri(vignetteSVG(W, b.vignette.strength)), opacity: 1, blend: 'multiply' }); // edge darkening (over the piece)
if (b.vignette && b.vignette.strength) push(90, { href: uri(vignetteSVG(W, b.vignette)), opacity: 1, blend: 'multiply' }); // edge darkening (over the piece)
if (b.aging && b.aging.opacity !== 0) push(80, { href: uri(agingSVG(W, b.aging)), opacity: b.aging.opacity ?? 0.55, blend: 'multiply' });
if (b.grain && b.grain.opacity !== 0) push(95, { href: uri(grainSVG(W, { seed: b.grain.seed, tone: b.grain.tone, intensity: b.grain.intensity })), opacity: b.grain.opacity ?? 0.42, blend: 'multiply' });
break;

View File

@@ -14,17 +14,18 @@ export const DEFAULT_COMPOSITION = {
aging: { opacity: 0.5, scratches: 5, dust: 0.45, foxing: 0.5, seed: 5 },
grain: { opacity: 0.42, intensity: 0.42, seed: 19 },
glow: { strength: 0.55, followSun: true }, // lit-page glow; centre tracks the disk
vignette: { strength: 0.4 }, // soft darkening toward the edges
vignette: { strength: 0.4, mode: 'radial', cx: 0.5, cy: 0.48, radius: 0.75, angle: 90, start: 0.5 },
},
fieldSea: {
enabled: true, layerOpacity: 1, transform: { x: 0, y: 0, rotation: 0, scale: 1 }, seed: 'VACUUM-5113',
color: { hueBack: 0.54, hueFront: 0.47, sat: 0.6 },
color: { hueBack: 0.54, hueFront: 0.47, sat: 0.6, light: 0.36 }, // light > ~0.6 → waves lighter than ground
layers: 3, chaos: 0.3, blips: 0.7, mound: 0.3, horizonY: 0.36, lines: 46,
},
fieldGrid: {
enabled: true, layerOpacity: 1, pos: 'over', style: 'floor', transform: { x: 0, y: 0, rotation: 0, scale: 1 },
color: { hue: 0.5, hue2: 0.55, sat: 0.32, lightNear: 0.34, lightFar: 0.62 }, opacity: 0.44,
pitch: 0.45, yaw: 0.42, persp: 1.1, dist: 2.8, nx: 16, nz: 24, originY: 0.34, ripple: { amp: 0, freqI: 0.5, freqK: 0.35, phase: 0 },
resonance: { amp: 0, q: 14, axis: 'ties', hue: 0.06, sat: 0.7, light: 0.45 },
},
disk: {
enabled: true, layerOpacity: 1, transform: { x: 0, y: -0.26, rotation: 0, scale: 0.78 },
@@ -58,6 +59,11 @@ export const GROUPS_SCHEMA = [
R('glow.strength', 'Page glow', 0, 1, 0.01),
{ path: 'glow.followSun', label: 'Glow follows sun', type: 'toggle' },
R('vignette.strength', 'Vignette', 0, 1, 0.01),
{ path: 'vignette.mode', label: 'Vignette mode', type: 'select', options: ['radial', 'linear'] },
R('vignette.cx', 'Vignette X', 0, 1, 0.01),
R('vignette.cy', 'Vignette Y', 0, 1, 0.01),
R('vignette.radius', 'Vignette radius', 0.3, 1.4, 0.01),
R('vignette.angle', 'Vignette angle°', 0, 360, 1),
],
},
{
@@ -65,6 +71,7 @@ export const GROUPS_SCHEMA = [
R('color.hueBack', 'Hue · far', 0, 1, 0.005),
R('color.hueFront', 'Hue · near', 0, 1, 0.005),
R('color.sat', 'Saturation', 0, 1, 0.01),
R('color.light', 'Wave luminosity', 0, 1, 0.01),
R('layers', 'Plate layers', 1, 3, 1),
R('chaos', 'Chaos', 0, 1, 0.01),
R('blips', 'Blips', 0, 2, 0.05),
@@ -89,6 +96,9 @@ export const GROUPS_SCHEMA = [
R('color.hue', 'Hue', 0, 1, 0.005),
R('color.sat', 'Saturation', 0, 1, 0.01),
R('ripple.amp', 'Floor ripple', 0, 2, 0.02),
R('resonance.amp', 'Resonance (high-Q)', 0, 1, 0.01),
R('resonance.q', 'Resonance Q', 2, 40, 1),
R('resonance.hue', 'Resonance hue', 0, 1, 0.005),
],
},
{

View File

@@ -50,6 +50,10 @@ export function perspFloorSVG(size, opts = {}) {
rippleAmp: 0, rippleFreqI: 0.5, rippleFreqK: 0.4, ripplePhase: 0,
hue: 0.56, hue2: 0.5, sat: 0.26, lightNear: 0.32, lightFar: 0.62,
stroke: 1.5, strokeFar: 0.45, opacityMul: 1,
// high-Q RESONANCE on one axis, in a distinct colour (a standing wave threading
// the grid). resAmp 0 = off. resAxis 'ties' (transverse) | 'rails' (depth) |
// 'both' (a vibrating membrane — the floor stops reading as a floor).
resAmp: 0, resQ: 14, resAxis: 'ties', resHue: 0.06, resSat: 0.7, resLight: 0.45,
}, opts);
const W = size, H = size, u = size / 1000;
const paper = resolveSubstrate(o.substrate).paper.flat;
@@ -90,10 +94,30 @@ export function perspFloorSVG(size, opts = {}) {
const t = fade((a.depth + b.depth) / 2);
return `<line x1="${a.x.toFixed(1)}" y1="${a.y.toFixed(1)}" x2="${b.x.toFixed(1)}" y2="${b.y.toFixed(1)}" stroke="${colAt(t)}" stroke-width="${swAt(t).toFixed(2)}" stroke-opacity="${opAt(t).toFixed(2)}" stroke-linecap="round"/>`;
};
const resOn = (o.resAmp || 0) > 0;
const resCol = resOn ? cssR(hslToRgb(o.resHue, o.resSat, o.resLight)) : '';
const resWave = (idx, len) => o.resAmp * 0.14 * Lh * Math.sin(2 * Math.PI * o.resQ * (idx / Math.max(1, len - 1)));
const resStroke = (sw, op) => `stroke="${resCol}" stroke-width="${(sw * 0.95).toFixed(2)}" stroke-opacity="${op.toFixed(2)}" stroke-linecap="round" stroke-linejoin="round"`;
let body = '';
for (let i = 0; i < V.length; i++) for (let k = 0; k < V[i].length; k++) {
if (k + 1 < V[i].length) body += seg(V[i][k], V[i][k + 1]); // rails (constant i, into depth)
if (i + 1 < V.length) body += seg(V[i][k], V[i + 1][k]); // ties (constant k, across)
// RAILS (constant i, into depth)
for (let i = 0; i < V.length; i++) {
if (resOn && (o.resAxis === 'rails' || o.resAxis === 'both')) {
const pts = [];
for (let k = 0; k < V[i].length; k++) { const p = V[i][k]; if (p.clip) continue; pts.push({ x: p.x + resWave(k, V[i].length) * (0.3 + 0.7 * fade(p.depth)), y: p.y }); }
if (pts.length > 1) body += `<path d="${smoothPath(pts)}" fill="none" ${resStroke(swAt(0.6), opAt(0.8))}/>`;
} else {
for (let k = 0; k + 1 < V[i].length; k++) body += seg(V[i][k], V[i][k + 1]);
}
}
// TIES (constant k, across width)
for (let k = 0; k < V[0].length; k++) {
if (resOn && ((o.resAxis || 'ties') === 'ties' || o.resAxis === 'both')) {
const pts = [];
for (let i = 0; i < V.length; i++) { const p = V[i][k]; if (p.clip) continue; pts.push({ x: p.x, y: p.y + resWave(i, V.length) * (0.3 + 0.7 * fade(p.depth)) }); }
if (pts.length > 1) body += `<path d="${smoothPath(pts)}" fill="none" ${resStroke(swAt(0.6), opAt(0.8))}/>`;
} else {
for (let i = 0; i + 1 < V.length; i++) body += seg(V[i][k], V[i + 1][k]);
}
}
const bg = transparent ? '' : `<rect width="${W}" height="${H}" fill="${cssR(paper)}"/>`;
return `<?xml version="1.0" encoding="UTF-8"?>

View File

@@ -24,6 +24,18 @@ const TRACK_LAYERS = [
{ id: 'tracks-delta', label: 'Tracks · δ-rays (curls)', kinds: ['delta'] },
];
// rgb[0..255] → [h,s,l] (0..1), for hue-preserving darkening
function rgb2hsl([r, g, b]) {
r /= 255; g /= 255; b /= 255;
const mx = Math.max(r, g, b), mn = Math.min(r, g, b); let h = 0, s = 0; const l = (mx + mn) / 2;
if (mx !== mn) {
const d = mx - mn; s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
h /= 6;
}
return [h, s, l];
}
export function renderSVG(scene, params, sizePx = 4800) {
const w = sizePx, h = sizePx;
const scale = (w / 2) * (1 - MARGIN);
@@ -44,9 +56,11 @@ export function renderSVG(scene, params, sizePx = 4800) {
const paperRGB = pal.paper(); // {flat,glowIn,glowOut} rgb arrays
const paperC = { flat: rgbHex(paperRGB.flat), glowIn: rgbHex(paperRGB.glowIn), glowOut: rgbHex(paperRGB.glowOut) };
const ink = rgbHex(pal.feature()); // non-particle marks (optics, disk, damage, header)
// diskPressure: a darker, denser core (compressed gas = higher pressure)
// diskPressure: a darker, denser core — a DEEP version of the disk hue (not black),
// so the centre reads as compressed colour rather than a hole.
const press = Math.max(0, Math.min(1, params.diskPressure ?? 0));
const coreInk = rgbHex(mix(pal.feature(), [0, 0, 0], press * 0.7));
const [fh, fs, fl] = rgb2hsl(pal.feature());
const coreInk = rgbHex(hslToRgb(fh, Math.min(1, fs * (1 + 0.25 * press)), fl * (1 - 0.6 * press)));
const featKey = rgbKey(pal.feature());
const colorMap = new Map([[featKey, pal.feature()]]); // distinct bubble colours → gradients