Physics • Education
Orbital Mechanics
Interactive Kepler orbital mechanics simulator. Explore elliptical orbits, eccentricity, and the laws of planetary motion with real-time physics.
Simulation
Yellow/orange glowing body = central star. Colored bodies = orbiting planets. The dotted line shows the orbital path, foci mark the orbit geometry, and the blue arrow shows velocity direction.
Controls
Orbital Parameters
Semi-Major Axis (a)
150 km
Eccentricity (e)
0.50
Orbital Period (T)
1.00 year
Current Velocity
29.8 km/s
Distance from Focus
100 km
Specific Orbital Energy
-0.50 km²/s²
Periapsis (r_p)
75 km
Apoapsis (r_a)
225 km
All values update in real-time. Vis-viva equation: v² = GM(2/r − 1/a). Period: T² = 4π²a³/GM (Kepler's Third Law).
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Constants (all in SI-ish units for simplicity)
const G = 6.674e-11; // Gravitational constant (scaled for visualization)
const AU_KM = 1.496e8; // 1 AU in km (scaled down for canvas)
const EARTH_MASS = 5.972e24;
const SOLAR_MASS = 1.989e30;
const SPEED_OF_LIGHT = 3e8;
// Scale factors for visualization
const SCALE_POS = 1e-11; // Scale positions to canvas
const SCALE_VEL = 1e-3; // Scale velocities for arrows
const TIME_STEP = 0.01; // Years per frame (scaled)
// State
let state = {
mode: 'single',
eccentricity: 0.5,
semiMajor: 150, // km (relative units)
mass: 1.0, // Solar masses
speedMult: 1.0,
isRunning: false,
showOrbitPath: true,
showFoci: true,
showVelocity: true,
showSweepArea: false,
showLabels: true,
showStarfield: false,
showGrid: false,
time: 0,
bodies: []
};
// Canvas setup
const canvas = document.getElementById('orbitCanvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Initialize bodies for current mode
function initializeBodies() {
state.bodies = [];
if (state.mode === 'single') {
const body = {
x: 0, y: 0,
vx: 0, vy: 0,
mass: 1.0,
radius: 8,
color: '#FFD700',
glowing: true,
trails: []
};
state.bodies.push(body);
// Planet
const planet = createPlanet(state.semiMajor, state.eccentricity, 0.1, 4, '#3498db');
state.bodies.push(planet);
} else if (state.mode === 'solar') {
const sun = {
x: 0, y: 0,
vx: 0, vy: 0,
mass: 1.0,
radius: 10,
color: '#FFD700',
glowing: true,
trails: []
};
state.bodies.push(sun);
// Mercury, Venus, Earth, Mars
const planets = [
{ a: 60, e: 0.2, color: '#8B7355', r: 2 },
{ a: 100, e: 0.01, color: '#FFA500', r: 3 },
{ a: 150, e: 0.02, color: '#3498db', r: 3.5 },
{ a: 200, e: 0.1, color: '#E27B58', r: 2.5 }
];
planets.forEach((p, i) => {
const planet = createPlanet(p.a, p.e, 0.5 + i * 0.2, p.r, p.color);
state.bodies.push(planet);
});
} else if (state.mode === 'binary') {
const star1 = {
x: -50, y: 0,
vx: 0, vy: 20,
mass: 0.5,
radius: 6,
color: '#FFD700',
glowing: true,
trails: []
};
const star2 = {
x: 50, y: 0,
vx: 0, vy: -20,
mass: 0.5,
radius: 6,
color: '#FF6347',
glowing: true,
trails: []
};
state.bodies.push(star1, star2);
} else if (state.mode === 'transfer') {
const sun = {
x: 0, y: 0,
vx: 0, vy: 0,
mass: 1.0,
radius: 10,
color: '#FFD700',
glowing: true,
trails: []
};
state.bodies.push(sun);
// Circular orbit 1
const p1 = createPlanet(100, 0, 0, 3.5, '#3498db');
state.bodies.push(p1);
// Circular orbit 2
const p2 = createPlanet(200, 0, Math.PI, 3.5, '#E27B58');
state.bodies.push(p2);
// Transfer orbit (Hohmann)
const p3 = createPlanet(150, 0.333, 0, 2.5, '#00FF00');
p3.isTransfer = true;
state.bodies.push(p3);
}
state.time = 0;
}
function createPlanet(a, e, phase, radius, color) {
const r_p = a * (1 - e);
const x = r_p * Math.cos(phase);
const y = r_p * Math.sin(phase);
// Vis-viva: v = sqrt(GM(2/r - 1/a))
const r = Math.sqrt(x * x + y * y);
const vMagnitude = Math.sqrt(state.mass * (2 / r - 1 / a));
// Velocity perpendicular to radius
const vx = -vMagnitude * Math.sin(phase) * state.speedMult;
const vy = vMagnitude * Math.cos(phase) * state.speedMult;
return {
x, y, vx, vy,
a, e, phase,
mass: 0.01,
radius,
color,
glowing: false,
trails: []
};
}
// Physics integration (Leapfrog)
function updatePhysics() {
const n = state.bodies.length;
// Half-step velocity
for (let i = 0; i < n; i++) {
const body = state.bodies[i];
const acc = getAcceleration(i);
body.vx += acc.x * TIME_STEP * 0.5 * state.speedMult;
body.vy += acc.y * TIME_STEP * 0.5 * state.speedMult;
}
// Full-step position
for (let i = 0; i < n; i++) {
const body = state.bodies[i];
body.x += body.vx * TIME_STEP * state.speedMult;
body.y += body.vy * TIME_STEP * state.speedMult;
// Trail
body.trails.push({x: body.x, y: body.y});
if (body.trails.length > 500) body.trails.shift();
}
// Half-step velocity again
for (let i = 0; i < n; i++) {
const body = state.bodies[i];
const acc = getAcceleration(i);
body.vx += acc.x * TIME_STEP * 0.5 * state.speedMult;
body.vy += acc.y * TIME_STEP * 0.5 * state.speedMult;
}
state.time += TIME_STEP;
}
function getAcceleration(bodyIndex) {
const body = state.bodies[bodyIndex];
let ax = 0, ay = 0;
for (let i = 0; i < state.bodies.length; i++) {
if (i === bodyIndex) continue;
const other = state.bodies[i];
const dx = other.x - body.x;
const dy = other.y - body.y;
const r = Math.sqrt(dx * dx + dy * dy);
if (r > 0.1) {
const a = (state.mass * other.mass) / (r * r * r);
ax += a * dx;
ay += a * dy;
}
}
return { x: ax, y: ay };
}
// Rendering
function drawStarfield() {
if (!state.showStarfield) return;
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
for (let i = 0; i < 200; i++) {
const x = (Math.sin(i * 12.9898) * 43758.5453) % canvas.width;
const y = (Math.cos(i * 78.233) * 43758.5453) % canvas.height;
const size = Math.random() * 0.5;
ctx.fillRect(x, y, size, size);
}
}
function drawGrid() {
if (!state.showGrid) return;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
const spacing = 50;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let x = centerX % spacing; x < canvas.width; x += spacing) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = centerY % spacing; y < canvas.height; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
function canvasCoords(x, y) {
return {
x: canvas.width / 2 + x / SCALE_POS,
y: canvas.height / 2 - y / SCALE_POS
};
}
function drawOrbitPath(body) {
if (!state.showOrbitPath || !body.a) return;
const a = body.a;
const e = body.e;
const centerX = canvas.width / 2 - (a * e) / SCALE_POS;
const centerY = canvas.height / 2;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
let firstPoint = true;
for (let theta = 0; theta < Math.PI * 2; theta += 0.05) {
const r = (a * (1 - e * e)) / (1 + e * Math.cos(theta));
const px = centerX + (r * Math.cos(theta)) / SCALE_POS;
const py = centerY + (r * Math.sin(theta)) / SCALE_POS;
if (firstPoint) {
ctx.moveTo(px, py);
firstPoint = false;
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
ctx.stroke();
ctx.setLineDash([]);
// Draw foci
if (state.showFoci) {
const focusX = (a * e) / SCALE_POS;
ctx.fillStyle = 'rgba(255, 200, 100, 0.5)';
ctx.beginPath();
ctx.arc(centerX - focusX, centerY, 3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(centerX + focusX, centerY, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawBodies() {
state.bodies.forEach((body, i) => {
// Trail
if (body.trails.length > 1) {
ctx.strokeStyle = body.color.replace(')', ', 0.3)').replace('rgb', 'rgba');
ctx.lineWidth = 1;
ctx.beginPath();
const first = canvasCoords(body.trails[0].x, body.trails[0].y);
ctx.moveTo(first.x, first.y);
for (let j = 1; j < body.trails.length; j++) {
const pt = canvasCoords(body.trails[j].x, body.trails[j].y);
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
}
// Draw orbit path for single planet in single mode
if (state.mode === 'single' && i === 1) {
drawOrbitPath(body);
} else if (state.mode === 'solar' && i > 0) {
drawOrbitPath(body);
} else if (state.mode === 'transfer' && i > 0) {
drawOrbitPath(body);
}
// Body
const pos = canvasCoords(body.x, body.y);
if (body.glowing) {
const grad = ctx.createRadialGradient(pos.x, pos.y, body.radius * 0.5, pos.x, pos.y, body.radius * 2);
grad.addColorStop(0, body.color);
grad.addColorStop(1, 'rgba(255, 215, 0, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(pos.x, pos.y, body.radius * 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = body.color;
ctx.beginPath();
ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
ctx.fill();
// Velocity vector
if (state.showVelocity && (body.mass < 0.5 || state.mode === 'binary')) {
const vLen = Math.sqrt(body.vx * body.vx + body.vy * body.vy);
if (vLen > 0.01) {
const arrowLen = 30;
const ex = pos.x + (body.vx / vLen) * arrowLen;
const ey = pos.y - (body.vy / vLen) * arrowLen;
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
ctx.lineTo(ex, ey);
ctx.stroke();
// Arrow head
const headLen = 6;
const angle = Math.atan2(ey - pos.y, ex - pos.x);
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - headLen * Math.cos(angle - Math.PI / 6), ey - headLen * Math.sin(angle - Math.PI / 6));
ctx.moveTo(ex, ey);
ctx.lineTo(ex - headLen * Math.cos(angle + Math.PI / 6), ey - headLen * Math.sin(angle + Math.PI / 6));
ctx.stroke();
}
}
// Label
if (state.showLabels && body.a) {
ctx.fillStyle = '#e8e0d5';
ctx.font = 'bold 11px "DM Mono"';
ctx.textAlign = 'left';
const labels = ['Sun', 'Mercury', 'Venus', 'Earth', 'Mars', 'Star1', 'Star2', 'Planet1', 'Planet2', 'Transfer'];
ctx.fillText(labels[i] || `Body${i}`, pos.x + 15, pos.y - 5);
}
});
}
function render() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawStarfield();
drawGrid();
drawBodies();
}
// Update stats
function updateStats() {
if (!state.bodies[1]) return;
const body = state.bodies[1];
if (!body.a) {
document.getElementById('statA').textContent = '—';
document.getElementById('statE').textContent = '—';
document.getElementById('statT').textContent = '—';
document.getElementById('statV').textContent = '—';
document.getElementById('statR').textContent = '—';
document.getElementById('statEnergy').textContent = '—';
document.getElementById('statPeri').textContent = '—';
document.getElementById('statApo').textContent = '—';
return;
}
const a = body.a;
const e = body.e;
const r = Math.sqrt(body.x * body.x + body.y * body.y);
const v = Math.sqrt(body.vx * body.vx + body.vy * body.vy);
const T = 2 * Math.PI * Math.sqrt(a * a * a / state.mass);
const energy = v * v / 2 - state.mass / r;
const r_p = a * (1 - e);
const r_a = a * (1 + e);
document.getElementById('statA').textContent = `${a.toFixed(1)} km`;
document.getElementById('statE').textContent = e.toFixed(3);
document.getElementById('statT').textContent = `${(T / 1).toFixed(2)} year`;
document.getElementById('statV').textContent = `${v.toFixed(2)} km/s`;
document.getElementById('statR').textContent = `${r.toFixed(1)} km`;
document.getElementById('statEnergy').textContent = `${energy.toFixed(3)} km²/s²`;
document.getElementById('statPeri').textContent = `${r_p.toFixed(1)} km`;
document.getElementById('statApo').textContent = `${r_a.toFixed(1)} km`;
}
// Control handlers
document.getElementById('eccentricity').addEventListener('input', (e) => {
state.eccentricity = parseFloat(e.target.value);
document.getElementById('eccentricityValue').textContent = state.eccentricity.toFixed(2);
if (state.mode === 'single') initializeBodies();
});
document.getElementById('semiMajor').addEventListener('input', (e) => {
state.semiMajor = parseFloat(e.target.value);
document.getElementById('semiMajorValue').textContent = state.semiMajor.toFixed(0);
if (state.mode === 'single') initializeBodies();
});
document.getElementById('mass').addEventListener('input', (e) => {
state.mass = parseFloat(e.target.value);
document.getElementById('massValue').textContent = state.mass.toFixed(1);
initializeBodies();
});
document.getElementById('speedMult').addEventListener('input', (e) => {
state.speedMult = parseFloat(e.target.value);
document.getElementById('speedValue').textContent = state.speedMult.toFixed(1) + 'x';
});
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.mode = btn.dataset.mode;
initializeBodies();
});
});
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', () => {
btn.classList.toggle('active');
const key = 'show' + btn.dataset.toggle.charAt(0).toUpperCase() + btn.dataset.toggle.slice(1);
state[key] = !state[key];
});
});
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const preset = btn.dataset.preset;
if (preset === 'circle') {
state.eccentricity = 0;
} else if (preset === 'ellipse') {
state.eccentricity = 0.5;
} else if (preset === 'comet') {
state.eccentricity = 0.9;
}
document.getElementById('eccentricity').value = state.eccentricity;
document.getElementById('eccentricityValue').textContent = state.eccentricity.toFixed(2);
if (state.mode === 'single') initializeBodies();
});
});
document.getElementById('pausePlayBtn').addEventListener('click', (e) => {
state.isRunning = !state.isRunning;
e.target.textContent = state.isRunning ? 'Pause' : 'Play';
e.target.classList.toggle('active');
});
// Animation loop
function animate() {
if (state.isRunning) {
updatePhysics();
}
updateStats();
render();
requestAnimationFrame(animate);
}
// Initialize
initializeBodies();
animate();