From bdba1194848d457b4cf47188bb1266ebe26a407b Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Thu, 16 Jul 2026 13:17:37 +1000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=8C=98=20closer:=20event=20band=20?= =?UTF-8?q?=E2=80=94=20the=20=C2=B12=20yr=20window=20gets=20its=20own=20fu?= =?UTF-8?q?ll-width=20strip=20(opus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SYZYGY's 104 diamonds clustered into ~1.6% of the 250-yr timeline (invisible in deep time). Add a second strip under #timeline-outer that maps ONLY the computed ±2 yr window to full width — diamonds live there (same ev-* classes + click-jump), and the main strip gets a brass bracket marking the window's true extent (1.6% near, 0.5% in deep time). windowFraction exported + gated. Verified: 22/22 (+band gate); diamonds spread 0.4–98.7%; click-jump works in near AND deep mode; zero console errors both epochs. Co-Authored-By: Claude Opus 4.8 --- css/style.css | 17 +++++++++++++++++ js/layers/events.js | 28 ++++++++++++++++++++++++++-- verify.html | 17 +++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/css/style.css b/css/style.css index 5c34bbb..6927357 100644 --- a/css/style.css +++ b/css/style.css @@ -202,3 +202,20 @@ html, body { .ev-elong { top: -8px; background: #7fc7d6; box-shadow: 0 0 5px rgba(127, 199, 214, 0.6); } .ev-conj { top: 2px; width: 5px; height: 5px; background: var(--brass-dim); opacity: 0.6; } .ev-conj:hover { opacity: 1; } + +/* PERIHELION-O — event band: the computed ±2 yr window mapped to full width in a + strip below the timeline (diamonds spread out); a bracket on the main strip + marks the window's real extent. Reuses the ev-* diamond classes above. */ +#event-band { + position: relative; height: 14px; margin-top: 6px; + border-radius: 5px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); +} +#event-band #event-marks { inset: 1px 0; } +#event-band .ev-mark { top: 50%; transform: translate(-50%, -50%) rotate(45deg); } +#event-band .ev-mark:hover { transform: translate(-50%, -50%) rotate(45deg) scale(1.7); } +#event-band .ev-conj { top: 50%; } +#event-window-bracket { + position: absolute; top: -3px; bottom: -3px; z-index: 3; pointer-events: none; + border: 1px solid var(--brass); border-radius: 4px; + background: rgba(217, 164, 65, 0.14); box-shadow: 0 0 7px rgba(217, 164, 65, 0.45); +} diff --git a/js/layers/events.js b/js/layers/events.js index 3ef5a3c..c0cfaef 100644 --- a/js/layers/events.js +++ b/js/layers/events.js @@ -140,6 +140,13 @@ export function nextEvent(bodyId, jd) { return null; } +// Fraction of an instant within the event band's [winStart, winEnd] window (0..1). +// The band maps the ±2 yr window to full width regardless of near/deep epoch. +// Exported so verify.html gates the mapping without a DOM. +export function windowFraction(ms, winStartMs, winEndMs) { + return (ms - winStartMs) / ((winEndMs - winStartMs) || 1); +} + // ---- default factory: timeline diamonds + HUD row ---- const WINDOW_DAYS = 730; // ±2 years, computed lazily around the current sim date function cssKind(type) { return type === 'opposition' ? 'opp' : type.startsWith('elongation') ? 'elong' : 'conj'; } @@ -149,9 +156,19 @@ export default function create(ctx) { const strip = document.getElementById('timeline-outer'); if (!strip) return null; // no time bar (e.g. headless) — plain exports still work + // PERIHELION-O event band — the ±2 yr computed window mapped to FULL width in a + // second strip below the timeline, so diamonds spread out instead of clustering + // into ~1.6% of the 250-yr strip (worse in deep time). A bracket on the main + // strip marks the window's true extent, tying the two together. + const band = document.createElement('div'); + band.id = 'event-band'; const marks = document.createElement('div'); marks.id = 'event-marks'; - strip.appendChild(marks); + band.appendChild(marks); + strip.parentNode.insertBefore(band, strip.nextSibling); // directly under timeline-outer + const bracket = document.createElement('div'); + bracket.id = 'event-window-bracket'; + strip.appendChild(bracket); const minMs = CONFIG.time.minMs, maxMs = CONFIG.time.maxMs, spanMs = maxMs - minMs; let on = true, events = []; @@ -183,9 +200,10 @@ export default function create(ctx) { function render() { marks.textContent = ''; + const winStartMs = unixMsFromJd(winStart), winEndMs = unixMsFromJd(winEnd); for (const e of events) { const ms = unixMsFromJd(e.jd); - const f = (ms - minMs) / spanMs; + const f = windowFraction(ms, winStartMs, winEndMs); // position WITHIN the band if (f < 0 || f > 1) continue; const d = document.createElement('div'); d.className = `ev-mark ev-${cssKind(e.type)}`; @@ -197,6 +215,12 @@ export default function create(ctx) { }); marks.appendChild(d); } + // bracket: where the ±2 yr window sits within the full CONFIG.time range (tiny + // in near mode, tinier in deep time — that's the point of the band below it). + const bl = ((winStartMs - minMs) / spanMs) * 100; + const bw = ((winEndMs - winStartMs) / spanMs) * 100; + bracket.style.left = `${Math.min(Math.max(0, bl), 100).toFixed(4)}%`; + bracket.style.width = `${Math.max(0.5, Math.min(bw, 100)).toFixed(4)}%`; } return { diff --git a/verify.html b/verify.html index 26801a3..f2c2233 100644 --- a/verify.html +++ b/verify.html @@ -286,6 +286,23 @@ finish(); } + /* PERIHELION-O — event band: the computed ±2 yr window maps to full band width. + windowFraction is pure; the click-jump-in-band is a browser real-input test. */ + { + const { windowFraction, findEvents } = await import('./js/layers/events.js'); + const okEnds = windowFraction(1000, 1000, 2000) === 0 + && windowFraction(2000, 1000, 2000) === 1 + && windowFraction(1500, 1000, 2000) === 0.5; + // a real event (Mars 2027 opposition) lands strictly inside a ±2 yr band. + const c = lib.jdFromUnixMs(Date.UTC(2026, 6, 16)); + const wS = lib.unixMsFromJd(c - 730), wE = lib.unixMsFromJd(c + 730); + const opp = findEvents(c - 730, c + 730).find((e) => e.bodies[0] === 'mars' && e.type === 'opposition'); + const fr = opp ? windowFraction(lib.unixMsFromJd(opp.jd), wS, wE) : NaN; + row('event band maps ±2 yr window to full width', okEnds && !!opp && fr > 0 && fr < 1, + `ends 0/0.5/1 ok=${okEnds} · Mars-opp band-fraction=${opp ? fr.toFixed(3) : 'n/a'}`); + finish(); + } + /* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates. Capture the "mine" positions with the extended range ON, then restore it OFF synchronously and atomically: ES modules are singletons, so setExtendedRange From bcb05f051ed68c39e2c42a147c2b8784684d3651 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Thu, 16 Jul 2026 13:24:38 +1000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8E=A8=20closer:=20colourise=20the=20?= =?UTF-8?q?grayscale=20moons=20=E2=80=94=20directorial=20hue=20tint=20(opu?= =?UTF-8?q?s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit USGS has no colour equirect for Europa/Callisto/Titan/Phobos/Charon, so those mosaics shipped grayscale. Multiply each by its characteristic hue at render (bodies.js `tint`; moons.js tint-preserving loader keeps the real surface detail, adds the iconic colour — Titan orange, Europa/Charon tan, Callisto/Phobos warm). Io/Ganymede/Triton stay true USGS colour (white multiplier). Flagged in CREDITS as a directorial (non-data) choice per Part ω.2. User-directed ("colourise the moons"). Co-Authored-By: Claude Opus 4.8 --- assets/textures/CREDITS.md | 7 +++++++ js/bodies.js | 10 +++++----- js/layers/moons.js | 12 +++++++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/assets/textures/CREDITS.md b/assets/textures/CREDITS.md index cdc2d30..72dcbc6 100644 --- a/assets/textures/CREDITS.md +++ b/assets/textures/CREDITS.md @@ -53,6 +53,13 @@ under budget). Downloaded 2026-07-16. Host base URL: `https://planetarymaps.usgs.gov/mosaic/` (served from the USGS `asc-pds-services` bucket). USGS terms: . +**Directorial colourisation (PERIHELION closer).** USGS has no colour equirect for +Europa, Callisto, Titan, Phobos and Charon, so those mosaics ship grayscale. The +app multiplies each by its characteristic hue at render (`bodies.js` `tint`: +Titan orange, Europa/Charon tan, Callisto/Phobos warm-brown) — the *real surface +detail is preserved*; only the colour is an explicit directorial choice, not data. +Io, Ganymede and Triton are USGS colour mosaics and render untinted. + **Truth boundary (brief §7b):** these are the *real data* textures — real worlds get real maps. The stylized grimoire portraits in `../art/grimoire/` are the *aesthetic* layer (generated art), kept visually and directorially separate. diff --git a/js/bodies.js b/js/bodies.js index 94593aa..3aed108 100644 --- a/js/bodies.js +++ b/js/bodies.js @@ -62,18 +62,18 @@ export const BODIES = { parent: 'earth', moonOrbit: { aKm: 384400, periodDays: 27.322 } }, phobos: { name: 'Phobos', type: 'moon', radiusKm: 11.27, color: '#8a7c6e', - texture: '1k_phobos.jpg', parent: 'mars', moonOrbit: { aKm: 9378, periodDays: 0.319 } }, + texture: '1k_phobos.jpg', tint: '#a08a76', parent: 'mars', moonOrbit: { aKm: 9378, periodDays: 0.319 } }, deimos: { name: 'Deimos', type: 'moon', radiusKm: 6.2, color: '#9a8c7a', texture: null, parent: 'mars', moonOrbit: { aKm: 23459, periodDays: 1.262 } }, io: { name: 'Io', type: 'moon', radiusKm: 1821.6, color: '#e8d24b', texture: '1k_io.jpg', parent: 'jupiter', moonOrbit: { aKm: 421700, periodDays: 1.769 } }, europa: { name: 'Europa', type: 'moon', radiusKm: 1560.8, color: '#c9b79c', - texture: '1k_europa.jpg', parent: 'jupiter', moonOrbit: { aKm: 671034, periodDays: 3.551 } }, + texture: '1k_europa.jpg', tint: '#e6dcc8', parent: 'jupiter', moonOrbit: { aKm: 671034, periodDays: 3.551 } }, ganymede: { name: 'Ganymede', type: 'moon', radiusKm: 2634.1, color: '#9b8e7e', texture: '1k_ganymede.jpg', parent: 'jupiter', moonOrbit: { aKm: 1070412, periodDays: 7.155 } }, callisto: { name: 'Callisto', type: 'moon', radiusKm: 2410.3, color: '#6b5f52', - texture: '1k_callisto.jpg', parent: 'jupiter', moonOrbit: { aKm: 1882709, periodDays: 16.689 } }, + texture: '1k_callisto.jpg', tint: '#9a8778', parent: 'jupiter', moonOrbit: { aKm: 1882709, periodDays: 16.689 } }, mimas: { name: 'Mimas', type: 'moon', radiusKm: 198.2, color: '#c8c8c8', texture: null, parent: 'saturn', moonOrbit: { aKm: 185539, periodDays: 0.942 } }, @@ -86,7 +86,7 @@ export const BODIES = { rhea: { name: 'Rhea', type: 'moon', radiusKm: 763.8, color: '#c4c4c4', texture: null, parent: 'saturn', moonOrbit: { aKm: 527068, periodDays: 4.518 } }, titan: { name: 'Titan', type: 'moon', radiusKm: 2574.7, color: '#d9a441', - texture: '1k_titan.jpg', parent: 'saturn', moonOrbit: { aKm: 1221870, periodDays: 15.945 } }, + texture: '1k_titan.jpg', tint: '#e0a63f', parent: 'saturn', moonOrbit: { aKm: 1221870, periodDays: 15.945 } }, iapetus: { name: 'Iapetus', type: 'moon', radiusKm: 734.5, color: '#8a7a5e', texture: null, parent: 'saturn', moonOrbit: { aKm: 3560851, periodDays: 79.33 } }, @@ -114,7 +114,7 @@ export const BODIES = { w: 113.76329, ma: 14.86012204, epoch: 2451545.0 }, }, charon: { name: 'Charon', type: 'moon', radiusKm: 606.0, color: '#b3a894', - texture: '1k_charon.jpg', parent: 'pluto', moonOrbit: { aKm: 19591, periodDays: 6.387 } }, + texture: '1k_charon.jpg', tint: '#c4b7a4', parent: 'pluto', moonOrbit: { aKm: 19591, periodDays: 6.387 } }, }; // Ordered planet ids (draw / menu order). diff --git a/js/layers/moons.js b/js/layers/moons.js index aab958b..55793e9 100644 --- a/js/layers/moons.js +++ b/js/layers/moons.js @@ -17,7 +17,17 @@ export default function create(ctx) { const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 16), mat); mesh.visible = true; scene.add(mesh); - loadTextureInto(mat, b.texture); // NASA/USGS map when present; flat color otherwise (§7 fallback) + if (b.tint) { + // Directorial colourisation: the USGS mosaics for these moons are grayscale + // (no color equirect exists), so multiply the real surface by the moon's + // characteristic hue — keeps true detail, adds the iconic colour. On a load + // error the flat b.color already set on the material stands (§7 fallback). + new THREE.TextureLoader().load(CONFIG.textures.path + b.texture, (tex) => { + tex.colorSpace = THREE.SRGBColorSpace; mat.map = tex; mat.color.set(b.tint); mat.needsUpdate = true; + }); + } else { + loadTextureInto(mat, b.texture); // NASA/USGS map when present; flat color otherwise (§7 fallback) + } registerPick(mesh, id); const lbl = makeLabel(mesh, b.name, 'body-label moon'); lbl.obj.visible = false; // CSS2DRenderer honors obj.visible (not div.style.display) From 1092e2f0f835a2cf5da59ea474b178c9df3bf84a Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Thu, 16 Jul 2026 13:26:14 +1000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=96=20closer:=20README=20refresh?= =?UTF-8?q?=20=E2=80=94=20wave=202=20state=20(opus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was wave-1: CDN importmap, "schematic ring" NEOs, 13 gates. Now: vendored offline Three.js, real NEO orbits, 22 gates, Epoch (deep-time) toggle, Sky-events layer + event band, textured/tidally-locked/colourised moons, decade-century rates. Updated intro, verify table, layer table, controls, architecture, credits. Co-Authored-By: Claude Opus 4.8 --- README.md | 77 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 02aba21..475ebc5 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ A standalone, browser-based 3D solar-system explorer — the interplanetary sibling of [GODSIGH](../GODSIGH), which watches Earth. Float above a compressed-scale ("MEGA") solar system with every world visible at once, moving on **real JPL -ephemerides**; scrub a time bar across centuries; fly the camera into orbit around -any planet, its moons circling; toggle **TRUE scale** and watch the system animate -out into its real, humbling emptiness. Live layers plot where Voyager 1, JWST and -Parker Solar Probe are *right now*, plus real asteroids, comets and Earth -close-approaches. Everything is client-side, free, and keyless. +ephemerides**; scrub a time bar across **six thousand years**; fly the camera into +orbit around any planet, its moons circling, textured and tidally locked; toggle +**TRUE scale** and watch the system animate out into its real, humbling emptiness. +Live layers plot where Voyager 1, JWST and Parker Solar Probe are *right now*, real +main-belt asteroids, comets, and near-Earth close-approaches on their **true +orbits** — and a computed almanac of oppositions and elongations on the time bar. +Everything is client-side, free, keyless, and runs **fully offline**. ![SOLARGOD](docs/hero.jpg) @@ -20,24 +22,29 @@ python3 serve.py # stdlib only — no npm, no build step `serve.py` statically serves the app and proxies the JPL feeds a browser can't call directly (Horizons / SBDB / CAD — no CORS), with a disk cache in `cache/`. Three.js -loads from a pinned CDN importmap. The deterministic core (Sun, planets, moons, -orbits, time travel) runs with **zero network calls** — positions are computed -locally from Keplerian elements; network layers are additive and fail soft. +is **vendored** under `vendor/three/`, so the app runs with no CDN. The +deterministic core (Sun, planets, moons, orbits, sky events, time travel) runs with +**zero network calls** — positions are computed locally from Keplerian elements; +network layers (spacecraft, asteroids, comets, close-approaches) are additive and +fail soft. ## Verify Open **http://127.0.0.1:8147/verify.html** — an in-browser gate runner that checks the ephemeris against **JPL Horizons ground truth** (hard-coded checkpoints + live -queries through the proxy). All 13 gates should read PASS, e.g.: +queries through the proxy). All **22 gates** should read PASS, e.g.: | Gate | Result | |------|--------| | Mars / Jupiter / Earth @ 2026-07-15 (heliocentric ecliptic AU) | Δ ≤ 3.3e-4 / 2.9e-3 / 3.9e-5 AU | -| Kepler solver converges at e=0.97 · orbitPath closes · frame round-trips | exact | -| MEGA→TRUE preserves direction (unit dot ≈ 1) | 1.000000000000 | +| Kepler @ e=0.97 · orbitPath closes · frame round-trips · MEGA→TRUE keeps direction | exact | | Mars @ J2000 · Jupiter @ 1900 vs **live** Horizons | Δ 4.8e-5 / 2.0e-3 AU | | Main belt between Mars & Jupiter · Halley aphelion beyond Neptune | pass | | Voyager 1 @ 2026-07-15 = (−32.07, −136.21, +98.55) AU · JWST hugs Earth | Δ 0.002 / 0.011 AU | +| **NEO** propagated miss vs CAD (real orbit) | ≤ 25% (median ~0.6%) | +| **Sky events** — Mars opposition ±2 d of 2027-02-19 · Venus elongation cadence · nextEvent | pass | +| **Deep time** — Jupiter @ 1000 BC · Mercury @ 2500 vs live Horizons (Table 2a/2b) | Δ 0.004 / 3.3e-5 AU | +| **Event band** maps the ±2 yr window to full width | pass | ## Layers @@ -45,54 +52,60 @@ queries through the proxy). All 13 gates should read PASS, e.g.: |-------|--------|:------:|-------| | **Planets & orbits** | JPL SSD approximate elements (Standish) | on | 8 planets + Pluto, textured, MEGA-compressed orbit paths | | **Body labels** | — | on | names; the focused body's children also get distance labels | -| **Moons** | JPL satellite data | on | 21 moons, per-parent local compression, retrograde Triton | +| **Moons** | NASA/USGS mosaics | on | 21 moons, per-parent compression, retrograde Triton; 8 with real surfaces, **tidally locked**; grayscale mosaics directorially tinted (Titan orange, Europa tan…) | | **Rings** | true km, scaled to parent | on | Saturn (textured annulus) · Uranus (schematic) | | **Spacecraft** | JPL **Horizons** (proxy) | on | Voyager 1/2, New Horizons, JWST, Parker, Juno, Lucy, Psyche — live distance + one-way light-time | | **Main belt** | JPL **SBDB** (proxy) | off | ~1500 main-belt asteroids, shared Kepler core | | **Comets** | JPL **SBDB** (proxy) | on | 1P/Halley, 2P/Encke, 67P, 96P — full orbits + anti-sunward 1/r² tail | -| **Close approaches** | JPL **CAD** (proxy) | off | ±30-day NEO radar around Earth, miss distance in lunar distances | +| **Close approaches** | JPL **CAD + SBDB** (proxy) | off | ±30-day NEOs at their **true propagated positions** with real orbits (per-object fail-soft to a schematic ring); miss distance in lunar distances | +| **Sky events** | computed (no network) | on | oppositions, conjunctions & greatest elongations as diamonds on the timeline's **event band**; click one to jump the clock | ## Controls - **Focus** — click any body (or use the Focus menu) → the camera flies into orbit around it; breadcrumb `SOL ▸ JUPITER ▸ EUROPA`; `Esc` steps out one level. - **MEGA / TRUE** — toggle the scale mode; the transition animates over ~2.5 s. +- **Epoch** — `1800–2050` / `3000 BC – 3000 AD`: switch to the extended + Table 2a/2b range (reloads the session, carried in the hash as `&e=1`). - **Body scale** — 1× / 100× / 1200× radius exaggeration. -- **Time bar** — NOW, play/pause, reverse, rate presets (`1× · 1 min/s … 1 yr/s`), - and a scrubbable 1800–2050 timeline. A **LIVE / SCRUBBED** chip mirrors GODSIGH. +- **Time bar** — NOW, play/pause, reverse, rate presets (`1× · 1 min/s … 1 yr/s`, + plus `1 decade/s · 1 century/s` for deep time), a scrubbable timeline, and an + **event band** below it mapping the computed ±2 yr window to full width (a bracket + on the main strip marks its extent). A **LIVE / SCRUBBED** chip mirrors GODSIGH. - **Info panel** — appears on focus: a grimoire card with a physical fact sheet, hand-written prose, a stylized portrait, and **live computed** facts (distance - from Sun & Earth, sunlight age, orbital speed, day & year length). Focusing Earth - reveals a link to the GODSIGH world view. + from Sun & Earth, sunlight age, orbital speed, day & year length, and the next + opposition/elongation). Focusing Earth reveals a link to the GODSIGH world view. - **Shareable URLs** — the view serializes to the hash - (`#f=&t=