solargod/js/layers/moons.js
monsterrobotparty be5f8ad544 🌖 selene: real NASA/USGS maps + tidal lock for the major moons (opus)
- source 8 public-domain equirectangular mosaics (io/europa/ganymede/callisto/
  titan/triton/charon/phobos) from usgs astrogeology, downscale to 1024x512 jpg
  (0.92 mb added total, each <=143 kb); credits.md lists source/mission/license
- bodies.js: texture field on the 8 sourced moon entries only
- moons.js: load textures via ctx.loadTextureInto (same path as planets.js, so
  the moon's own 2k map also renders now); flat-color 404 fallback preserved
- moons.js tick: tidal lock rotation.y = 2pi*(jd-J2000)/period + pi (signed
  period gives retrograde spin for free)
2026-07-16 09:55:07 +10:00

50 lines
2.2 KiB
JavaScript

// SOLARGOD moons layer (Stage 3) — every moon in the registry as a lit sphere,
// positioned by ctx.bodyWorld (main.js computes the local compressed offset in the
// parent's equatorial plane; retrograde via negative period). Moons draw at the
// SAME exaggeration as their parent, so the true radius ratio (e.g. Io/Jupiter
// ≈ 0.026) is preserved. Labels appear only when the parent (or the moon) is the
// focus, to keep the wide view clean (brief §5).
export default function create(ctx) {
const { THREE, scene, CONFIG, lib, ui, scale, bodies, focus, toLocal, effectiveE,
makeLabel, registerPick, loadTextureInto } = ctx;
const entries = [];
for (const id in bodies) {
const b = bodies[id];
if (b.type !== 'moon') continue;
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(b.color), roughness: 1, metalness: 0 });
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)
registerPick(mesh, id);
const lbl = makeLabel(mesh, b.name, 'body-label moon');
lbl.obj.visible = false; // CSS2DRenderer honors obj.visible (not div.style.display)
entries.push({ id, b, mesh, lbl });
}
let show = true;
ui.addLayer('moons', 'Moons', true, (on) => { show = on; for (const e of entries) e.mesh.visible = on; });
ui.setStatus('moons', `${entries.length} moons`, 'ok');
return {
id: 'moons',
onClockTick(simMs, jd) {
for (const e of entries) {
const abs = ctx.bodyWorld[e.id];
if (!abs) continue;
toLocal(abs, e.mesh.position);
e.mesh.scale.setScalar(scale.drawRadius(e.b.radiusKm, effectiveE(e.b.parent)));
// tidal lock: spin at the SAME signed orbital rate main.js positions by, so
// one face stays toward the parent (negative period → retrograde for free).
const theta = 2 * Math.PI * ((jd - lib.J2000_JD) / e.b.moonOrbit.periodDays);
e.mesh.rotation.y = theta + Math.PI;
const near = focus.id === e.b.parent || focus.id === e.id;
e.lbl.obj.visible = show && near;
e.lbl.div.classList.toggle('focused', e.id === focus.id);
}
},
};
}