Special Relativity
Time Dilation
Interactive visualization of Einstein's theory: watch how time slows, objects contract, and the universe warps near light speed.
Controls
0.5c (50% light speed)
Lorentz Factor γ
1.15
γ = 1/√(1−β²)
Time Dilation
1.15 ×
Slower on ship
At this speed:
1 year aboard = 0.87 years on Earth
1 year aboard = 0.87 years on Earth
Twin Clock Paradox
Left: Stationary Earth clock. Right: Moving spaceship clock (Δt = proper time × γ). Watch the moving clock tick slower due to time dilation.
Statistics
β (v/c ratio)
0.500
γ (Lorentz factor)
1.15
Coordinate Time Δt
1.15 y
Proper Time Δτ
1.00 y
Length Contraction
86.8 m
Relativistic KE
0.15 mc²
Real-World Examples
GPS Satellites
v ≈ 3.87 km/s → γ ≈ 1.000000082
Clocks run 38 microseconds faster per day
v ≈ 3.87 km/s → γ ≈ 1.000000082
Clocks run 38 microseconds faster per day
Cosmic Ray Muons
v ≈ 0.9994c → γ ≈ 28.9
Lifetime dilates from 2.2 μs → 63 μs
v ≈ 0.9994c → γ ≈ 28.9
Lifetime dilates from 2.2 μs → 63 μs
LHC Protons
v ≈ 0.9999991c → γ ≈ 6927
Rest mass appears 6927× heavier
v ≈ 0.9999991c → γ ≈ 6927
Rest mass appears 6927× heavier
Relativistic Equations
Lorentz Factor
γ = 1/√(1−β²) where β = v/c
Time Dilation
Δt = γ·Δτ
Moving clock runs slower by factor γ
Length Contraction
L = L₀/γ
Only in direction of motion
Relativistic Energy
E = γmc²
KE = (γ−1)mc²
Relativistic Momentum
p = γmv
Increases dramatically near c
Speed of Light
c = 3×10⁸ m/s
≈ 299,792 km/s
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Constants
const SPEED_OF_LIGHT = 299792.458; // km/s
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 400;
// State
let state = {
beta: 0.5,
properTime: 1.0,
restLength: 100,
scenario: 'clock',
animating: false,
animationFrame: 0,
animationTime: 0,
simSpeed: 1
};
// DOM elements
const velocitySlider = document.getElementById('velocitySlider');
const velocityDisplay = document.getElementById('velocityDisplay');
const properTimeInput = document.getElementById('properTimeInput');
const restLengthInput = document.getElementById('restLengthInput');
const gammaValue = document.getElementById('gammaValue');
const gammaStatValue = document.getElementById('gammaStatValue');
const betaValue = document.getElementById('betaValue');
const timeDilationValue = document.getElementById('timeDilationValue');
const coordTimeValue = document.getElementById('coordTimeValue');
const properTimeValue = document.getElementById('properTimeValue');
const contractionValue = document.getElementById('contractionValue');
const keValue = document.getElementById('keValue');
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
const animateBtn = document.getElementById('animateBtn');
const resetBtn = document.getElementById('resetBtn');
const scenarioTitle = document.getElementById('scenarioTitle');
const canvasDescription = document.getElementById('canvasDescription');
const realworldExample = document.getElementById('realworldExample');
const gpsExample = document.getElementById('gpsExample');
const muonExample = document.getElementById('muonExample');
const lhcExample = document.getElementById('lhcExample');
// Calculate Lorentz factor
function gamma(beta) {
if (beta >= 0.9999) beta = 0.9999;
return 1 / Math.sqrt(1 - beta * beta);
}
// Update all displays
function updateDisplay() {
const g = gamma(state.beta);
const coordTime = state.properTime * g;
const contractedLength = state.restLength / g;
const ke = (g - 1); // Normalized to mc²
// Update sliders and inputs
velocityDisplay.textContent = `${(state.beta * 100).toFixed(1)}c (${(state.beta * 100).toFixed(1)}% light speed)`;
// Update statistics
betaValue.textContent = state.beta.toFixed(3);
gammaValue.textContent = g.toFixed(2);
gammaStatValue.textContent = g.toFixed(2);
timeDilationValue.textContent = g.toFixed(2) + ' ×';
coordTimeValue.textContent = coordTime.toFixed(2) + ' y';
properTimeValue.textContent = state.properTime.toFixed(2) + ' y';
contractionValue.textContent = contractedLength.toFixed(1) + ' m';
keValue.textContent = ke.toFixed(2) + ' mc²';
// Real-world examples
const earthTimeRatio = coordTime / state.properTime;
realworldExample.textContent = `1 year aboard = ${(state.properTime / coordTime * coordTime).toFixed(2)} years on Earth (${(coordTime / state.properTime).toFixed(2)}× slower)`;
// GPS (v ≈ 3.87 km/s)
const gpsBeta = 3.87 / SPEED_OF_LIGHT;
const gpsGamma = gamma(gpsBeta);
const gpsTimeDiff = (gpsGamma - 1) * 86400 * 1e6; // microseconds per day
gpsExample.textContent = `v ≈ 3.87 km/s → γ ≈ ${gpsGamma.toFixed(9)}\nClocks run ${gpsTimeDiff.toFixed(1)} microseconds faster per day`;
// Muon (v ≈ 0.9994c)
const muonBeta = 0.9994;
const muonGamma = gamma(muonBeta);
const muonLifetime = 2.2 * muonGamma;
muonExample.textContent = `v ≈ 0.9994c → γ ≈ ${muonGamma.toFixed(1)}\nLifetime dilates from 2.2 μs → ${muonLifetime.toFixed(1)} μs`;
// LHC (v ≈ 0.9999991c)
const lhcBeta = 0.9999991;
const lhcGamma = gamma(lhcBeta);
lhcExample.textContent = `v ≈ 0.9999991c → γ ≈ ${lhcGamma.toFixed(0)}\nRest mass appears ${lhcGamma.toFixed(0)}× heavier`;
}
// Draw clock face
function drawClockFace(ctx, x, y, radius, time, isMoving = false) {
// Determine label color
const color = isMoving ? '#ff2200' : '#00c896';
// Draw outer circle
ctx.fillStyle = '#111111';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// Draw hour markers
ctx.fillStyle = color;
ctx.font = 'bold 12px DM Mono';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 1; i <= 12; i++) {
const angle = (i * 30 - 90) * Math.PI / 180;
const markX = x + Math.cos(angle) * (radius - 15);
const markY = y + Math.sin(angle) * (radius - 15);
ctx.fillText(i, markX, markY);
}
// Draw center dot
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
// Calculate hand angles (time in hours, 0-12)
const hours = (time % 12);
const minutes = (time * 60) % 60;
const seconds = (time * 3600) % 60;
// Hour hand
const hourAngle = (hours * 30 + minutes * 0.5 - 90) * Math.PI / 180;
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(hourAngle) * (radius * 0.5), y + Math.sin(hourAngle) * (radius * 0.5));
ctx.stroke();
// Minute hand
const minuteAngle = (minutes * 6 + seconds * 0.1 - 90) * Math.PI / 180;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(minuteAngle) * (radius * 0.65), y + Math.sin(minuteAngle) * (radius * 0.65));
ctx.stroke();
// Seconds hand
const secondAngle = (seconds * 6 - 90) * Math.PI / 180;
ctx.strokeStyle = '#ff5555';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(secondAngle) * (radius * 0.7), y + Math.sin(secondAngle) * (radius * 0.7));
ctx.stroke();
// Draw label
ctx.fillStyle = color;
ctx.font = 'bold 12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText(isMoving ? 'Spaceship' : 'Earth', x, y + radius + 25);
}
// Draw twin clocks scenario
function drawTwinClocks() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const g = gamma(state.beta);
const earthTime = state.properTime;
const spaceshipTime = state.properTime / g;
// Draw clocks
const clockRadius = 50;
drawClockFace(ctx, 150, 150, clockRadius, earthTime, false);
drawClockFace(ctx, 650, 150, clockRadius, spaceshipTime, true);
// Draw spaceship
const shipX = 250 + state.animationTime * 300;
const shipWidth = 60;
const shipHeight = 40;
ctx.fillStyle = 'rgba(255, 34, 0, 0.2)';
ctx.fillRect(shipX, 250, shipWidth, shipHeight);
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.strokeRect(shipX, 250, shipWidth, shipHeight);
// Draw window
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(shipX + 10, 260, 10, 10);
ctx.strokeStyle = '#ff2200';
ctx.strokeRect(shipX + 10, 260, 10, 10);
// Draw trajectory
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(100, 270);
ctx.lineTo(CANVAS_WIDTH - 100, 270);
ctx.stroke();
ctx.setLineDash([]);
// Draw light cone
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
const coneX = CANVAS_WIDTH / 2;
const coneY = 270;
ctx.beginPath();
ctx.moveTo(coneX, coneY);
ctx.lineTo(coneX + 100, coneY - 100);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(coneX, coneY);
ctx.lineTo(coneX + 100, coneY + 100);
ctx.stroke();
ctx.setLineDash([]);
// Time comparison
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
ctx.textAlign = 'left';
ctx.fillText(`Earth: ${earthTime.toFixed(2)} years`, 20, 30);
ctx.fillStyle = '#ff2200';
ctx.fillText(`Spaceship: ${spaceshipTime.toFixed(2)} years`, 20, 50);
ctx.fillStyle = '#00c896';
ctx.fillText(`Time ratio: ${(g).toFixed(2)}×`, 20, 70);
}
// Draw ruler scenario
function drawRuler() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const g = gamma(state.beta);
const contractedLength = state.restLength / g;
// Draw stationary ruler
const rulerY1 = 100;
const scale = (CANVAS_WIDTH - 100) / state.restLength;
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(50, rulerY1);
ctx.lineTo(50 + state.restLength * scale, rulerY1);
ctx.stroke();
// Draw tick marks (stationary)
for (let i = 0; i <= state.restLength; i += 20) {
const x = 50 + i * scale;
ctx.beginPath();
ctx.moveTo(x, rulerY1 - 5);
ctx.lineTo(x, rulerY1 + 5);
ctx.stroke();
}
ctx.fillStyle = '#00c896';
ctx.font = '12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText(`L₀ = ${state.restLength} m`, 50 + state.restLength * scale / 2, rulerY1 - 25);
// Draw moving ruler
const rulerY2 = 250;
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(50, rulerY2);
ctx.lineTo(50 + contractedLength * scale, rulerY2);
ctx.stroke();
// Draw tick marks (moving)
for (let i = 0; i <= contractedLength; i += 20) {
const x = 50 + i * scale;
ctx.beginPath();
ctx.moveTo(x, rulerY2 - 5);
ctx.lineTo(x, rulerY2 + 5);
ctx.stroke();
}
ctx.fillStyle = '#ff2200';
ctx.fillText(`L' = ${contractedLength.toFixed(1)} m`, 50 + contractedLength * scale / 2, rulerY2 - 25);
// Draw velocity arrow
ctx.fillStyle = '#ff2200';
ctx.font = '12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText(`v = ${(state.beta * 100).toFixed(1)}% c`, CANVAS_WIDTH - 100, rulerY2 + 40);
// Draw contraction percentage
const contractionPercent = (1 - contractedLength / state.restLength) * 100;
ctx.fillStyle = '#ff5555';
ctx.fillText(`Contracted by ${contractionPercent.toFixed(1)}%`, CANVAS_WIDTH / 2, 30);
// Draw gamma factor
ctx.fillStyle = '#00c896';
ctx.fillText(`γ = ${gamma(state.beta).toFixed(2)}`, 100, 30);
}
// Draw spacetime diagram
function drawSpacetime() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const g = gamma(state.beta);
const centerX = CANVAS_WIDTH / 2;
const centerY = CANVAS_HEIGHT / 2;
const scale = 60;
// Draw axes
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerX, 20);
ctx.lineTo(centerX, CANVAS_HEIGHT - 20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20, centerY);
ctx.lineTo(CANVAS_WIDTH - 20, centerY);
ctx.stroke();
// Draw grid
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 0.5;
for (let i = -5; i <= 5; i++) {
if (i !== 0) {
// Vertical grid
ctx.beginPath();
ctx.moveTo(centerX + i * scale, 20);
ctx.lineTo(centerX + i * scale, CANVAS_HEIGHT - 20);
ctx.stroke();
// Horizontal grid
ctx.beginPath();
ctx.moveTo(20, centerY + i * scale);
ctx.lineTo(CANVAS_WIDTH - 20, centerY + i * scale);
ctx.stroke();
}
}
// Draw light cone
ctx.strokeStyle = 'rgba(255, 200, 0, 0.3)';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
// Forward light cone
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + 150, centerY - 150);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + 150, centerY + 150);
ctx.stroke();
// Backward light cone
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX - 150, centerY - 150);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX - 150, centerY + 150);
ctx.stroke();
ctx.setLineDash([]);
// Draw stationary world line (vertical)
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX, centerY - 150);
ctx.lineTo(centerX, centerY + 150);
ctx.stroke();
// Draw moving world line (tilted)
const slope = state.beta;
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX - 150 * slope, centerY - 150);
ctx.lineTo(centerX + 150 * slope, centerY + 150);
ctx.stroke();
// Draw simultaneity lines
ctx.strokeStyle = 'rgba(100, 150, 255, 0.2)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
// Horizontal line (Earth frame simultaneity)
ctx.beginPath();
ctx.moveTo(centerX - 100, centerY);
ctx.lineTo(centerX + 100, centerY);
ctx.stroke();
// Tilted line (Spaceship frame simultaneity)
ctx.beginPath();
ctx.moveTo(centerX - 150 * slope, centerY - 150 / slope);
ctx.lineTo(centerX + 150 * slope, centerY + 150 / slope);
ctx.stroke();
ctx.setLineDash([]);
// Labels
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
ctx.textAlign = 'right';
ctx.fillText('ct (time)', centerX - 10, 30);
ctx.textAlign = 'left';
ctx.fillText('x (space)', CANVAS_WIDTH - 20, centerY + 20);
// Legend
ctx.fillStyle = '#00c896';
ctx.textAlign = 'left';
ctx.fillText('● Stationary', 20, 30);
ctx.fillStyle = '#ff2200';
ctx.fillText('● Moving', 20, 50);
ctx.fillStyle = 'rgba(255, 200, 0, 0.6)';
ctx.fillText('◆ Light Cone (45°)', 20, 70);
// Show angle
ctx.fillStyle = '#ff2200';
ctx.font = 'bold 14px DM Mono';
ctx.textAlign = 'center';
const angle = Math.atan(slope) * 180 / Math.PI;
ctx.fillText(`Angle: ${angle.toFixed(1)}°`, CANVAS_WIDTH / 2, CANVAS_HEIGHT - 30);
}
// Draw canvas based on scenario
function drawCanvas() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
if (state.scenario === 'clock') {
drawTwinClocks();
scenarioTitle.textContent = 'Twin Clock Paradox';
canvasDescription.textContent = 'Left: Stationary Earth clock. Right: Moving spaceship clock. Watch how the moving clock ticks slower due to time dilation.';
} else if (state.scenario === 'ruler') {
drawRuler();
scenarioTitle.textContent = 'Length Contraction';
canvasDescription.textContent = 'Top: Ruler at rest (L₀). Bottom: Same ruler moving at velocity v. Notice how it contracts along the direction of motion.';
} else if (state.scenario === 'spacetime') {
drawSpacetime();
scenarioTitle.textContent = 'Spacetime Diagram (Minkowski)';
canvasDescription.textContent = 'Vertical line = stationary observer (Earth). Tilted line = moving observer (Spaceship). Light cone shows causality limit. Horizontal vs tilted lines show relative simultaneity.';
}
}
// Animation loop
function animate() {
if (!state.animating) {
drawCanvas();
return;
}
state.animationTime += 0.01 * state.simSpeed;
if (state.animationTime > 1) {
state.animationTime = 0;
}
drawCanvas();
requestAnimationFrame(animate);
}
// Event listeners
document.getElementById('simSpeedSlider').addEventListener('input', (e) => {
state.simSpeed = parseFloat(e.target.value);
document.getElementById('simSpeedValue').textContent = state.simSpeed.toFixed(1);
});
velocitySlider.addEventListener('input', (e) => {
state.beta = parseFloat(e.target.value);
updateDisplay();
drawCanvas();
});
properTimeInput.addEventListener('change', (e) => {
state.properTime = parseFloat(e.target.value);
updateDisplay();
drawCanvas();
});
restLengthInput.addEventListener('change', (e) => {
state.restLength = parseFloat(e.target.value);
updateDisplay();
drawCanvas();
});
document.querySelectorAll('.scenario-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
state.scenario = e.target.dataset.scenario;
state.animationTime = 0;
drawCanvas();
});
});
animateBtn.addEventListener('click', () => {
state.animating = !state.animating;
animateBtn.textContent = state.animating ? 'Stop Animation' : 'Start Animation';
if (state.animating) {
animate();
}
});
resetBtn.addEventListener('click', () => {
state.beta = 0.5;
state.properTime = 1.0;
state.restLength = 100;
state.animating = false;
state.animationTime = 0;
velocitySlider.value = 0.5;
properTimeInput.value = 1.0;
restLengthInput.value = 100;
animateBtn.textContent = 'Start Animation';
updateDisplay();
drawCanvas();
});
// Initialize
updateDisplay();
drawCanvas();