USGS has no colour equirect for Europa/Callisto/Titan/Phobos/Charon, so those
mosaics shipped grayscale. Multiply each by its characteristic hue at render
(bodies.js `tint`; moons.js tint-preserving loader keeps the real surface detail,
adds the iconic colour — Titan orange, Europa/Charon tan, Callisto/Phobos warm).
Io/Ganymede/Triton stay true USGS colour (white multiplier). Flagged in CREDITS as
a directorial (non-data) choice per Part ω.2. User-directed ("colourise the moons").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.8 KiB
JavaScript
60 lines
2.8 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);
|
|
if (b.tint) {
|
|
// Directorial colourisation: the USGS mosaics for these moons are grayscale
|
|
// (no color equirect exists), so multiply the real surface by the moon's
|
|
// characteristic hue — keeps true detail, adds the iconic colour. On a load
|
|
// error the flat b.color already set on the material stands (§7 fallback).
|
|
new THREE.TextureLoader().load(CONFIG.textures.path + b.texture, (tex) => {
|
|
tex.colorSpace = THREE.SRGBColorSpace; mat.map = tex; mat.color.set(b.tint); mat.needsUpdate = true;
|
|
});
|
|
} else {
|
|
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);
|
|
}
|
|
},
|
|
};
|
|
}
|