Physics Lab

COLLISION SIMULATOR

Explore elastic and inelastic collisions. Watch momentum and kinetic energy conservation in action.

Simulation

1D
2D

Parameters

Inelastic (0) 1.00 Elastic (1)

Presets

Collision Analysis

Momentum Before
0.00
kg·m/s
Momentum After
0.00
kg·m/s
Kinetic Energy Before
0.00
J
Kinetic Energy After
0.00
J
Momentum Conservation
p₁ Before
0
p₂ Before
0
p₁ After
0
p₂ After
0
Kinetic Energy
KE₁ Before
0
KE₂ Before
0
KE₁ After
0
KE₂ After
0

Collision Details

Collision Type:

Relative Velocity: 0.00 m/s

Energy Lost: 0 J (Elastic)

Center of Mass Velocity: 0.00 m/s

Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// State
    const state = {
      dimension: '1d',
      running: false,
      slowmotion: false,
      paused: false,
      collided: false,
      collisionTime: 0,

      // Simulation parameters
      m1: 1,
      m2: 1,
      v1: 5,
      v2: 0,
      e: 1,

      // Current velocities
      currentV1: 5,
      currentV2: 0,

      // 2D state
      x1: 100,
      y1: 150,
      x2: 400,
      y2: 150,
      vx1: 5,
      vy1: 0,
      vx2: 0,
      vy2: 0,

      // Canvas
      canvas: null,
      ctx: null,

      // Trail
      trail1: [],
      trail2: [],
      maxTrailLength: 15
    };

    const presets = {
      'equal-elastic': { m1: 1, m2: 1, v1: 5, v2: 0, e: 1 },
      'heavy-light': { m1: 2, m2: 1, v1: 3, v2: 0, e: 0.9 },
      'light-heavy': { m1: 1, m2: 2, v1: 5, v2: 0, e: 0.9 },
      'inelastic': { m1: 1, m2: 1, v1: 5, v2: -3, e: 0 },
      'cradle': { m1: 1, m2: 1, v1: 4, v2: 0, e: 1 }
    };

    // DOM Elements
    const canvas = document.getElementById('simulationCanvas');
    const ctx = canvas.getContext('2d');
    const mass1Input = document.getElementById('mass1');
    const mass2Input = document.getElementById('mass2');
    const vel1Input = document.getElementById('vel1');
    const vel2Input = document.getElementById('vel2');
    const restitutionInput = document.getElementById('restitution');
    const collisionTypeSelect = document.getElementById('collisionType');
    const launchBtn = document.getElementById('launchBtn');
    const pauseBtn = document.getElementById('pauseBtn');
    const resetBtn = document.getElementById('resetBtn');
    const slowmoBtn = document.getElementById('slowmoBtn');
    const dimensionToggles = document.querySelectorAll('[data-dimension]');
    const presetButtons = document.querySelectorAll('[data-preset]');

    // Initialize canvas
    state.canvas = canvas;
    state.ctx = ctx;

    // Set canvas resolution
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    ctx.scale(dpr, dpr);

    // Event listeners
    mass1Input.addEventListener('change', () => {
      state.m1 = parseFloat(mass1Input.value);
      updateDisplay();
    });

    mass2Input.addEventListener('change', () => {
      state.m2 = parseFloat(mass2Input.value);
      updateDisplay();
    });

    vel1Input.addEventListener('change', () => {
      state.v1 = parseFloat(vel1Input.value);
      state.currentV1 = state.v1;
      updateDisplay();
    });

    vel2Input.addEventListener('change', () => {
      state.v2 = parseFloat(vel2Input.value);
      state.currentV2 = state.v2;
      updateDisplay();
    });

    restitutionInput.addEventListener('input', (e) => {
      state.e = parseFloat(restitutionInput.value);
      document.getElementById('restitutionValue').textContent = state.e.toFixed(2);
      collisionTypeSelect.value = 'custom';
      updateDisplay();
    });

    collisionTypeSelect.addEventListener('change', (e) => {
      if (e.target.value !== 'custom') {
        const e_map = { elastic: 1, inelastic: 0.5, stick: 0 };
        state.e = e_map[e.target.value];
        restitutionInput.value = state.e;
        document.getElementById('restitutionValue').textContent = state.e.toFixed(2);
      }
    });

    launchBtn.addEventListener('click', () => {
      state.running = true;
      state.paused = false;
      state.collided = false;
      state.trail1 = [];
      state.trail2 = [];
      state.currentV1 = state.v1;
      state.currentV2 = state.v2;

      if (state.dimension === '2d') {
        state.x1 = 80;
        state.y1 = canvas.height / 2;
        state.x2 = canvas.width - 80;
        state.y2 = canvas.height / 2;
        state.vx1 = state.v1;
        state.vy1 = 0;
        state.vx2 = state.v2;
        state.vy2 = 0;
      }

      launchBtn.textContent = 'Running...';
      launchBtn.disabled = true;
      updateDisplay();
    });

    pauseBtn.addEventListener('click', () => {
      state.paused = !state.paused;
      pauseBtn.textContent = state.paused ? 'Resume' : 'Pause';
    });

    resetBtn.addEventListener('click', () => {
      state.running = false;
      state.paused = false;
      state.collided = false;
      state.collisionTime = 0;
      state.trail1 = [];
      state.trail2 = [];
      state.currentV1 = state.v1;
      state.currentV2 = state.v2;
      launchBtn.textContent = 'Launch';
      launchBtn.disabled = false;
      updateDisplay();
      draw();
    });

    slowmoBtn.addEventListener('click', () => {
      state.slowmotion = !state.slowmotion;
      slowmoBtn.classList.toggle('btn-primary', state.slowmotion);
      slowmoBtn.classList.toggle('btn-secondary', !state.slowmotion);
    });

    dimensionToggles.forEach(toggle => {
      toggle.addEventListener('click', (e) => {
        dimensionToggles.forEach(t => t.classList.remove('active'));
        e.target.classList.add('active');
        state.dimension = e.target.dataset.dimension;
        state.running = false;
        state.paused = false;
        state.trail1 = [];
        state.trail2 = [];
        launchBtn.textContent = 'Launch';
        launchBtn.disabled = false;
        updateDisplay();
        draw();
      });
    });

    presetButtons.forEach(btn => {
      btn.addEventListener('click', (e) => {
        const preset = presets[e.target.dataset.preset];
        mass1Input.value = preset.m1;
        mass2Input.value = preset.m2;
        vel1Input.value = preset.v1;
        vel2Input.value = preset.v2;
        restitutionInput.value = preset.e;
        collisionTypeSelect.value = 'custom';

        state.m1 = preset.m1;
        state.m2 = preset.m2;
        state.v1 = preset.v1;
        state.v2 = preset.v2;
        state.e = preset.e;
        state.currentV1 = state.v1;
        state.currentV2 = state.v2;
        document.getElementById('restitutionValue').textContent = state.e.toFixed(2);

        updateDisplay();
      });
    });

    function calculatePostCollisionVelocities() {
      const m1 = state.m1;
      const m2 = state.m2;
      const v1 = state.currentV1;
      const v2 = state.currentV2;
      const e = state.e;

      const denom = m1 + m2;
      const v1_new = ((m1 - e * m2) * v1 + (1 + e) * m2 * v2) / denom;
      const v2_new = ((m2 - e * m1) * v2 + (1 + e) * m1 * v1) / denom;

      return { v1_new, v2_new };
    }

    function calculateMomentum(v1, v2) {
      return state.m1 * v1 + state.m2 * v2;
    }

    function calculateKineticEnergy(v1, v2) {
      return 0.5 * state.m1 * v1 * v1 + 0.5 * state.m2 * v2 * v2;
    }

    function getCollisionType(e) {
      if (e > 0.99) return 'Perfectly Elastic';
      if (e < 0.01) return 'Perfectly Inelastic (Stick)';
      return `Partially Elastic (e=${e.toFixed(2)})`;
    }

    function updateDisplay() {
      const m1 = state.m1;
      const m2 = state.m2;
      const v1 = state.currentV1;
      const v2 = state.currentV2;

      const momentumBefore = calculateMomentum(state.v1, state.v2);
      const momentumAfter = state.collided ? calculateMomentum(state.currentV1, state.currentV2) : momentumBefore;

      const energyBefore = calculateKineticEnergy(state.v1, state.v2);
      const energyAfter = state.collided ? calculateKineticEnergy(state.currentV1, state.currentV2) : energyBefore;
      const energyLost = Math.max(0, energyBefore - energyAfter);
      const energyLostPercent = energyBefore > 0 ? (energyLost / energyBefore * 100) : 0;

      const relVel = Math.abs(state.v1 - state.v2);
      const comVel = (m1 * state.v1 + m2 * state.v2) / (m1 + m2);

      document.getElementById('momentumBefore').textContent = momentumBefore.toFixed(2);
      document.getElementById('momentumAfter').textContent = momentumAfter.toFixed(2);
      document.getElementById('energyBefore').textContent = energyBefore.toFixed(2);
      document.getElementById('energyAfter').textContent = energyAfter.toFixed(2);

      document.getElementById('collisionTypeDisplay').textContent = getCollisionType(state.e);
      document.getElementById('relativeVelocity').textContent = relVel.toFixed(2);
      document.getElementById('comVelocity').textContent = comVel.toFixed(2);

      if (energyLostPercent > 0.5) {
        document.getElementById('energyLostIndicator').style.display = 'inline';
        document.getElementById('noEnergyLost').style.display = 'none';
        document.getElementById('energyLostValue').textContent = energyLostPercent.toFixed(1) + '%';
      } else {
        document.getElementById('energyLostIndicator').style.display = 'none';
        document.getElementById('noEnergyLost').style.display = 'inline';
      }

      // Update bar charts (scaled to max momentum/energy)
      const maxMomentum = Math.max(Math.abs(state.m1 * state.v1), Math.abs(state.m2 * state.v2), 10);
      const maxEnergy = Math.max(energyBefore, energyAfter, 10);

      const p1Before = Math.abs(state.m1 * state.v1);
      const p2Before = Math.abs(state.m2 * state.v2);
      const p1After = Math.abs(state.m1 * state.currentV1);
      const p2After = Math.abs(state.m2 * state.currentV2);

      const ke1Before = 0.5 * state.m1 * state.v1 * state.v1;
      const ke2Before = 0.5 * state.m2 * state.v2 * state.v2;
      const ke1After = 0.5 * state.m1 * state.currentV1 * state.currentV1;
      const ke2After = 0.5 * state.m2 * state.currentV2 * state.currentV2;

      setBarHeight('barP1Before', p1Before, maxMomentum);
      setBarHeight('barP2Before', p2Before, maxMomentum);
      setBarHeight('barP1After', p1After, maxMomentum);
      setBarHeight('barP2After', p2After, maxMomentum);

      setBarHeight('barKE1Before', ke1Before, maxEnergy);
      setBarHeight('barKE2Before', ke2Before, maxEnergy);
      setBarHeight('barKE1After', ke1After, maxEnergy);
      setBarHeight('barKE2After', ke2After, maxEnergy);
    }

    function setBarHeight(elementId, value, maxValue) {
      const element = document.getElementById(elementId).parentElement;
      const height = (value / maxValue) * 100;
      element.style.height = Math.max(20, height) + 'px';
      document.getElementById(elementId).textContent = value.toFixed(1);
    }

    function getRadiusFromMass(mass) {
      return 15 + Math.cbrt(mass) * 5;
    }

    function getColorFromSpeed(speed, maxSpeed = 10) {
      const ratio = Math.min(Math.abs(speed) / maxSpeed, 1);
      if (ratio < 0.5) {
        const r = Math.floor(68 + ratio * 2 * 92);
        const g = Math.floor(136 + ratio * 2 * 24);
        return `rgb(${r}, ${g}, 255)`;
      } else {
        const r = Math.floor(160 + (ratio - 0.5) * 2 * 95);
        const g = Math.floor(160 - (ratio - 0.5) * 2 * 160);
        return `rgb(${r}, ${g}, 0)`;
      }
    }

    function draw1D() {
      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;

      // Background
      ctx.fillStyle = '#111111';
      ctx.fillRect(0, 0, width, height);

      // Track
      ctx.strokeStyle = '#1e1e1e';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(50, height / 2);
      ctx.lineTo(width - 50, height / 2);
      ctx.stroke();

      // Walls
      ctx.fillStyle = '#2a2a2a';
      ctx.fillRect(0, height / 2 - 30, 40, 60);
      ctx.fillRect(width - 40, height / 2 - 30, 40, 60);

      // Calculate positions
      const r1 = getRadiusFromMass(state.m1);
      const r2 = getRadiusFromMass(state.m2);

      const x1 = 100 + state.currentV1 * 20;
      const x2 = width - 100 + state.currentV2 * 20;

      // Draw trails
      ctx.strokeStyle = 'rgba(255, 34, 0, 0.1)';
      ctx.lineWidth = 1;
      if (state.trail1.length > 1) {
        ctx.beginPath();
        ctx.moveTo(state.trail1[0], height / 2);
        for (let i = 1; i < state.trail1.length; i++) {
          ctx.lineTo(state.trail1[i], height / 2);
        }
        ctx.stroke();
      }

      ctx.strokeStyle = 'rgba(68, 136, 255, 0.1)';
      if (state.trail2.length > 1) {
        ctx.beginPath();
        ctx.moveTo(state.trail2[0], height / 2);
        for (let i = 1; i < state.trail2.length; i++) {
          ctx.lineTo(state.trail2[i], height / 2);
        }
        ctx.stroke();
      }

      // Draw balls
      ctx.fillStyle = '#ff2200';
      ctx.beginPath();
      ctx.arc(x1, height / 2, r1, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#4488ff';
      ctx.beginPath();
      ctx.arc(x2, height / 2, r2, 0, Math.PI * 2);
      ctx.fill();

      // Draw velocity arrows
      ctx.strokeStyle = '#ff2200';
      ctx.fillStyle = '#ff2200';
      ctx.lineWidth = 2;
      drawArrow(x1, height / 2, x1 + state.currentV1 * 10, height / 2);

      ctx.strokeStyle = '#4488ff';
      ctx.fillStyle = '#4488ff';
      drawArrow(x2, height / 2, x2 + state.currentV2 * 10, height / 2);

      // Draw collision indicator
      if (state.collided) {
        ctx.fillStyle = 'rgba(255, 34, 0, 0.2)';
        ctx.fillRect(Math.min(x1, x2) - 40, height / 2 - 60, Math.abs(x2 - x1) + 80, 120);
      }

      // Label
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'left';
      ctx.fillText(`v₁=${state.currentV1.toFixed(1)} m/s`, x1 - r1 - 50, height / 2 - r1 - 20);
      ctx.fillText(`v₂=${state.currentV2.toFixed(1)} m/s`, x2 - r2, height / 2 - r2 - 20);
    }

    function draw2D() {
      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;

      // Background
      ctx.fillStyle = '#111111';
      ctx.fillRect(0, 0, width, height);

      // Boundaries
      ctx.strokeStyle = '#1e1e1e';
      ctx.lineWidth = 2;
      ctx.strokeRect(30, 30, width - 60, height - 60);

      // Draw trails
      ctx.strokeStyle = 'rgba(255, 34, 0, 0.15)';
      ctx.lineWidth = 1;
      for (let i = 0; i < state.trail1.length - 1; i++) {
        ctx.beginPath();
        ctx.moveTo(state.trail1[i].x, state.trail1[i].y);
        ctx.lineTo(state.trail1[i + 1].x, state.trail1[i + 1].y);
        ctx.stroke();
      }

      ctx.strokeStyle = 'rgba(68, 136, 255, 0.15)';
      for (let i = 0; i < state.trail2.length - 1; i++) {
        ctx.beginPath();
        ctx.moveTo(state.trail2[i].x, state.trail2[i].y);
        ctx.lineTo(state.trail2[i + 1].x, state.trail2[i + 1].y);
        ctx.stroke();
      }

      // Draw balls
      const r1 = getRadiusFromMass(state.m1);
      const r2 = getRadiusFromMass(state.m2);

      ctx.fillStyle = '#ff2200';
      ctx.beginPath();
      ctx.arc(state.x1, state.y1, r1, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = '#4488ff';
      ctx.beginPath();
      ctx.arc(state.x2, state.y2, r2, 0, Math.PI * 2);
      ctx.fill();

      // Draw velocity arrows
      ctx.strokeStyle = '#ff2200';
      ctx.fillStyle = '#ff2200';
      ctx.lineWidth = 2;
      const speed1 = Math.sqrt(state.vx1 * state.vx1 + state.vy1 * state.vy1);
      if (speed1 > 0.1) {
        drawArrow(state.x1, state.y1, state.x1 + state.vx1 * 10, state.y1 + state.vy1 * 10);
      }

      ctx.strokeStyle = '#4488ff';
      ctx.fillStyle = '#4488ff';
      const speed2 = Math.sqrt(state.vx2 * state.vx2 + state.vy2 * state.vy2);
      if (speed2 > 0.1) {
        drawArrow(state.x2, state.y2, state.x2 + state.vx2 * 10, state.y2 + state.vy2 * 10);
      }
    }

    function drawArrow(fromX, fromY, toX, toY) {
      const headlen = 8;
      const angle = Math.atan2(toY - fromY, toX - fromX);

      ctx.beginPath();
      ctx.moveTo(fromX, fromY);
      ctx.lineTo(toX, toY);
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(toX, toY);
      ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
      ctx.moveTo(toX, toY);
      ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
      ctx.stroke();
    }

    function draw() {
      if (state.dimension === '1d') {
        draw1D();
      } else {
        draw2D();
      }
    }

    function simulate1D(dt) {
      if (!state.running || state.paused) return;

      const width = canvas.width / window.devicePixelRatio;
      const r1 = getRadiusFromMass(state.m1);
      const r2 = getRadiusFromMass(state.m2);


      const x1 = 100 + state.currentV1 * 20;
      const x2 = width - 100 + state.currentV2 * 20;

      // Trail
      state.trail1.push(x1);
      state.trail2.push(x2);
      if (state.trail1.length > state.maxTrailLength) state.trail1.shift();
      if (state.trail2.length > state.maxTrailLength) state.trail2.shift();

      // Collision detection
      const distance = Math.abs(x2 - x1);
      if (distance < r1 + r2 && !state.collided) {
        state.collided = true;
        state.collisionTime = 0;

        const { v1_new, v2_new } = calculatePostCollisionVelocities();
        state.currentV1 = v1_new;
        state.currentV2 = v2_new;

        playCollisionSound();
        canvas.parentElement.classList.add('collision-flash');
        setTimeout(() => canvas.parentElement.classList.remove('collision-flash'), 300);

        updateDisplay();
      }

      // Wall collision
      const margin = 50;
      if (x1 < margin && state.currentV1 < 0) {
        state.currentV1 *= -0.9;
      }
      if (x1 > width - margin && state.currentV1 > 0) {
        state.currentV1 *= -0.9;
      }
      if (x2 < margin && state.currentV2 < 0) {
        state.currentV2 *= -0.9;
      }
      if (x2 > width - margin && state.currentV2 > 0) {
        state.currentV2 *= -0.9;
      }

      // Check if simulation should stop
      if (Math.abs(state.currentV1) < 0.01 && Math.abs(state.currentV2) < 0.01) {
        state.running = false;
        launchBtn.textContent = 'Launch';
        launchBtn.disabled = false;
      }
    }

    function simulate2D(dt) {
      if (!state.running || state.paused) return;

      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;
      const r1 = getRadiusFromMass(state.m1);
      const r2 = getRadiusFromMass(state.m2);

      // Update positions
      state.x1 += state.vx1 * dt;
      state.y1 += state.vy1 * dt;
      state.x2 += state.vx2 * dt;
      state.y2 += state.vy2 * dt;

      // Trail
      state.trail1.push({ x: state.x1, y: state.y1 });
      state.trail2.push({ x: state.x2, y: state.y2 });
      if (state.trail1.length > state.maxTrailLength) state.trail1.shift();
      if (state.trail2.length > state.maxTrailLength) state.trail2.shift();

      // Ball-ball collision
      const dx = state.x2 - state.x1;
      const dy = state.y2 - state.y1;
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (distance < r1 + r2 && !state.collided) {
        state.collided = true;
        state.collisionTime = 0;

        // 2D collision resolution
        const nx = dx / distance;
        const ny = dy / distance;

        const dvx = state.vx2 - state.vx1;
        const dvy = state.vy2 - state.vy1;
        const dvn = dvx * nx + dvy * ny;

        const m1 = state.m1;
        const m2 = state.m2;
        const e = state.e;

        const impulse = -(1 + e) * dvn / (1/m1 + 1/m2);

        state.vx1 -= (impulse / m1) * nx;
        state.vy1 -= (impulse / m1) * ny;
        state.vx2 += (impulse / m2) * nx;
        state.vy2 += (impulse / m2) * ny;

        playCollisionSound();
        canvas.parentElement.classList.add('collision-flash');
        setTimeout(() => canvas.parentElement.classList.remove('collision-flash'), 300);

        updateDisplay();
      }

      // Wall collision
      const margin = 30;
      if (state.x1 - r1 < margin) {
        state.x1 = margin + r1;
        state.vx1 *= -0.9;
      }
      if (state.x1 + r1 > width - margin) {
        state.x1 = width - margin - r1;
        state.vx1 *= -0.9;
      }
      if (state.y1 - r1 < margin) {
        state.y1 = margin + r1;
        state.vy1 *= -0.9;
      }
      if (state.y1 + r1 > height - margin) {
        state.y1 = height - margin - r1;
        state.vy1 *= -0.9;
      }

      if (state.x2 - r2 < margin) {
        state.x2 = margin + r2;
        state.vx2 *= -0.9;
      }
      if (state.x2 + r2 > width - margin) {
        state.x2 = width - margin - r2;
        state.vx2 *= -0.9;
      }
      if (state.y2 - r2 < margin) {
        state.y2 = margin + r2;
        state.vy2 *= -0.9;
      }
      if (state.y2 + r2 > height - margin) {
        state.y2 = height - margin - r2;
        state.vy2 *= -0.9;
      }

      // Stop if motion is negligible
      const speed1 = Math.sqrt(state.vx1 * state.vx1 + state.vy1 * state.vy1);
      const speed2 = Math.sqrt(state.vx2 * state.vx2 + state.vy2 * state.vy2);
      if (speed1 < 0.01 && speed2 < 0.01) {
        state.running = false;
        launchBtn.textContent = 'Launch';
        launchBtn.disabled = false;
      }
    }

    function playCollisionSound() {
      try {
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();

        oscillator.frequency.value = 800 + Math.random() * 200;
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);

        gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
        gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);

        oscillator.start(audioContext.currentTime);
        oscillator.stop(audioContext.currentTime + 0.1);
      } catch (e) {
        // Audio not available
      }
    }

    let lastTime = performance.now();
    function animate(currentTime) {
      const deltaTime = (currentTime - lastTime) / 1000;
      lastTime = currentTime;

      const dt = state.slowmotion ? deltaTime * 0.1 : deltaTime * 0.5;

      if (state.dimension === '1d') {
        simulate1D(dt);
      } else {
        simulate2D(dt);
      }

      draw();
      requestAnimationFrame(animate);
    }

    // Initialize
    updateDisplay();
    draw();
    requestAnimationFrame(animate);