solargod/js/layers/planets.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

94 lines
3.8 KiB
JavaScript

// SOLARGOD planets layer (Stage 1) — the eight planets + Pluto as textured,
// exaggerated spheres on compressed orbit paths, with labels. Positions come from
// ctx.bodyWorld (computed centrally in main.js); this layer only renders. Orbit
// geometry is rebuilt only when the MEGA/TRUE exponent is animating or elements
// drift past CONFIG.orbitRebuildCy (brief §11).
export default function create(ctx) {
const { THREE, scene, CONFIG, lib, ui, scale, ephem, bodies, worldGroup,
clock, focus, toLocal, effectiveE, makeLabel, registerPick, loadTextureInto, PLANET_ORDER } = ctx;
const ROSTER = [...PLANET_ORDER, 'pluto'];
const orbitMat = new THREE.LineBasicMaterial({ color: CONFIG.colors.orbit, transparent: true, opacity: 0.6 });
const N = CONFIG.orbitSamples;
const entries = [];
const tmp = new THREE.Vector3();
const _v = {};
for (const id of ROSTER) {
const b = bodies[id];
// tilt group holds the spinning sphere so axial tilt + rotation compose.
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(b.color), roughness: 1, metalness: 0 });
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 48, 32), mat);
group.rotation.z = (b.axialTiltDeg || 0) * lib.DEG;
group.add(mesh);
scene.add(group);
loadTextureInto(mat, b.texture);
registerPick(mesh, id);
const lbl = makeLabel(group, b.name, 'body-label');
// orbit path (absolute view-world, lives under worldGroup)
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array((N + 1) * 3), 3));
const line = new THREE.LineLoop(geo, orbitMat);
line.frustumCulled = false;
worldGroup.add(line);
const e = { id, b, group, mesh, mat, geo, line, lbl, auPath: null, lastT: NaN };
buildAuPath(e);
fillView(e);
entries.push(e);
}
function buildAuPath(e) {
e.auPath = e.b.ephemId
? ephem.orbitPath(e.b.ephemId, clock.jd, N)
: ephem.orbitPathFromElements(e.b.elements, N);
e.lastT = lib.centuriesSinceJ2000(clock.jd);
}
function fillView(e) {
const pos = e.geo.attributes.position.array;
const au = e.auPath;
for (let k = 0; k <= N; k++) {
_v.x = au[k * 3]; _v.y = au[k * 3 + 1]; _v.z = au[k * 3 + 2];
scale.viewFromEcl(_v, _v);
pos[k * 3] = _v.x; pos[k * 3 + 1] = _v.y; pos[k * 3 + 2] = _v.z;
}
e.geo.attributes.position.needsUpdate = true;
e.geo.computeBoundingSphere();
}
let showOrbits = true, showLabels = true;
ui.addLayer('orbits', 'Orbit paths', true, (on) => { showOrbits = on; for (const e of entries) e.line.visible = on; });
ui.addLayer('labels', 'Body labels', true, (on) => { showLabels = on; for (const e of entries) e.lbl.obj.visible = on; });
ui.setStatus('orbits', `${entries.length} orbits`, 'ok');
ui.setStatus('labels', 'names shown', 'ok');
return {
id: 'planets',
onClockTick(simMs, jd, isLive) {
const transitioning = scale.isTransitioning();
for (const e of entries) {
const abs = ctx.bodyWorld[e.id];
if (!abs) continue;
// position (floating-origin JS subtraction) + exaggerated radius
toLocal(abs, e.group.position);
const r = scale.drawRadius(e.b.radiusKm, effectiveE(e.id));
e.mesh.scale.setScalar(r);
// spin: fraction of a rotation since J2000 (retrograde via sign)
if (e.b.rotationHours) {
const rot = ((jd - lib.J2000_JD) * 24) / e.b.rotationHours;
e.mesh.rotation.y = rot * 2 * Math.PI;
}
// focus highlight
e.lbl.div.classList.toggle('focused', e.id === focus.id);
// orbit rebuild only when needed
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - e.lastT) > CONFIG.orbitRebuildCy;
if (needAu) buildAuPath(e);
if (needAu || transitioning) fillView(e);
}
},
};
}