Godstrument/viz/spheres.js
monsterrobotparty 925617d782 🪐 resonance: music of the spheres — planets as modulators (Stage 1)
Vendor SOLARGOD@6391273's dependency-free JPL ephemeris (viz/spheres.js) and
make the solar system a modulation source, computed locally at the instrument's
own clock — no network, no server, no sockets.

- viz/spheres.js: ephem.js verbatim + the minimal lib subset inlined; loaded via
  a <script type="module"> bridge that self-checks (Mars @ JD 2461236.5 within
  0.005 AU of Horizons ground truth) and hangs a frozen window.SPHERES; every
  consumer guarded so a missing/broken file still boots the app.
- sphere.<planet>.{phase,dist,beat} sources for the 8 planets, routable like any
  S&H source; the great-year clock maps 1 bar = N days of sky (log knob 1..3650,
  default 30) off seqPhase alone, so scrub/tempo/reload reproduce the same sky.
- 🪐 spheres panel (S / right-click sky): per-planet live meters, rate knob,
  ☸ orbital-ratios button (drum wheels → 4:8:16 Io:Europa:Ganymede, 8:13
  Venus:Earth pentagram), on/off; godspeak: spheres on/off·faster/slower,
  "<planet> drive <dest>". spheres {on,rate} travel in the patch.
- grimoire chapter + regenerated viz/manual.html.

Verified: self-check green; determinism (jupiter.phase@bar16 identical across
reload); pentagram (venus.beat period 1.59 bars ≈ 583.9/365, within 2%); ratios
set exact wlens and ⌘Z reverts; zero console errors; ♪ T O B G + ⌘Z all work;
spheres default off on fresh boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:04:22 +10:00

204 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// vendored from SOLARGOD@6391273 — do not edit; re-vendor to update.
// This is js/ephem.js (whole) with the minimal js/lib.js subset it needs inlined
// (centuriesSinceJ2000, normDegPM180, DEG, J2000_JD, jdFromUnixMs) so the file
// stands alone with no import of Three.js or the rest of SOLARGOD. The IIFE in
// index.html imports from here via a <script type="module"> bridge that hangs a
// frozen window.SPHERES. Godstrument computes the sky itself, deterministically,
// at its own musical clock — no network, no server, no sockets.
// ---- lib.js subset (SOLARGOD@6391273/js/lib.js — pure, dependency-free) ----
export const MS_PER_DAY = 86400000;
export const J2000_JD = 2451545.0; // 2000-Jan-01 12:00 TT
export const UNIX_EPOCH_JD = 2440587.5; // JD of 1970-Jan-01 00:00 UTC
export const DEG = Math.PI / 180;
// JD from unix-ms: divide by ms/day, add the unix-epoch JD.
export function jdFromUnixMs(ms) { return ms / MS_PER_DAY + UNIX_EPOCH_JD; }
// Julian centuries since J2000.0.
export function centuriesSinceJ2000(jd) { return (jd - J2000_JD) / 36525; }
// Normalize degrees to (180, +180]. Must run in float64 AFTER any large
// subtraction (Mercury's L rate is 149473°/Cy → M can be tens of thousands of
// degrees before reduction).
export function normDegPM180(d) {
d = d % 360;
if (d <= -180) d += 360;
else if (d > 180) d -= 360;
return d;
}
// ===========================================================================
// js/ephem.js — JPL SSD "Approximate Positions of the Planets" (Standish).
// Tables extracted 2026-07-15 from https://ssd.jpl.nasa.gov/planets/approx_pos.html
// — authoritative, verbatim. Angles in degrees, rates per Julian century; `e` is
// dimensionless (the page's "rad" header for e is its own typo). No n-body
// integration — this IS the spec.
// Row layout: el = [a(au), e, I(deg), L(deg), ϖ long.peri(deg), Ω long.node(deg)]
// rate = same order, per century.
const TABLE1 = {
mercury: { el: [0.38709927, 0.20563593, 7.00497902, 252.25032350, 77.45779628, 48.33076593],
rate: [0.00000037, 0.00001906, -0.00594749, 149472.67411175, 0.16047689, -0.12534081] },
venus: { el: [0.72333566, 0.00677672, 3.39467605, 181.97909950, 131.60246718, 76.67984255],
rate: [0.00000390, -0.00004107, -0.00078890, 58517.81538729, 0.00268329, -0.27769418] },
earth: { el: [1.00000261, 0.01671123, -0.00001531, 100.46457166, 102.93768193, 0.0],
rate: [0.00000562, -0.00004392, -0.01294668, 35999.37244981, 0.32327364, 0.0] },
mars: { el: [1.52371034, 0.09339410, 1.84969142, -4.55343205, -23.94362959, 49.55953891],
rate: [0.00001847, 0.00007882, -0.00813131, 19140.30268499, 0.44441088, -0.29257343] },
jupiter: { el: [5.20288700, 0.04838624, 1.30439695, 34.39644051, 14.72847983, 100.47390909],
rate: [-0.00011607, -0.00013253, -0.00183714, 3034.74612775, 0.21252668, 0.20469106] },
saturn: { el: [9.53667594, 0.05386179, 2.48599187, 49.95424423, 92.59887831, 113.66242448],
rate: [-0.00125060, -0.00050991, 0.00193609, 1222.49362201, -0.41897216, -0.28867794] },
uranus: { el: [19.18916464, 0.04725744, 0.77263783, 313.23810451, 170.95427630, 74.01692503],
rate: [-0.00196176, -0.00004397, -0.00242939, 428.48202785, 0.40805281, 0.04240589] },
neptune: { el: [30.06992276, 0.00859048, 1.77004347, -55.12002969, 44.96476227, 131.78422574],
rate: [0.00026291, 0.00005105, 0.00035372, 218.45945325, -0.32241464, -0.00508664] },
};
// Table 2a — valid 3000 BC3000 AD. Shipped for the extended-range config swap
// (CONFIG.useExtendedRange); unused by v1 UI.
const TABLE2A = {
mercury: { el: [0.38709843, 0.20563661, 7.00559432, 252.25166724, 77.45771895, 48.33961819],
rate: [0.00000000, 0.00002123, -0.00590158, 149472.67486623, 0.15940013, -0.12214182] },
venus: { el: [0.72332102, 0.00676399, 3.39777545, 181.97970850, 131.76755713, 76.67261496],
rate: [-0.00000026, -0.00005107, 0.00043494, 58517.81560260, 0.05679648, -0.27274174] },
earth: { el: [1.00000018, 0.01673163, -0.00054346, 100.46691572, 102.93005885, -5.11260389],
rate: [-0.00000003, -0.00003661, -0.01337178, 35999.37306329, 0.31795260, -0.24123856] },
mars: { el: [1.52371243, 0.09336511, 1.85181869, -4.56813164, -23.91744784, 49.71320984],
rate: [0.00000097, 0.00009149, -0.00724757, 19140.29934243, 0.45223625, -0.26852431] },
jupiter: { el: [5.20248019, 0.04853590, 1.29861416, 34.33479152, 14.27495244, 100.29282654],
rate: [-0.00002864, 0.00018026, -0.00322699, 3034.90371757, 0.18199196, 0.13024619] },
saturn: { el: [9.54149883, 0.05550825, 2.49424102, 50.07571329, 92.86136063, 113.63998702],
rate: [-0.00003065, -0.00032044, 0.00451969, 1222.11494724, 0.54179478, -0.25015002] },
uranus: { el: [19.18797948, 0.04685740, 0.77298127, 314.20276625, 172.43404441, 73.96250215],
rate: [-0.00020455, -0.00001550, -0.00180155, 428.49512595, 0.09266985, 0.05739699] },
neptune: { el: [30.06952752, 0.00895439, 1.77005520, 304.22289287, 46.68158724, 131.78635853],
rate: [0.00006447, 0.00000818, 0.00022400, 218.46515314, 0.01009938, -0.00606302] },
};
// Table 2b — extra terms added to M (deg) for JupiterNeptune, Table 2a only:
// M += b·T² + c·cos(f·T) + s·sin(f·T). [b, c, s, f]
const TABLE2B = {
jupiter: [-0.00012452, 0.06064060, -0.35635438, 38.35125000],
saturn: [0.00025899, -0.13434469, 0.87320147, 38.35125000],
uranus: [0.00058331, -0.97731848, 0.17689245, 7.67025000],
neptune: [-0.00041348, 0.68346318, -0.10162547, 7.67025000],
};
export const PLANET_IDS = ['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
let useExtended = false;
export function setExtendedRange(on) { useExtended = !!on; }
// Solve Kepler's equation E e·sinE = M (radians) by NewtonRaphson.
// Seed E₀ = M + e·sinM; converges in a few iters even for e→0.97 (verify gate).
export function solveKepler(M, e, tol = 1e-9, maxIter = 20) {
let E = M + e * Math.sin(M);
for (let i = 0; i < maxIter; i++) {
const dE = (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
E -= dE;
if (Math.abs(dE) < tol) break;
}
return E;
}
// The reusable core: resolved classical elements → heliocentric J2000 ecliptic
// {x,y,z} in AU. a[au], e, and iDeg/OmDeg(Ω)/wDeg(ω arg.peri)/Mdeg all in degrees.
// Shared by planets (element+rate) and small bodies (epoch elements) — brief §3.
export function eclFromElements(a, e, iDeg, OmDeg, wDeg, Mdeg, out = {}) {
const M = normDegPM180(Mdeg) * DEG;
const E = solveKepler(M, e);
// Orbital-plane coords (perifocal), AU.
const xp = a * (Math.cos(E) - e);
const yp = a * Math.sqrt(1 - e * e) * Math.sin(E);
const w = wDeg * DEG, Om = OmDeg * DEG, I = iDeg * DEG;
const cw = Math.cos(w), sw = Math.sin(w);
const cO = Math.cos(Om), sO = Math.sin(Om);
const cI = Math.cos(I), sI = Math.sin(I);
// Perifocal → ecliptic rotation (brief §3 step 5 = standard R_z(Ω)R_x(I)R_z(ω)).
out.x = (cw * cO - sw * sO * cI) * xp + (-sw * cO - cw * sO * cI) * yp;
out.y = (cw * sO + sw * cO * cI) * xp + (-sw * sO + cw * cO * cI) * yp;
out.z = (sw * sI) * xp + (cw * sI) * yp;
return out;
}
// Resolve a planet's elements at jd (Table 1, 18002050) → {a,e,I,Om,w,M} deg.
function resolvePlanet(bodyId, jd) {
const T = centuriesSinceJ2000(jd);
const row = TABLE1[bodyId];
if (!row) throw new Error(`ephem: unknown planet ${bodyId}`);
const a = row.el[0] + row.rate[0] * T;
const e = row.el[1] + row.rate[1] * T;
const I = row.el[2] + row.rate[2] * T;
const L = row.el[3] + row.rate[3] * T;
const wb = row.el[4] + row.rate[4] * T; // ϖ longitude of perihelion
const Om = row.el[5] + row.rate[5] * T; // Ω longitude of ascending node
const w = wb - Om; // ω argument of perihelion
const M = L - wb; // mean anomaly (normalize AFTER subtract)
return { a, e, I, Om, w, M };
}
// Extended-range resolver (Table 2a + the 2b M-correction for JupiterNeptune,
// valid 3000 BC3000 AD). f·T uses T in centuries; f is deg-per-century so f·T
// is degrees → convert to rad for the trig. Only used when useExtended is set.
function resolvePlanetExt(bodyId, jd) {
const T = centuriesSinceJ2000(jd);
const row = TABLE2A[bodyId];
const a = row.el[0] + row.rate[0] * T;
const e = row.el[1] + row.rate[1] * T;
const I = row.el[2] + row.rate[2] * T;
const L = row.el[3] + row.rate[3] * T;
const wb = row.el[4] + row.rate[4] * T;
const Om = row.el[5] + row.rate[5] * T;
const w = wb - Om;
let M = L - wb;
const b2 = TABLE2B[bodyId];
if (b2) {
const [b, c, s, f] = b2;
M += b * T * T + c * Math.cos(f * T * DEG) + s * Math.sin(f * T * DEG);
}
return { a, e, I, Om, w, M };
}
// Public: heliocentric J2000 ecliptic position of a planet at jd, AU.
export function helioEcl(bodyId, jd, out = {}) {
const p = useExtended ? resolvePlanetExt(bodyId, jd) : resolvePlanet(bodyId, jd);
return eclFromElements(p.a, p.e, p.I, p.Om, p.w, p.M, out);
}
// Mean motion [deg/day] from semi-major axis via Kepler's third law (Gaussian).
export function meanMotionDegPerDay(a) { return 0.9856076686 / Math.pow(a, 1.5); }
// Resolved semi-major axis [AU] of a planet at jd (for vis-viva speed, year length).
export function semiMajor(bodyId, jd) {
const p = useExtended ? resolvePlanetExt(bodyId, jd) : resolvePlanet(bodyId, jd);
return p.a;
}
// Small-body position (asteroids/comets/Pluto): epoch elements propagated by
// M = M0 + n·(jd epoch). el = {a,e,i,om(Ω),w(ω),ma(M0),epoch}.
export function smallBodyEcl(el, jd, out = {}) {
const n = meanMotionDegPerDay(el.a);
const M = el.ma + n * (jd - el.epoch);
return eclFromElements(el.a, el.e, el.i, el.om, el.w, M, out);
}
// One full orbit sampled in mean anomaly, as a Float64Array of xyz triples
// (nSamples+1 points; last == first so a LineLoop/LineSegments closes cleanly —
// verify gate "orbitPath endpoints join"). Elements frozen at jd (they drift
// slowly; caller rebuilds only when |ΔT| > 0.1 Cy, brief §11).
export function orbitPath(bodyId, jd, nSamples = 256) {
const p = useExtended ? resolvePlanetExt(bodyId, jd) : resolvePlanet(bodyId, jd);
return samplePath(p.a, p.e, p.I, p.Om, p.w, nSamples);
}
export function orbitPathFromElements(el, nSamples = 256) {
return samplePath(el.a, el.e, el.i, el.om, el.w, nSamples);
}
function samplePath(a, e, I, Om, w, nSamples) {
const out = new Float64Array((nSamples + 1) * 3);
const tmp = {};
for (let k = 0; k <= nSamples; k++) {
const M = (360 * k) / nSamples; // 0..360; k=nSamples reproduces k=0
eclFromElements(a, e, I, Om, w, M, tmp);
out[k * 3] = tmp.x; out[k * 3 + 1] = tmp.y; out[k * 3 + 2] = tmp.z;
}
return out;
}