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>
94 lines
3.8 KiB
JavaScript
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);
|
|
}
|
|
},
|
|
};
|
|
}
|