Added Ridgeline Plots and layers output
This commit is contained in:
136
src/qft/carpet.js
Normal file
136
src/qft/carpet.js
Normal 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>`;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 0–1) 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user