🌘 closer: event band — the ±2 yr window gets its own full-width strip (opus)

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 <noreply@anthropic.com>
This commit is contained in:
monsterrobotparty 2026-07-16 13:17:37 +10:00
parent dec2b89de6
commit bdba119484
3 changed files with 60 additions and 2 deletions

View File

@ -202,3 +202,20 @@ html, body {
.ev-elong { top: -8px; background: #7fc7d6; box-shadow: 0 0 5px rgba(127, 199, 214, 0.6); } .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 { top: 2px; width: 5px; height: 5px; background: var(--brass-dim); opacity: 0.6; }
.ev-conj:hover { opacity: 1; } .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);
}

View File

@ -140,6 +140,13 @@ export function nextEvent(bodyId, jd) {
return null; 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 ---- // ---- default factory: timeline diamonds + HUD row ----
const WINDOW_DAYS = 730; // ±2 years, computed lazily around the current sim date 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'; } 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'); const strip = document.getElementById('timeline-outer');
if (!strip) return null; // no time bar (e.g. headless) — plain exports still work 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'); const marks = document.createElement('div');
marks.id = 'event-marks'; 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; const minMs = CONFIG.time.minMs, maxMs = CONFIG.time.maxMs, spanMs = maxMs - minMs;
let on = true, events = []; let on = true, events = [];
@ -183,9 +200,10 @@ export default function create(ctx) {
function render() { function render() {
marks.textContent = ''; marks.textContent = '';
const winStartMs = unixMsFromJd(winStart), winEndMs = unixMsFromJd(winEnd);
for (const e of events) { for (const e of events) {
const ms = unixMsFromJd(e.jd); 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; if (f < 0 || f > 1) continue;
const d = document.createElement('div'); const d = document.createElement('div');
d.className = `ev-mark ev-${cssKind(e.type)}`; d.className = `ev-mark ev-${cssKind(e.type)}`;
@ -197,6 +215,12 @@ export default function create(ctx) {
}); });
marks.appendChild(d); 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 { return {

View File

@ -286,6 +286,23 @@
finish(); 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. /* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates.
Capture the "mine" positions with the extended range ON, then restore it OFF Capture the "mine" positions with the extended range ON, then restore it OFF
synchronously and atomically: ES modules are singletons, so setExtendedRange synchronously and atomically: ES modules are singletons, so setExtendedRange