Skip to content

Kepler's Laws

Equal areas in equal times, drawn as wedges.

Intermediate / gravity / integration / conservation

A planet integrates around a star on an adjustable ellipse. The shaded wedges are swept in equal times, so they have equal areas - Kepler's second law as a picture - and the period readout feeds the third law's T-squared over a-cubed. Angular momentum conservation is what enforces both.

Tagged: ellipse, angular momentum, T-squared equals a-cubed, vis-viva

Live simulation

Equal-time wedges all sweep equal area: Kepler's second law as a picture.

Speed

Equal-time wedges all sweep equal area: Kepler's second law as a picture.

Mathematics

Kepler's laws in one integration: the orbit is an ellipse with the star at a focus (first law), the radius vector sweeps equal areas in equal times because angular momentum is conserved (second law), and T^2 / a^3 is the same constant for every orbit (third law).

r(theta) = a (1 - e^2) / (1 + e cos theta)

angular momentum L = r^2 dtheta/dt (conserved)

swept area rate = L / 2 => equal areas in equal times

The integration happens in polar coordinates with the radial acceleration written directly from the angular-momentum invariant - so the second law cannot be violated by numerical error, only confirmed. The wedges are drawn from the actual swept-area rate over fixed intervals: their equal sizes are the measurement.

How it works

Initial conditions come from the vis-viva equation at perihelion for the chosen eccentricity. The orbit overlay is the analytic ellipse; the planet's motion is integrated, and where the two agree is the point.

The period counter records one full revolution of theta, feeding the T^2/a^3 readout directly.

Implementation

src/engine/experiments/kepler-orbits.ts · 187 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, ThemeColors } from "../core/types";
3
4/**
5 * Kepler's laws, observed: a planet on an ellipse with adjustable
6 * eccentricity around a central star. Equal-time wedges make the second law
7 * (equal areas in equal times) visible, and the live numbers test the third.
8 * Integrated in Cartesian coordinates with semi-implicit Euler substeps -
9 * stable at perihelion for every eccentricity the slider offers.
10 */
11export class KeplerOrbitsExperiment extends BaseExperiment {
12 readonly id = "kepler-orbits";
13
14 private a = 150; // semi-major axis, px
15 private ecc = 0.5;
16 private x = 0;
17 private y = 0;
18 private vx = 0;
19 private vy = 0;
20 private mu: number;
21 private time = 0;
22 private lastPeriod = 0;
23 private periodClock = 0;
24 private sweptAreas: number[] = [];
25 private rNow = 0;
26
27 constructor(mu = 2_000_000) {
28 super();
29 this.mu = mu;
30 }
31
32 protected params(): ParameterDef[] {
33 return [
34 { key: "ecc", label: "Eccentricity", min: 0, max: 0.85, step: 0.01, defaultValue: 0.5 },
35 {
36 key: "showWedges",
37 label: "Equal-time wedges",
38 options: [
39 { value: "on", label: "On" },
40 { value: "off", label: "Off" },
41 ],
42 defaultValue: "on",
43 },
44 ];
45 }
46
47 protected onReset(): void {
48 this.ecc = this.num("ecc");
49 this.a = Math.min(this.width, this.height) * 0.32;
50 // Start at perihelion, moving purely tangentially (vis-viva).
51 this.x = this.a * (1 - this.ecc);
52 this.y = 0;
53 this.vx = 0;
54 this.vy = Math.sqrt((this.mu * (1 + this.ecc)) / (this.a * (1 - this.ecc)));
55 this.time = 0;
56 this.periodClock = 0;
57 this.sweptAreas = [];
58 this.rNow = this.x;
59 }
60
61 protected onParameterChange(key: string): void {
62 this.reset();
63 void key;
64 }
65
66 resize(width: number, height: number): void {
67 this.width = width;
68 this.height = height;
69 this.reset();
70 }
71
72 /** Angular momentum per unit mass (conserved). */
73 private angularMomentum(): number {
74 return this.x * this.vy - this.y * this.vx;
75 }
76
77 private radius(): number {
78 return Math.hypot(this.x, this.y);
79 }
80
81 protected onUpdate(dt: number): void {
82 const sub = dt / 10;
83 for (let s = 0; s < 10; s++) {
84 const r = this.radius();
85 const accel = -this.mu / (r * r * r);
86 this.vx += accel * this.x * sub;
87 this.vy += accel * this.y * sub;
88 this.x += this.vx * sub;
89 this.y += this.vy * sub;
90 }
91 this.time += dt;
92 this.periodClock += dt;
93 this.rNow = this.radius();
94 if (this.y >= 0 && this.vy > 0 && this.x > 0 && this.periodClock > 0.1) {
95 // Crossed +x moving upward: one revolution complete.
96 this.lastPeriod = this.periodClock;
97 this.periodClock = 0;
98 }
99 if (this.str("showWedges") === "on") {
100 const areaRate = this.angularMomentum() / 2;
101 this.sweptAreas.push(areaRate * dt);
102 if (this.sweptAreas.length > 64) this.sweptAreas.shift();
103 } else if (this.sweptAreas.length > 0) {
104 this.sweptAreas = [];
105 }
106 }
107
108 private focus(): { x: number; y: number } {
109 return { x: this.width / 2, y: this.height / 2 };
110 }
111
112 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
113 ctx.fillStyle = theme.bg;
114 ctx.fillRect(0, 0, this.width, this.height);
115 const focus = this.focus();
116 // Analytic ellipse overlay
117 ctx.strokeStyle = theme.line;
118 ctx.lineWidth = 1;
119 ctx.beginPath();
120 for (let deg = 0; deg <= 360; deg += 1) {
121 const th = (deg * Math.PI) / 180;
122 const rr = (this.a * (1 - this.ecc * this.ecc)) / (1 + this.ecc * Math.cos(th));
123 const px = focus.x + rr * Math.cos(th);
124 const py = focus.y - rr * Math.sin(th);
125 if (deg === 0) ctx.moveTo(px, py);
126 else ctx.lineTo(px, py);
127 }
128 ctx.stroke();
129 // Star at the focus
130 ctx.fillStyle = theme.accent;
131 ctx.beginPath();
132 ctx.arc(focus.x, focus.y, 6, 0, Math.PI * 2);
133 ctx.fill();
134 // Planet + radius vector + wedges
135 const px = focus.x + this.x;
136 const py = focus.y - this.y;
137 if (this.sweptAreas.length > 1) {
138 ctx.fillStyle = theme.accent;
139 let startAngle = Math.atan2(-this.y, this.x);
140 for (let i = this.sweptAreas.length - 1; i >= 0; i--) {
141 const area = this.sweptAreas[i];
142 const r = Math.max(4, this.rNow);
143 const dTheta = (2 * area) / (r * r);
144 const a1 = startAngle;
145 const a2 = startAngle - dTheta;
146 ctx.globalAlpha = 0.06 + 0.12 * (i / this.sweptAreas.length);
147 ctx.beginPath();
148 ctx.moveTo(focus.x, focus.y);
149 ctx.arc(focus.x, focus.y, r, a1, a2, true);
150 ctx.closePath();
151 ctx.fill();
152 startAngle = a2;
153 }
154 ctx.globalAlpha = 1;
155 }
156 ctx.strokeStyle = theme.fgSecondary;
157 ctx.beginPath();
158 ctx.moveTo(focus.x, focus.y);
159 ctx.lineTo(px, py);
160 ctx.stroke();
161 ctx.fillStyle = theme.fg;
162 ctx.beginPath();
163 ctx.arc(px, py, 4.5, 0, Math.PI * 2);
164 ctx.fill();
165 }
166
167 getMetrics() {
168 const t = this.lastPeriod > 0 ? this.lastPeriod : 0;
169 const ratio = t > 0 ? (t * t) / this.a ** 3 : 0;
170 return {
171 eccentricity: this.ecc.toFixed(2),
172 distance: `${(this.rNow / 10).toFixed(1)} Mm`,
173 period: `${t.toFixed(2)} s`,
174 t2_over_a3: ratio > 0 ? ratio.toExponential(3) : "measuring",
175 angMomentum: this.angularMomentum().toFixed(0),
176 };
177 }
178
179 describe(): string {
180 return `A planet on an e=${this.ecc.toFixed(2)} ellipse; the shaded wedges are swept in equal times, so they all have equal area - Kepler's second law as a picture.`;
181 }
182
183 entities(): number {
184 return 2;
185 }
186}
187

Performance

Time per tick
O(1) per tick
Space
The wedge list (64 samples)
Notes
Set eccentricity to zero and the wedges stay constant-width; raise it and watch the planet sprint through perihelion.

Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.