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

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
Cosmic Ray Muons
v ≈ 0.9994c → γ ≈ 28.9
Lifetime dilates from 2.2 μs → 63 μs
LHC Protons
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();