Templates-Meta Art

This commit is contained in:
2026-06-02 20:32:41 -04:00
parent 219eb6632c
commit 6438d2c4d2
111 changed files with 7076 additions and 472 deletions

View File

@@ -62,10 +62,10 @@ function agingSVG(W, o = {}) {
// ---------- lit-page glow + vignette (restores the photographic 'glow') ----------
function parseRGB(c) { const m = (c || '').match(/\d+/g); return m ? m.slice(0, 3).map(Number) : [229, 222, 203]; }
function glowSVG(W, baseRGB, strength) {
function glowSVG(W, baseRGB, strength, cx = 0.5, cy = 0.44) {
const lc = baseRGB.map(v => Math.min(255, Math.round(v + 30))); // lighter warm centre
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${W}" viewBox="0 0 ${W} ${W}">
<defs><radialGradient id="glow" cx="50%" cy="44%" r="76%">
<defs><radialGradient id="glow" cx="${(cx * 100).toFixed(1)}%" cy="${(cy * 100).toFixed(1)}%" r="76%">
<stop offset="0%" stop-color="rgb(${lc})" stop-opacity="${(0.95 * strength).toFixed(3)}"/>
<stop offset="62%" stop-color="rgb(${lc})" stop-opacity="0"/></radialGradient></defs>
<rect width="${W}" height="${W}" fill="url(#glow)"/></svg>`;
@@ -106,6 +106,9 @@ function bubbleLayer(W, seed, b) {
primaries: b.primaries ?? 18, sweepers: b.sweepers ?? 5, cosmics: b.cosmics ?? 6,
vdecay: b.vdecay ?? 3, burst: b.burst ?? 0.7, eloss: b.eloss ?? 0.34, bfield: b.bfield ?? 1.0,
pspread: b.pspread ?? 0.6, deltaRate: b.deltaRate ?? 0.6, deltaTight: b.deltaTight ?? 0.7,
// chamber boundary arc + structural geometry (disk-coloured), controllable here
showBoundary: b.showBoundary !== false, boundaryR: b.boundaryR ?? 0.45, boundaryY: b.boundaryY ?? 0.35,
boundaryOpacity: b.boundaryOpacity ?? 0.4, instrument: b.instrument ?? 0.35,
emit: ['bubble'],
});
return renderSVG(generateScene(p), p, W);
@@ -180,7 +183,13 @@ export function buildGroupPieces(group, comp, W) {
const b = comp.background || {};
const rgb = parseRGB(b.color);
push(0, { rect: b.color || 'rgb(229,222,203)' });
if (b.glow && b.glow.strength) push(0.6, { href: uri(glowSVG(W, rgb, b.glow.strength)), opacity: 1 }); // lit-page glow (behind the fields)
if (b.glow && b.glow.strength) {
// glow centre follows the sun (disk) unless followSun:false
const dt = comp.disk?.transform || {};
const fs = b.glow.followSun !== false && comp.disk?.enabled !== false;
const gcx = fs ? (((dt.x ?? 0) + 1) / 2) : 0.5, gcy = fs ? (((dt.y ?? 0) + 1) / 2) : 0.44;
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.aging && b.aging.opacity !== 0) push(80, { href: uri(agingSVG(W, b.aging)), opacity: b.aging.opacity ?? 0.55, blend: 'multiply' });
@@ -189,13 +198,13 @@ export function buildGroupPieces(group, comp, W) {
}
case 'fieldSea': {
const f = comp.fieldSea; if (!f || f.enabled === false) break;
seaSheets(W, f).forEach((s, i) => push(10 + i, { href: s.href, blur: s.blur, opacity: s.opacity, transform: f.transform }));
seaSheets(W, f).forEach((s, i) => push(10 + i, { href: s.href, blur: s.blur, opacity: s.opacity, transform: f.transform, clip: f.clip }));
break;
}
case 'fieldGrid': {
const f = comp.fieldGrid; if (!f || f.enabled === false) break;
const z0 = f.pos === 'behind' ? 5 : 25;
gridSheets(W, f).forEach((s, i) => push(z0 + i, { href: s.href, blur: s.blur, opacity: s.opacity, transform: f.transform }));
gridSheets(W, f).forEach((s, i) => push(z0 + i, { href: s.href, blur: s.blur, opacity: s.opacity, transform: f.transform, clip: f.clip }));
break;
}
case 'disk': {
@@ -237,13 +246,19 @@ export function assemblePieces(pieces, W) {
const H = W, u = W / 1000;
const blurFilters = new Map();
const blurId = (px) => { if (!px) return null; const k = (+px).toFixed(2); if (!blurFilters.has(k)) blurFilters.set(k, `bl${blurFilters.size}`); return blurFilters.get(k); };
const clips = []; // {id, x0,y0,x1,y1} in 0..1 frame fractions — confine a layer to a band/stripe
const body = [...pieces].sort((a, b) => a.z - b.z).map((p) => {
if (p.rect) return `<rect width="${W}" height="${H}" fill="${p.rect}"/>`;
const bid = blurId(p.blur), t = tfStr(p.transform, W, H);
const img = `<image x="0" y="0" width="${W}" height="${H}" href="${p.href}" opacity="${p.opacity ?? 1}"${bid ? ` filter="url(#${bid})"` : ''}${p.blend ? ` style="mix-blend-mode:${p.blend}"` : ''}/>`;
return t ? `<g transform="${t}">${img}</g>` : img;
let el = t ? `<g transform="${t}">${img}</g>` : img;
if (p.clip && p.clip.length === 4) { // clip in FRAME space (outside the transform)
const id = `clip${clips.length}`; clips.push({ id, c: p.clip });
el = `<g clip-path="url(#${id})">${el}</g>`;
}
return el;
});
const defs = `<defs>${[...blurFilters.entries()].map(([px, id]) => `<filter id="${id}" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${((+px) * u).toFixed(2)}"/></filter>`).join('')}</defs>`;
const defs = `<defs>${[...blurFilters.entries()].map(([px, id]) => `<filter id="${id}" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${((+px) * u).toFixed(2)}"/></filter>`).join('')}${clips.map(({ id, c }) => `<clipPath id="${id}"><rect x="${(c[0] * W).toFixed(0)}" y="${(c[1] * H).toFixed(0)}" width="${((c[2] - c[0]) * W).toFixed(0)}" height="${((c[3] - c[1]) * H).toFixed(0)}"/></clipPath>`).join('')}</defs>`;
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
${defs}
${body.join('\n')}

View File

@@ -5,15 +5,29 @@
a plate caption. Lives on its own (does NOT scale with the event).
Coordinates are frame-normalized [-1,1]; (0,0) = centre.
============================================================ */
import { makeRng, gauss } from '../rng.js';
import { makeRng, gauss, cyrb53 } from '../rng.js';
// a hand-noted impression of the event — bra-ket, quantum concepts, words of wonder.
// label:'auto' (the default) picks one deterministically from the seed; type any
// word to override.
export const ANNOTATION_VOCAB = [
'⟨ψ|', '|ψ⟩', '|Ψ⟩', '⟨φ|ψ⟩', '|0⟩', '⟨x|p⟩', 'ψ', 'Φ',
'superposition', 'entangled', 'decoherence', 'eigenstate', 'collapse',
'the vacuum', 'uncertainty', 'annihilation', 'creation', 'ℏ', 'quanta',
'coherence', 'the observer', 'the unseen', 'a trace', 'evidence', 'wonder',
];
export function fiduciarySVG(size, opts = {}) {
const o = Object.assign({
label: 'No 001', target: [0, 0], from: null,
pencil: '#39312a', width: 1.0, fontSize: 0.03, opacity: 0.9,
label: 'auto', target: [0, 0], from: null,
pencil: '#39312a', width: 1.0, fontSize: 0.024, opacity: 0.85,
arrow: true, corners: false, caption: null, captionAt: [-0.92, 0.93],
seed: 'FID', salt: 'a',
}, opts);
// resolve 'auto'/empty → a seeded word from the vocabulary
const label = (!o.label || o.label === 'auto')
? ANNOTATION_VOCAB[parseInt(cyrb53(o.seed || 'x'), 16) % ANNOTATION_VOCAB.length] // cyrb53 → hex string
: o.label;
const W = size, H = size, u = size / 1000;
const rng = makeRng(o.seed, o.salt);
const X = (nx) => (nx + 1) / 2 * W, Y = (ny) => (ny + 1) / 2 * H;
@@ -41,9 +55,9 @@ export function fiduciarySVG(size, opts = {}) {
}
// the label, set near the arrow's tail
if (o.label) {
if (label) {
const lx = X(from[0] + (from[0] <= 0 ? 0.02 : -0.02)), ly = Y(from[1] - 0.03);
body += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" font-size="${(o.fontSize * size).toFixed(0)}" font-family="'Bradley Hand','Segoe Script','Comic Sans MS',cursive" fill="${o.pencil}" fill-opacity="${o.opacity}" transform="rotate(${(gauss(rng) * 2).toFixed(1)} ${lx.toFixed(1)} ${ly.toFixed(1)})">${esc(o.label)}</text>\n`;
body += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" font-size="${(o.fontSize * size).toFixed(0)}" font-family="'Bradley Hand','Segoe Script','Comic Sans MS',cursive" fill="${o.pencil}" fill-opacity="${o.opacity}" transform="rotate(${(gauss(rng) * 2).toFixed(1)} ${lx.toFixed(1)} ${ly.toFixed(1)})">${esc(label)}</text>\n`;
}
// optional registration "+" corners

View File

@@ -13,7 +13,7 @@ export const DEFAULT_COMPOSITION = {
film: { opacity: 0.6, density: 0.5, seed: 8 },
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 }, // lit-page glow (lighter warm centre)
glow: { strength: 0.55, followSun: true }, // lit-page glow; centre tracks the disk
vignette: { strength: 0.4 }, // soft darkening toward the edges
},
fieldSea: {
@@ -34,9 +34,10 @@ export const DEFAULT_COMPOSITION = {
enabled: true, layerOpacity: 1, transform: { x: 0.28, y: -0.1, rotation: 10, scale: 0.78 },
palette: 'magentarise', saturation: 1.05, traceHue: 0, transparentBase: true,
primaries: 18, sweepers: 5, cosmics: 6, eloss: 0.34, bfield: 1.0, deltaRate: 0.6,
showBoundary: true, boundaryR: 0.45, boundaryY: 0.35, boundaryOpacity: 0.4, instrument: 0.35,
},
fiduciaries: {
enabled: true, layerOpacity: 1, label: 'No 001', pencil: '#39312a', width: 1.0, arrow: true, corners: false, caption: '',
enabled: true, layerOpacity: 1, label: 'auto', pencil: '#39312a', width: 1.0, arrow: true, corners: false, caption: '',
},
};
@@ -55,6 +56,7 @@ export const GROUPS_SCHEMA = [
R('grain.opacity', 'Grain opacity', 0, 1, 0.01),
R('grain.intensity', 'Grain intensity', 0, 1, 0.01),
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),
],
},
@@ -112,6 +114,11 @@ export const GROUPS_SCHEMA = [
R('eloss', 'Energy loss', 0, 1.5, 0.01),
R('bfield', 'B-field', 0.2, 3, 0.01),
R('deltaRate', 'δ-ray rate', 0, 1, 0.01),
{ path: 'showBoundary', label: 'Chamber arc', type: 'toggle' },
R('boundaryR', 'Arc radius', 0.1, 1.0, 0.01),
R('boundaryY', 'Arc position Y', -0.3, 0.7, 0.01),
R('boundaryOpacity', 'Arc opacity', 0, 1, 0.01),
R('instrument', 'Structural geometry', 0, 1, 0.01),
],
},
{
@@ -129,8 +136,8 @@ export const GROUPS_SCHEMA = [
// the common transform block, generated for groups with transform:true
export const COMMON_CONTROLS = [
R('layerOpacity', 'Opacity', 0, 1, 0.01),
R('transform.x', 'Centre X', -1, 1, 0.01),
R('transform.y', 'Centre Y', -1, 1, 0.01),
R('transform.x', 'Centre X', -1.6, 1.6, 0.01), // beyond ±1 → bleed off-canvas
R('transform.y', 'Centre Y', -1.6, 1.6, 0.01),
R('transform.rotation', 'Rotation°', -180, 180, 1),
R('transform.scale', 'Scale', 0.1, 2.5, 0.01),
R('transform.scale', 'Scale', 0.1, 3, 0.01), // small ↔ overfill / eclipse
];

153
src/compose/templates.js Normal file
View File

@@ -0,0 +1,153 @@
/* ============================================================
compose/templates.js — composition TEMPLATES: the geometric
skeletons of famous pictures, applied to our vocabulary. Each
template is a coherent point in the 7-axis composition space
(horizon ratio · focal placement · thrust · symmetry · figure:void
· atmosphere · palette). build(seed, i) → a full composition config;
the seed supplies content, the template supplies structure.
See docs/COMPOSITION-TEMPLATES.md.
============================================================ */
import { DEFAULT_COMPOSITION } from './schema.js';
const clone = (o) => JSON.parse(JSON.stringify(o));
const merge = (a, b) => {
const o = { ...a };
for (const k in b) o[k] = (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) ? merge(a[k] || {}, b[k]) : b[k];
return o;
};
// variation per index: 0 = as-authored · 1 = mirror across X · 2 = tonal/horizon drift
function vary(c, i) {
if (i === 1) {
for (const g of ['disk', 'bubble', 'fiduciaries', 'fieldGrid', 'fieldSea'])
if (c[g] && c[g].transform) c[g].transform.x = -(c[g].transform.x || 0);
if (c.fieldGrid) { if (c.fieldGrid.yaw != null) c.fieldGrid.yaw = -c.fieldGrid.yaw; if (c.fieldGrid.originX != null) c.fieldGrid.originX = -c.fieldGrid.originX; }
if (c.fiduciaries && c.fiduciaries.from) c.fiduciaries.from = [-c.fiduciaries.from[0], c.fiduciaries.from[1]];
} else if (i === 2) {
if (c.fieldSea) {
c.fieldSea.horizonY = Math.min(0.74, (c.fieldSea.horizonY ?? 0.36) + 0.05);
if (c.fieldSea.color) { c.fieldSea.color.hueBack = (c.fieldSea.color.hueBack ?? 0.5) + 0.04; c.fieldSea.color.hueFront = (c.fieldSea.color.hueFront ?? 0.5) + 0.04; }
}
if (c.disk && c.disk.transform) c.disk.transform.y = (c.disk.transform.y || 0) - 0.04;
}
return c;
}
export const TEMPLATES = [
// 1 — HOKUSAI · dynamic asymmetry: low horizon, swelling sea, tiny low event,
// small high sun; indigo/Prussian. ×3
{
id: 'great-wave', name: 'Great Wave', source: 'Hokusai', reps: 3,
seeds: ['HOKUSAI-3311', 'KANAGAWA-1820', 'SURGE-7782'],
over: {
background: { color: 'rgb(224,223,213)', glow: { strength: 0.42, followSun: true }, vignette: { strength: 0.34 } },
fieldSea: { color: { hueBack: 0.62, hueFront: 0.58, sat: 0.55 }, chaos: 0.5, blips: 1.2, mound: 0.62, horizonY: 0.5, lines: 54 },
fieldGrid: { opacity: 0.3, pos: 'behind', color: { hue: 0.6, hue2: 0.58, sat: 0.3, lightNear: 0.34, lightFar: 0.62 }, pitch: 0.42, yaw: 0.5, dist: 3.0 },
disk: { transform: { x: 0.42, y: -0.5, rotation: 0, scale: 0.5 }, hue: 0.05, sat: 0.85, size: 0.13, pressure: 0.7 },
bubble: { transform: { x: -0.24, y: 0.3, rotation: -12, scale: 0.55 }, palette: 'magentarise', primaries: 16, sweepers: 6, eloss: 0.32 },
fiduciaries: { label: 'auto', from: [0.3, -0.4] },
},
},
// 2 — FRIEDRICH · the sublime void: horizon sliver low, one tiny cool sun, ~85%
// empty, no grid/fiduciaries. ×2
{
id: 'monk-by-sea', name: 'Monk by the Sea', source: 'Friedrich', reps: 2,
seeds: ['CASPAR-1810', 'RUCKEN-0042'],
over: {
background: { color: 'rgb(214,216,215)', glow: { strength: 0.5, followSun: false }, vignette: { strength: 0.28 } },
fieldSea: { color: { hueBack: 0.6, hueFront: 0.58, sat: 0.26 }, chaos: 0.18, blips: 0.4, mound: 0.12, horizonY: 0.72, lines: 40, layerOpacity: 0.82 },
fieldGrid: { enabled: false },
disk: { transform: { x: -0.06, y: 0.46, rotation: 0, scale: 0.32 }, hue: 0.58, sat: 0.42, size: 0.12, pressure: 0.4 },
bubble: { transform: { x: 0.12, y: 0.5, rotation: 0, scale: 0.3 }, palette: 'mono', primaries: 8, sweepers: 2, cosmics: 2, deltaRate: 0.4, layerOpacity: 0.8 },
fiduciaries: { enabled: false },
},
},
// 3 — ROTHKO · colour fields: flat band over a contrasting ground, centred seam,
// strong glow + vignette, no figure. ×2
{
id: 'rothko', name: 'Colour Fields', source: 'Rothko', reps: 2,
seeds: ['MARK-1957', 'SEAGRAM-0015'],
over: {
background: { color: 'rgb(178,108,94)', glow: { strength: 0.6, followSun: false }, vignette: { strength: 0.56 } },
fieldSea: { color: { hueBack: 0.99, hueFront: 0.02, sat: 0.5 }, chaos: 0.1, blips: 0.18, mound: 0.0, horizonY: 0.52, lines: 32, layerOpacity: 0.9 },
fieldGrid: { enabled: false }, disk: { enabled: false }, bubble: { enabled: false }, fiduciaries: { enabled: false },
},
},
// 4 — TURNER · atmospheric dissolution: high warm glow, low opacities everywhere,
// bright high sun, dissolving warm grid; gold-haze. ×3
{
id: 'turner', name: 'Atmospheric Dissolution', source: 'Turner', reps: 3,
seeds: ['JMW-1844', 'RAIN-STEAM-09', 'NORHAM-1798'],
over: {
background: { color: 'rgb(233,222,196)', glow: { strength: 0.95, followSun: true }, vignette: { strength: 0.24 } },
fieldSea: { color: { hueBack: 0.1, hueFront: 0.08, sat: 0.35 }, chaos: 0.3, blips: 0.6, mound: 0.3, horizonY: 0.46, lines: 46, layerOpacity: 0.58 },
fieldGrid: { opacity: 0.22, color: { hue: 0.1, hue2: 0.08, sat: 0.2, lightNear: 0.5, lightFar: 0.68 }, pitch: 0.3, yaw: 0.3, ripple: { amp: 0.4 }, layerOpacity: 0.7 },
disk: { transform: { x: 0.08, y: -0.34, rotation: 0, scale: 0.6 }, hue: 0.1, sat: 0.8, size: 0.18, pressure: 0.3, intensity: 0.95 },
bubble: { transform: { x: 0.12, y: -0.04, rotation: 0, scale: 0.7 }, palette: 'kindrise', saturation: 0.9, primaries: 16, eloss: 0.3, layerOpacity: 0.68 },
fiduciaries: { enabled: false },
},
},
// 5 — HIROSHIGE / MA · asymmetric negative space: forms to one corner, a bold
// two-point diagonal, a large quiet void opposite. ×2
{
id: 'ma', name: 'Ma · Negative Space', source: 'Hiroshige', reps: 2,
seeds: ['ANDO-1857', 'EDO-0100'],
over: {
background: { color: 'rgb(228,224,210)', glow: { strength: 0.4, followSun: false }, vignette: { strength: 0.3 } },
fieldSea: { color: { hueBack: 0.5, hueFront: 0.52, sat: 0.3 }, chaos: 0.22, blips: 0.5, mound: 0.2, horizonY: 0.42, lines: 42, layerOpacity: 0.8 },
fieldGrid: { opacity: 0.4, pos: 'over', color: { hue: 0.55, hue2: 0.5, sat: 0.28, lightNear: 0.34, lightFar: 0.64 }, pitch: 0.3, yaw: 0.72, dist: 2.6, originX: 0.3 },
disk: { transform: { x: 0.55, y: -0.46, rotation: 0, scale: 0.4 }, hue: 0.06, sat: 0.82, size: 0.12, pressure: 0.7 },
bubble: { transform: { x: 0.5, y: 0.4, rotation: 8, scale: 0.5 }, palette: 'magentarise', primaries: 14, sweepers: 5 },
fiduciaries: { label: 'auto', from: [0.2, -0.05] },
},
},
// 6 — MALEVICH · single form on void: one bold dark/saturated disk off-centre,
// sea off, a single faint tilted grid plane. ×2
{
id: 'suprematist', name: 'Single Form on Void', source: 'Malevich', reps: 2,
seeds: ['KAZIMIR-1915', 'BLACK-SQ-00'],
over: {
background: { color: 'rgb(233,230,220)', glow: { strength: 0.3, followSun: true }, vignette: { strength: 0.2 } },
fieldSea: { enabled: false },
fieldGrid: { opacity: 0.26, pos: 'behind', transform: { x: 0, y: 0, rotation: 18, scale: 1.1 }, pitch: 0.18, yaw: 0.0, color: { hue: 0.0, hue2: 0.02, sat: 0.18, lightNear: 0.4, lightFar: 0.66 } },
disk: { transform: { x: -0.2, y: -0.1, rotation: 0, scale: 1.0 }, hue: 0.0, sat: 0.92, size: 0.2, pressure: 0.95, intensity: 0.9, striations: 0.15 },
bubble: { transform: { x: 0.34, y: 0.28, rotation: 0, scale: 0.34 }, palette: 'mono', primaries: 7, sweepers: 1, deltaRate: 0.5, layerOpacity: 0.85, showBoundary: false, instrument: 0 },
fiduciaries: { enabled: false },
},
},
// 7 — RENAISSANCE φ · classical balance: sun on a φ point, event on the opposing
// third, horizon on the lower third, a measured grid. ×3
{
id: 'golden-thirds', name: 'Golden Section', source: 'Renaissance φ', reps: 3,
seeds: ['VITRUVIAN-1490', 'PHI-1618', 'URBINO-1465'],
over: {
background: { color: 'rgb(230,224,206)', glow: { strength: 0.55, followSun: true }, vignette: { strength: 0.35 } },
fieldSea: { color: { hueBack: 0.52, hueFront: 0.47, sat: 0.5 }, chaos: 0.28, blips: 0.7, mound: 0.3, horizonY: 0.62, lines: 46 },
fieldGrid: { opacity: 0.34, pos: 'over', pitch: 0.4, yaw: 0.38, color: { hue: 0.54, hue2: 0.5, sat: 0.28, lightNear: 0.34, lightFar: 0.64 } },
disk: { transform: { x: -0.236, y: -0.382, rotation: 0, scale: 0.6 }, hue: 0.07, sat: 0.82, size: 0.15, pressure: 0.8 },
bubble: { transform: { x: 0.236, y: 0.1, rotation: 8, scale: 0.62 }, palette: 'magentarise', primaries: 18, sweepers: 5 },
fiduciaries: { label: 'auto' },
},
},
// 8 — FRANK LLOYD WRIGHT · organic rectilinearity: low strong horizon, bold near-
// frontal orthogonal grid, flat horizontal sea, hearth-anchor disk (Cherokee
// red, dark core) off-centre; earthy Taliesin palette. ×3
{
id: 'wright', name: 'Cantilevered Horizontal', source: 'Frank Lloyd Wright', reps: 3,
seeds: ['TALIESIN-1911', 'FALLINGWATER-37', 'PRAIRIE-1901'],
over: {
background: { color: 'rgb(226,218,196)', glow: { strength: 0.45, followSun: true }, vignette: { strength: 0.34 } },
fieldSea: { color: { hueBack: 0.1, hueFront: 0.13, sat: 0.4 }, chaos: 0.15, blips: 0.3, mound: 0.05, horizonY: 0.6, lines: 40, layerOpacity: 0.85 },
fieldGrid: { opacity: 0.5, pos: 'over', style: 'floor', pitch: 0.5, yaw: 0.08, persp: 1.0, dist: 2.8, nx: 18, nz: 20, originY: 0.3, color: { hue: 0.08, hue2: 0.06, sat: 0.3, lightNear: 0.32, lightFar: 0.6 } },
disk: { transform: { x: -0.32, y: 0.02, rotation: 0, scale: 0.8 }, hue: 0.03, sat: 0.85, size: 0.16, pressure: 0.9, striations: 0.4 },
bubble: { transform: { x: 0.3, y: -0.05, rotation: 0, scale: 0.7 }, palette: 'kind', saturation: 0.95, primaries: 16, sweepers: 3, bfield: 1.2 },
fiduciaries: { label: 'auto' },
},
},
];
// build the composition for a template at variation index i
export function buildTemplate(t, i) {
const c = merge(clone(DEFAULT_COMPOSITION), t.over);
c.seed = t.seeds[i % t.seeds.length];
return vary(c, i);
}

View File

@@ -62,11 +62,12 @@ export function renderSVG(scene, params, sizePx = 4800) {
/* ---------- Chamber optics ---------- */
let optics = '';
if (params.showBoundary) {
const bcx = cx, bcy = cy + h * 0.35, br = w * 0.45;
// the chamber boundary arc — position/size/weight parameterised
const bcx = cx, bcy = cy + h * (params.boundaryY ?? 0.35), br = w * (params.boundaryR ?? 0.45);
const a1 = Math.PI * 0.15, a2 = Math.PI - Math.PI * 0.15;
const x1 = bcx + Math.cos(Math.PI + a1) * br, y1 = bcy + Math.sin(Math.PI + a1) * br;
const x2 = bcx + Math.cos(Math.PI + a2) * br, y2 = bcy + Math.sin(Math.PI + a2) * br;
optics += `<path d="M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${br} ${br} 0 0 1 ${x2.toFixed(1)} ${y2.toFixed(1)}" fill="none" stroke="${ink}" stroke-opacity="0.4" stroke-width="${2 * u}"/>\n`;
optics += `<path d="M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${br} ${br} 0 0 1 ${x2.toFixed(1)} ${y2.toFixed(1)}" fill="none" stroke="${ink}" stroke-opacity="${(params.boundaryOpacity ?? 0.4)}" stroke-width="${(params.boundaryWidth ?? 2) * u}"/>\n`;
}
if (scene.instrument) {
const inst = scene.instrument;