The brief
The commission: a hands-on field guide to orbital mechanics in the explorable-explanation tradition. Five chapters, each a working instrument: Newton's cannonball, the shape of speed, the Hohmann transfer, the Mars window, and a rendezvous. One hard law governed everything: understanding comes from hands, never from scroll position. No scrolljacking, no step-through theater. Scrolling moves the reader; only dragging moves the sky.
The argument against scrolljacking
A scroll-driven explainer decides for you when things happen, which means it is a film. Films are fine, but nobody ever learned to ride a bicycle from one. The claim this page stakes is that orbital mechanics is a skill feeling: the trade of speed here for altitude there has to be felt in the fingers to lodge in the head. So every model on the page changes state only through pointer, touch, keyboard, or an explicit button. There is exactly one scroll-linked behavior on the whole site: sections fade in once as they arrive, a purely cosmetic reveal that touches no simulation state and disappears entirely under prefers-reduced-motion.
The physics core
Everything orbital reduces to two functions. The first reads an entire conic section out of one position and one velocity: eccentricity vector, semi-major axis, the two apsides, the period. This is what lets Chapter 2 redraw the whole ellipse the instant you drag the arrow: the future is algebra, not simulation.
Listing 1 · orbit.js: the whole orbit from one state vector
export function conicFromState(mu, r, v) {
const rm = Math.hypot(r.x, r.y);
const v2 = v.x * v.x + v.y * v.y;
const rv = r.x * v.x + r.y * v.y;
const h = r.x * v.y - r.y * v.x; // specific angular momentum (z)
const ex = ((v2 - mu / rm) * r.x - rv * v.x) / mu;
const ey = ((v2 - mu / rm) * r.y - rv * v.y) / mu;
const e = Math.hypot(ex, ey);
const energy = v2 / 2 - mu / rm;
const p = h * h / mu; // semi-latus rectum
const a = -mu / (2 * energy); // negative for hyperbolic
const omega = Math.atan2(ey, ex); // direction of periapsis
const rp = p / (1 + e);
const ra = e < 1 ? p / (1 - e) : Infinity;
const T = e < 1 ? 2 * Math.PI * Math.sqrt(a * a * a / mu) : Infinity;
let nu = Math.atan2(r.y, r.x) - omega;
nu = Math.atan2(Math.sin(nu), Math.cos(nu));
return { e, a, p, h, omega, rp, ra, T, energy, nu };
}
The second function moves ships. It is velocity Verlet, a symplectic integrator: it conserves the orbit's energy over long runs instead of quietly spiraling inward the way naive Euler integration does. That is why a closed orbit in Chapter 1 can loop for minutes and come home every lap.
Listing 2 · orbit.js: velocity Verlet, the honest clock
export function stepLeapfrog(mu, s, dt) {
const a1 = accel(mu, s.r);
const vhx = s.v.x + a1.x * dt / 2;
const vhy = s.v.y + a1.y * dt / 2;
s.r.x += vhx * dt;
s.r.y += vhy * dt;
const a2 = accel(mu, s.r);
s.v.x = vhx + a2.x * dt / 2;
s.v.y = vhy + a2.y * dt / 2;
}
Real constants throughout: μEarth = 398,600.4418 km³/s², μSun = 1.327×10¹¹ km³/s², Earth radius 6,371 km, Mars orbit 227.9 million km. The numbers the copy quotes (7.73 km/s circular at the cannon's altitude, a 3.43 km/s Hohmann optimum, the 44.3 degree Mars phase angle, the 780 day synodic period) all fall out of these constants in the code; none are hand-typed theater.
The chalk
The visual thesis is a drafting table at night: a precise printed grid underneath, hand-drawn chalk on top. The grid is ruled with crisp one-pixel lines. Every chalk stroke, by contrast, runs through a jitter pass seeded per shape: the wobble is stable frame to frame (no shimmer), but no line is ever ruler-straight. A soft wide underpass beneath each stroke leaves chalk dust rather than neon glow.
Listing 3 · chalk.js: strokes that remember a hand drew them
jittered(pts, seed, amp) {
const out = new Array(pts.length);
for (let i = 0; i < pts.length; i++) {
out[i] = {
x: pts[i].x + (smoothNoise(seed, i * 0.42) - 0.5) * 2 * amp,
y: pts[i].y + (smoothNoise(seed + 41.7, i * 0.42) - 0.5) * 2 * amp,
};
}
return out;
}
stroke(pts, opt = {}) {
const { width = 2, color = INK.chalk, alpha = 0.9, seed = 7,
jitter = 1.0, dust = 0.16, close = false } = opt;
if (!pts || pts.length < 2) return;
const c = this.ctx;
let p = this.jittered(pts, seed, jitter);
if (close) p = p.concat([p[0]]);
c.save();
c.lineJoin = 'round'; c.lineCap = 'round';
if (dust > 0) { // the chalk-dust underpass
c.globalAlpha = alpha * dust; c.strokeStyle = color; c.lineWidth = width * 2.2;
this.path(p); c.stroke();
}
c.globalAlpha = alpha; c.strokeStyle = color; c.lineWidth = width;
this.path(p); c.stroke();
c.restore();
}
Labels are set in Space Mono with a plate-colored knockout stroke behind them, so they stay readable when they cross the grid or a hatched planet, the way a textbook pastes its captions over the figure. Leader lines get a dot at the anchor and a short horizontal elbow, which is the oldest trick in technical illustration and still the best one.
The hands
Every draggable on the page is a real button, absolutely positioned over the canvas at the handle's location, 44 pixels square for touch. The canvas drawing underneath is just its portrait. That one decision buys the whole accessibility story: each handle is focusable, carries role="slider" with a live aria-valuenow and a human aria-valuetext, and answers to arrow keys with Shift for coarse moves. Dragging the drawn art directly also works: the canvas forwards a pointer-down to the nearest handle.
Listing 4 · handles.js: the keyboard is a hand too
b.addEventListener('keydown', (ev) => {
const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'Home', 'End', 'Enter', ' '];
if (!keys.includes(ev.key)) return;
const handled = spec.onKey(ev.key, ev.shiftKey);
if (handled) {
ev.preventDefault();
spec.onDragEnd?.();
inst.markUsed(spec.id);
inst.requestRender();
}
});
Numerical honesty in odd corners
One subtlety worth recording. On a near-circular orbit the eccentricity vector is numerically arbitrary, so true anomaly is a lie of roundoff. The transfer chapter's burn trigger therefore compares world angles, not anomalies, and computes angular rate as h/r², which is exact on any conic:
Listing 5 · ch3_transfer.js: firing the node geometrically
const thNode = this.nuNode + this.conic.omega;
const thShip = Math.atan2(this.ship.r.y, this.ship.r.x);
const ahead = ((thNode - thShip) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
const h = this.ship.r.x * this.ship.v.y - this.ship.r.y * this.ship.v.x;
const rate = Math.abs(h) / (Math.hypot(this.ship.r.x, this.ship.r.y) ** 2);
if (ahead < rate * step * 1.5 || ahead > 2 * Math.PI - 0.02) this._executeBurn();
Rest, motion, and mercy
The page composes itself at rest: every instrument draws one complete, labeled figure before anyone touches it, and nothing orbits on its own. Simulations begin only on interaction (a drag, a Run button, a Launch). IntersectionObserver pauses any running loop the moment its plate leaves the viewport. Under prefers-reduced-motion the reveals vanish, the stamp animation is cut, and running clocks render in discrete hops (about three frames every two seconds) instead of smooth flight, while staying fully functional.
Asset provenance
Fully procedural. Every figure, planet, orbit, gauge, stamp, and seal on the site is drawn at runtime by canvas 2D or inline SVG; there are no images, no textures, no photographs, and nothing generated elsewhere. The only external requests are the two Google Fonts families (Instrument Serif and Space Mono). The favicon is an inline SVG data URI.
What each pass caught
| Pass | Top findings fixed | Deliberate upgrade |
|---|---|---|
| 1 | Newton plate was mostly dead space (Earth rescaled to command the frame); Chapter 2's default orbit read as nested circles (new eccentric default, e near 0.47); hint labels clipped at plate edges; the rendezvous opened wrongly zoomed into proximity view with kinked polylines. | The Society's warm-up arcs and seeded ghost ellipses, so every plate is composed before the first touch. |
| 2 | Label collisions (Mons Newton leader, Mars against the arrival mark, parking-orbit tag under the node); circular-orbit burn trigger rebuilt geometrically; one-affordance-at-a-time hints in Chapter 2. | The quartermaster's delta-v gauge in Chapter 3: budget arc, Hohmann tick, needle for spent fuel, planned burn hatched in ahead of the needle. |
| 3 | Driven-browser interaction audit (keyboard on every handle, full transfer flown, launch and burns exercised) with the console clean; mobile recomposition checks at 390 px; widow and dash sweeps on all copy. | Milestone marginalia: the cannon plate now keeps a running tally of distance fallen without landing once a shot closes its first orbit. |
Colophon
Static HTML, one stylesheet, eight small ES modules, no build step, no framework. Set in Instrument Serif and Space Mono. Written, drawn, argued over, and stamped by the Apoapsis Society, which does not exist, about physics, which does.
Return to the manual and go fail Exercise 005 with dignity.