Physics Lab

STANDING WAVES

Explore standing waves, harmonics, and vibrational modes. Visualize nodes, antinodes, and the interference of traveling waves.

Standing Wave Simulation
Frequency
110.0
Hz
Wavelength
2.00
m
Nodes
2
Antinodes
1
Two-Wave Decomposition
3D String Oscillation (Isometric)
2D Membrane Modes (m,n)
1
1
Developer Reference

Core Algorithm & Standalone Script

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

// Global state
    const state = {
      isPlaying: true,
      harmonic: 1,
      waveSpeed: 100,
      amplitude: 0.5,
      stringLength: 2,
      damping: 0,
      boundaryCondition: 'fixed-fixed',
      animationSpeed: 1,
      time: 0,
      showNodes: true,
      showAntinodes: true,
      showEnvelope: true,
      showGrid: false,
      mode: 'string',
      modeM: 1,
      modeN: 1,
      decay: 1,
    };

    // DOM Elements
    const stringCanvas = document.getElementById('stringCanvas');
    const decompositionCanvas = document.getElementById('decompositionCanvas');
    const canvas3D = document.getElementById('canvas3D');
    const membraneCanvas = document.getElementById('membraneCanvas');

    const stringCtx = stringCanvas.getContext('2d');
    const decompositionCtx = decompositionCanvas.getContext('2d');
    const ctx3D = canvas3D.getContext('2d');
    const membraneCtx = membraneCanvas.getContext('2d');

    // Initialize harmonic buttons
    function initHarmonicSelector() {
      const selector = document.getElementById('harmonicSelector');
      selector.innerHTML = '';
      for (let n = 1; n <= 8; n++) {
        const btn = document.createElement('button');
        btn.className = `harmonic-btn ${n === 1 ? 'active' : ''}`;
        btn.textContent = n;
        btn.onclick = () => {
          state.harmonic = n;
          updateHarmonicSelector();
          document.getElementById('harmonic').value = n;
          document.getElementById('harmonicLabel').textContent = n;
          updateStats();
        };
        selector.appendChild(btn);
      }
    }

    function updateHarmonicSelector() {
      document.querySelectorAll('.harmonic-btn').forEach((btn, i) => {
        btn.classList.toggle('active', i + 1 === state.harmonic);
      });
    }

    // Update stats
    function updateStats() {
      const n = state.harmonic;
      const L = state.stringLength;
      const v = state.waveSpeed;

      const frequency = (n * v) / (2 * L);
      const wavelength = (2 * L) / n;
      const nodes = n + 1;
      const antinodes = n;

      document.getElementById('freqStat').textContent = frequency.toFixed(1);
      document.getElementById('wavelengthStat').textContent = wavelength.toFixed(2);
      document.getElementById('nodesStat').textContent = nodes;
      document.getElementById('antinodesStat').textContent = antinodes;
    }

    // Drawing functions
    function drawStringWave() {
      const canvas = stringCanvas;
      const ctx = stringCtx;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;

      // Set actual canvas size
      canvas.width = width;
      canvas.height = height;

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

      const n = state.harmonic;
      const L = state.stringLength;
      const A = state.amplitude;
      const v = state.waveSpeed;
      const centerY = height / 2;
      const pixelsPerMeter = width / L;

      // Draw grid
      if (state.showGrid) {
        ctx.strokeStyle = '#2a2a2a';
        ctx.lineWidth = 0.5;
        for (let i = 0; i <= n; i++) {
          const x = (i * width) / n;
          ctx.beginPath();
          ctx.moveTo(x, 0);
          ctx.lineTo(x, height);
          ctx.stroke();
        }
      }

      // Calculate wave data
      const waveData = [];
      for (let x = 0; x < width; x++) {
        const position = x / pixelsPerMeter;
        const k = (n * Math.PI) / L;
        const omega = (n * Math.PI * v) / L;
        const y = 2 * A * Math.sin(k * position) * Math.cos(omega * state.time) * state.decay;
        const amplitude = 2 * A * Math.abs(Math.sin(k * position));
        waveData.push({ x, y, amplitude });
      }

      // Draw envelope
      if (state.showEnvelope) {
        ctx.strokeStyle = 'rgba(68, 136, 255, 0.3)';
        ctx.lineWidth = 1;
        ctx.setLineDash([5, 5]);

        ctx.beginPath();
        for (let i = 0; i < waveData.length; i++) {
          const px = waveData[i].x;
          const py = centerY - waveData[i].amplitude * 50;
          if (i === 0) ctx.moveTo(px, py);
          else ctx.lineTo(px, py);
        }
        ctx.stroke();

        ctx.beginPath();
        for (let i = 0; i < waveData.length; i++) {
          const px = waveData[i].x;
          const py = centerY + waveData[i].amplitude * 50;
          if (i === 0) ctx.moveTo(px, py);
          else ctx.lineTo(px, py);
        }
        ctx.stroke();
        ctx.setLineDash([]);
      }

      // Draw wave with color gradient
      for (let i = 0; i < waveData.length - 1; i++) {
        const d1 = waveData[i];
        const d2 = waveData[i + 1];

        const amp = (d1.amplitude + d2.amplitude) / 2;
        const normalized = Math.min(amp / (2 * A), 1);

        // Color: blue at nodes, red at antinodes
        const r = Math.floor(255 * normalized);
        const g = Math.floor(136 * (1 - normalized));
        const b = Math.floor(255 * (1 - normalized));

        ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
        ctx.lineWidth = 3;

        ctx.beginPath();
        ctx.moveTo(d1.x, centerY - d1.y * 50);
        ctx.lineTo(d2.x, centerY - d2.y * 50);
        ctx.stroke();
      }

      // Draw nodes
      if (state.showNodes) {
        ctx.fillStyle = '#4488ff';
        for (let m = 0; m <= n; m++) {
          const nodeX = (m * width) / n;
          ctx.beginPath();
          ctx.arc(nodeX, centerY, 4, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      // Draw antinodes
      if (state.showAntinodes) {
        ctx.fillStyle = '#ff2200';
        for (let m = 0; m < n; m++) {
          const antinodeX = ((2 * m + 1) * width) / (2 * n);
          ctx.beginPath();
          ctx.arc(antinodeX, centerY, 3, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      // Draw boundary conditions
      ctx.strokeStyle = '#e8e0d5';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(0, centerY);
      ctx.lineTo(width, centerY);
      ctx.stroke();

      // Boundary markers
      ctx.fillStyle = '#e8e0d5';
      ctx.fillRect(-2, centerY - 5, 4, 10);
      ctx.fillRect(width - 2, centerY - 5, 4, 10);
    }

    function drawDecomposition() {
      const canvas = decompositionCanvas;
      const ctx = decompositionCtx;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;

      canvas.width = width;
      canvas.height = height;

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

      const n = state.harmonic;
      const L = state.stringLength;
      const A = state.amplitude;
      const v = state.waveSpeed;
      const centerY = height / 2;
      const pixelsPerMeter = width / (L * 1.5);

      const rowHeight = height / 3;

      // Wave 1: moving right (red)
      ctx.fillStyle = 'rgba(26, 26, 26, 0.7)';
      ctx.fillRect(0, 0, width, rowHeight);

      const k = (n * Math.PI) / L;
      const omega = (n * Math.PI * v) / L;
      const phase = omega * state.time;

      // Right-moving wave
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.beginPath();
      for (let x = 0; x < width; x++) {
        const pos = x / pixelsPerMeter;
        const y = A * Math.sin(k * pos - phase) * state.decay;
        const py = rowHeight / 2 - y * 50;
        if (x === 0) ctx.moveTo(x, py);
        else ctx.lineTo(x, py);
      }
      ctx.stroke();

      // Wave 2: moving left (blue)
      ctx.fillStyle = 'rgba(26, 26, 26, 0.7)';
      ctx.fillRect(0, rowHeight, width, rowHeight);

      ctx.strokeStyle = '#4488ff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      for (let x = 0; x < width; x++) {
        const pos = x / pixelsPerMeter;
        const y = A * Math.sin(k * pos + phase) * state.decay;
        const py = rowHeight + rowHeight / 2 - y * 50;
        if (x === 0) ctx.moveTo(x, py);
        else ctx.lineTo(x, py);
      }
      ctx.stroke();

      // Standing wave (sum)
      ctx.fillStyle = 'rgba(26, 26, 26, 0.7)';
      ctx.fillRect(0, rowHeight * 2, width, rowHeight);

      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 3;
      ctx.beginPath();
      for (let x = 0; x < width; x++) {
        const pos = x / pixelsPerMeter;
        const y = 2 * A * Math.sin(k * pos) * Math.cos(omega * state.time) * state.decay;
        const py = rowHeight * 2 + rowHeight / 2 - y * 50;
        if (x === 0) ctx.moveTo(x, py);
        else ctx.lineTo(x, py);
      }
      ctx.stroke();

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.fillText('→ Right-moving wave', 10, 20);
      ctx.fillText('← Left-moving wave', 10, rowHeight + 20);
      ctx.fillText('= Standing wave (sum)', 10, rowHeight * 2 + 20);
    }

    function draw3D() {
      const canvas = canvas3D;
      const ctx = ctx3D;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;

      canvas.width = width;
      canvas.height = height;

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

      const n = state.harmonic;
      const L = state.stringLength;
      const A = state.amplitude;
      const v = state.waveSpeed;
      const centerX = width / 2;
      const centerY = height / 2;
      const scale = Math.min(width, height) / 8;

      // Isometric projection helper
      function isometricProject(x, y, z) {
        const angle = Math.PI / 6;
        const screenX = (x - y) * Math.cos(angle);
        const screenY = (x + y) * Math.sin(angle) - z;
        return { x: centerX + screenX * scale, y: centerY + screenY * scale };
      }

      const k = (n * Math.PI) / L;
      const omega = (n * Math.PI * v) / L;

      // Draw 3D string oscillating
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;

      const segments = 50;
      for (let seg = 0; seg < segments; seg++) {
        const x1 = (seg / segments) * L - L / 2;
        const x2 = ((seg + 1) / segments) * L - L / 2;

        const z1 = 2 * A * Math.sin(k * (x1 + L / 2)) * Math.cos(omega * state.time) * state.decay;
        const z2 = 2 * A * Math.sin(k * (x2 + L / 2)) * Math.cos(omega * state.time) * state.decay;

        const p1 = isometricProject(x1, 0, z1);
        const p2 = isometricProject(x2, 0, z2);

        ctx.beginPath();
        ctx.moveTo(p1.x, p1.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.stroke();
      }

      // Draw reference axes
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 1;
      ctx.setLineDash([2, 2]);

      const p0 = isometricProject(-L / 2, 0, 0);
      const pL = isometricProject(L / 2, 0, 0);
      ctx.beginPath();
      ctx.moveTo(p0.x, p0.y);
      ctx.lineTo(pL.x, pL.y);
      ctx.stroke();

      ctx.setLineDash([]);

      // Draw nodes
      if (state.showNodes) {
        ctx.fillStyle = '#4488ff';
        for (let m = 0; m <= n; m++) {
          const x = (m / n) * L - L / 2;
          const p = isometricProject(x, 0, 0);
          ctx.beginPath();
          ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.fillText('3D Standing Wave (Isometric)', 10, 20);
    }

    function drawMembrane() {
      const canvas = membraneCanvas;
      const ctx = membraneCtx;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;

      canvas.width = width;
      canvas.height = height;

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

      const m = state.modeM;
      const n = state.modeN;
      const scale = 1;

      // Simple 2D surface plot representation
      const pixelSize = 4;
      const rows = Math.floor(height / pixelSize);
      const cols = Math.floor(width / pixelSize);

      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          const x = (col / cols) * Math.PI;
          const y = (row / rows) * Math.PI;

          const z = Math.sin(m * x) * Math.sin(n * y) * Math.cos(state.time * 2) * state.decay;
          const normalized = (z + 1) / 2;

          const hue = normalized * 240;
          const r = Math.sin(hue * Math.PI / 180) * 255;
          const g = Math.sin((hue + 120) * Math.PI / 180) * 255;
          const b = Math.sin((hue + 240) * Math.PI / 180) * 255;

          ctx.fillStyle = `rgb(${Math.max(0, r)}, ${Math.max(0, g)}, ${Math.max(0, b)})`;
          ctx.fillRect(col * pixelSize, row * pixelSize, pixelSize, pixelSize);
        }
      }

      // Draw boundary
      ctx.strokeStyle = '#e8e0d5';
      ctx.lineWidth = 2;
      ctx.strokeRect(0, 0, width, height);
    }

    // Animation loop
    function animate() {
      if (state.isPlaying) {
        state.time += 0.016 * state.animationSpeed;
        state.decay = Math.max(0, 1 - state.time * state.damping * 0.1);
      }

      if (state.mode === 'string') {
        drawStringWave();
      } else if (state.mode === 'decomposition') {
        drawDecomposition();
      } else if (state.mode === '3d') {
        draw3D();
      } else if (state.mode === 'membrane') {
        drawMembrane();
      }

      requestAnimationFrame(animate);
    }

    // Event listeners
    document.getElementById('harmonic').addEventListener('input', (e) => {
      state.harmonic = parseInt(e.target.value);
      document.getElementById('harmonicLabel').textContent = state.harmonic;
      updateHarmonicSelector();
      updateStats();
    });

    document.getElementById('waveSpeed').addEventListener('input', (e) => {
      state.waveSpeed = parseInt(e.target.value);
      document.getElementById('waveSpeedLabel').textContent = state.waveSpeed;
      updateStats();
    });

    document.getElementById('amplitude').addEventListener('input', (e) => {
      state.amplitude = parseFloat(e.target.value);
      document.getElementById('amplitudeLabel').textContent = state.amplitude.toFixed(2);
    });

    document.getElementById('stringLength').addEventListener('input', (e) => {
      state.stringLength = parseFloat(e.target.value);
      document.getElementById('stringLengthLabel').textContent = state.stringLength.toFixed(2);
      updateStats();
    });

    document.getElementById('damping').addEventListener('input', (e) => {
      state.damping = parseFloat(e.target.value);
      document.getElementById('dampingLabel').textContent = state.damping.toFixed(2);
    });

    document.getElementById('animationSpeed').addEventListener('input', (e) => {
      state.animationSpeed = parseFloat(e.target.value);
      document.getElementById('animationSpeedLabel').textContent = state.animationSpeed.toFixed(1) + '×';
    });

    document.getElementById('boundaryCondition').addEventListener('change', (e) => {
      state.boundaryCondition = e.target.value;
    });

    document.getElementById('playPauseBtn').addEventListener('click', () => {
      state.isPlaying = !state.isPlaying;
      const btn = document.getElementById('playPauseBtn');
      btn.textContent = state.isPlaying ? 'Pause' : 'Play';
      btn.classList.toggle('active');
    });

    document.getElementById('resetBtn').addEventListener('click', () => {
      state.time = 0;
      state.decay = 1;
    });

    document.getElementById('showNodes').addEventListener('change', (e) => {
      state.showNodes = e.target.checked;
    });

    document.getElementById('showAntinodes').addEventListener('change', (e) => {
      state.showAntinodes = e.target.checked;
    });

    document.getElementById('showEnvelope').addEventListener('change', (e) => {
      state.showEnvelope = e.target.checked;
    });

    document.getElementById('showGrid').addEventListener('change', (e) => {
      state.showGrid = e.target.checked;
    });

    document.getElementById('modeM').addEventListener('input', (e) => {
      state.modeM = parseInt(e.target.value);
      document.getElementById('modeMLabel').textContent = state.modeM;
    });

    document.getElementById('modeN').addEventListener('input', (e) => {
      state.modeN = parseInt(e.target.value);
      document.getElementById('modeNLabel').textContent = state.modeN;
    });

    // Mode switching
    document.querySelectorAll('.mode-btn').forEach((btn) => {
      btn.addEventListener('click', () => {
        const mode = btn.dataset.mode;
        state.mode = mode;

        document.querySelectorAll('.mode-btn').forEach((b) => b.classList.remove('active'));
        btn.classList.add('active');

        document.querySelectorAll('.mode-section').forEach((sec) => sec.classList.remove('active'));
        document.getElementById(mode + '-mode').classList.add('active');
      });
    });

    // Initialize
    initHarmonicSelector();
    updateStats();
    animate();