Physics Simulation

Double Pendulum

Interactive chaos simulation using Lagrangian mechanics and RK4 integration. Visualize how small differences in initial conditions lead to dramatically different trajectories.

Status Running
Time (s) 0.00
θ₁ (°) 0.00
θ₂ (°) 0.00
Energy (J) 0.00

Pendulum Params

Initial Angles

Visual Markers & Vectors

Trail Settings

Multiple pendulums show chaos—identical except for tiny initial angle differences.

Simulation

Equations of Motion

θ₁'' = [-g(2m₁+m₂)sin(θ₁) - m₂g·sin(θ₁-2θ₂) - 2sin(θ₁-θ₂)m₂(θ₂'²L₂+θ₁'²L₁cos(θ₁-θ₂))] / [L₁(2m₁+m₂-m₂cos(2θ₁-2θ₂))]
θ₂'' = [2sin(θ₁-θ₂)(θ₁'²L₁(m₁+m₂)+g(m₁+m₂)cos(θ₁)+θ₂'²L₂m₂cos(θ₁-θ₂))] / [L₂(2m₁+m₂-m₂cos(2θ₁-2θ₂))]

Integrated using RK4 (4th-order Runge-Kutta) with dt=0.02. Trails cycle through a rainbow gradient showing temporal evolution.

Developer Reference

Core Algorithm & Standalone Script

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

const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    // Responsive canvas sizing
    function resizeCanvas() {
      const rect = canvas.parentElement.getBoundingClientRect();
      canvas.width = rect.width - 32; // account for padding
      canvas.height = Math.max(400, window.innerHeight * 0.6);
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    // UI Elements
    const l1Slider = document.getElementById('l1-slider');
    const l2Slider = document.getElementById('l2-slider');
    const m1Slider = document.getElementById('m1-slider');
    const m2Slider = document.getElementById('m2-slider');
    const gSlider = document.getElementById('g-slider');
    const angle1Slider = document.getElementById('angle1-slider');
    const angle2Slider = document.getElementById('angle2-slider');
    const trailSlider = document.getElementById('trail-slider');
    const countSlider = document.getElementById('count-slider');

    // Checkbox elements for visual overlays
    const showLabelsCheck = document.getElementById('show-labels-check');
    const showVectorsCheck = document.getElementById('show-vectors-check');
    const showForcesCheck = document.getElementById('show-forces-check');
    const showAnglesCheck = document.getElementById('show-angles-check');
    const showTrail1Check = document.getElementById('show-trail1-check');
    const showPathMarksCheck = document.getElementById('show-pathmarks-check');

    const pauseBtn = document.getElementById('pause-btn');
    const resetBtn = document.getElementById('reset-btn');
    const clearTrailBtn = document.getElementById('clear-trail-btn');
    const randomBtn = document.getElementById('random-btn');

    // Update display values
    function updateSliderDisplays() {
      document.getElementById('l1-val').textContent = l1Slider.value;
      document.getElementById('l2-val').textContent = l2Slider.value;
      document.getElementById('m1-val').textContent = m1Slider.value;
      document.getElementById('m2-val').textContent = m2Slider.value;
      document.getElementById('g-val').textContent = parseFloat(gSlider.value).toFixed(2);
      document.getElementById('angle1-val').textContent = angle1Slider.value;
      document.getElementById('angle2-val').textContent = angle2Slider.value;
      document.getElementById('trail-val').textContent = trailSlider.value;
      document.getElementById('count-val').textContent = countSlider.value;
    }

    [l1Slider, l2Slider, m1Slider, m2Slider, gSlider, angle1Slider, angle2Slider, trailSlider, countSlider].forEach(el => {
      el.addEventListener('input', updateSliderDisplays);
    });

    // Pendulum state
    let paused = false;
    let time = 0;
    let pendulums = [];

    const dt = 0.02; // RK4 time step
    const colors = [
      '#ff2200',  // red
      '#ff8800',  // orange
      '#ffcc00'   // yellow
    ];

    // Helper functions for drawing anchored marks, vectors, and arcs
    function drawArrow(ctx, fromX, fromY, toX, toY, color, label, lineWidth = 2) {
      const dx = toX - fromX;
      const dy = toY - fromY;
      const len = Math.hypot(dx, dy);
      if (len < 3) return;

      const angle = Math.atan2(dy, dx);
      const headLen = Math.min(8, len * 0.4);

      ctx.save();
      ctx.strokeStyle = color;
      ctx.fillStyle = color;
      ctx.lineWidth = lineWidth;

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

      // Arrowhead
      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.lineTo(toX - headLen * Math.cos(angle + Math.PI / 6), toY - headLen * Math.sin(angle + Math.PI / 6));
      ctx.closePath();
      ctx.fill();

      // Label at arrowhead
      if (label) {
        ctx.font = '500 10px "DM Mono", monospace';
        const textWidth = ctx.measureText(label).width;
        const lx = toX + Math.cos(angle) * 10;
        const ly = toY + Math.sin(angle) * 10;

        ctx.fillStyle = 'rgba(10, 10, 15, 0.85)';
        ctx.fillRect(lx - 4, ly - 9, textWidth + 8, 14);
        ctx.strokeStyle = color;
        ctx.lineWidth = 1;
        ctx.strokeRect(lx - 4, ly - 9, textWidth + 8, 14);

        ctx.fillStyle = color;
        ctx.textAlign = 'left';
        ctx.textBaseline = 'middle';
        ctx.fillText(label, lx, ly - 2);
      }
      ctx.restore();
    }

    function drawMassBadge(ctx, x, y, labelText, color, offsetX = 16, offsetY = -16) {
      ctx.save();
      ctx.font = '500 11px "DM Mono", monospace';
      const textWidth = ctx.measureText(labelText).width;
      const badgeW = textWidth + 12;
      const badgeH = 18;
      const bx = x + offsetX;
      const by = y + offsetY;

      // Leader line connecting center of mass to floating badge
      ctx.strokeStyle = color;
      ctx.lineWidth = 1;
      ctx.setLineDash([2, 2]);
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(bx < x ? bx + badgeW : bx, by + badgeH / 2);
      ctx.stroke();
      ctx.setLineDash([]);

      // Badge background pill
      ctx.fillStyle = 'rgba(12, 12, 18, 0.88)';
      ctx.beginPath();
      ctx.roundRect(bx, by, badgeW, badgeH, 4);
      ctx.fill();

      // Border
      ctx.strokeStyle = color;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.roundRect(bx, by, badgeW, badgeH, 4);
      ctx.stroke();

      // Text inside badge
      ctx.fillStyle = '#e8e0d5';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'middle';
      ctx.fillText(labelText, bx + 6, by + badgeH / 2 + 1);
      ctx.restore();
    }

    function drawAngleArc(ctx, cx, cy, thetaRad, radius, labelText, color) {
      ctx.save();
      // Downward vertical reference line
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
      ctx.setLineDash([3, 3]);
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.lineTo(cx, cy + radius + 15);
      ctx.stroke();

      // Arc from downward vertical (Math.PI / 2) to rod angle
      const startA = Math.PI / 2;
      const rodA = Math.atan2(Math.cos(thetaRad), Math.sin(thetaRad));

      ctx.setLineDash([]);
      ctx.strokeStyle = color;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(cx, cy, radius, Math.min(startA, rodA), Math.max(startA, rodA));
      ctx.stroke();

      if (labelText) {
        const midA = (startA + rodA) / 2;
        const lx = cx + Math.cos(midA) * (radius + 16);
        const ly = cy + Math.sin(midA) * (radius + 16);
        ctx.font = '500 10px "DM Mono", monospace';
        ctx.fillStyle = color;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(labelText, lx, ly);
      }
      ctx.restore();
    }

    function drawRodLabel(ctx, x1, y1, x2, y2, labelText, color) {
      ctx.save();
      const mx = (x1 + x2) / 2;
      const my = (y1 + y2) / 2;
      const dx = x2 - x1;
      const dy = y2 - y1;
      const angle = Math.atan2(dy, dx);

      // Perpendicular offset
      const nx = -Math.sin(angle) * 14;
      const ny = Math.cos(angle) * 14;

      ctx.font = '500 10px "DM Mono", monospace';
      const textWidth = ctx.measureText(labelText).width;

      ctx.fillStyle = 'rgba(10, 10, 15, 0.75)';
      ctx.fillRect(mx + nx - textWidth / 2 - 4, my + ny - 7, textWidth + 8, 14);

      ctx.fillStyle = color;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(labelText, mx + nx, my + ny);
      ctx.restore();
    }

    // Pendulum class
    class Pendulum {
      constructor(theta1, theta2, offsetAngle = 0) {
        this.theta1 = (theta1 + offsetAngle) * Math.PI / 180;
        this.theta2 = (theta2 + offsetAngle) * Math.PI / 180;
        this.theta1_vel = 0;
        this.theta2_vel = 0;
        this.trail = [];   // trail for m2
        this.trail1 = [];  // trail for m1
        this.colorIdx = Math.floor(Math.random() * colors.length);
      }

      getAccelerations(theta1, theta2, theta1_vel, theta2_vel, L1, L2, m1, m2, g) {
        const dtheta = theta1 - theta2;
        const c = Math.cos(dtheta);
        const s = Math.sin(dtheta);

        const denom = L1 * (2 * m1 + m2 - m2 * Math.cos(2 * (theta1 - theta2)));
        if (Math.abs(denom) < 1e-6) return [0, 0];

        const num1 = -g * (2 * m1 + m2) * Math.sin(theta1)
                   - m2 * g * Math.sin(theta1 - 2 * theta2)
                   - 2 * s * m2 * (theta2_vel * theta2_vel * L2 + theta1_vel * theta1_vel * L1 * c);
        const theta1_acc = num1 / denom;

        const denom2 = L2 * (2 * m1 + m2 - m2 * Math.cos(2 * (theta1 - theta2)));
        const num2 = 2 * s * (theta1_vel * theta1_vel * L1 * (m1 + m2) + g * (m1 + m2) * Math.cos(theta1)
                            + theta2_vel * theta2_vel * L2 * m2 * c);
        const theta2_acc = num2 / denom2;

        return [theta1_acc, theta2_acc];
      }

      step(L1, L2, m1, m2, g) {
        const rk4Step = (theta, theta_vel, accel_fn) => {
          const k1 = theta_vel;
          const ak1 = accel_fn(theta, theta_vel);

          const k2 = theta_vel + ak1 * dt / 2;
          const ak2 = accel_fn(theta + k1 * dt / 2, k2);

          const k3 = theta_vel + ak2 * dt / 2;
          const ak3 = accel_fn(theta + k2 * dt / 2, k3);

          const k4 = theta_vel + ak3 * dt;
          const ak4 = accel_fn(theta + k3 * dt, k4);

          return {
            pos: theta + (k1 + 2 * k2 + 2 * k3 + k4) * dt / 6,
            vel: theta_vel + (ak1 + 2 * ak2 + 2 * ak3 + ak4) * dt / 6
          };
        };

        const theta1_accel_fn = (t1, tv1) => {
          const [a1] = this.getAccelerations(t1, this.theta2, tv1, this.theta2_vel, L1, L2, m1, m2, g);
          return a1;
        };

        const theta2_accel_fn = (t2, tv2) => {
          const [, a2] = this.getAccelerations(this.theta1, t2, this.theta1_vel, tv2, L1, L2, m1, m2, g);
          return a2;
        };

        const res1 = rk4Step(this.theta1, this.theta1_vel, theta1_accel_fn);
        const res2 = rk4Step(this.theta2, this.theta2_vel, theta2_accel_fn);

        this.theta1 = res1.pos;
        this.theta1_vel = res1.vel;
        this.theta2 = res2.pos;
        this.theta2_vel = res2.vel;
      }

      addTrailPoint(x1, y1, x2, y2) {
        this.trail.push({ x: x2, y: y2, age: 0 });
        this.trail1.push({ x: x1, y: y1, age: 0 });
        const maxLen = parseInt(trailSlider.value);
        if (this.trail.length > maxLen) {
          this.trail.shift();
        }
        if (this.trail1.length > maxLen) {
          this.trail1.shift();
        }
      }

      ageDtrail() {
        this.trail.forEach(pt => pt.age++);
        this.trail1.forEach(pt => pt.age++);
      }

      getPosition(L1, L2, pivotX, pivotY) {
        const x1 = pivotX + L1 * Math.sin(this.theta1);
        const y1 = pivotY + L1 * Math.cos(this.theta1);
        const x2 = x1 + L2 * Math.sin(this.theta2);
        const y2 = y1 + L2 * Math.cos(this.theta2);
        return { x1, y1, x2, y2 };
      }

      getEnergy(L1, L2, m1, m2, g, pivotY) {
        const { x1, y1, x2, y2 } = this.getPosition(L1, L2, canvas.width / 2, pivotY);
        const h1 = pivotY - y1;
        const h2 = pivotY - y2;
        const pe = m1 * g * h1 + m2 * g * h2;
        const ke = 0.5 * m1 * (L1 * this.theta1_vel) ** 2 + 0.5 * m2 * ((L1 * this.theta1_vel) ** 2 + (L2 * this.theta2_vel) ** 2 + 2 * L1 * L2 * this.theta1_vel * this.theta2_vel * Math.cos(this.theta1 - this.theta2));
        return pe + ke;
      }
    }

    function initPendulums() {
      pendulums = [];
      const count = parseInt(countSlider.value);
      const angle1 = parseInt(angle1Slider.value);
      const angle2 = parseInt(angle2Slider.value);
      for (let i = 0; i < count; i++) {
        const offsetAngle = i === 0 ? 0 : (0.5 + i * 0.2); // chaos: tiny angle offsets
        pendulums.push(new Pendulum(angle1, angle2, offsetAngle));
      }
      time = 0;
    }

    function updateStats() {
      if (pendulums.length === 0) return;
      const p = pendulums[0]; // display first pendulum
      const L1 = parseInt(l1Slider.value);
      const L2 = parseInt(l2Slider.value);
      const m1 = parseFloat(m1Slider.value);
      const m2 = parseFloat(m2Slider.value);
      const g = parseFloat(gSlider.value);

      document.getElementById('time-display').textContent = time.toFixed(2);
      document.getElementById('theta1-display').textContent = (p.theta1 * 180 / Math.PI % 360).toFixed(2);
      document.getElementById('theta2-display').textContent = (p.theta2 * 180 / Math.PI % 360).toFixed(2);

      const energy = p.getEnergy(L1, L2, m1, m2, g, 80);
      document.getElementById('energy-display').textContent = energy.toFixed(2);
    }

    function drawPendulum(pend, idx) {
      const L1 = parseInt(l1Slider.value);
      const L2 = parseInt(l2Slider.value);
      const m1 = parseFloat(m1Slider.value);
      const m2 = parseFloat(m2Slider.value);
      const g = parseFloat(gSlider.value);

      const pivotX = canvas.width / 2;
      const pivotY = 80;

      const { x1, y1, x2, y2 } = pend.getPosition(L1, L2, pivotX, pivotY);

      // Update trail
      pend.addTrailPoint(x1, y1, x2, y2);
      pend.ageDtrail();

      // Draw m2 trail (rainbow)
      if (pend.trail.length > 1) {
        const maxTrailLen = parseInt(trailSlider.value);
        const hueOffset = (time * 0.5) % 360;

        for (let i = 0; i < pend.trail.length - 1; i++) {
          const pt = pend.trail[i];
          const ptNext = pend.trail[i + 1];
          const ratio = i / pend.trail.length;
          const hue = (hueOffset + ratio * 120) % 360;
          const color = `hsl(${hue}, 100%, 50%)`;

          const ageFactor = 1 - (pt.age / maxTrailLen);
          ctx.globalAlpha = Math.max(0, ageFactor * 0.6);
          ctx.strokeStyle = color;
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(pt.x, pt.y);
          ctx.lineTo(ptNext.x, ptNext.y);
          ctx.stroke();
        }
      }

      // Draw m1 trail (if enabled)
      if (showTrail1Check && showTrail1Check.checked && pend.trail1.length > 1) {
        const maxTrailLen = parseInt(trailSlider.value);
        for (let i = 0; i < pend.trail1.length - 1; i++) {
          const pt = pend.trail1[i];
          const ptNext = pend.trail1[i + 1];
          const ageFactor = 1 - (pt.age / maxTrailLen);
          ctx.globalAlpha = Math.max(0, ageFactor * 0.4);
          ctx.strokeStyle = '#ff00aa';
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.moveTo(pt.x, pt.y);
          ctx.lineTo(ptNext.x, ptNext.y);
          ctx.stroke();
        }
      }

      ctx.globalAlpha = 1;

      // Trajectory Path Markers (milestone dots along path)
      if (showPathMarksCheck && showPathMarksCheck.checked && pend.trail.length > 15) {
        const step = Math.max(12, Math.floor(pend.trail.length / 8));
        for (let i = step; i < pend.trail.length; i += step) {
          const pt = pend.trail[i];
          ctx.save();
          ctx.fillStyle = idx === 0 ? '#00c896' : '#ffcc00';
          ctx.beginPath();
          ctx.arc(pt.x, pt.y, 2.5, 0, Math.PI * 2);
          ctx.fill();

          ctx.font = '9px "DM Mono", monospace';
          ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
          ctx.fillText(`p${i}`, pt.x + 4, pt.y - 3);
          ctx.restore();
        }
      }

      // Draw angle arcs
      if (showAnglesCheck && showAnglesCheck.checked && idx === 0) {
        const deg1 = (pend.theta1 * 180 / Math.PI % 360).toFixed(1);
        const deg2 = (pend.theta2 * 180 / Math.PI % 360).toFixed(1);
        drawAngleArc(ctx, pivotX, pivotY, pend.theta1, 35, `θ₁=${deg1}°`, '#ff6600');
        drawAngleArc(ctx, x1, y1, pend.theta2, 30, `θ₂=${deg2}°`, '#00d2ff');
      }

      // Draw rods
      ctx.strokeStyle = idx === 0 ? '#ff2200' : '#ff8800';
      ctx.lineWidth = 6;
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';
      ctx.beginPath();
      ctx.moveTo(pivotX, pivotY);
      ctx.lineTo(x1, y1);
      ctx.stroke();

      ctx.strokeStyle = idx === 0 ? '#e8e0d5' : '#ffcc00';
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();

      // Rod labels
      if (showLabelsCheck && showLabelsCheck.checked && idx === 0) {
        drawRodLabel(ctx, pivotX, pivotY, x1, y1, `L₁=${L1}px`, 'rgba(255, 255, 255, 0.7)');
        drawRodLabel(ctx, x1, y1, x2, y2, `L₂=${L2}px`, 'rgba(255, 255, 255, 0.7)');
      }

      // Draw masses
      const radius1 = 4 + m1 * 2;
      const radius2 = 4 + m2 * 2;

      // Mass 1
      ctx.fillStyle = '#ff2200';
      ctx.shadowColor = 'rgba(255, 34, 0, 0.6)';
      ctx.shadowBlur = 12;
      ctx.beginPath();
      ctx.arc(x1, y1, radius1, 0, Math.PI * 2);
      ctx.fill();

      // Mass 2
      ctx.fillStyle = idx === 0 ? '#00c896' : '#ffff00';
      ctx.shadowColor = idx === 0 ? 'rgba(0, 200, 150, 0.6)' : 'rgba(255, 255, 0, 0.6)';
      ctx.beginPath();
      ctx.arc(x2, y2, radius2, 0, Math.PI * 2);
      ctx.fill();

      ctx.shadowColor = 'transparent';
      ctx.shadowBlur = 0;

      // Object Marks / Badges (anchored to moving objects)
      if (showLabelsCheck && showLabelsCheck.checked) {
        const pPrefix = pendulums.length > 1 ? `P${idx + 1}:` : '';
        if (idx === 0) {
          drawMassBadge(ctx, pivotX, pivotY, 'O (0,0)', '#888888', -45, -25);
        }
        drawMassBadge(ctx, x1, y1, `${pPrefix}m₁ (${m1.toFixed(1)}kg)`, '#ff4400', 16, -20);
        drawMassBadge(ctx, x2, y2, `${pPrefix}m₂ (${m2.toFixed(1)}kg)`, idx === 0 ? '#00c896' : '#ffff00', 16, -20);
      }

      // Velocities and velocity vector arrows
      const vx1 = L1 * pend.theta1_vel * Math.cos(pend.theta1);
      const vy1 = -L1 * pend.theta1_vel * Math.sin(pend.theta1);
      const vx2 = vx1 + L2 * pend.theta2_vel * Math.cos(pend.theta2);
      const vy2 = vy1 - L2 * pend.theta2_vel * Math.sin(pend.theta2);
      const speed1 = Math.hypot(vx1, vy1);
      const speed2 = Math.hypot(vx2, vy2);

      if (showVectorsCheck && showVectorsCheck.checked && idx === 0) {
        const vScale = 0.15;
        drawArrow(ctx, x1, y1, x1 + vx1 * vScale, y1 + vy1 * vScale, '#00d2ff', `v₁ (${speed1.toFixed(1)})`);
        drawArrow(ctx, x2, y2, x2 + vx2 * vScale, y2 + vy2 * vScale, '#00ffaa', `v₂ (${speed2.toFixed(1)})`);
      }

      // Forces (Gravity & Tension)
      if (showForcesCheck && showForcesCheck.checked && idx === 0) {
        const gScale = 3.5;
        drawArrow(ctx, x1, y1, x1, y1 + m1 * g * gScale, '#ff4444', `m₁g`);
        drawArrow(ctx, x2, y2, x2, y2 + m2 * g * gScale, '#ff4444', `m₂g`);

        const dx1 = pivotX - x1, dy1 = pivotY - y1;
        const d1 = Math.hypot(dx1, dy1) || 1;
        drawArrow(ctx, x1, y1, x1 + (dx1 / d1) * 35, y1 + (dy1 / d1) * 35, '#ffbb00', `T₁`);

        const dx2 = x1 - x2, dy2 = y1 - y2;
        const d2 = Math.hypot(dx2, dy2) || 1;
        drawArrow(ctx, x2, y2, x2 + (dx2 / d2) * 35, y2 + (dy2 / d2) * 35, '#ffbb00', `T₂`);
      }
    }

    function animate() {
      // Clear canvas
      ctx.fillStyle = '#000000';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // Update physics
      if (!paused) {
        const L1 = parseInt(l1Slider.value);
        const L2 = parseInt(l2Slider.value);
        const m1 = parseFloat(m1Slider.value);
        const m2 = parseFloat(m2Slider.value);
        const g = parseFloat(gSlider.value);

        pendulums.forEach(pend => {
          pend.step(L1, L2, m1, m2, g);
        });

        time += dt;
      }

      // Draw pivot point
      ctx.fillStyle = '#555555';
      ctx.beginPath();
      ctx.arc(canvas.width / 2, 80, 6, 0, Math.PI * 2);
      ctx.fill();

      // Draw all pendulums
      pendulums.forEach((pend, idx) => {
        drawPendulum(pend, idx);
      });

      updateStats();
      requestAnimationFrame(animate);
    }

    // Event listeners
    pauseBtn.addEventListener('click', () => {
      paused = !paused;
      pauseBtn.textContent = paused ? 'Resume' : 'Pause';
      const badge = document.getElementById('status-badge');
      const text = document.getElementById('status-text');
      if (paused) {
        badge.classList.remove('running');
        badge.classList.add('paused');
        text.textContent = 'Paused';
      } else {
        badge.classList.remove('paused');
        badge.classList.add('running');
        text.textContent = 'Running';
      }
    });

    resetBtn.addEventListener('click', () => {
      initPendulums();
      paused = false;
      pauseBtn.textContent = 'Pause';
      const badge = document.getElementById('status-badge');
      const text = document.getElementById('status-text');
      badge.classList.remove('paused');
      badge.classList.add('running');
      text.textContent = 'Running';
    });

    clearTrailBtn.addEventListener('click', () => {
      pendulums.forEach(pend => {
        pend.trail = [];
        pend.trail1 = [];
      });
    });

    randomBtn.addEventListener('click', () => {
      angle1Slider.value = Math.random() * 360;
      angle2Slider.value = Math.random() * 360;
      updateSliderDisplays();
      initPendulums();
    });

    // Parameter change listeners
    [l1Slider, l2Slider, m1Slider, m2Slider, gSlider, angle1Slider, angle2Slider].forEach(el => {
      el.addEventListener('input', () => {
        initPendulums();
      });
    });

    countSlider.addEventListener('input', () => {
      initPendulums();
    });

    // Start
    updateSliderDisplays();
    initPendulums();
    animate();