SolarSystem Web

Architecture — Three.js, real orbital mechanics, and zero dependencies

System Architecture

graph TD A["Current Date/Time"] --> B["Julian Date Converter"] B --> C["Orbital Mechanics Engine
(CPU, Keplerian)"] C --> D["Planet Positions
(Heliocentric Ecliptic)"] D --> E["Log Distance +
Sqrt Radius Scaler"] B --> R["IAU Rotation
(Quaternion Tilt + Spin)"] E --> F["Three.js Scene Graph
(GPU, WebGL, PBR)"] R --> F F --> G["WebGLRenderer
(Canvas Element)"] G --> H["HTML/CSS Overlay
(Labels, HUD, Zoom)"] H --> I["CameraController
(Mouse + Touch)"] I --> G J["JPL J2000.0
Elements"] --> C K["NASA/Cassini
Textures (JPEG)"] --> F L["Time Controls
(Speed/Pause/Reset)"] --> A M["HYG Star
Catalogue (CSV)"] --> F style A fill:#1a1a3e,stroke:#ffaa33 style C fill:#1a2a1a,stroke:#44aa44 style R fill:#1a2a1a,stroke:#44aa44 style F fill:#1a1a3e,stroke:#4488ff style H fill:#2a1a2a,stroke:#aa44ff style I fill:#1a2a2a,stroke:#44aaaa style M fill:#1a1a3e,stroke:#88bbff

Module Architecture

graph TD HTML["index.html
(UI + CSS)"] --> MAIN["main.js
(Entry Point)"] MAIN --> SB["sceneBuilder.js
(Scene Graph)"] MAIN --> CC["cameraController.js
(Input Handling)"] MAIN --> OM["orbitalMechanics.js
(Kepler Solver)"] MAIN --> SD["solarSystemData.js
(Body Catalogue)"] SB --> TG["textureGenerator.js
(Procedural Sun/Glow)"] SB --> OM SB --> SD SB --> THREE["Three.js r170
(CDN)"] CC --> THREE TG --> THREE style HTML fill:#2a1a2a,stroke:#aa44ff style MAIN fill:#1a1a3e,stroke:#ffaa33 style THREE fill:#1a2a1a,stroke:#44aa44 style SB fill:#1a1a3e,stroke:#4488ff style CC fill:#1a2a2a,stroke:#44aaaa

Camera Controller

graph TD A["Input Events"] --> B["Left-Drag / 1-Finger"] A --> C["Right-Drag / 2-Finger"] A --> D["Scroll / Pinch"] A --> E["Click / Tap"] A --> F["Double-Click / Double-Tap"] B --> G["Translate Target
(Camera-local plane)"] C --> H["Orbit Angles
(Azimuth + Elevation)"] D --> I["Camera Distance
(0.5 – 250)"] E --> J["Raycaster Hit Test"] F --> K["Reset to Overview"] G --> L["updateCamera()
(Spherical → Cartesian)"] H --> L I --> L J --> M["selectBody()
→ focusCamera()"] K --> L M --> L L --> N["Camera Position
+ lookAt Target"] style L fill:#1a2a2a,stroke:#44aaaa style N fill:#1a1a3e,stroke:#ffaa33

Scene Graph

graph TD S["THREE.Scene"] --> PL["PointLight
(Sun, warm white)"] S --> AL["AmbientLight
(dim fill)"] S --> SF["Starfield Points
(4 brightness tiers)"] S --> SUN["Sun Mesh
(MeshBasicMaterial)"] SUN --> G1["Sprite: Inner Glow"] SUN --> G2["Sprite: Mid Glow"] SUN --> G3["Sprite: Outer Glow"] SUN --> G4["Sprite: Corona"] S --> P1["Planet Meshes
(MeshStandardMaterial)"] P1 --> SAT["Saturn Rings
(BufferGeometry Disc)"] S --> ORB["Orbit Lines
(LineLoop)"] S --> MOO["Moon Meshes
(MeshStandardMaterial)"] style S fill:#1a1a3e,stroke:#ffaa33 style SUN fill:#2a2a0a,stroke:#ffaa33 style P1 fill:#1a1a3e,stroke:#4488ff style SF fill:#0d0d1a,stroke:#88bbff

iOS to Web: Technology Mapping

This app is a faithful port of the iOS SolarSystem app. Every algorithm, constant, and visual detail maps directly.

iOS (SceneKit)Web (Three.js)Notes
SCNSceneTHREE.SceneBlack background, same graph structure
SCNSphereSphereGeometrySame segment count (36/48)
PBR SCNMaterialMeshStandardMaterialSame roughness/metalness values
.constant lightingMeshBasicMaterialSun, orbit lines
.add blend modeAdditiveBlendingSun corona glow
SCNView.projectPoint()Vector3.project()Label screen projection
SCNView hit testRaycasterBody selection on click/tap
CADisplayLinkrequestAnimationFrame60fps render loop
UIGraphicsImageRendererCanvas2DProcedural textures
UIPanGestureRecognizermouse/touch eventsCustom gesture handling
simd_quatfTHREE.QuaternionIAU rotation model
UserDefaultsFuture: localStorage

Orbital Mechanics Pipeline

1
Julian Date — Convert JavaScript Date to Julian Date Number via the Meeus algorithm. Essential for astronomical time: a continuous day count since 4713 BC, eliminating calendar irregularities.
2
Julian CenturiesT = (JD - 2451545.0) / 36525.0. Time since J2000.0 epoch in centuries. All JPL element rates are per-century.
3
Osculating Elementsa = a0 + aRate × T for all six Keplerian elements. Linear approximation valid for several centuries around J2000.
4
Mean AnomalyM = L - wBar, normalised to [0, 2π). The angle that increases uniformly with time.
5
Kepler Solver — Newton-Raphson iteration: E ← E - (E - e·sin(E) - M) / (1 - e·cos(E)). Converges in 3–5 iterations for all planetary eccentricities.
6
True Anomaly — Half-angle formula: ν = 2·atan2(√(1+e)·sin(E/2), √(1-e)·cos(E/2)). The actual angle of the planet from perihelion.
7
Heliocentric Position — Distance r = a(1 - e·cos(E)), then rotate by ω, I, w from the orbital plane to ecliptic (x, y, z) coordinates in AU.
8
Scene Position — Logarithmic distance compression log(1 + AU/0.5) × 15, then coordinate swap: Three.js x = ecliptic x, y = ecliptic z, z = -ecliptic y.
Distance Scaling sceneUnits = log(1 + AU / 0.5) × 15
Mercury ≈ 7.5 | Earth ≈ 16.5 | Jupiter ≈ 36.5 | Neptune ≈ 63 Radius Scaling sceneRadius = sqrt(km) × 0.00125
Jupiter ≈ 0.33 | Earth ≈ 0.10 | Mercury ≈ 0.06 Moon Distance Compression moonSceneDist = parentRadius × pow(realRatio, MOON_DIST_EXPONENT) × MOON_DIST_SCALE   // 0.6, 1.5 — centralised in sceneBuilder.js
Moon ≈ 8.8× Earth | Io ≈ 3.1× Jupiter | Callisto ≈ 5.7× Jupiter

IAU Rotation Model

Every body has a sidereal rotation period, axial obliquity, and prime meridian at J2000.0. Rotation is applied using quaternion composition to avoid Euler angle wobble:

tiltQuat = Quaternion.setFromAxisAngle(X, obliquity)
spinQuat = Quaternion.setFromAxisAngle(Y, spinAngle)
mesh.quaternion = tiltQuat × spinQuat

Tilt is fixed in world space. Spin happens around the tilted pole.
Euler angles (tilt, spin, 0) cause wobble because Three.js applies Y-X-Z order,
making the tilt axis rotate with each spin cycle.

Saturn's Rings

Rings are child nodes of Saturn, so they inherit the planet's rotation. To keep them fixed in the equatorial plane while the cloud bands rotate beneath, the ring node applies an inverse spin quaternion each frame:

ringNode.quaternion = Quaternion.setFromAxisAngle(Y, -spinAngle)

Retrograde Rotators

Venus (177.4° tilt, -5832.5h period), Uranus (97.8° tilt, -17.24h period), and Pluto (122.5° tilt, -153.3h period) all rotate backwards. The negative period flips the spin direction; the extreme obliquity tilts the axis past vertical.

Star Rendering

8,920 stars from the HYG v38 catalogue (Hipparcos/Yale/Gliese amalgamation), filtered to naked-eye visibility (magnitude ≤ 6.5).

Position Mapping

Each star's Right Ascension (hours) and Declination (degrees) are converted to a point on a celestial sphere of radius 500 scene units. The coordinate transform: x = R·cos(dec)·cos(ra), y = R·sin(dec), z = -R·cos(dec)·sin(ra).

Brightness Tiers

TierMagnitudeCountPoint Size
0 (brightest)< 1.5~204.0 px
11.5 – 3.5~2002.5 px
23.5 – 5.0~15001.5 px
3 (faintest)5.0 – 6.5~70000.8 px

B-V Colour Index

Per-vertex colour computed from B-V index using a piecewise linear approximation of the Planckian locus. B-V ranges from -0.4 (hot blue-white O/B stars) through 0.0 (white A stars) to 2.0+ (cool red M stars). Rendered via PointsMaterial with vertexColors: true and sizeAttenuation: false.

UI Layout

The application uses HTML/CSS overlays on top of the full-screen WebGL canvas. All UI panels use glass-morphism (backdrop-filter: blur(20px)) for depth.

WebGL Canvas (Three.js) 5 Apr 2026, 10:47 100x Credits Earth Planet · r: 6k km · 1.00 AU × + ⏱ 1x 🏷 ↑ Labels overlay (DOM positioned via Vector3.project) ↑

Planet Strip

The toolbar's right section contains textured circular thumbnails for each body. Each thumbnail uses the actual NASA JPEG as a CSS background-image, clipped to a circle. Saturn includes a decorative ring overlay using a CSS border-radius: 50% ellipse with rotateX(65deg) 3D transform. The selected body gets an orange border highlight.

Adaptive Layout

On wide screens (>700px), the planet strip sits inline in the toolbar alongside playback controls. On narrow screens (phones in portrait), a CSS flex-direction: column media query breaks it into its own row above the controls:

> 700px (Desktop) ⏸ ⏱ ◯ 🏷 Single row — controls + planets inline ≤ 700px (Phone Portrait) ⏸ ⏱ 1x  ◯ 🏷 Two rows — planets on top, controls below

Focus Zoom Strategy

When selecting a body, the camera distance is calculated to frame the body and its moon system. Planets with moons use a 2.2× base multiplier on the system extent. Moonless planets use 6.0× so they appear at a similar visual scale. On portrait viewports, the multiplier is scaled down by aspect ratio (0.5 + 0.5 × min(aspect, 1.0)) so planets fill the narrower width appropriately.

portraitFactor = min(width / height, 1.0)
multiplier = baseMultiplier × (0.5 + 0.5 × portraitFactor)

Landscape (16:9): factor = 1.0 → full multiplier
Portrait phone (9:16): factor = 0.56 → 78% multiplier

iOS Safari Compatibility

Three issues required specific fixes for iOS Safari, all related to how the browser handles touch events on overlaid elements:

1
touch-action: none — When applied to a full-screen container, iOS Safari's gesture system captures all touches before they reach overlaid UI elements, even those with higher z-index. Fix: apply touch-action: none only to the <canvas> element, not the container.
2
-webkit-user-select: none — When set on <body>, suppresses click event synthesis from touch events. Fix: scope to the canvas container only, leaving UI elements with default user-select.
3
Click event synthesis — Even after fixing the above, iOS Safari may still not fire click events reliably. Fix: the onTap() helper binds both touchend (with preventDefault()) and click, using a flag to prevent double-firing on devices where both work.
The onTap() pattern

All interactive elements use onTap(element, handler) instead of addEventListener('click', handler). The touchend fires immediately (no 300ms delay), and the click serves as a mouse fallback. A touchFired flag prevents the click from re-firing after a touch.

Icon rendering & HTML caching

Two more mobile gotchas surfaced after deployment. First, unicode emoji icons (e.g. 🚀) render inconsistently across mobile browsers — some show tofu boxes. All toolbar icons were replaced with inline SVG paths using currentColor fill so they render identically everywhere. Second, Mobile Safari aggressively caches HTML, leaving users stuck on old versions after a deployment. The page now sets Cache-Control: no-cache, no-store, must-revalidate plus Pragma: no-cache and Expires: 0 meta tags. Textures (large, immutable) still cache normally.

Saturn Ring Geometry

Saturn's rings use a custom BufferGeometry disc with radial UV mapping, identical to the iOS app's approach. The key insight is that the UV u coordinate must run radially (inner to outer), not linearly across the disc face.

Linear UVs (wrong) Bands are straight lines Radial UVs (correct) Bands curve concentrically u: inner→outer | v: around circumference

The geometry is built with 72 radial segments × 4 ring segments (288 triangles). Two Cassini textures are applied: a colour map (saturn_ring_color.jpg) for the band colours and an alpha map (saturn_ring_alpha.gif) for the transparency structure including the Cassini Division gap. The ring is a child of the Saturn mesh but counter-rotates each frame to cancel the parent's spin, keeping the rings fixed in the equatorial plane.

Sun Corona Rendering

Four billboard sprites with additive blending create the multi-layer corona glow. Each sprite uses a procedural radial gradient CanvasTexture.

Layer 4: 4.0× radius a: 0.03, extended corona Layer 3: 2.8× radius a: 0.08, faint orange Layer 2: 1.8× radius a: 0.20, mid orange Layer 1: 1.3× radius a: 0.40, hot white-yellow Sun: r = 0.8 scene units MeshBasicMaterial, procedural AdditiveBlending depthWrite: false

Each layer is a THREE.Sprite with AdditiveBlending and depthWrite: false. Sprites always face the camera (billboard), making them ideal for glow effects. The procedural texture is a 256×256 radial gradient with four colour stops fading from the target colour to transparent.

Coordinate System Transform

Orbital mechanics operates in heliocentric ecliptic coordinates. Three.js uses a Y-up right-handed system. The mapping preserves handedness:

Ecliptic (Orbital) x y z x,y: ecliptic plane z: perpendicular (north) Three.js (Scene) x = x y = z z = -y x,z: ground plane y: up
scene.x = ecliptic.x × scale
scene.y = ecliptic.z × scale   (north pole → up)
scene.z = -ecliptic.y × scale   (flip to right-handed)

where scale = sceneDistance(|pos|) / |pos|

Deployment Model

Zero Build, Zero Install

The entire app is static files. No npm, no webpack, no transpilation. Three.js is loaded at runtime from a CDN via an ES module import map in index.html. A convenience script starts the local server:

./web-server.sh   # or: python3 -m http.server 8080

For production deployment, copy the entire directory to any static hosting (GitHub Pages, Netlify, S3, etc.). No server-side logic needed.

Why Not file:// ?

Browsers enforce strict CORS restrictions on file:// URLs. ES module import statements, fetch() for the star catalogue CSV, and TextureLoader for JPEG textures all fail with opaque origin errors. A local HTTP server solves this by serving from http://localhost.

Browser Compatibility

FeatureChromeFirefoxSafariEdge
WebGL256+51+15+79+
ES Modules61+60+11+79+
Import Maps89+108+16.4+89+
backdrop-filter76+103+9+79+

Import maps are the newest requirement — Safari 16.4 (March 2023) is the effective minimum.

15. Mission System Architecture

graph TD MD[Mission Data
waypoints + events + vehicles] --> MM[MissionManager] MM --> ROT[Moon Direction
atan2 at flyby time] ROT --> WP[Rotated Waypoints
ecliptic frame km] WP --> CR[CatmullRom Curve
time-parameterised] CR --> SC[Scene Coordinates
pow 0.6 compression] SC --> GRP[THREE.Group
at Earth position] GRP --> LINE[Trajectory Line
vertex colours] GRP --> GLOW[Glow Points
additive blend] GRP --> MRK[Vehicle Markers
depthTest false] GRP --> EVT[Event Markers
TLI Flyby etc] MM --> TEL[Telemetry
MET dist speed] MM --> BAN[Event Banners
animated overlay] style MD fill:#332200,stroke:#ffaa33,color:#fff style MM fill:#1a1a3e,stroke:#4488ff,color:#fff style GRP fill:#1a2a1a,stroke:#44aa44,color:#fff style TEL fill:#2a1a00,stroke:#ffaa33,color:#fff style BAN fill:#2a1a00,stroke:#ffaa33,color:#fff

Mission Multi-Vehicle Architecture

Data Model

Each mission contains a vehicles array. Each vehicle has independent waypoints, colour, and a primary flag for camera tracking. This supports missions where craft separate — Artemis II has SLS, SRBs, and Orion; Apollo 11 has Columbia (CSM) and Eagle (LM) diverging at the Moon.

Coordinate Frame

Waypoints are defined in a Moon-aligned geocentric frame: X points toward the Moon at flyby time, Y is perpendicular in the orbital plane, Z is out-of-ecliptic. At initialization, the code computes the Moon’s ecliptic direction via moonPosition() and rotates all waypoints. This means any launch date works — the trajectory always wraps around the Moon’s actual position.

Distance Compression

Trajectories use the same pow(ratio, MOON_DIST_EXPONENT) × MOON_DIST_SCALE compression as moon positioning (constants centralised in sceneBuilder.js: 0.6 and 1.5). The 0.6 exponent (changed from 0.4) gives more realistic proportions — the Moon appears at 17.6 Earth radii (was 7.7). Flyby waypoints at ~398,000 km (close to real 395,000 km) now produce visible separation.

Distance Compression Effects on Trajectory Appearance

The pow(0.6) compression (changed from 0.4) has two non-obvious effects on trajectory rendering, though both are less severe than before. First, it amplifies out-of-plane (Z) motion: at Moon distance, X and Y coordinates are compressed but Z stays relatively large in the scene. A constant Z offset (e.g. 4,500 km) makes the trajectory appear to pass over the Moon’s pole instead of behind the far side — Z must be reduced to near-zero at the flyby point. Second, the compression still reduces separation at large distances, though the 0.6 exponent preserves more detail than 0.4 — flyby waypoints at ~398,000 km (close to the real 395,000 km) now produce visible separation from the Moon. These effects mean geocentric waypoints still benefit from scene-aware tuning.

Time-Parameterised CatmullRom

The CatmullRom curve gives smooth visual shape, but is sampled at uniform time steps (not arc-length). Waypoint index maps to curve parameter u = (i + frac) / (N-1), ensuring the line progresses in sync with mission elapsed time.

Moon Orbit & Landing Runtime Computation

Vehicles with a moonOrbit: { startTime, endTime, periodHours, radiusKm } property orbit the Moon at runtime using smooth cos/sin circular motion around the Moon’s actual scene position. The period is typically 2x real (e.g. 4 hours vs ~2 hours) for visual comfort at replay speeds. Apollo 11’s Columbia uses this for the full lunar orbit phase; Eagle has a three-phase lifecycle: moonOrbit (orbits with Columbia pre-descent), moonLanding (tracks Moon surface during EVA), moonOrbitReturn (orbits post-ascent). Eagle undocks from Columbia’s computed orbit position, not from a fixed waypoint. All Moon-relative positions use the Moon’s semi-major axis distance (matching the rendered mesh) rather than the varying actual distance from moonPosition(), which fluctuates ±21,000 km due to eccentricity.

Lazy-Follow Camera for Missions

The mission camera uses a two-part strategy. Initial snap: the camera snaps to Earth’s current position + the trajectory’s local center, zoomed to fit just the trajectory extent. getMissionBounds() returns both wide absolute bounds (center/radius — Earth start+end + trajectory at both) and tight trajectory-only bounds (localCenter/localRadius — relative to Earth). Initial framing uses localRadius so the trajectory fills the view tightly. The camera azimuth is set ~31° off the Sun direction (0.55 radians) for a dramatic two-thirds illuminated view; elevation 0.3 radians (~17°) above the ecliptic.

Lazy follow during playback: each frame, the camera target lerps toward Earth’s current position + trajectory center at lerp(0.02). This keeps the trajectory large and centered while Earth visibly drifts through space — stars move in the background. The initial snap position matches the lazy-follow target, so there is no animation glitch at start. User interaction (drag/scroll) clears activeMissionId, breaking the lazy follow and giving full manual control.

Telemetry

getTelemetry() computes distance from geocentric position length, and speed from a finite-difference derivative (position delta over 0.01 hours). Displayed in dual units: km + miles, km/s + mph.

16. Heliocentric Missions & Satellite System

graph TD DEF[Mission Definition
type: geocentric | heliocentric] --> TYPE{Coordinate
Frame?} TYPE -->|geocentric| GEO[Moon-Aligned Frame
Earth-centred km] TYPE -->|heliocentric| HEL[Ecliptic Frame
Sun-centred AU] HEL --> AB[anchorBody Resolution
import heliocentricPosition] AB --> SNAP[Waypoints Snapped
to Real Planet Positions] SNAP --> AUTO{autoTrajectory?} AUTO -->|yes| ARC[_generateTransferArc
Hohmann-style ellipse] AUTO -->|no| MANUAL[Manual Waypoints
CatmullRom curve] ARC --> SCENE[Scene Coordinates
log distance scaling] MANUAL --> SCENE GEO --> ROT[Moon Direction
atan2 at flyby time] ROT --> SCENE SCENE --> GRP[THREE.Group
trajectory + markers] GRP --> LINE[Trajectory Line
vertex colours] GRP --> VEH[Vehicle Markers
depthTest false] DEF --> SAT{Satellite?} SAT -->|ISS| PROC[Procedural Geometry
truss + panels + radiators] PROC --> MOON[Added as Earth Moon
period 0.064 days] DEF --> TL[Timeline Slider
live scrub] TL --> POS[Position at Time
curve.getPointAt] TL --> UI[UI Update
telemetry + banners + labels] DEF --> URL[URL Parameters
?mission=voyager1] style DEF fill:#332200,stroke:#ffaa33,color:#fff style TYPE fill:#1a1a3e,stroke:#4488ff,color:#fff style HEL fill:#1a2a1a,stroke:#44aa44,color:#fff style GEO fill:#1a2a1a,stroke:#44aa44,color:#fff style AB fill:#2a1a2a,stroke:#cc88ff,color:#fff style SNAP fill:#2a1a2a,stroke:#cc88ff,color:#fff style AUTO fill:#1a1a3e,stroke:#4488ff,color:#fff style ARC fill:#2a1a00,stroke:#ffaa33,color:#fff style PROC fill:#1a2a2a,stroke:#44aacc,color:#fff style MOON fill:#1a2a2a,stroke:#44aacc,color:#fff style TL fill:#2a2a1a,stroke:#aaaa44,color:#fff style URL fill:#2a2a1a,stroke:#aaaa44,color:#fff style SAT fill:#1a1a3e,stroke:#4488ff,color:#fff

Heliocentric Missions & Satellite Deep Dive

anchorBody System

Heliocentric missions (Voyager, New Horizons, Pioneer, etc.) define waypoints at planetary encounters, but approximate coordinates would not match the actual computed planet positions in the scene. The anchorBody property on each waypoint names a planet (e.g. "jupiter", "saturn"). At mission initialisation, the code imports heliocentricPosition() from orbitalMechanics.js and resolves each anchored waypoint to the planet’s real ecliptic position at the waypoint’s timestamp. This guarantees the trajectory passes exactly through each planet’s rendered position.

anchorBody resolution (per waypoint): wp.anchorBody = "jupiter"
elements = solarSystemData[wp.anchorBody]
T = julianCenturies(wp.date)
pos = heliocentricPosition(elements, T)
wp.x = pos.x; wp.y = pos.y; wp.z = pos.z

Coordinates are in AU, matching the heliocentric orbital mechanics pipeline.
Scene position uses the same log(1 + AU/0.5) × 15 scaling as planets.

anchorMoon System (Geocentric)

The geocentric equivalent of anchorBody: waypoints with anchorMoon: true are resolved to the Moon’s actual ecliptic position at that timestamp using moonPosition(), converting AU to km and using the semi-major axis for distance matching. Used for Apollo 11 Columbia’s LOI, TEI, and early return waypoints so the trajectory line departs smoothly from where the Moon actually is.

Interplanetary Speed Consistency

Anchored waypoints (where the trajectory snaps to a planet) can cause speed spikes because adjacent waypoints may be far in distance but close in time. The fix is to always add transition waypoints ~1,000–2,000 hours before and after anchored points. Applied across Cassini (near Venus/Saturn), New Horizons (near Jupiter/Pluto), Voyager 1 (post-Jupiter tightened from 30+ km/s to ~17 km/s), and Voyager 2 (consistent 12–17 km/s throughout).

autoTrajectory Transfer Arc Generation

Many interplanetary missions follow Hohmann-like transfer orbits between planetary encounters. Rather than requiring dozens of hand-placed intermediate waypoints, the autoTrajectory flag triggers the _generateTransferArc() method. This computes a smooth elliptical arc between consecutive anchor points:

_generateTransferArc(from, to): r1 = |from|   heliocentric distance (AU)
r2 = |to|
a = (r1 + r2) / 2   semi-major axis of transfer ellipse
angle1 = atan2(from.y, from.x)
angle2 = atan2(to.y, to.x)

Interpolate angle from angle1 to angle2 in N steps.
At each step, radius follows the ellipse: r = a(1-e²) / (1 + e·cos(θ)).
The generated points form a visually smooth arc through the ecliptic plane.

ISS as Earth Moon with Procedural Geometry

The International Space Station is modelled as an Earth satellite (moon) with an orbital period of 0.064 days (~92 minutes). Its visual representation uses procedural Three.js geometry rather than a texture-mapped sphere:

ISS procedural model: Central truss: elongated BoxGeometry (main backbone)
Solar panels: 4× thin BoxGeometry, gold/blue tint, attached to truss ends
Radiator panels: 2× white BoxGeometry, perpendicular to solar panels
Habitat modules: 2× small CylinderGeometry at truss centre

All geometry combined into a single THREE.Group.
Rendered at moon-scale minimum floor (0.012 scene units).
Listed in Satellites menu, separate from natural moons.

Timeline Slider Live Scrub

The timeline slider allows scrubbing through a mission’s duration. The slider is time-mapped: position 0% corresponds to the mission start date and 100% to the end date. During scrub, the simulation date is overridden (even while paused) and all dependent systems update:

Timeline scrub update chain: sliderFraction simDate = start + fraction × duration
simDate updatePositions()   planets + moons move
simDate missionManager.update()   vehicle position on curve
simDate updateTelemetry()   MET, distance, speed
simDate updateLabels()   DOM overlay repositioned
simDate updateBanners()   event markers

The animation loop may be paused, but the scrub triggers a full render pass.
This ensures the scene is always visually consistent with the slider position.

End-of-Mission Speed Reset

Each frame, the animation loop checks whether a mission is selected and the elapsed time has exceeded the mission’s durationHours. If so and timeScale > 1, it snaps back to 1x real-time, preventing the simulation from continuing to race at high speed after the mission completes.

URL Parameter Mission Selection

Missions can be deep-linked via URL query parameters: ?mission=voyager1 loads the page with that mission pre-selected, the camera focused on the trajectory, and the timeline slider visible. The parameter is parsed from window.location.search at startup and matched against mission IDs in the data.