solargod/js/layers/almanac.js
monsterrobotparty 1f9664a6f9 📖 almanac: knowing — info panel, live facts, grimoire art, real textures, README (opus)
Stage 6. almanac.js: on-focus grimoire card — physical fact sheet, hand-written
prose (grimoire voice), MODELBEAST portrait, and LIVE computed facts free from the
ephemeris: distance from Sun & Earth, sunlight age, vis-viva orbital speed, day &
year length. Earth easter egg → GODSIGH world view (:8137). Real Solar System Scope
2k textures (CC BY, 6.1 MB) now paint every world + the Milky Way skybox; ephem
gains semiMajor() for vis-viva. Generated grimoire portraits (FLUX via MODELBEAST,
10 bodies, seed logged in art/PROMPTS.md) are the AESTHETIC layer only — real
worlds get real maps (truth boundary). README with layer table + quickstart +
verify; docs/hero.jpg. Also lands the Stage-3 planets label fix (obj.visible).
Gate: every clickable body has a card; quickstart works from a fresh clone.

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

122 lines
7.6 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.
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 = {};
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.parent) return helioOf(b.parent, jd, out); // moon ≈ parent at AU scale
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)' : ''}`));
}
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);
},
};
}