Physics
This document explains the physical formulas used by the visualisations and which aspects of each view are physically accurate vs. stylised.
Quick summary
| View | Orbital speeds | Sizes | Distances | Notes |
|---|---|---|---|---|
| solar-system | Accurate | Stylised | Stylised | Compressed for visibility |
| scale | Accurate | Real | Real | Planets are tiny — use markers |
| galaxy | Accurate | Stylised | Stylised | Flat rotation curve via shader |
| universe | — | Stylised | Stylised | Named objects at real RA/Dec |
All per-planet and per-moon astronomical data lives in
src/data/planets.js and
src/data/moons.js. Physical constants are in
src/data/constants.js. The formulas below are
implemented in src/physics/formulas.js.
Orbital periods (Kepler)
We use the sidereal orbital period of each planet directly (NASA factsheets).
At 1× simulation speed, Earth completes one full orbit in
EARTH_SIM_PERIOD_SEC = 20 seconds of real time. Every other planet orbits
at the same ratio relative to Earth as it does in reality — Mercury zips
around every 4.8 s, Neptune crawls for 3 300 s.
ω = 2π / (EARTH_SIM_PERIOD_SEC × (period_days / 365.25))
This is equivalent to Kepler’s 3rd law (T² ∝ a³) if you already know the orbital period — we’re not computing periods from gravitation, we’re just re-using the empirical periods.
Elliptical orbits
Unused in the shipped views but kept as a reference formula: for a body on
an ellipse with semi-major axis a and eccentricity e, the Sun-to-body
distance as a function of true anomaly θ is
r(θ) = a(1 - e²) / (1 + e·cos θ)
Conservation of angular momentum (Kepler’s 2nd law) gives dθ/dt ∝ 1/r², so
the body speeds up near perihelion and slows down at aphelion. This is
implemented in keplerRadius() in src/physics/formulas.js.
Galactic rotation
The naive Keplerian expectation for the Milky Way (ω ∝ 1/r^1.5) is wrong — stars far from the galactic centre orbit much faster than their visible mass would predict. This discrepancy is the primary evidence for dark matter: the Milky Way’s rotation curve is flat out to large radii.
We approximate this with a constant circular velocity:
v_c ≈ 220 km/s (essentially constant across the disk)
ω(r) = v_c / r
Inner stars have higher angular velocity; outer stars lag. Stars at the
Sun’s radius (~26 000 ly) orbit at the Sun’s own rate, so in the galaxy view
they appear nearly stationary while inner stars whip around the bulge. This
is implemented in the vertex shader in galaxy.html (see the starUniforms
and vertex shader blocks near the top of the script).
Special relativity (reference)
For completeness, the shipped views don’t require relativistic physics, but
src/physics/formulas.js exposes lorentzFactor(β) and relativisticTravel()
in case a future view needs them:
γ = 1 / √(1 - β²) (β = v/c)
t_ship = t_earth / γ (time dilation)
At β = 0.99, γ ≈ 7.09 — a 10-year Earth-frame trip is 1.4 years for the crew.
Scale and compression
See scales.md for the specific compression factors used on each view and why “true proportional” visualisation doesn’t fit on a screen.
Lunar phase model
The phase angle (used for illumination, phase name, and the next-phase scanner) is computed from the same Meeus-abridged Brown’s-series longitude that drives the Moon’s ecliptic position (see Lunar geocentric position below):
- Phase angle:
φ = wrap2π(λ_moon − λ_sun), whereλ_moonis the apparent geocentric ecliptic longitude from §47 andλ_sun = λ_earth_helio + πis the Sun’s apparent geocentric longitude derived from Earth’s Keplerian heliocentric longitude. - 0 = New, π/2 = First Quarter, π = Full, 3π/2 = Last Quarter.
- Illuminated fraction:
(1 − cos φ) / 2
Accuracy: a few minutes on the time of any new/full/quarter phase across
decades, vs ±0.5 day for the linear single-term model that this replaced.
nextLunarPhase in src/physics/events.js runs Newton iteration on φ
(initial step from the synodic-rate estimate, ±3-day clamp on subsequent
corrections), converging to ~1e-9 rad in 3–4 iterations. Functions in
src/physics/moon.js.
Lunar geocentric position (Meeus abridged)
For eclipse detection and a Moon that visibly crosses the ecliptic at the right inclination, the simple linear model is not enough — the Moon’s periodic perturbations push its real ecliptic latitude away from zero by up to ±5°, and that latitude is exactly what determines whether a new/full moon also produces an eclipse. We use the leading 6 longitude + 6 latitude
- 6 distance periodic terms from Meeus, Astronomical Algorithms, 2nd ed., §47:
- Mean elements (
L',D,M,M',F) advance linearly withT = (JD − 2451545.0) / 36525. Σ_l(longitude perturbation) sums six sin terms — equation of center, evection, variation, …Σ_b(latitude perturbation) sums six sin terms — main inclination, …Σ_r(distance perturbation) sums six cos terms; coefficients in km.- Apparent geocentric longitude:
λ = L' + Σ_l - Apparent geocentric latitude:
β = Σ_b - Apparent geocentric distance:
Δ = 385000.56 km + Σ_r
Terms scaled by Earth’s orbital eccentricity e(T) = 1 − 0.002516·T − 0.0000074·T²
(those involving M, the Sun’s anomaly) are flagged in the table by an
ePow column.
Accuracy: ~5 arcmin on (λ, β) and a few hundred km on Δ across
decades — well inside the ~1.5° latitude window that determines whether a
lunation produces an eclipse, and within ~1% of the published 356 000 –
407 000 km perigee/apogee bounds. Coefficients in src/data/moon.js,
evaluator lunarPosition(jd) in src/physics/moon.js.
solar-system.html’s Moon uses lunarPosition directly: longitude and
latitude both feed the schematic position so eclipse instants visibly
collapse to the Sun–Earth axis (β ≈ 0) while ordinary new/full moons sit
visibly above or below it (|β| ~ a few degrees). Distance stays at the
schematic Earth–Moon distance for visibility — true distance is invisible
at this view’s compression.
Eclipse detection
A lunar eclipse happens at full moon when the Moon enters Earth’s shadow cone; a solar eclipse happens at new moon when the Moon crosses the Sun-Earth line. Both reduce to the same geometric question: is the Moon’s ecliptic latitude small enough at this lunation?
nextLunarEclipse(jd) and nextSolarEclipse(jd) in
src/physics/events.js walk lunations using the closed-form
nextLunarPhase scanner, then for each candidate:
- Refine the mean-longitude new/full estimate to true-ephemeris accuracy
by Newton iteration on
(λ_moon − (λ_earth_helio + π)). Three to four steps; per-step clamp prevents a Newton jump from crossing a neighbouring lunation. - Reject the lunation if
|β| > 1.5°— the Moon misses the shadow / Sun-disc cone entirely. The 1.5° threshold is generous: it covers the penumbra geometry for lunar eclipses (Meeus §54) and the Moon-disc + Sun-disc + solar-parallax window for solar eclipses (Meeus §51).
Accuracy: matches NASA’s Five-Millennium Catalog of Solar / Lunar
Eclipses within ~1 hour for the 2017–2024 reference set
(test/eclipses.test.js). The error budget is dominated by truncating
Σ_l / Σ_b to six terms — adding more terms would cut it to seconds,
but the visualisation-only feature does not require it.
Solar declination and seasons (#82)
The latitude on Earth where the Sun is directly overhead at local noon — the solar declination δ — is a closed-form expression of Earth’s heliocentric longitude:
δ = arcsin(sin(ε) · sin(L_sun))
where ε = Earth’s obliquity (~23.44°, stored as axialTiltRad in
src/data/planets.js) and L_sun is the Sun’s geocentric ecliptic
longitude (Earth’s heliocentric longitude + π, wrapped to [0, 2π)).
δ oscillates between ±ε across a year. Implemented in
src/physics/seasons.js.
The four cardinal solar events (March equinox, June solstice,
September equinox, December solstice) are when L_sun crosses 0, π/2, π,
3π/2 respectively. Found by Newton iteration on the longitude residual
in src/physics/events.js
(nextSolarEvent), mirroring nextLunarPhase. Accuracy: better than
~0.01° / a few minutes within the J2000-fit window (1800–2050).
Season name at any JD is the one that started at the most recent cardinal event (Northern-hemisphere convention). The Southern hemisphere sees the inverted season (handled in the UI tooltip, not the physics).
Equilibrium tides (src/physics/tides.js)
The visualization uses the second-degree zonal term of the lunar + solar tidal potential:
h(rHat; moonHat, sunHat) = (3·(rHat·moonHat)² − 1) / 2 + k_S · (3·(rHat·sunHat)² − 1) / 2
with k_S = M_sun · r_moon³ / (M_moon · r_sun³) ≈ 0.46 — the ratio of solar to lunar tidal
force. This is a unit-amplitude shape function; the renderer multiplies by a tunable display
constant (TIDE_DISPLAY_AMPLIFICATION) to exaggerate the millimetre-scale real bulges into
something visible at the display scale of the Earth mesh.
Real equilibrium tides are ~50 cm (lunar) and ~20 cm (solar). Coastal tides diverge from the equilibrium model because of basin shape, resonance, Coriolis, and friction — the readout’s tooltip says so. This is a teaching diorama, not a tide-prediction tool.
The two-bulge geometry follows from the gradient-of-potential model: the cos² term peaks at
both rHat·moonHat = +1 (sub-lunar) and rHat·moonHat = −1 (antipodal). A naïve
“pull-toward-Moon” force model produces a single bulge and is wrong.
For the on-surface force arrows, the renderer uses the gradient form a = 3·(rHat·bodyHat)·bodyHat − rHat projected onto the local tangent plane (the part perpendicular to rHat). This horizontal component is what physically drives water flow; it vanishes at the sub-body point and the antipodal point and peaks at ~45° in the rHat-bodyHat plane.