solargod/js/layers/almanac.js

152 lines
9.1 KiB
JavaScript
Raw 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.

// SOLARGOD almanac (Stage 6) — the info panel that knows. On focus it shows a
// grimoire card: a physical fact sheet + hand-written prose + (if generated) a
// MODELBEAST portrait, plus LIVE computed facts that come free from the ephemeris
// — distance from Sun and Earth, sunlight age (light-time), orbital speed, and the
// length of the body's day and year (brief §5). Focusing Earth reveals the GODSIGH
// easter egg. Not a toggleable feed — no HUD row.
//
// Wave 2 (SYZYGY): planets also get a predictive fact — Next opposition (outer) /
// Next elongation (inner) — computed from the same ephemeris via events.js.
import { nextEvent, isOuter, isInner } from './events.js';
const GM_SUN = 1.32712440018e11; // km^3/s^2, heliocentric gravitational parameter
const YEAR_DAYS = 365.256;
const BLURBS = {
sun: "The hearth of the system and 99.86% of its mass — a self-gravitating furnace fusing 600 million tonnes of hydrogen every second. Every world here is a fleck of its leftover disk, still falling around it.",
mercury: "A cratered iron cinder with almost no air, swinging so close to the Sun that it laps every other world. Its days are longer than its years; three rotations fit in two of its tight orbits.",
venus: "Earth's twin gone wrong: a runaway greenhouse under sulphuric cloud, hot enough to melt lead and crushing as the deep sea. It turns backwards, so slowly that a Venusian day outlasts its year.",
earth: "The one world we know that woke up. A thin film of water and air over spinning iron, tilted just so for seasons — the pale blue vantage from which all the rest of this was measured.",
mars: "A rusted desert half Earth's size, streaked with the dry beds of ancient rivers and crowned by Olympus Mons, the tallest volcano known. Cold, thin-aired, and the most-visited graveyard of robots off-world.",
jupiter: "A failed star and the system's great vacuum cleaner, its storms wider than Earth and its gravity shepherding the asteroid belt. Four bright moons circle it in a clockwork Galileo used to unseat the Earth from the centre.",
saturn: "The jewel — a globe you could float on water, wrapped in rings of ice no thicker than a mountain is tall. Titan below hides a haze-orange world of methane rain and hydrocarbon seas.",
uranus: "An ice giant knocked onto its side, likely by an ancient impact, so it rolls around the Sun pole-first. Its faint rings and pale cyan haze mark the coldest atmosphere of any planet.",
neptune: "The last great world, a deep-blue storm-lantern found by mathematics before any eye saw it. Its winds are the fastest in the system, and Triton, its captured moon, orbits backwards toward a slow doom.",
pluto: "The heart-marked dwarf at the edge of the known, half rock and half ice, locked in a slow gravitational dance with its outsized moon Charon. Demoted from planet, it remains the king of the Kuiper Belt.",
moon: "Earth's single great moon, born of a Mars-sized collision and slowly drifting away. It keeps one face turned to us forever, and its pull raises the tides that may have coaxed life ashore.",
io: "The most volcanic body in the system, flexed and heated by Jupiter's tides until it turns itself inside out in sulphur.",
europa: "A cracked shell of ice over a global ocean — perhaps twice Earth's water — and one of the best places to look for life beyond home.",
ganymede: "The largest moon in the system, bigger than Mercury, and the only one with its own magnetic field.",
callisto: "The most heavily cratered world known, a dark ancient face that has barely changed in four billion years.",
titan: "The only moon with a thick atmosphere, raining methane onto seas of liquid hydrocarbon under an orange sky.",
enceladus: "A tiny snowball that jets plumes of ocean water into space from cracks at its south pole.",
triton: "A captured Kuiper Belt world orbiting Neptune backwards, spouting nitrogen geysers across a frozen pink surface.",
charon: "Pluto's giant partner — the two are tidally locked face to face, forever hanging still in each other's sky.",
};
export default function create(ctx) {
const { bodies, ephem, lib, focus } = ctx;
const panel = document.getElementById('infopanel');
let lastFocus = null;
let lastWall = 0;
const _b = {}, _e = {};
// Memo for the predictive fact so we don't rescan the ephemeris every 200 ms —
// recompute only on focus change, when the event passes, or a backward scrub.
let neCache = { id: null, fromJd: 0, ev: undefined };
function helioOf(id, jd, out) {
const b = bodies[id];
if (id === 'sun') { out.x = out.y = out.z = 0; return out; }
if (b.ephemId) return ephem.helioEcl(b.ephemId, jd, out);
if (b.elements) return ephem.smallBodyEcl(b.elements, jd, out);
if (b.moonOrbit && b.parent) {
// parent heliocentric + the moon's TRUE orbital offset (AU) — so e.g. the
// Moon's distance from Earth is its real ~384,400 km, not 0.
helioOf(b.parent, jd, out);
const theta = 2 * Math.PI * ((jd - lib.J2000_JD) / b.moonOrbit.periodDays);
const rAU = b.moonOrbit.aKm / lib.AU_KM;
const tilt = (bodies[b.parent].axialTiltDeg || 0) * lib.DEG;
const ex = Math.cos(theta) * rAU, ey = Math.sin(theta) * rAU;
out.x += ex; out.y += ey * Math.cos(tilt); out.z += ey * Math.sin(tilt);
return out;
}
if (b.parent) return helioOf(b.parent, jd, out);
out.x = out.y = out.z = 0; return out;
}
function render(id) {
const b = bodies[id];
const artPath = `assets/art/grimoire/${id}.jpg`;
const earthEgg = id === 'earth'
? `<a class="ip-link" href="http://127.0.0.1:8137" target="_blank" rel="noopener">▸ open GODSIGH world view</a>` : '';
panel.innerHTML = `
<div class="ip-close" role="button" aria-label="close">×</div>
<div class="ip-title"></div>
<div class="ip-type"></div>
<img class="ip-art" alt="" />
<div class="ip-blurb"></div>
<div class="ip-facts"></div>
${earthEgg}`;
panel.querySelector('.ip-title').textContent = b.name;
panel.querySelector('.ip-type').textContent = b.type;
panel.querySelector('.ip-blurb').textContent = BLURBS[id] || `A ${b.type}${b.parent ? ' of ' + bodies[b.parent].name : ''} in the solar system.`;
const img = panel.querySelector('.ip-art');
img.src = artPath;
img.onerror = () => { img.style.display = 'none'; };
panel.querySelector('.ip-close').addEventListener('click', () => { panel.hidden = true; });
panel.hidden = false;
}
function fact(label, value) {
return `<div class="ip-fact"><span>${label}</span><span>${value}</span></div>`;
}
function updateLive(id, jd) {
const b = bodies[id];
helioOf(id, jd, _b);
const rSun = Math.hypot(_b.x, _b.y, _b.z); // AU
ephem.helioEcl('earth', jd, _e);
const rEarth = id === 'earth' ? 0 : Math.hypot(_b.x - _e.x, _b.y - _e.y, _b.z - _e.z);
const rows = [];
// physical
rows.push(fact('Radius', lib.fmtKm(b.radiusKm)));
if (b.axialTiltDeg != null) rows.push(fact('Axial tilt', `${b.axialTiltDeg.toFixed(1)}°`));
// live distances
if (id !== 'sun') rows.push(fact('From Sun', `${lib.fmtAU(rSun)} · ${lib.fmtLightTime(rSun)} light`));
rows.push(fact('From Earth', id === 'earth' ? '—' : `${lib.fmtAU(rEarth)} · ${lib.fmtLightTime(rEarth)} light`));
// orbital speed + periods
if (b.type === 'moon') {
const per = Math.abs(b.moonOrbit.periodDays);
const v = (2 * Math.PI * b.moonOrbit.aKm) / (per * 86400);
rows.push(fact('Orbits', bodies[b.parent].name));
rows.push(fact('Orbital speed', lib.fmtSpeed(v)));
rows.push(fact('Orbital period', `${lib.fmtDuration(per)}${b.moonOrbit.periodDays < 0 ? ' (retrograde)' : ''}`));
} else if (id !== 'sun') {
const a = b.elements ? b.elements.a : ephem.semiMajor(b.ephemId, jd);
const rKm = rSun * lib.AU_KM, aKm = a * lib.AU_KM;
const v = Math.sqrt(GM_SUN * (2 / rKm - 1 / aKm)); // vis-viva
rows.push(fact('Orbital speed', lib.fmtSpeed(v)));
rows.push(fact('Year', lib.fmtDuration(YEAR_DAYS * Math.pow(a, 1.5))));
}
if (b.rotationHours) {
rows.push(fact('Day (rotation)', `${lib.fmtDuration(Math.abs(b.rotationHours) / 24)}${b.rotationHours < 0 ? ' (retrograde)' : ''}`));
}
// predictive sky event (SYZYGY): outer → Next opposition, inner → Next elongation.
if (isOuter(id) || isInner(id)) {
if (neCache.id !== id || neCache.ev === undefined || (neCache.ev && jd > neCache.ev.jd) || jd < neCache.fromJd) {
neCache = { id, fromJd: jd, ev: nextEvent(id, jd) };
}
const ev = neCache.ev;
if (ev) {
rows.push(fact(isOuter(id) ? 'Next opposition' : 'Next elongation',
`${lib.formatUTCDate(lib.unixMsFromJd(ev.jd))} · in ${lib.fmtDuration(ev.jd - jd)}`));
}
}
const facts = panel.querySelector('.ip-facts');
if (facts) facts.innerHTML = rows.join('');
}
return {
id: 'almanac',
onClockTick(simMs, jd) {
if (focus.id !== lastFocus) { lastFocus = focus.id; render(focus.id); lastWall = 0; }
if (panel.hidden) return;
const now = performance.now();
if (now - lastWall < 200) return; // ≤5 Hz
lastWall = now;
updateLive(focus.id, jd);
},
};
}