feat: Solar System — orbit planets, live orrery, info cards + mission trajectories
Orbit-lock: traveling to a planet re-anchors drag/scroll to orbit THAT body (lookAtTransform), not the whole system. Cleared on the next flight / exit. Animated orrery: planets sweep their orbits at correct relative speeds in the overview (simDay advances in real time). Since a moving textured ellipsoid loses its image material, each planet is split into an animated dot + a frozen textured sphere (revealed + snapshotted forward on travel). New 'System overview' button. Camera flights: enter/travel/overview run a manual eased setView tween (Cesium's flyTo is dead in this mode). Infographic cards: ssdata.js FACTS — a stat card + fun fact per body (not Earth), auto-shown on travel and mission-select. Space missions: ssdata.js MISSIONS — 11 real probes with date-accurate launch/ flyby/arrival waypoints, drawn as schematic heliocentric transfer arcs (multi-leg grand tours included). Toggle + list; select highlights the path + shows a card. Chrome: hides the OSINT clock/timeline/live-chip in SS mode; Travel-to panel is now collapsible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e9b687d983
commit
92c6e0ebc3
@ -290,7 +290,17 @@ html, body {
|
||||
font: 12px ui-monospace, Menlo, monospace;
|
||||
color: var(--text);
|
||||
}
|
||||
#ss-panel .ss-title { font-weight: 700; letter-spacing: 1px; color: var(--accent); font-size: 11px; margin-bottom: 10px; }
|
||||
#ss-panel .ss-title {
|
||||
font-weight: 700; letter-spacing: 1px; color: var(--accent); font-size: 11px;
|
||||
margin-bottom: 10px; cursor: pointer; user-select: none;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
#ss-panel .ss-collapse { font-size: 9px; transition: transform 0.15s; }
|
||||
#ss-panel.collapsed .ss-collapse { transform: rotate(-90deg); }
|
||||
#ss-panel.collapsed { width: auto; }
|
||||
#ss-panel.collapsed .ss-title { margin-bottom: 0; }
|
||||
#ss-panel.collapsed .ss-body { display: none; }
|
||||
#ss-panel .ss-overview { color: var(--accent); border-color: rgba(53,224,255,0.3); margin-bottom: 8px; }
|
||||
#ss-panel .ss-dests { display: flex; flex-direction: column; gap: 4px; }
|
||||
#ss-panel .ss-dest {
|
||||
text-align: left;
|
||||
@ -307,3 +317,64 @@ html, body {
|
||||
#ss-panel .ss-back { margin-top: 8px; color: var(--dim); }
|
||||
#ss-panel .ss-foot { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); color: var(--dim); font-size: 9.5px; line-height: 1.4; }
|
||||
#ss-panel .ss-foot a { color: var(--dim); }
|
||||
|
||||
/* Solar System mode hides the Earth/OSINT clock + timeline chrome for a clean view. */
|
||||
body.ss-active .cesium-viewer-timelineContainer,
|
||||
body.ss-active .cesium-viewer-animationContainer,
|
||||
body.ss-active #live-chip { display: none !important; }
|
||||
|
||||
/* ---- Space missions list (in the Travel-to panel) ---- */
|
||||
#ss-panel .ss-missions-toggle { margin-top: 8px; }
|
||||
#ss-panel .ss-missions-toggle.active { color: #04121a; background: var(--accent); border-color: var(--accent); font-weight: 700; }
|
||||
#ss-panel .ss-missions-hint { color: var(--dim); font-size: 9px; margin: 6px 2px 4px; }
|
||||
#ss-panel .ss-missions { display: flex; flex-direction: column; gap: 2px; max-height: 220px; overflow-y: auto; }
|
||||
#ss-panel .ss-mission {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
padding: 5px 8px; font: inherit; font-size: 11px; text-align: left;
|
||||
color: var(--text); background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid transparent; border-radius: 6px; cursor: pointer; transition: all 0.12s;
|
||||
}
|
||||
#ss-panel .ss-mission:hover { background: rgba(255, 255, 255, 0.06); }
|
||||
#ss-panel .ss-mission.active { background: rgba(53, 224, 255, 0.12); border-color: var(--accent); }
|
||||
#ss-panel .ss-mission-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; box-shadow: 0 0 5px currentColor; }
|
||||
#ss-panel .ss-mission-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#ss-panel .ss-mission-tgt { color: var(--dim); font-size: 12px; }
|
||||
|
||||
/* ---- Infographic card (planet facts + mission info) ---- */
|
||||
#ss-card {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 268px;
|
||||
max-height: calc(100vh - 28px);
|
||||
overflow-y: auto;
|
||||
z-index: 13;
|
||||
padding: 16px;
|
||||
background: var(--panel-solid);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
font: 12px ui-monospace, Menlo, monospace;
|
||||
color: var(--text);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
/* Sit below the Solar System button, which shares the top-right corner. */
|
||||
body.ss-active #ss-card { top: 62px; }
|
||||
#ss-card .ss-card-x {
|
||||
position: absolute; top: 10px; right: 12px; width: 20px; height: 20px;
|
||||
padding: 0; font: 16px/1 ui-monospace, monospace; color: var(--dim);
|
||||
background: none; border: none; cursor: pointer;
|
||||
}
|
||||
#ss-card .ss-card-x:hover { color: var(--text); }
|
||||
#ss-card .ss-card-title { font-size: 16px; font-weight: 700; color: #fff; margin: 0 20px 8px 0; letter-spacing: 0.5px; }
|
||||
#ss-card .ss-card-blurb { color: var(--text); font-size: 11px; line-height: 1.5; margin-bottom: 12px; }
|
||||
#ss-card .ss-card-stats { display: flex; flex-direction: column; gap: 3px; margin-bottom: 12px; }
|
||||
#ss-card .ss-card-row { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
#ss-card .ss-card-row span:first-child { color: var(--dim); }
|
||||
#ss-card .ss-card-row span:last-child { color: var(--text); text-align: right; }
|
||||
#ss-card .ss-card-fact {
|
||||
font-size: 10.5px; line-height: 1.45; color: var(--accent);
|
||||
background: rgba(53, 224, 255, 0.08); border-left: 2px solid var(--accent);
|
||||
padding: 8px 10px; border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
// Cesium's globe is Earth-at-origin, so the mode HIDES the globe + all Earth
|
||||
// layers and places the Sun at the scene origin; exiting restores everything.
|
||||
|
||||
import { FACTS, MISSIONS } from './ssdata.js';
|
||||
|
||||
export default function initSolarSystem(ctx) {
|
||||
const { viewer, Cesium, lib } = ctx;
|
||||
const scene = viewer.scene;
|
||||
@ -73,13 +75,55 @@ export default function initSolarSystem(ctx) {
|
||||
const ds = new Cesium.CustomDataSource('solarsystem');
|
||||
ds.show = false;
|
||||
viewer.dataSources.add(ds);
|
||||
const bodyEntities = {}; // key → entity (for travel-to framing)
|
||||
const bodyEntities = {}; // key → primary entity (sun billboard; planets' sphere)
|
||||
const sphereEntities = {}; // key → textured ellipsoid (frozen position, visible up close)
|
||||
const dotEntities = {}; // key → colored dot + label (animates along the orbit)
|
||||
const ringEntities = {}; // key → Saturn ring plane
|
||||
|
||||
// Orbital time-lapse: `simDay` is the heliocentric day number driving the dots.
|
||||
// It advances in real time while `playing` (overview only) so the planets sweep
|
||||
// their orbits at correct RELATIVE speeds (Mercury fast, Neptune a crawl). The
|
||||
// textured spheres stay FROZEN — a moving ellipsoid goes onto Cesium's dynamic
|
||||
// path and loses its image material (blank white), so only the dots move; the
|
||||
// sphere is snapshotted to the current position the moment you travel to it.
|
||||
let simDay = 0, playing = false, animTick = null, lastAnimMs = 0;
|
||||
const DAYS_PER_SEC = 8;
|
||||
|
||||
function posOf(key, d) {
|
||||
if (key === 'sun') return new Cesium.Cartesian3(0, 0, 0);
|
||||
if (key === 'earth') return scaledPos(earthHelio(d));
|
||||
return scaledPos(helio(key.charAt(0).toUpperCase() + key.slice(1), d));
|
||||
}
|
||||
// Raw heliocentric AU vector (before the display compression) — for mission arcs.
|
||||
function helioAU(body, d) {
|
||||
if (body === 'sun') return { x: 0, y: 0, z: 0 };
|
||||
if (body === 'earth') return earthHelio(d);
|
||||
return helio(body.charAt(0).toUpperCase() + body.slice(1), d);
|
||||
}
|
||||
const dayFromISO = (iso) => new Date(iso + 'T00:00:00Z').getTime() / 86400000 - 10956.0;
|
||||
|
||||
// Schematic transfer arc for a mission: for each leg, connect the two real
|
||||
// heliocentric endpoints (body positions at their dated waypoints) with a smooth
|
||||
// prograde curve — angle swept the short way round the Sun, radius eased between
|
||||
// the two orbital distances. Not a true Kepler arc, but date-accurate at every
|
||||
// waypoint and it reads clearly as an interplanetary transfer.
|
||||
function missionArc(mission) {
|
||||
const pts = [];
|
||||
for (let leg = 0; leg < mission.waypoints.length - 1; leg++) {
|
||||
const a = mission.waypoints[leg], b = mission.waypoints[leg + 1];
|
||||
const pa = helioAU(a.body, dayFromISO(a.date)), pb = helioAU(b.body, dayFromISO(b.date));
|
||||
const r1 = Math.hypot(pa.x, pa.y), th1 = Math.atan2(pa.y, pa.x), z1 = pa.z;
|
||||
const r2 = Math.hypot(pb.x, pb.y), th2 = Math.atan2(pb.y, pb.x), z2 = pb.z;
|
||||
let dth = (th2 - th1) % (2 * Math.PI); if (dth < 0) dth += 2 * Math.PI; // prograde CCW
|
||||
const N = 60;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i / N, s = t * t * (3 - 2 * t);
|
||||
const rr = r1 + (r2 - r1) * s, th = th1 + dth * t, zz = z1 + (z2 - z1) * s;
|
||||
pts.push(scaledPos({ x: rr * Math.cos(th), y: rr * Math.sin(th), z: zz }));
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// Orthographic planet disc from an equirectangular texture, drawn onto a canvas
|
||||
// used as a BILLBOARD image. Billboards render instantly (no async worker
|
||||
@ -135,8 +179,8 @@ export default function initSolarSystem(ctx) {
|
||||
|
||||
function build(d) {
|
||||
ds.entities.removeAll();
|
||||
simDay = d;
|
||||
for (const b of BODIES) {
|
||||
const pos = posOf(b.key, d);
|
||||
const r = bodyRadius(b.km, b.sun);
|
||||
|
||||
// The Sun is a star: keep it a bright, always-lit billboard disc (a lit
|
||||
@ -144,7 +188,7 @@ export default function initSolarSystem(ctx) {
|
||||
if (b.sun) {
|
||||
const ent = ds.entities.add({
|
||||
name: b.name,
|
||||
position: pos,
|
||||
position: new Cesium.Cartesian3(0, 0, 0),
|
||||
billboard: {
|
||||
image: makeDisc(b),
|
||||
scaleByDistance: new Cesium.NearFarScalar(r * 2.5, 2.4, 9e8, 0.12),
|
||||
@ -156,21 +200,28 @@ export default function initSolarSystem(ctx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ent = ds.entities.add({
|
||||
// Textured sphere — FROZEN position (constant property keeps the image
|
||||
// material; snapshotted forward when you travel to it). Only visible up close.
|
||||
const sphere = ds.entities.add({
|
||||
name: b.name,
|
||||
position: pos,
|
||||
position: new Cesium.ConstantPositionProperty(posOf(b.key, simDay)),
|
||||
orientation: tiltQuat(b),
|
||||
ellipsoid: {
|
||||
radii: new Cesium.Cartesian3(r, r, r),
|
||||
material: new Cesium.ImageMaterialProperty({ image: b.tex, color: Cesium.Color.WHITE }),
|
||||
slicePartitions: 64, stackPartitions: 64,
|
||||
// Only render the real sphere once you're close enough for it to read as a
|
||||
// textured planet. Far away it minifies to a grey ball — hide it and show
|
||||
// the colored dot instead. Clean handoff at r*30.
|
||||
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, r * 30),
|
||||
},
|
||||
// Colored dot + label are the orrery-overview representation; they vanish as
|
||||
// you approach (r*30) and the textured 3D planet takes over.
|
||||
});
|
||||
sphereEntities[b.key] = sphere;
|
||||
bodyEntities[b.key] = sphere;
|
||||
|
||||
// Colored dot + label — ANIMATED: position reads live `simDay`, so it sweeps
|
||||
// the orbit while playing. Point is a cheap primitive (no image material), so
|
||||
// a time-varying position is safe here. Vanishes up close (r*30 handoff).
|
||||
const dot = ds.entities.add({
|
||||
name: b.name,
|
||||
position: new Cesium.CallbackProperty(() => posOf(b.key, simDay), false),
|
||||
point: {
|
||||
pixelSize: 7,
|
||||
color: Cesium.Color.fromCssColorString(b.color),
|
||||
@ -180,29 +231,25 @@ export default function initSolarSystem(ctx) {
|
||||
},
|
||||
label: labelGraphics(b),
|
||||
});
|
||||
bodyEntities[b.key] = ent;
|
||||
dotEntities[b.key] = dot;
|
||||
|
||||
// Saturn's rings: a flat quad in the equatorial plane, textured with a
|
||||
// top-down ring annulus (transparent centre + corners). Depth-tested so the
|
||||
// planet occludes the far half of the ring — the real Saturn silhouette.
|
||||
// planet occludes the far half of the ring. Frozen with the sphere.
|
||||
if (b.ring) {
|
||||
const side = r * 6.6; // hole ≈ planet radius, outer edge ≈ 3.1 radii
|
||||
ds.entities.add({
|
||||
position: pos,
|
||||
ringEntities[b.key] = ds.entities.add({
|
||||
position: new Cesium.ConstantPositionProperty(posOf(b.key, simDay)),
|
||||
plane: {
|
||||
plane: new Cesium.Plane(ringNormal(b.obliq), 0.0),
|
||||
dimensions: new Cesium.Cartesian2(side, side),
|
||||
material: new Cesium.ImageMaterialProperty({ image: b.ring, transparent: true }),
|
||||
// Match the sphere's visibility window (r*30). Beyond it the ring both
|
||||
// minifies to an ugly opaque grey card AND would float without a planet,
|
||||
// so hide it — at that range Saturn is just its labelled dot.
|
||||
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, r * 30),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Orbit: trace one full revolution by sweeping the anomaly.
|
||||
// arcType NONE = straight segments in space (not draped on an ellipsoid).
|
||||
// Orbit ring: one full revolution swept by anomaly. Static — the dot rides it.
|
||||
const pts = [];
|
||||
for (let a = 0; a <= 360; a += 3) {
|
||||
const v = b.key === 'earth' ? earthHelioAt(d, a) : helioAt(b.key, d, a);
|
||||
@ -251,8 +298,9 @@ export default function initSolarSystem(ctx) {
|
||||
// and setView each frame. Guaranteed smooth because setView is reliable.
|
||||
let flightTick = null;
|
||||
const V3 = Cesium.Cartesian3;
|
||||
function flyCamera(destPos, destDir, destUp, duration) {
|
||||
function flyCamera(destPos, destDir, destUp, duration, onArrive) {
|
||||
if (flightTick) { flightTick(); flightTick = null; }
|
||||
scene.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); // clear any planet orbit-lock so setView is world-space
|
||||
const cam = scene.camera;
|
||||
const startPos = cam.positionWC.clone();
|
||||
const startDir = cam.directionWC.clone();
|
||||
@ -268,10 +316,64 @@ export default function initSolarSystem(ctx) {
|
||||
const right = V3.normalize(V3.cross(dir, up, new V3()), new V3());
|
||||
up = V3.normalize(V3.cross(right, dir, new V3()), new V3()); // re-orthonormalize
|
||||
scene.camera.setView({ destination: pos, orientation: { direction: dir, up } });
|
||||
if (u >= 1 && flightTick) { flightTick(); flightTick = null; }
|
||||
if (u >= 1 && flightTick) { flightTick(); flightTick = null; if (onArrive) onArrive(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Re-anchor drag/scroll to orbit a specific body (not the Sun at the origin).
|
||||
// lookAtTransform with NO offset preserves the camera's current pose (no snap) and
|
||||
// just moves the controller's pivot to `pos`; cleared on the next flight
|
||||
// (flyCamera resets to IDENTITY) or on exit.
|
||||
function lockOrbit(pos) {
|
||||
scene.camera.lookAtTransform(Cesium.Transforms.eastNorthUpToFixedFrame(pos));
|
||||
}
|
||||
|
||||
// Snapshot ALL planet spheres+rings forward to the current sim time (constant
|
||||
// position keeps the image material). Freezing every body — not just the target —
|
||||
// keeps any sphere that drifts into view aligned with its animated dot.
|
||||
function freezeAll() {
|
||||
for (const bb of BODIES) {
|
||||
if (bb.sun) continue;
|
||||
const p = posOf(bb.key, simDay);
|
||||
if (sphereEntities[bb.key]) sphereEntities[bb.key].position = new Cesium.ConstantPositionProperty(p);
|
||||
if (ringEntities[bb.key]) ringEntities[bb.key].position = new Cesium.ConstantPositionProperty(p);
|
||||
}
|
||||
}
|
||||
// Spheres are the "settled" (paused) representation; while the orrery plays only
|
||||
// the animated dots show, so a frozen sphere can never appear at a stale spot.
|
||||
function showSpheres(v) {
|
||||
for (const bb of BODIES) {
|
||||
if (bb.sun) continue;
|
||||
if (sphereEntities[bb.key]) sphereEntities[bb.key].show = v;
|
||||
if (ringEntities[bb.key]) ringEntities[bb.key].show = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Orbital time-lapse loop — advances simDay by real elapsed time while playing.
|
||||
function startAnim() {
|
||||
if (animTick) return;
|
||||
lastAnimMs = performance.now();
|
||||
animTick = scene.preRender.addEventListener(() => {
|
||||
const now = performance.now();
|
||||
let dt = (now - lastAnimMs) / 1000; lastAnimMs = now;
|
||||
if (dt > 0.1) dt = 0.1; // clamp tab-switch / hitch jumps
|
||||
if (playing) simDay += dt * DAYS_PER_SEC;
|
||||
});
|
||||
}
|
||||
function stopAnim() { if (animTick) { animTick(); animTick = null; } }
|
||||
|
||||
const OVERVIEW = {
|
||||
pos: new Cesium.Cartesian3(1.6e8, -3.4e8, 2.6e8),
|
||||
dir: Cesium.Cartesian3.normalize(new Cesium.Cartesian3(-1.6e8, 3.4e8, -2.6e8), new Cesium.Cartesian3()),
|
||||
up: new Cesium.Cartesian3(0, 0, 1),
|
||||
};
|
||||
function goOverview() {
|
||||
hideCard();
|
||||
playing = !missionsOn; // resume the orrery (stay paused if browsing missions)
|
||||
showSpheres(false); // dots-only
|
||||
flyCamera(OVERVIEW.pos, OVERVIEW.dir, OVERVIEW.up, 2.0);
|
||||
}
|
||||
|
||||
function enter() {
|
||||
if (active) return;
|
||||
active = true;
|
||||
@ -325,26 +427,31 @@ export default function initSolarSystem(ctx) {
|
||||
});
|
||||
|
||||
ds.show = true;
|
||||
document.body.classList.add('ss-active'); // CSS hides the OSINT timeline/animation chrome
|
||||
document.getElementById('hud').style.display = 'none';
|
||||
panel.style.display = 'block';
|
||||
document.getElementById('ss-btn').classList.add('active');
|
||||
|
||||
// Start the orbital time-lapse (planets sweep their orbits in the overview).
|
||||
playing = true;
|
||||
showSpheres(false); // dots-only while animating
|
||||
startAnim();
|
||||
|
||||
// Swoop out from "Sun in your face" (camera starts at the old Earth-surface
|
||||
// spot, and the Sun now sits where Earth's centre was) to a clean oblique view
|
||||
// of the whole system.
|
||||
flyCamera(
|
||||
new Cesium.Cartesian3(1.6e8, -3.4e8, 2.6e8),
|
||||
Cesium.Cartesian3.normalize(new Cesium.Cartesian3(-1.6e8, 3.4e8, -2.6e8), new Cesium.Cartesian3()),
|
||||
new Cesium.Cartesian3(0, 0, 1),
|
||||
2.6,
|
||||
);
|
||||
flyCamera(OVERVIEW.pos, OVERVIEW.dir, OVERVIEW.up, 2.6);
|
||||
}
|
||||
|
||||
let headlight = null;
|
||||
function exit() {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
closeMissions(false); hideCard();
|
||||
if (flightTick) { flightTick(); flightTick = null; } // stop any in-progress swoop
|
||||
stopAnim(); playing = false;
|
||||
scene.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); // clear planet orbit-lock before returning to Earth
|
||||
document.body.classList.remove('ss-active');
|
||||
ds.show = false;
|
||||
for (const [s, show] of saved.dataSources) s.show = show;
|
||||
for (const [p, show] of saved.primitives) p.show = show;
|
||||
@ -376,21 +483,96 @@ export default function initSolarSystem(ctx) {
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
// Deterministic framing in WORLD space. flyTo(entity)/flyToBoundingSphere both
|
||||
// misbehave out here (the ENU frame they offset within is degenerate 2e8 m off
|
||||
// Earth), so compute the destination + orientation directly. Camera sits up and
|
||||
// to the side of the ecliptic → a 3/4 view that shows Saturn's ring tilt.
|
||||
const ent = bodyEntities[key];
|
||||
const pos = ent.position.getValue(viewer.clock.currentTime);
|
||||
if (missionsOn) closeMissions(false);
|
||||
// Pause the orrery, freeze every sphere to the current sim time and reveal them
|
||||
// (textured, aligned with their dots). The close-up is a constant-position
|
||||
// sphere → keeps its image material.
|
||||
playing = false;
|
||||
freezeAll();
|
||||
showSpheres(true);
|
||||
showBodyCard(key); // infographic while you fly in
|
||||
const V = Cesium.Cartesian3;
|
||||
const pos = b.sun ? new V(0, 0, 0) : posOf(key, simDay);
|
||||
const r = bodyRadius(b.km, b.sun);
|
||||
const range = (b.sun ? 3.0 : b.ring ? 6.0 : 5.0) * r;
|
||||
const V = Cesium.Cartesian3;
|
||||
// Deterministic framing in WORLD space (flyTo/flyToBoundingSphere misbehave out
|
||||
// here). Camera up and to the side → a 3/4 view; Saturn's ring tilt reads well.
|
||||
const offDir = V.normalize(new V(0.55, -0.25, 0.45), new V());
|
||||
const dest = V.add(pos, V.multiplyByScalar(offDir, range, new V()), new V());
|
||||
const dir = V.negate(offDir, new V());
|
||||
const right = V.normalize(V.cross(dir, new V(0, 0, 1), new V()), new V());
|
||||
const up = V.normalize(V.cross(right, dir, new V()), new V());
|
||||
flyCamera(dest, dir, up, 2.2);
|
||||
// On arrival, re-anchor drag/scroll to orbit THIS planet (not the whole system).
|
||||
flyCamera(dest, dir, up, 2.2, b.sun ? null : () => lockOrbit(pos));
|
||||
}
|
||||
|
||||
// ---- infographic card (planet facts + mission info) ----
|
||||
const card = document.createElement('aside');
|
||||
card.id = 'ss-card';
|
||||
card.style.display = 'none';
|
||||
document.body.appendChild(card);
|
||||
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const fmtDate = (iso) => { const [y, m, d] = iso.split('-'); return `${+d} ${MON[+m - 1]} ${y}`; };
|
||||
function hideCard() { card.style.display = 'none'; }
|
||||
function fillCard(title, blurb, stats, fact, accent) {
|
||||
const rows = stats.map(([k, v]) => `<div class="ss-card-row"><span>${k}</span><span>${v}</span></div>`).join('');
|
||||
card.innerHTML = `<button class="ss-card-x" title="Close">×</button>
|
||||
<div class="ss-card-title"${accent ? ` style="color:${accent}"` : ''}>${title}</div>
|
||||
<div class="ss-card-blurb">${blurb}</div>
|
||||
<div class="ss-card-stats">${rows}</div>
|
||||
${fact ? `<div class="ss-card-fact">${fact}</div>` : ''}`;
|
||||
card.querySelector('.ss-card-x').addEventListener('click', hideCard);
|
||||
card.style.display = 'block';
|
||||
}
|
||||
function showBodyCard(key) {
|
||||
const f = FACTS[key]; if (!f) return hideCard();
|
||||
const b = BODIES.find((x) => x.key === key);
|
||||
fillCard(b.name, f.blurb, f.stats, '💡 ' + f.fact, b.color);
|
||||
}
|
||||
function showMissionCard(m) {
|
||||
const tgt = BODIES.find((b) => b.key === m.target);
|
||||
fillCard('🛰 ' + m.name, m.blurb, [
|
||||
['Agency', m.agency], ['Type', m.type],
|
||||
['Launched', fmtDate(m.waypoints[0].date)],
|
||||
['Arrived', fmtDate(m.waypoints[m.waypoints.length - 1].date)],
|
||||
['Destination', tgt ? tgt.name : m.target],
|
||||
], '', m.color);
|
||||
}
|
||||
|
||||
// ---- space-missions overlay (schematic transfer arcs) ----
|
||||
let missionsOn = false, selectedMission = null;
|
||||
const missionArcs = {};
|
||||
function drawMissions() {
|
||||
for (const m of MISSIONS) {
|
||||
missionArcs[m.id] = ds.entities.add({
|
||||
polyline: { positions: missionArc(m), width: 1.4, arcType: Cesium.ArcType.NONE, material: lib.cz(m.color, 0.4) },
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearMissions() { for (const id in missionArcs) { ds.entities.remove(missionArcs[id]); delete missionArcs[id]; } }
|
||||
function selectMission(id) {
|
||||
selectedMission = id;
|
||||
for (const mid in missionArcs) {
|
||||
const mm = MISSIONS.find((x) => x.id === mid), sel = mid === id;
|
||||
missionArcs[mid].polyline.width = sel ? 3.5 : 1.1;
|
||||
missionArcs[mid].polyline.material = lib.cz(mm.color, sel ? 0.95 : 0.14);
|
||||
}
|
||||
showMissionCard(MISSIONS.find((x) => x.id === id));
|
||||
missionList.querySelectorAll('.ss-mission').forEach((b) => b.classList.toggle('active', b.dataset.id === id));
|
||||
}
|
||||
function openMissions() {
|
||||
missionsOn = true; missionsBtn.classList.add('active'); missionsWrap.style.display = 'block';
|
||||
playing = false; showSpheres(false); hideCard();
|
||||
clearMissions(); drawMissions();
|
||||
scene.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
flyCamera(OVERVIEW.pos, OVERVIEW.dir, OVERVIEW.up, 1.8);
|
||||
}
|
||||
function closeMissions(resume) {
|
||||
missionsOn = false; selectedMission = null;
|
||||
missionsBtn.classList.remove('active'); missionsWrap.style.display = 'none';
|
||||
clearMissions(); hideCard();
|
||||
missionList.querySelectorAll('.ss-mission').forEach((b) => b.classList.remove('active'));
|
||||
if (resume) playing = true;
|
||||
}
|
||||
|
||||
// ---- UI: enter button + destination panel ----
|
||||
@ -405,8 +587,19 @@ export default function initSolarSystem(ctx) {
|
||||
const panel = document.createElement('aside');
|
||||
panel.id = 'ss-panel';
|
||||
panel.style.display = 'none';
|
||||
panel.innerHTML = `<div class="ss-title">TRAVEL TO</div><div class="ss-dests"></div>
|
||||
<div class="ss-foot">Positions computed live · planet maps: NASA/JPL & <a href="https://www.solarsystemscope.com/" target="_blank" rel="noopener">Solar System Scope</a> (CC BY 4.0)</div>`;
|
||||
panel.innerHTML = `<div class="ss-title" title="Collapse / expand">TRAVEL TO<span class="ss-collapse">▾</span></div>
|
||||
<div class="ss-body">
|
||||
<button class="ss-dest ss-overview">⊙ System overview</button>
|
||||
<div class="ss-dests"></div>
|
||||
<button class="ss-dest ss-missions-toggle">🛰 Space missions</button>
|
||||
<div class="ss-missions-wrap" style="display:none">
|
||||
<div class="ss-missions-hint">Approx. transfer paths — pick one:</div>
|
||||
<div class="ss-missions"></div>
|
||||
</div>
|
||||
<div class="ss-foot">Positions computed live · planet maps: NASA/JPL & <a href="https://www.solarsystemscope.com/" target="_blank" rel="noopener">Solar System Scope</a> (CC BY 4.0)</div>
|
||||
</div>`;
|
||||
panel.querySelector('.ss-title').addEventListener('click', () => panel.classList.toggle('collapsed'));
|
||||
panel.querySelector('.ss-overview').addEventListener('click', goOverview);
|
||||
const dests = panel.querySelector('.ss-dests');
|
||||
for (const b of BODIES) {
|
||||
const d = document.createElement('button');
|
||||
@ -415,11 +608,26 @@ export default function initSolarSystem(ctx) {
|
||||
d.addEventListener('click', () => travelTo(b.key));
|
||||
dests.appendChild(d);
|
||||
}
|
||||
const missionsBtn = panel.querySelector('.ss-missions-toggle');
|
||||
const missionsWrap = panel.querySelector('.ss-missions-wrap');
|
||||
const missionList = panel.querySelector('.ss-missions');
|
||||
missionsBtn.addEventListener('click', () => (missionsOn ? closeMissions(true) : openMissions()));
|
||||
for (const m of MISSIONS) {
|
||||
const tgt = BODIES.find((x) => x.key === m.target);
|
||||
const b = document.createElement('button');
|
||||
b.className = 'ss-mission';
|
||||
b.dataset.id = m.id;
|
||||
b.innerHTML = `<span class="ss-mission-dot" style="background:${m.color}"></span>` +
|
||||
`<span class="ss-mission-name">${m.name}</span>` +
|
||||
`<span class="ss-mission-tgt">${tgt ? tgt.name.split(' ')[0] : ''}</span>`;
|
||||
b.addEventListener('click', () => selectMission(m.id));
|
||||
missionList.appendChild(b);
|
||||
}
|
||||
const back = document.createElement('button');
|
||||
back.className = 'ss-dest ss-back';
|
||||
back.textContent = '↩ Return to Earth (OSINT)';
|
||||
back.addEventListener('click', exit);
|
||||
panel.appendChild(back);
|
||||
panel.querySelector('.ss-body').appendChild(back);
|
||||
document.body.appendChild(panel);
|
||||
|
||||
return { enter, exit, isActive: () => active };
|
||||
|
||||
181
js/ssdata.js
Normal file
181
js/ssdata.js
Normal file
@ -0,0 +1,181 @@
|
||||
// Static data for Solar System mode: infographic facts per body (no Earth — that's
|
||||
// the OSINT globe) and a curated set of real planetary missions with date-accurate
|
||||
// launch / flyby / arrival waypoints (fed through the live ephemeris to draw
|
||||
// schematic heliocentric transfer arcs, including gravity-assist grand tours).
|
||||
|
||||
export const FACTS = {
|
||||
sun: {
|
||||
blurb: 'The star at the heart of it all — a giant ball of plasma holding 99.86% of the solar system’s mass.',
|
||||
stats: [
|
||||
['Type', 'G2V yellow dwarf'],
|
||||
['Diameter', '1,391,000 km'],
|
||||
['Mass', '333,000 × Earth'],
|
||||
['Surface', '~5,500 °C'],
|
||||
['Core', '~15,000,000 °C'],
|
||||
['Age', '~4.6 billion yrs'],
|
||||
['Made of', 'Hydrogen · Helium'],
|
||||
['Light to Earth', '8 min 20 s'],
|
||||
],
|
||||
fact: 'About 1.3 million Earths would fit inside it.',
|
||||
},
|
||||
mercury: {
|
||||
blurb: 'The smallest planet and the closest to the Sun — a scorched, cratered ball of rock with almost no atmosphere.',
|
||||
stats: [
|
||||
['Diameter', '4,879 km'],
|
||||
['Gravity', '0.38 g'],
|
||||
['Day', '176 Earth days'],
|
||||
['Year', '88 Earth days'],
|
||||
['Temp', '−173 to 427 °C'],
|
||||
['Moons', '0'],
|
||||
['From Sun', '0.39 AU'],
|
||||
],
|
||||
fact: 'A day on Mercury (sunrise to sunrise) lasts two of its years.',
|
||||
},
|
||||
venus: {
|
||||
blurb: 'Earth’s hellish twin — a runaway greenhouse wrapped in sulfuric-acid clouds, hot enough to melt lead.',
|
||||
stats: [
|
||||
['Diameter', '12,104 km'],
|
||||
['Gravity', '0.90 g'],
|
||||
['Day', '243 days (backwards)'],
|
||||
['Year', '225 Earth days'],
|
||||
['Surface', '~465 °C'],
|
||||
['Moons', '0'],
|
||||
['From Sun', '0.72 AU'],
|
||||
],
|
||||
fact: 'It spins backwards, and its day is longer than its year.',
|
||||
},
|
||||
mars: {
|
||||
blurb: 'The Red Planet — a cold desert of rusted iron with the tallest volcano and deepest canyon in the solar system.',
|
||||
stats: [
|
||||
['Diameter', '6,779 km'],
|
||||
['Gravity', '0.38 g'],
|
||||
['Day', '24 h 37 m'],
|
||||
['Year', '687 Earth days'],
|
||||
['Temp', 'avg −63 °C'],
|
||||
['Moons', '2 (Phobos, Deimos)'],
|
||||
['From Sun', '1.52 AU'],
|
||||
],
|
||||
fact: 'Olympus Mons rises 22 km — nearly 3× the height of Everest.',
|
||||
},
|
||||
jupiter: {
|
||||
blurb: 'The king of the planets — a gas giant so massive it could swallow all the others, wrapped in banded storms.',
|
||||
stats: [
|
||||
['Diameter', '139,820 km'],
|
||||
['Gravity', '2.53 g'],
|
||||
['Day', '9 h 56 m (fastest)'],
|
||||
['Year', '11.9 Earth years'],
|
||||
['Cloud tops', '~−110 °C'],
|
||||
['Moons', '95 known'],
|
||||
['From Sun', '5.20 AU'],
|
||||
],
|
||||
fact: 'The Great Red Spot is a storm wider than the whole Earth.',
|
||||
},
|
||||
saturn: {
|
||||
blurb: 'The jewel of the solar system — a gas giant circled by billions of ice and rock chunks in a dazzling ring system.',
|
||||
stats: [
|
||||
['Diameter', '116,460 km'],
|
||||
['Gravity', '1.07 g'],
|
||||
['Day', '10 h 42 m'],
|
||||
['Year', '29.5 Earth years'],
|
||||
['Cloud tops', '~−140 °C'],
|
||||
['Moons', '146 known'],
|
||||
['From Sun', '9.54 AU'],
|
||||
],
|
||||
fact: 'So light for its size it would float in a big enough bathtub.',
|
||||
},
|
||||
uranus: {
|
||||
blurb: 'A pale blue-green ice giant that got knocked over — it orbits the Sun tipped completely onto its side.',
|
||||
stats: [
|
||||
['Diameter', '50,724 km'],
|
||||
['Gravity', '0.89 g'],
|
||||
['Day', '17 h 14 m'],
|
||||
['Year', '84 Earth years'],
|
||||
['Cloud tops', '~−195 °C'],
|
||||
['Moons', '28 known'],
|
||||
['From Sun', '19.2 AU'],
|
||||
],
|
||||
fact: 'Its 98° tilt means each pole gets 42 years of sunlight, then 42 of dark.',
|
||||
},
|
||||
neptune: {
|
||||
blurb: 'The farthest planet — a deep-blue ice giant whipped by the fastest winds anywhere in the solar system.',
|
||||
stats: [
|
||||
['Diameter', '49,244 km'],
|
||||
['Gravity', '1.14 g'],
|
||||
['Day', '16 h 06 m'],
|
||||
['Year', '165 Earth years'],
|
||||
['Cloud tops', '~−200 °C'],
|
||||
['Moons', '16 known'],
|
||||
['From Sun', '30.1 AU'],
|
||||
],
|
||||
fact: 'Supersonic winds reach 2,100 km/h — no one fully knows why.',
|
||||
},
|
||||
};
|
||||
|
||||
// Missions: waypoints[0] is launch (Earth); the rest are real flyby / arrival dates
|
||||
// at the named body. Dates are ISO (UTC). `target` is the headline destination.
|
||||
export const MISSIONS = [
|
||||
{
|
||||
id: 'mariner2', name: 'Mariner 2', agency: 'NASA', target: 'venus', color: '#f4a259', type: 'Venus flyby',
|
||||
waypoints: [{ body: 'earth', date: '1962-08-27' }, { body: 'venus', date: '1962-12-14' }],
|
||||
blurb: 'The first spacecraft ever to fly past another planet, confirming Venus’s furnace-hot, cloud-choked surface.',
|
||||
},
|
||||
{
|
||||
id: 'mariner4', name: 'Mariner 4', agency: 'NASA', target: 'mars', color: '#e07a5f', type: 'Mars flyby',
|
||||
waypoints: [{ body: 'earth', date: '1964-11-28' }, { body: 'mars', date: '1965-07-15' }],
|
||||
blurb: 'Returned the first close-up photos of Mars — 21 grainy frames that ended a century of “canal” speculation.',
|
||||
},
|
||||
{
|
||||
id: 'viking1', name: 'Viking 1', agency: 'NASA', target: 'mars', color: '#c1502a', type: 'Orbiter + lander',
|
||||
waypoints: [{ body: 'earth', date: '1975-08-20' }, { body: 'mars', date: '1976-06-19' }],
|
||||
blurb: 'First fully successful Mars landing; its biology experiments searched the soil for signs of life.',
|
||||
},
|
||||
{
|
||||
id: 'voyager1', name: 'Voyager 1', agency: 'NASA', target: 'saturn', color: '#8ecae6', type: 'Jupiter · Saturn',
|
||||
waypoints: [{ body: 'earth', date: '1977-09-05' }, { body: 'jupiter', date: '1979-03-05' }, { body: 'saturn', date: '1980-11-12' }],
|
||||
blurb: 'Now the most distant human-made object, sailing through interstellar space over 24 billion km away.',
|
||||
},
|
||||
{
|
||||
id: 'voyager2', name: 'Voyager 2', agency: 'NASA', target: 'neptune', color: '#ffd166', type: 'Grand Tour',
|
||||
waypoints: [
|
||||
{ body: 'earth', date: '1977-08-20' }, { body: 'jupiter', date: '1979-07-09' },
|
||||
{ body: 'saturn', date: '1981-08-25' }, { body: 'uranus', date: '1986-01-24' }, { body: 'neptune', date: '1989-08-25' },
|
||||
],
|
||||
blurb: 'The only spacecraft ever to visit all four giant planets, riding a rare alignment on a single grand tour.',
|
||||
},
|
||||
{
|
||||
id: 'magellan', name: 'Magellan', agency: 'NASA', target: 'venus', color: '#f2cc8f', type: 'Venus radar mapper',
|
||||
waypoints: [{ body: 'earth', date: '1989-05-04' }, { body: 'venus', date: '1990-08-10' }],
|
||||
blurb: 'Radar-mapped 98% of Venus’s surface through its opaque clouds, revealing volcanoes and vast lava plains.',
|
||||
},
|
||||
{
|
||||
id: 'galileo', name: 'Galileo', agency: 'NASA', target: 'jupiter', color: '#b5e48c', type: 'Jupiter orbiter (VEEGA)',
|
||||
waypoints: [
|
||||
{ body: 'earth', date: '1989-10-18' }, { body: 'venus', date: '1990-02-10' },
|
||||
{ body: 'earth', date: '1990-12-08' }, { body: 'earth', date: '1992-12-08' }, { body: 'jupiter', date: '1995-12-07' },
|
||||
],
|
||||
blurb: 'First to orbit Jupiter and drop a probe into its atmosphere — after slingshots off Venus and Earth twice.',
|
||||
},
|
||||
{
|
||||
id: 'cassini', name: 'Cassini–Huygens', agency: 'NASA/ESA', target: 'saturn', color: '#e6dbb0', type: 'Saturn orbiter',
|
||||
waypoints: [
|
||||
{ body: 'earth', date: '1997-10-15' }, { body: 'venus', date: '1998-04-26' }, { body: 'venus', date: '1999-06-24' },
|
||||
{ body: 'earth', date: '1999-08-18' }, { body: 'jupiter', date: '2000-12-30' }, { body: 'saturn', date: '2004-07-01' },
|
||||
],
|
||||
blurb: 'Orbited Saturn for 13 years and landed the Huygens probe on Titan — the most distant landing ever.',
|
||||
},
|
||||
{
|
||||
id: 'juno', name: 'Juno', agency: 'NASA', target: 'jupiter', color: '#a8dadc', type: 'Jupiter polar orbiter',
|
||||
waypoints: [{ body: 'earth', date: '2011-08-05' }, { body: 'earth', date: '2013-10-09' }, { body: 'jupiter', date: '2016-07-04' }],
|
||||
blurb: 'Loops over Jupiter’s poles to peer beneath the clouds and map its deep interior and monster magnetic field.',
|
||||
},
|
||||
{
|
||||
id: 'curiosity', name: 'Curiosity', agency: 'NASA', target: 'mars', color: '#e76f51', type: 'Mars rover',
|
||||
waypoints: [{ body: 'earth', date: '2011-11-26' }, { body: 'mars', date: '2012-08-06' }],
|
||||
blurb: 'Lowered into Gale Crater by a rocket-powered sky crane; still roving and drilling more than a decade on.',
|
||||
},
|
||||
{
|
||||
id: 'perseverance', name: 'Perseverance', agency: 'NASA', target: 'mars', color: '#ee6c4d', type: 'Mars rover',
|
||||
waypoints: [{ body: 'earth', date: '2020-07-30' }, { body: 'mars', date: '2021-02-18' }],
|
||||
blurb: 'Caching rock samples for a future return to Earth — and flew Ingenuity, the first aircraft on another world.',
|
||||
},
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user