Physics Lab

DOUBLE SLIT DIFFRACTION

Interactive simulation of wave interference and diffraction patterns. Explore Huygens principle, constructive/destructive interference, and how light behaves when passing through single or multiple slits.

How it works: Light waves passing through narrow slits diffract and create an interference pattern on a distant screen. The bright and dark bands represent constructive and destructive interference — where waves add up or cancel out.

Wave Visualization

Fringe Spacing (Δy):
1st Minimum at θ:
Screen Intensity:

Controls

Slit Mode

Wave Properties

Animation

Display

Central Peak
100%
Visibility
High
Developer Reference

Core Algorithm & Standalone Script

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

// State
    const state = {
      mode: 'double',
      wavelength: 550, // nm
      separation: 2.0, // μm
      slitWidth: 0.5, // μm
      distance: 0.5, // m
      numSlits: 2,
      waveSpeed: 1.0,
      showIndividualWaves: true,
      showEnvelope: true,
      showPathDifference: false,
      animationTime: 0
    };

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

    // DPI scaling for sharp canvas
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    ctx.scale(dpr, dpr);
    const displayWidth = rect.width;
    const displayHeight = rect.height;

    // Physics calculations
    function calculateIntensity(sinTheta, wavelength, separation, slitWidth, numSlits) {
      wavelength *= 1e-9; // Convert nm to m
      separation *= 1e-6; // Convert μm to m
      slitWidth *= 1e-6; // Convert μm to m

      if (Math.abs(sinTheta) > 1) return 0;

      // Single slit diffraction envelope
      const alpha = Math.PI * slitWidth * sinTheta / wavelength;
      let singleSlitFactor = 1;
      if (Math.abs(alpha) > 1e-6) {
        singleSlitFactor = Math.sin(alpha) / alpha;
      }

      // Multi-slit interference
      let intensity = 1;
      if (numSlits > 1) {
        const beta = Math.PI * separation * sinTheta / wavelength;
        const numerator = Math.sin(numSlits * beta);
        const denominator = Math.sin(beta);
        if (Math.abs(denominator) > 1e-6) {
          intensity = (numerator / denominator) ** 2;
        } else {
          intensity = numSlits * numSlits;
        }
      }

      const totalIntensity = singleSlitFactor * singleSlitFactor * intensity;
      return Math.max(0, Math.min(1, totalIntensity));
    }

    function drawWave(x, y, amplitude, frequency, phase, color, alpha) {
      ctx.strokeStyle = color;
      ctx.globalAlpha = alpha;
      ctx.beginPath();
      for (let i = 0; i < 100; i++) {
        const px = x + i * 3;
        const py = y + amplitude * Math.sin(frequency * i - phase);
        if (i === 0) ctx.moveTo(px, py);
        else ctx.lineTo(px, py);
      }
      ctx.stroke();
      ctx.globalAlpha = 1;
    }

    function drawCircularWave(cx, cy, radius, color, alpha) {
      ctx.strokeStyle = color;
      ctx.globalAlpha = alpha;
      ctx.beginPath();
      ctx.arc(cx, cy, radius, 0, Math.PI * 2);
      ctx.stroke();
      ctx.globalAlpha = 1;
    }

    function hueToRgb(hue) {
      // Hue: 400nm (violet) to 700nm (red)
      let h = (700 - hue) / (700 - 400) * 240; // 240=violet, 0=red
      h = Math.max(0, Math.min(240, h));

      // Simple HSL to RGB approximation
      if (h < 60) return { r: 255, g: h * 4.25, b: 0 };
      if (h < 120) return { r: 255 - (h - 60) * 4.25, g: 255, b: 0 };
      if (h < 180) return { r: 0, g: 255, b: (h - 120) * 4.25 };
      if (h < 240) return { r: 0, g: 255 - (h - 180) * 4.25, b: 255 };
      return { r: (h - 240) * 4.25, g: 0, b: 255 };
    }

    function drawSimulation() {
      ctx.clearRect(0, 0, displayWidth, displayHeight);
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, displayWidth, displayHeight);

      const centerY = displayHeight / 2;
      const margin = 30;
      const slitAreaX = displayWidth * 0.3;
      const screenX = displayWidth - margin - 20;

      // Draw wave source (left side)
      const sourceX = margin;
      const sourceY = centerY;
      ctx.fillStyle = '#ff2200';
      ctx.fillRect(sourceX - 5, sourceY - 15, 10, 30);

      // Animated ripples from source
      const rippleRadius = (state.animationTime * state.waveSpeed) % 60 + 10;
      ctx.strokeStyle = 'rgba(255, 34, 0, 0.3)';
      ctx.lineWidth = 1;
      for (let r = rippleRadius; r > 0; r -= 15) {
        ctx.beginPath();
        ctx.arc(sourceX, sourceY, r, 0, Math.PI * 2);
        ctx.stroke();
      }

      // Draw barrier and slits
      const slitHeight = 80;
      const slitSpacing = 50;
      ctx.fillStyle = '#1e1e1e';
      ctx.fillRect(slitAreaX - 10, 0, 20, centerY - slitHeight / 2 - slitSpacing / 2);
      ctx.fillRect(slitAreaX - 10, centerY + slitHeight / 2 + slitSpacing / 2, 20, displayHeight - centerY - slitHeight / 2 - slitSpacing / 2);

      // Draw slits
      let slits = [];
      if (state.mode === 'single') {
        slits = [{ y: centerY, width: 30 }];
      } else if (state.mode === 'double') {
        slits = [
          { y: centerY - slitSpacing / 2, width: 30 },
          { y: centerY + slitSpacing / 2, width: 30 }
        ];
      } else if (state.mode === 'multiple') {
        const totalSpacing = (state.numSlits - 1) * slitSpacing;
        for (let i = 0; i < state.numSlits; i++) {
          slits.push({
            y: centerY - totalSpacing / 2 + i * slitSpacing,
            width: Math.max(20, 40 - state.numSlits * 5)
          });
        }
      }

      // Draw diffracted waves from slits
      slits.forEach((slit, idx) => {
        const waveX = slitAreaX + 20;
        const waveY = slit.y;
        const animPhase = (state.animationTime * state.waveSpeed) % 100;

        if (state.showIndividualWaves) {
          for (let d = 0; d < 150; d += 15) {
            const r = d + animPhase * 1.5;
            const alpha = Math.max(0, 0.3 - r / 150);
            ctx.strokeStyle = `rgba(255, 100, 0, ${alpha})`;
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.arc(waveX, waveY, r, 0, Math.PI * 2);
            ctx.stroke();
          }
        }
      });

      // Draw intensity pattern on screen
      const patternHeight = displayHeight * 0.8;
      const patternTop = (displayHeight - patternHeight) / 2;
      const pixelWidth = 2;

      // Calculate and draw intensity pattern
      for (let py = patternTop; py < patternTop + patternHeight; py += pixelWidth) {
        const y = (py - patternTop - patternHeight / 2) / (displayHeight * 0.4); // normalized
        const sinTheta = y / Math.sqrt(1 + y * y);

        let intensity = 0;
        if (state.mode === 'single') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.slitWidth * 10, state.slitWidth, 1);
        } else if (state.mode === 'double') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, 2);
        } else if (state.mode === 'multiple') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, state.numSlits);
        }

        const brightness = Math.floor(intensity * 200) + 55;
        ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;
        ctx.fillRect(screenX - 30, py, 25, pixelWidth);
      }

      // Draw intensity curve overlay
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.beginPath();
      let firstPoint = true;
      for (let py = patternTop; py < patternTop + patternHeight; py += 2) {
        const y = (py - patternTop - patternHeight / 2) / (displayHeight * 0.4);
        const sinTheta = y / Math.sqrt(1 + y * y);

        let intensity = 0;
        if (state.mode === 'single') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.slitWidth * 10, state.slitWidth, 1);
        } else if (state.mode === 'double') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, 2);
        } else if (state.mode === 'multiple') {
          intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, state.numSlits);
        }

        const px = screenX - 5 - intensity * 25;
        if (firstPoint) {
          ctx.moveTo(px, py);
          firstPoint = false;
        } else {
          ctx.lineTo(px, py);
        }
      }
      ctx.stroke();

      // Draw screen label
      ctx.fillStyle = '#555555';
      ctx.font = '12px DM Mono';
      ctx.save();
      ctx.translate(screenX + 5, centerY);
      ctx.rotate(Math.PI / 2);
      ctx.fillText('SCREEN', 0, 0);
      ctx.restore();

      // Draw path difference lines if enabled
      if (state.showPathDifference && state.mode === 'double') {
        const slit1Y = centerY - 25;
        const slit2Y = centerY + 25;
        const observationY = centerY + 50;
        const observationX = slitAreaX + 150;

        ctx.strokeStyle = 'rgba(255, 200, 0, 0.3)';
        ctx.lineWidth = 1;
        ctx.setLineDash([5, 5]);
        ctx.beginPath();
        ctx.moveTo(slitAreaX + 10, slit1Y);
        ctx.lineTo(observationX, observationY);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(slitAreaX + 10, slit2Y);
        ctx.lineTo(observationX, observationY);
        ctx.stroke();
        ctx.setLineDash([]);
      }

      // Info text
      ctx.fillStyle = '#555555';
      ctx.font = '11px DM Mono';
      ctx.fillText(`λ: ${state.wavelength} nm`, margin, margin + 20);
      ctx.fillText(`d: ${state.separation} μm`, margin, margin + 35);
      ctx.fillText(`L: ${state.distance} m`, margin, margin + 50);

      state.animationTime += state.waveSpeed;
      requestAnimationFrame(drawSimulation);
    }

    // Update calculations and labels
    function updateCalculations() {
      // Fringe spacing: Δy = λL/d
      const fringeSpacing = (state.wavelength * state.distance * 1e-6) / (state.separation * 1e-6);
      document.getElementById('fringeSpacing').textContent = fringeSpacing.toFixed(3) + ' mm';

      // First minimum: sinθ = λ/a
      const sinFirstMin = state.wavelength / (state.slitWidth * 1000);
      const firstMinAngle = Math.asin(Math.min(1, sinFirstMin)) * 180 / Math.PI;
      document.getElementById('firstMinimum').textContent = firstMinAngle.toFixed(2) + '°';

      // Central peak intensity
      const centerIntensity = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
      document.getElementById('centralPeakIntensity').textContent = Math.round(centerIntensity * 100) + '%';

      // Visibility (contrast)
      const maxIntensity = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
      const minIntensity = calculateIntensity(0.01, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
      const visibility = (maxIntensity - minIntensity) / (maxIntensity + minIntensity);
      document.getElementById('visibility').textContent = visibility > 0.7 ? 'High' : (visibility > 0.4 ? 'Medium' : 'Low');

      // Screen intensity at center
      const centerIntensity2 = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
      document.getElementById('screenIntensity').textContent = Math.round(centerIntensity2 * 100) + '%';
    }

    // Event listeners
    document.getElementById('wavelength').addEventListener('input', (e) => {
      state.wavelength = parseInt(e.target.value);
      document.getElementById('wavelengthLabel').textContent = state.wavelength + ' nm';
      updateCalculations();
    });

    document.getElementById('separation').addEventListener('input', (e) => {
      state.separation = parseFloat(e.target.value);
      document.getElementById('separationLabel').textContent = state.separation.toFixed(1) + ' μm';
      updateCalculations();
    });

    document.getElementById('slitWidth').addEventListener('input', (e) => {
      state.slitWidth = parseFloat(e.target.value);
      document.getElementById('slitWidthLabel').textContent = state.slitWidth.toFixed(2) + ' μm';
      updateCalculations();
    });

    document.getElementById('distance').addEventListener('input', (e) => {
      state.distance = parseFloat(e.target.value);
      document.getElementById('distanceLabel').textContent = state.distance.toFixed(1) + ' m';
      updateCalculations();
    });

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

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

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

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

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

    // Mode toggle
    document.querySelectorAll('.toggle-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
        e.target.classList.add('active');
        state.mode = e.target.dataset.mode;

        // Show/hide numSlits control
        if (state.mode === 'multiple') {
          document.getElementById('numSlitsControl').style.display = 'block';
        } else {
          document.getElementById('numSlitsControl').style.display = 'none';
        }

        updateCalculations();
      });
    });

    // Initial setup
    updateCalculations();
    drawSimulation();