6
Kepler's Laws: Equal Areas, Elliptical Orbits
drag up/down to set eccentricity; tap '2nd planet' for the third law
idle
145 lines · vanilla
view source
// Kepler's laws on an analytic two-body orbit (Newton-solved Kepler equation).
// Law 2 drawn live: the area swept in a fixed dt window is a constant.
const MU = 1, A1 = 1, TSCALE = 2 * Math.PI / 7; // T1 = 7 real seconds
const WIN = 0.7; // swept-window, sim-time units
const NPATH = 240, CAP = 512;
const BTN_W = 110, BTN_H = 44, PAD = 12;
let W = 0, H = 0;
let ecc, M1, M2, E1, E2, second, simT;
let path1, path2, col1; // orbit polylines + colors
let ringX, ringY, ringT, head, count; // wedge ring buffer
let vmin, vmax;
function kepE(M, e, guess) {
let E = guess;
for (let i = 0; i < 8; i++) E -= (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
return E;
}
function buildPaths() {
vmin = Math.sqrt(MU * (2 / (A1 * (1 + ecc)) - 1 / A1));
vmax = Math.sqrt(MU * (2 / (A1 * (1 - ecc)) - 1 / A1));
for (let i = 0; i < NPATH; i++) {
const E = i / NPATH * 2 * Math.PI;
const b = Math.sqrt(1 - ecc * ecc);
path1[i * 2] = A1 * (Math.cos(E) - ecc); path1[i * 2 + 1] = A1 * b * Math.sin(E);
path2[i * 2] = 2 * A1 * (Math.cos(E) - ecc); path2[i * 2 + 1] = 2 * A1 * b * Math.sin(E);
const r = A1 * (1 - ecc * Math.cos(E));
const v = Math.sqrt(MU * (2 / r - 1 / A1));
const t = vmax > vmin ? (v - vmin) / (vmax - vmin) : 0.5;
col1[i] = `hsl(${(230 - 230 * t).toFixed(0)},90%,${(55 + t * 12).toFixed(0)}%)`;
}
}
function init({ width, height }) {
W = width; H = height;
ecc = 0.6; M1 = 1.2; M2 = 0.4; E1 = M1; E2 = M2;
second = false; simT = 0;
path1 = new Float32Array(NPATH * 2); path2 = new Float32Array(NPATH * 2);
col1 = new Array(NPATH);
ringX = new Float32Array(CAP); ringY = new Float32Array(CAP); ringT = new Float32Array(CAP);
head = 0; count = 0;
buildPaths();
}
function planetPos(a, e, E, out) {
out[0] = a * (Math.cos(E) - e); out[1] = a * Math.sqrt(1 - e * e) * Math.sin(E);
}
const P1 = [0, 0], P2 = [0, 0];
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
const btn = { x: W - BTN_W - PAD, y: H - BTN_H - PAD, w: BTN_W, h: BTN_H };
for (const c of input.consumeClicks()) {
if (c.x >= btn.x && c.x <= btn.x + btn.w && c.y >= btn.y && c.y <= btn.y + btn.h)
second = !second;
}
// drag vertically anywhere else -> eccentricity 0..0.85
if (input.mouseDown && !(input.mouseX >= btn.x && input.mouseY >= btn.y - 8)) {
const e2 = Math.max(0, Math.min(0.85, (1 - input.mouseY / H) * 1.05));
if (Math.abs(e2 - ecc) > 0.002) {
ecc = e2; buildPaths(); head = 0; count = 0; // re-launch wedge
}
}
// advance orbits
const h = Math.min(dt, 0.05) * TSCALE;
simT += h;
M1 += h * Math.sqrt(MU / (A1 ** 3));
M2 += h * Math.sqrt(MU / ((2 * A1) ** 3));
const m1 = M1 % (2 * Math.PI), m2 = M2 % (2 * Math.PI);
E1 = kepE(m1, ecc, m1 + ecc * Math.sin(m1));
E2 = kepE(m2, ecc, m2 + ecc * Math.sin(m2));
planetPos(A1, ecc, E1, P1); planetPos(2 * A1, ecc, E2, P2);
const r1 = Math.hypot(P1[0], P1[1]);
const v1 = Math.sqrt(MU * (2 / r1 - 1 / A1));
// wedge ring buffer
ringX[head] = P1[0]; ringY[head] = P1[1]; ringT[head] = simT;
head = (head + 1) % CAP; if (count < CAP) count++;
// layout: fit the active system
const amax = (second ? 2 : 1) * A1 * (1 + ecc);
const sc = (Math.min(W, H) / 2 - 28) / amax;
const fx = W / 2 + (second ? 2 : 1) * A1 * ecc * sc * 0.5, fy = H / 2; // focus (Sun)
ctx.fillStyle = "#05060c"; ctx.fillRect(0, 0, W, H);
// ellipse outlines (faint) + speed-colored trail for planet 1
ctx.lineWidth = 1; ctx.strokeStyle = "rgba(140,160,210,0.18)";
if (second) {
ctx.beginPath();
for (let i = 0; i <= NPATH; i++) {
const j = i % NPATH, x = fx + path2[j * 2] * sc, y = fy - path2[j * 2 + 1] * sc;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
}
ctx.lineWidth = 2.2; ctx.lineCap = "round";
for (let i = 0; i < NPATH; i++) {
const j = (i + 1) % NPATH;
ctx.strokeStyle = col1[i];
ctx.beginPath();
ctx.moveTo(fx + path1[i * 2] * sc, fy - path1[i * 2 + 1] * sc);
ctx.lineTo(fx + path1[j * 2] * sc, fy - path1[j * 2 + 1] * sc);
ctx.stroke();
}
// swept-area wedge over the last WIN sim-seconds
let area = 0, oldest = -1;
ctx.fillStyle = "rgba(255,210,90,0.30)";
ctx.beginPath(); ctx.moveTo(fx, fy);
for (let i = count - 1; i >= 0; i--) {
const idx = (head - 1 - i + 2 * CAP) % CAP;
if (simT - ringT[idx] > WIN) continue;
if (oldest < 0) oldest = idx;
ctx.lineTo(fx + ringX[idx] * sc, fy - ringY[idx] * sc);
}
ctx.closePath(); ctx.fill();
if (oldest >= 0) {
let px = ringX[oldest], py = ringY[oldest], started = false;
for (let i = count - 1; i >= 0; i--) {
const idx = (head - 1 - i + 2 * CAP) % CAP;
if (simT - ringT[idx] > WIN) continue;
if (started) area += 0.5 * (px * ringY[idx] - py * ringX[idx]);
px = ringX[idx]; py = ringY[idx]; started = true;
}
}
area = Math.abs(area);
// foci: Sun at one, empty focus ghosted
const c1 = A1 * ecc;
ctx.strokeStyle = "rgba(160,180,230,0.4)"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(fx - 2 * c1 * sc, fy, 4, 0, Math.PI * 2); ctx.stroke();
const sg = ctx.createRadialGradient(fx, fy, 1, fx, fy, 14);
sg.addColorStop(0, "#fff7c0"); sg.addColorStop(0.5, "#ffb627"); sg.addColorStop(1, "rgba(255,120,20,0)");
ctx.fillStyle = sg; ctx.beginPath(); ctx.arc(fx, fy, 14, 0, Math.PI * 2); ctx.fill();
// planets
const t1 = vmax > vmin ? (v1 - vmin) / (vmax - vmin) : 0.5;
ctx.fillStyle = `hsl(${(230 - 230 * t1).toFixed(0)},95%,70%)`;
ctx.beginPath(); ctx.arc(fx + P1[0] * sc, fy - P1[1] * sc, 6, 0, Math.PI * 2); ctx.fill();
if (second) {
ctx.fillStyle = "#9ad6a8";
ctx.beginPath(); ctx.arc(fx + P2[0] * sc, fy - P2[1] * sc, 5, 0, Math.PI * 2); ctx.fill();
}
// HUD
const T1 = 7.0, T2 = 7.0 * Math.pow(2, 1.5);
ctx.fillStyle = "rgba(0,0,0,0.55)"; ctx.fillRect(PAD, PAD, 212, second ? 100 : 68);
ctx.font = "12px monospace"; ctx.fillStyle = "#dfe7ff"; ctx.textAlign = "left";
ctx.fillText(`e = ${ecc.toFixed(2)} v = ${v1.toFixed(2)}`, PAD + 10, PAD + 18);
ctx.fillStyle = "#ffd98a";
ctx.fillText(`dA/dt = ${(area / WIN).toFixed(3)} ← constant`, PAD + 10, PAD + 36);
ctx.fillStyle = "#dfe7ff";
ctx.fillText(`L/2 = ${(0.5 * Math.sqrt(MU * A1 * (1 - ecc * ecc))).toFixed(3)}`, PAD + 10, PAD + 54);
if (second) {
ctx.fillStyle = "#9ad6a8";
ctx.fillText(`T₁=${T1.toFixed(1)}s T₂=${T2.toFixed(1)}s`, PAD + 10, PAD + 72);
ctx.fillText(`T²/a³: ${(T1 * T1 / 1).toFixed(0)} = ${(T2 * T2 / 8).toFixed(0)} ✓`, PAD + 10, PAD + 90);
}
ctx.fillStyle = "rgba(180,200,240,0.55)"; ctx.font = "11px system-ui, sans-serif";
ctx.fillText("drag up/down: eccentricity", PAD, H - PAD - 4);
// button
ctx.fillStyle = second ? "rgba(80,160,110,0.85)" : "rgba(0,0,0,0.65)";
ctx.fillRect(btn.x, btn.y, btn.w, btn.h);
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.strokeRect(btn.x + 0.5, btn.y + 0.5, btn.w - 1, btn.h - 1);
ctx.fillStyle = "#fff"; ctx.font = "bold 13px system-ui, sans-serif";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(second ? "2nd planet ✓" : "2nd planet", btn.x + btn.w / 2, btn.y + btn.h / 2);
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
}
Comments (0)
Log in to comment.