Added Ridgeline Plots and layers output

This commit is contained in:
2026-05-29 17:17:06 -04:00
parent 56c59a1f9c
commit e38f11f71a
79 changed files with 356982 additions and 21 deletions

136
src/qft/carpet.js Normal file
View File

@@ -0,0 +1,136 @@
/* ============================================================
qft/carpet.js — the VACUUM CARPET: a ridgeline / joyplot field.
Rows of low-frequency field waves + soft-edged sinusoidal "blips"
(Gaussian-windowed wave-packets) that drift and rotate phase across
rows, so localized excitations SPIRAL through the depth of the
stack. Perspective-compressed to a horizon → an infinite carpet of
quantum fluctuations. Rows are rendered as smooth Catmull-Rom
curves (no triangular peaks).
Two modes:
solid opaque paper + hidden-line occlusion (single-sheet joyplot art)
plate transparent, strokes only (for stacking on spaced plexi sheets)
============================================================ */
import { makeRng, range, chance } from '../rng.js';
import { resolveSubstrate } from './palette.js';
function hslToRgb(h, s, l) {
h = ((h % 1) + 1) % 1;
const a = s * Math.min(l, 1 - l);
const f = (n) => { const k = (n + h * 12) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); };
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
}
const css = (c) => `rgb(${c[0]},${c[1]},${c[2]})`;
// Catmull-Rom → cubic-bezier path: smooth curve through the points.
function smoothPath(pts) {
if (pts.length < 3) return 'M ' + pts.map(p => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' L ');
let d = `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)} `;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
const c1x = p1.x + (p2.x - p0.x) / 6, c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6, c2y = p2.y - (p3.y - p1.y) / 6;
d += `C ${c1x.toFixed(1)} ${c1y.toFixed(1)} ${c2x.toFixed(1)} ${c2y.toFixed(1)} ${p2.x.toFixed(1)} ${p2.y.toFixed(1)} `;
}
return d;
}
export function carpetSVG(size, opts = {}) {
const o = Object.assign({
seed: 'VACUUM-5113', salt: 'carpet', mode: 'solid', substrate: 'cream',
rows: 46, horizon: 0.34, wFar: 0.56, wNear: 0.68, overlap: 1.7, chaos: 0.5,
mound: 0.4, // 0 = flat band edge-to-edge · 1 = pronounced central mound
blips: 1.0, // density of the spiralling wave-packet excitations
hue: 0.52, hue2: 0.55, sat: 0.55, lightNear: 0.34, lightFar: 0.62,
strokeNear: 1.7, strokeFar: 0.5,
}, opts);
const W = size, H = size, u = size / 1000;
const paper = resolveSubstrate(o.substrate).paper.flat;
const rng = makeRng(o.seed, o.salt);
// ---- base sea: low-frequency field modes (phase drifts slowly per row) ----
const M = Math.round(4 + o.chaos * 8);
const modes = [];
for (let m = 0; m < M; m++) {
const f = range(rng, 0.4, 3.0 + o.chaos * 5); // low q: long swells dominate
modes.push({ f, a: 1 / (1 + f * 1.0), phi: range(rng, 0, Math.PI * 2), drift: range(rng, -1, 1) * (0.06 + f * 0.02) });
}
const norm = modes.reduce((s, m) => s + m.a, 0) || 1;
// ---- coherent excitations: soft wave-packets that drift + rotate phase
// across rows, so they SPIRAL through the depth of the stack ----
const nExc = Math.round((2 + o.chaos * 5) * o.blips);
const exc = [];
for (let e = 0; e < nExc; e++) {
exc.push({
x0: range(rng, 0.12, 0.88),
row0: range(rng, 0, o.rows - 1),
span: range(rng, 0.12, 0.3) * o.rows, // rows over which it lives
w: range(rng, 0.05, 0.11), // packet width (soft edge)
k: range(rng, 3.5, 7), // oscillations within the packet
amp: range(rng, 0.30, 0.7),
drift: range(rng, -0.018, 0.018), // lateral drift per row
phase: range(rng, 0, Math.PI * 2),
phaseAdv: range(rng, -0.45, 0.45), // phase rotation per row → spiral
sign: rng() < 0.5 ? -1 : 1,
});
}
const value = (t, r) => {
let s = 0;
for (const m of modes) s += m.a * Math.sin(2 * Math.PI * m.f * t + m.phi + r * m.drift);
s /= norm;
let blip = 0;
for (const e of exc) {
const dr = r - e.row0;
const env = Math.exp(-Math.pow(dr / (e.span * 0.5), 2));
if (env < 0.02) continue;
const cx = e.x0 + e.drift * dr;
const g = Math.exp(-Math.pow((t - cx) / e.w, 2)); // soft Gaussian window
blip += e.sign * e.amp * env * g * Math.cos((t - cx) / e.w * e.k + e.phase + e.phaseAdv * dr);
}
return s + blip * (0.5 + 0.5 * o.chaos);
};
// vertical placement: rows bunch at the horizon, spread to the front
const hY = o.horizon * H, bottom = H * 0.99;
const baseY = [];
for (let r = 0; r < o.rows; r++) baseY.push(hY + (bottom - hY) * Math.pow(r / (o.rows - 1), 1.7));
const transparent = o.mode === 'plate';
const eMin = 1 - 0.55 * o.mound;
const env = (t) => eMin + (1 - eMin) * Math.exp(-Math.pow((t - 0.5) / 0.42, 2));
let body = '';
for (let r = 0; r < o.rows; r++) {
const d = r / (o.rows - 1);
const half = (o.wFar + (o.wNear - o.wFar) * d) * W; // ≥ ~0.56W → bleeds off both edges
const cx = W / 2;
const localGap = r > 0 ? baseY[r] - baseY[r - 1] : (baseY[1] - baseY[0]);
const amp = Math.max(2 * u, o.overlap * localGap);
const npts = Math.max(56, Math.round(half / (2.6 * u)));
const pts = [];
for (let i = 0; i <= npts; i++) {
const t = i / npts;
const e = env(t);
pts.push({ x: cx - half + t * 2 * half, y: baseY[r] - amp * e * value(t, r) - amp * 0.55 * (e - eMin) * d });
}
const path = smoothPath(pts);
const hue = o.hue + (o.hue2 - o.hue) * (1 - d);
const light = o.lightFar + (o.lightNear - o.lightFar) * d;
const sat = o.sat * (0.6 + 0.4 * d);
const col = css(hslToRgb(hue, sat, light));
const sw = (o.strokeFar + (o.strokeNear - o.strokeFar) * d) * u;
if (!transparent) {
const fill = `${path} L ${(cx + half).toFixed(1)} ${bottom.toFixed(1)} L ${(cx - half).toFixed(1)} ${bottom.toFixed(1)} Z`;
body += `<path d="${fill}" fill="${css(paper)}" stroke="none"/>\n`;
}
const op = transparent ? (0.45 + 0.5 * d).toFixed(2) : 1;
body += `<path d="${path}" fill="none" stroke="${col}" stroke-width="${sw.toFixed(2)}" stroke-opacity="${op}" stroke-linecap="round" stroke-linejoin="round"/>\n`;
}
const bg = transparent ? '' : `<rect width="${W}" height="${H}" fill="${css(paper)}"/>`;
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
${bg}
<g>${body}</g>
</svg>`;
}

View File

@@ -32,6 +32,12 @@ export function paramsFromSeed(seed) {
cubicScale: r(0.92, 1.05), cubicRot: r(-0.25, 0.25),
cubicOriginX: 0, cubicOriginY: 0,
cubicN: 1, // half-range of the cubic lattice. 1=3³=27 verts/~54 edges (default); 2=5³=125 verts/~300 edges (denser); 3=7³=343 verts (heaviest).
// ---- cubic CAMERA: move the viewpoint of the cartesian grid ----
cubicYaw: -0.7854, cubicPitch: 0.6155, cubicRoll: 0, // default = classic isometric three-quarter
cubicPersp: 0, // 0 = isometric/orthographic; 1 = normal; up to 2 = exaggerated
cubicDist: 3.4, // camera distance (depth units); smaller = more dramatic foreshortening
cubicZShift: 0, // push camera INTO the grid (>0) for the "extends to infinity" look
cubicNx: null, cubicNy: null, cubicNz: null, // per-axis half-ranges (null → cubicN). wide+deep+shallow = infinite floor
photonCyclesPerUnit: 12, // wave frequency along photon edges; higher = finer ripples
schlegelScale: r(0.88, 1.02), schlegelRot: r(-0.30, 0.30),
schlegelOriginX: 0, schlegelOriginY: 0,

View File

@@ -36,7 +36,12 @@ function translateVerts(verts, dx, dy) {
export function generateQFTScene(params) {
// ---- cubic ---- (cubicN controls density: 1=27v, 2=125v, 3=343v)
const cubic = buildCubic(Math.max(1, params.cubicN | 0));
// camera (yaw/pitch/roll/persp/dist) moves the viewpoint of the cartesian grid
const cubic = buildCubic(Math.max(1, params.cubicN | 0), 1.0, {
yaw: params.cubicYaw, pitch: params.cubicPitch, roll: params.cubicRoll,
persp: params.cubicPersp, dist: params.cubicDist, zShift: params.cubicZShift,
nx: params.cubicNx, ny: params.cubicNy, nz: params.cubicNz,
});
rotateScale(cubic.vertices, params.cubicRot, params.cubicScale);
translateVerts(cubic.vertices, params.cubicOriginX, params.cubicOriginY);
cubic.id = 'cubic'; cubic.propagator = 'photon';

View File

@@ -11,33 +11,74 @@
simplified for legibility without literal E8 fidelity)
============================================================ */
/* 3D cubic lattice, isometric-projected to 2D and centred at origin.
/* 3D cubic lattice with a movable CAMERA, projected to 2D and centred at origin.
N is half-range: vertices at integer (i,j,k) ∈ [-N..N]^3.
N=1 → 3×3×3 = 27 vertices, ~54 edges. Plenty at thumbnail scale. */
export function buildCubic(N = 1, scale = 1.0) {
const vs = [], v3to1 = new Map();
// standard isometric, centred: x = (i-k)*cos30, y = (i+k)*sin30 - j
N=1 → 3×3×3 = 27 vertices, ~54 edges. Plenty at thumbnail scale.
opts (all optional) move the viewpoint of the cartesian grid:
yaw rotation about the vertical (Y) axis — spin left/right
pitch rotation about the horizontal (X) axis — tip toward/away
roll rotation about the view (Z) axis — cant
persp 0 = orthographic/isometric · 1 = normal · up to 2 = exaggerated
dist camera distance (in depth units); smaller = more dramatic
zShift push the lattice along the view axis — positive drives the CAMERA
INTO the grid so near cells blow off-page and far cells rush to a
vanishing point (the "extends to infinity" look)
nx,ny,nz per-axis half-ranges. Make a wide, deep, SHALLOW slab
(nx,nz big, ny 01) for an infinite FLOOR to the horizon rather
than a closed cube. Fall back to N when unset.
Defaults reproduce a classic isometric three-quarter view (persp 0). */
export function buildCubic(N = 1, scale = 1.0, opts = {}) {
const yaw = opts.yaw ?? -0.7854; // -45°
const pitch = opts.pitch ?? 0.6155; // ~35.26° → isometric
const roll = opts.roll ?? 0;
const persp = Math.max(0, Math.min(2, opts.persp ?? 0));
const nx = Math.max(1, Math.round(opts.nx ?? N));
const ny = Math.max(0, Math.round(opts.ny ?? N)); // 0 → a single flat layer (floor)
const nz = Math.max(1, Math.round(opts.nz ?? N));
const dist = (opts.dist ?? 3.4) * Math.max(1, nz);
const zShift = opts.zShift ?? 0;
const nearClip = 0.12 * dist; // cull cells at/behind the camera plane
const cy = Math.cos(yaw), sy = Math.sin(yaw);
const cx = Math.cos(pitch), sx = Math.sin(pitch);
const cz = Math.cos(roll), sz = Math.sin(roll);
const vs = [], clipped = [], v3to1 = new Map();
const proj = (i, j, k) => {
const ix = (i - k) * 0.866;
const iy = (i + k) * 0.5 - j;
// normalise so the cube fits within ~[-1,1] at scale=1
return { x: ix * scale * 0.5, y: iy * scale * 0.5 };
// raw lattice coords; j is the vertical (up) axis
let x = i, y = j, z = k;
let x1 = x * cy + z * sy; // yaw about Y
let z1 = -x * sy + z * cy;
let y1 = y;
let y2 = y1 * cx - z1 * sx; // pitch about X
let z2 = y1 * sx + z1 * cx;
let x2 = x1;
let xr = x2 * cz - y2 * sz; // roll about the view axis
let yr = x2 * sz + y2 * cz;
let zr = z2 + zShift;
const denom = dist - zr * persp; // camera on +z, looking toward -z
const isClip = persp > 0 && denom <= nearClip;
const f = persp > 0 ? dist / Math.max(denom, nearClip) : 1;
return { p: { x: xr * f * scale * 0.5, y: -yr * f * scale * 0.5 }, isClip };
};
for (let i = -N; i <= N; i++)
for (let j = -N; j <= N; j++)
for (let k = -N; k <= N; k++) {
for (let i = -nx; i <= nx; i++)
for (let j = -ny; j <= ny; j++)
for (let k = -nz; k <= nz; k++) {
v3to1.set(`${i},${j},${k}`, vs.length);
vs.push(proj(i, j, k));
const r = proj(i, j, k);
vs.push(r.p); clipped.push(r.isClip);
}
// edges along each axis (i / j / k adjacency)
// edges along each axis (i / j / k adjacency); skip any crossing the near plane
const es = [];
for (let i = -N; i <= N; i++)
for (let j = -N; j <= N; j++)
for (let k = -N; k <= N; k++) {
const link = (a, b) => { if (!clipped[a] && !clipped[b]) es.push({ a, b }); };
for (let i = -nx; i <= nx; i++)
for (let j = -ny; j <= ny; j++)
for (let k = -nz; k <= nz; k++) {
const a = v3to1.get(`${i},${j},${k}`);
if (i < N) es.push({ a, b: v3to1.get(`${i + 1},${j},${k}`) });
if (j < N) es.push({ a, b: v3to1.get(`${i},${j + 1},${k}`) });
if (k < N) es.push({ a, b: v3to1.get(`${i},${j},${k + 1}`) });
if (i < nx) link(a, v3to1.get(`${i + 1},${j},${k}`));
if (j < ny) link(a, v3to1.get(`${i},${j + 1},${k}`));
if (k < nz) link(a, v3to1.get(`${i},${j},${k + 1}`));
}
return { vertices: vs, edges: es };
}