Physics Lab

EM WAVE

Interactive electromagnetic wave propagation visualization. Explore electric and magnetic fields, polarization modes, and the Poynting vector.

Electromagnetic Spectrum 470 nm (Green)
Radio Micro IR Visible UV X-ray Gamma
470 nm
50 V/m
1.0x
20°
Wavelength (λ)
470
nm
Frequency (f)
6.38
×10¹⁴ Hz
Period (T)
1.57
×10⁻¹⁵ s
E₀
50
V/m
B₀ (E₀/c)
1.67
×10⁻⁷ T
Photon Energy
2.64
eV

Physics Fundamentals

E(x,t) = E₀ · sin(kx - ωt)
B(x,t) = B₀ · sin(kx - ωt) = (E₀/c) · sin(kx - ωt)
Poynting Vector: S = E × B / μ₀
Photon Energy: E = h·f = ℏ·ω

The electric field (red) and magnetic field (blue) oscillate perpendicular to each other and to the direction of propagation (x-axis). The wave travels at the speed of light c = 1/√(μ₀ε₀) ≈ 3×10⁸ m/s. The Poynting vector (yellow) shows the direction and intensity of energy flow.

Developer Reference

Core Algorithm & Standalone Script

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

// Constants
    const c = 3e8; // speed of light m/s
    const h = 6.626e-34; // Planck constant
    const mu0 = 4 * Math.PI * 1e-7; // permeability of free space
    const eV = 1.602e-19; // electron volt

    // Canvas setup
    const canvas = document.getElementById('waveCanvas');
    const ctx = canvas.getContext('2d');

    // Responsive canvas
    function resizeCanvas() {
      const rect = canvas.parentElement.getBoundingClientRect();
      canvas.width = rect.width - 40;
      canvas.height = 400;
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    // State
    let state = {
      wavelength: 470e-9, // m
      amplitude: 50, // V/m
      animationSpeed: 1,
      viewRotation: 20, // degrees
      polarization: 'linear',
      showE: true,
      showB: true,
      showS: false,
      time: 0
    };

    // Update wavelength from spectrum position
    function updateFromWavelength(wl) {
      state.wavelength = wl * 1e-9;
      updateStats();
      updateSpectrumMarker();
    }

    // Calculate derived values
    function getWaveParams() {
      const wavelength = state.wavelength;
      const frequency = c / wavelength;
      const omega = 2 * Math.PI * frequency;
      const k = 2 * Math.PI / wavelength;
      const period = 1 / frequency;
      const B0 = state.amplitude / c;
      const photonEnergy = h * frequency / eV;

      return { frequency, omega, k, period, B0, photonEnergy, wavelength };
    }

    function updateStats() {
      const p = getWaveParams();
      const wlNm = p.wavelength * 1e9;

      document.getElementById('statWavelength').textContent = wlNm.toFixed(0);
      document.getElementById('statFrequency').textContent = (p.frequency / 1e14).toFixed(2);
      document.getElementById('statPeriod').textContent = (p.period / 1e-15).toFixed(2);
      document.getElementById('statE0').textContent = state.amplitude.toFixed(0);
      document.getElementById('statB0').textContent = (p.B0 * 1e7).toFixed(2);
      document.getElementById('statPhoton').textContent = p.photonEnergy.toFixed(2);

      // Frequency display
      const wl = state.wavelength * 1e9;
      let region = 'Radio';
      if (wl < 1e-12) region = 'Gamma';
      else if (wl < 10e-9) region = 'X-ray';
      else if (wl < 400) region = 'UV';
      else if (wl < 700) region = 'Visible';
      else if (wl < 1e6) region = 'IR';
      else if (wl < 1e9) region = 'Micro';

      const color = wl < 380 ? 'Violet' : wl < 450 ? 'Blue' : wl < 495 ? 'Cyan' :
                    wl < 570 ? 'Green' : wl < 590 ? 'Yellow' : wl < 620 ? 'Orange' : 'Red';

      if (wl >= 380 && wl <= 700) {
        document.getElementById('freqDisplay').textContent = `${wl.toFixed(0)} nm (${color})`;
      } else {
        document.getElementById('freqDisplay').textContent = `${wl.toFixed(1)} nm (${region})`;
      }
    }

    function updateSpectrumMarker() {
      const wl = state.wavelength * 1e9;
      // Map wavelength to position (10 nm to 1000 nm)
      const position = Math.max(0, Math.min(100, (Math.log10(wl) - 1) / 2 * 100));
      document.getElementById('spectrumMarker').style.left = position + '%';
    }

    // Draw 3D wave
    function drawWave() {
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const p = getWaveParams();
      const centerY = canvas.height / 2;
      const centerX = 50;
      const amplitude = state.amplitude / 50; // Scale for display
      const displayWavelength = canvas.width / 5; // Show ~3-4 wavelengths

      // View angle in radians
      const viewAngle = state.viewRotation * Math.PI / 180;
      const scale = Math.cos(viewAngle) * 0.7 + 0.5;

      state.time += state.animationSpeed * 0.05;
      const phase = state.time;

      // Draw propagation axis
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(20, centerY);
      ctx.lineTo(canvas.width - 20, centerY);
      ctx.stroke();

      // Arrow at end
      const arrowX = canvas.width - 20;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.moveTo(arrowX, centerY);
      ctx.lineTo(arrowX - 10, centerY - 6);
      ctx.lineTo(arrowX - 10, centerY + 6);
      ctx.closePath();
      ctx.fill();

      // Label 'c'
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 14px DM Mono';
      ctx.fillText('c', arrowX - 20, centerY - 15);

      // Draw E field (red sine wave)
      if (state.showE) {
        ctx.strokeStyle = '#ff2200';
        ctx.lineWidth = 3;
        ctx.beginPath();
        let first = true;
        for (let x = 20; x < canvas.width - 20; x += 2) {
          const normalizedX = (x - 20) / displayWavelength;
          const y = Math.sin(normalizedX * 2 * Math.PI - phase) * amplitude * 40 * scale;
          const screenY = centerY - y;
          if (first) {
            ctx.moveTo(x, screenY);
            first = false;
          } else {
            ctx.lineTo(x, screenY);
          }
        }
        ctx.stroke();

        // E field label and arrows
        ctx.fillStyle = '#ff2200';
        ctx.font = '12px DM Mono';
        ctx.fillText('E', 10, centerY - 50);
      }

      // Draw B field (blue sine wave, offset for isometric view)
      if (state.showB) {
        ctx.strokeStyle = '#4488ff';
        ctx.lineWidth = 3;
        ctx.beginPath();
        let first = true;
        const zOffset = 30 * Math.sin(viewAngle);
        for (let x = 20; x < canvas.width - 20; x += 2) {
          const normalizedX = (x - 20) / displayWavelength;
          const y = Math.sin(normalizedX * 2 * Math.PI - phase) * amplitude * 40 * scale;
          const screenY = centerY + zOffset - y * Math.sin(viewAngle * 0.5);
          if (first) {
            ctx.moveTo(x, screenY);
            first = false;
          } else {
            ctx.lineTo(x, screenY);
          }
        }
        ctx.stroke();

        // B field label
        ctx.fillStyle = '#4488ff';
        ctx.font = '12px DM Mono';
        ctx.fillText('B', 10, centerY + 50 + zOffset);
      }

      // Draw Poynting vector
      if (state.showS) {
        ctx.strokeStyle = '#ffff00';
        ctx.lineWidth = 2;
        const sx = canvas.width * 0.15;
        const sy = 80;
        ctx.beginPath();
        ctx.moveTo(canvas.width * 0.05, sy);
        ctx.lineTo(canvas.width * 0.05 + sx, sy);
        ctx.stroke();

        // Arrow
        ctx.fillStyle = '#ffff00';
        ctx.beginPath();
        ctx.moveTo(canvas.width * 0.05 + sx, sy);
        ctx.lineTo(canvas.width * 0.05 + sx - 8, sy - 5);
        ctx.lineTo(canvas.width * 0.05 + sx - 8, sy + 5);
        ctx.closePath();
        ctx.fill();

        ctx.fillStyle = '#ffff00';
        ctx.font = '12px DM Mono';
        ctx.fillText('S = E × B / μ₀', canvas.width * 0.05, sy - 10);
      }

      // Apply polarization effects
      if (state.polarization === 'circular') {
        ctx.strokeStyle = 'rgba(200, 100, 255, 0.3)';
        ctx.lineWidth = 1;
        ctx.setLineDash([2, 2]);
        ctx.beginPath();
        const radius = amplitude * 30;
        ctx.arc(100, centerY, radius, 0, 2 * Math.PI);
        ctx.stroke();
        ctx.setLineDash([]);
      } else if (state.polarization === 'elliptical') {
        ctx.strokeStyle = 'rgba(200, 100, 255, 0.3)';
        ctx.lineWidth = 1;
        ctx.setLineDash([2, 2]);
        ctx.beginPath();
        ctx.ellipse(100, centerY, amplitude * 35, amplitude * 20, 0, 0, 2 * Math.PI);
        ctx.stroke();
        ctx.setLineDash([]);
      }
    }

    // Control listeners
    document.getElementById('wavelengthSlider').addEventListener('input', (e) => {
      const wl = parseFloat(e.target.value);
      updateFromWavelength(wl);
      document.getElementById('wavelengthValue').textContent = wl + ' nm';
    });

    document.getElementById('amplitudeSlider').addEventListener('input', (e) => {
      state.amplitude = parseFloat(e.target.value);
      document.getElementById('amplitudeValue').textContent = e.target.value + ' V/m';
      updateStats();
    });

    document.getElementById('speedSlider').addEventListener('input', (e) => {
      state.animationSpeed = parseFloat(e.target.value);
      document.getElementById('speedValue').textContent = e.target.value + 'x';
    });

    document.getElementById('rotationSlider').addEventListener('input', (e) => {
      state.viewRotation = parseFloat(e.target.value);
      document.getElementById('rotationValue').textContent = e.target.value + '°';
    });

    // Polarization buttons
    document.querySelectorAll('[data-polarization]').forEach(btn => {
      btn.addEventListener('click', (e) => {
        document.querySelectorAll('[data-polarization]').forEach(b => b.classList.remove('active'));
        e.target.classList.add('active');
        state.polarization = e.target.dataset.polarization;
      });
    });

    // Field toggles
    document.querySelectorAll('[data-field]').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const field = e.target.dataset.field;
        e.target.classList.toggle('active');
        state['show' + field] = e.target.classList.contains('active');
      });
    });

    // Animation loop
    function animate() {
      drawWave();
      requestAnimationFrame(animate);
    }

    // Initialize
    updateStats();
    updateSpectrumMarker();
    animate();