Physics Lab

PHOTOELECTRIC EFFECT

Einstein's revolutionary equation explains how light ejects electrons from metal surfaces. Explore the quantum nature of light and energy quantization.

Einstein's Photoelectric Equation:
KE_max = hf − φ
h = Planck's constant (6.626×10⁻³⁴ J·s) | f = frequency | φ = work function

Main Scene

Photocurrent
0.00 µA
Stopping Voltage
0.00 V
NO EMISSION
Wavelength
500 nm
Intensity
50%
Stopping Voltage
0.0 V
Metal
Photon Energy
2.48 eV
Work Function
2.3 eV
KE (max)
0.18 eV
Threshold λ₀
540 nm
KE (max) vs Frequency
Photocurrent vs Intensity
Photocurrent vs Stopping Voltage
Developer Reference

Core Algorithm & Standalone Script

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

// Constants
    const h = 6.626e-34; // Planck's constant (J·s)
    const c = 3e8; // Speed of light (m/s)
    const e = 1.602e-19; // Elementary charge (C)
    const eV_to_J = 1.602e-19; // eV to Joules

    // Metals work functions (in eV)
    const metals = {
      sodium: { phi: 2.3, color: '#ffd700' },
      potassium: { phi: 2.2, color: '#ffaa00' },
      zinc: { phi: 4.3, color: '#c0c0c0' },
      copper: { phi: 4.7, color: '#b87333' },
      gold: { phi: 5.1, color: '#ffd700' }
    };

    // State
    let state = {
      wavelength: 500, // nm
      intensity: 50, // %
      voltage: 0, // V
      metal: 'sodium',
      quantumEfficiency: true,
      electrons: [] // Active electron particles
    };

    // DOM elements
    const mainCanvas = document.getElementById('mainCanvas');
    const ctx = mainCanvas.getContext('2d');
    const wavelengthSlider = document.getElementById('wavelengthSlider');
    const intensitySlider = document.getElementById('intensitySlider');
    const voltageSlider = document.getElementById('voltageSlider');
    const quantumEfficiencyCheckbox = document.getElementById('quantumEfficiency');
    const metalButtons = document.querySelectorAll('.metal-btn');

    const graphKECanvas = document.getElementById('graphKEFrequency');
    const graphCurrentCanvas = document.getElementById('graphCurrentIntensity');
    const graphVoltageCanvas = document.getElementById('graphCurrentVoltage');

    // Get canvas contexts at correct DPI
    function getCanvasContext(canvas) {
      const rect = canvas.getBoundingClientRect();
      canvas.width = rect.width * window.devicePixelRatio;
      canvas.height = rect.height * window.devicePixelRatio;
      const ctx = canvas.getContext('2d');
      ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
      return ctx;
    }

    // Physics calculations
    function getPhotonEnergy(wavelengthNm) {
      const wavelengthM = wavelengthNm * 1e-9;
      const freq = c / wavelengthM;
      return (h * freq) / eV_to_J; // in eV
    }

    function getFrequency(wavelengthNm) {
      return c / (wavelengthNm * 1e-9);
    }

    function getMaxKE(photonEnergyEV, workFunctionEV) {
      const ke = photonEnergyEV - workFunctionEV;
      return Math.max(0, ke);
    }

    function getThresholdWavelength(workFunctionEV) {
      const freq0 = (workFunctionEV * eV_to_J) / h;
      return (c / freq0) * 1e9; // in nm
    }

    function getStoppingVoltage(keEV) {
      return keEV; // In eV, equals max KE / e when converted to volts
    }

    function wavelengthToColor(wavelengthNm) {
      let r, g, b;

      if (wavelengthNm < 380) {
        r = g = b = 0;
      } else if (wavelengthNm < 440) {
        r = -(wavelengthNm - 440) / (440 - 380);
        g = 0;
        b = 1;
      } else if (wavelengthNm < 490) {
        r = 0;
        g = (wavelengthNm - 440) / (490 - 440);
        b = 1;
      } else if (wavelengthNm < 510) {
        r = 0;
        g = 1;
        b = -(wavelengthNm - 510) / (510 - 490);
      } else if (wavelengthNm < 580) {
        r = (wavelengthNm - 510) / (580 - 510);
        g = 1;
        b = 0;
      } else if (wavelengthNm < 645) {
        r = 1;
        g = -(wavelengthNm - 645) / (645 - 580);
        b = 0;
      } else {
        r = 1;
        g = 0;
        b = 0;
      }

      // Apply intensity correction
      const intensity = 0.5 + 0.5 * (780 - Math.max(380, Math.min(780, wavelengthNm))) / 400;

      r = Math.round(r * 255 * intensity);
      g = Math.round(g * 255 * intensity);
      b = Math.round(b * 255 * intensity);

      return `rgb(${r}, ${g}, ${b})`;
    }

    // Update color preview
    function updateColorPreview() {
      const color = wavelengthToColor(state.wavelength);
      document.getElementById('colorPreview').style.background = color;
    }

    // Animation loop
    function animate() {
      // Update electrons
      const gravity = 0.5;
      for (let i = state.electrons.length - 1; i >= 0; i--) {
        const e = state.electrons[i];
        e.vy -= gravity; // Gravity pulls down (opposing initial upward motion)
        e.y -= e.vy;
        e.x += e.vx;
        e.life--;

        if (e.life <= 0 || e.y < -50) {
          state.electrons.splice(i, 1);
        }
      }

      draw();
      drawGraphs();
      updateUI();

      requestAnimationFrame(animate);
    }

    // Draw main scene
    function draw() {
      const width = mainCanvas.width;
      const height = mainCanvas.height;

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

      // Draw metal surface at bottom
      ctx.fillStyle = '#444';
      ctx.fillRect(0, height - 40, width, 40);

      // Add texture to metal
      ctx.strokeStyle = '#666';
      ctx.lineWidth = 1;
      for (let i = 0; i < width; i += 20) {
        ctx.beginPath();
        ctx.moveTo(i, height - 40);
        ctx.lineTo(i, height);
        ctx.stroke();
      }

      const photonEnergy = getPhotonEnergy(state.wavelength);
      const workFunction = metals[state.metal].phi;
      const emitting = photonEnergy > workFunction;

      // Draw light beam
      const lightColor = wavelengthToColor(state.wavelength);
      ctx.strokeStyle = lightColor;
      ctx.lineWidth = 3;
      ctx.globalAlpha = 0.7;

      for (let i = 0; i < 5; i++) {
        ctx.beginPath();
        ctx.moveTo(100 + i * 30, 20);
        ctx.lineTo(100 + i * 30, height - 40);
        ctx.stroke();
      }
      ctx.globalAlpha = 1;

      // Draw photon wave representation
      ctx.strokeStyle = lightColor;
      ctx.lineWidth = 2;
      ctx.globalAlpha = 0.5;
      for (let i = 0; i < 3; i++) {
        ctx.beginPath();
        for (let x = 50; x < width - 50; x += 5) {
          const y = 30 + i * 80 + Math.sin(x * 0.02 + Date.now() * 0.003) * 10;
          if (x === 50) ctx.moveTo(x, y);
          else ctx.lineTo(x, y);
        }
        ctx.stroke();
      }
      ctx.globalAlpha = 1;

      // Draw electrons
      ctx.fillStyle = '#00ff00';
      for (const electron of state.electrons) {
        ctx.globalAlpha = electron.life / 100;
        ctx.beginPath();
        ctx.arc(electron.x, electron.y, 3, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;

      // Draw status text
      ctx.fillStyle = '#e8e0d5';
      ctx.font = 'bold 14px "DM Mono"';
      if (emitting) {
        ctx.fillStyle = '#00c896';
        ctx.fillText('✓ ELECTRONS EJECTED', 20, 30);
      } else {
        ctx.fillStyle = '#ff5555';
        ctx.fillText('✗ NO EMISSION (below threshold)', 20, 30);
      }
    }

    // Generate electrons when emission occurs
    function generateElectrons() {
      const photonEnergy = getPhotonEnergy(state.wavelength);
      const workFunction = metals[state.metal].phi;

      if (photonEnergy <= workFunction) return;

      const ke = getMaxKE(photonEnergy, workFunction);
      // Velocity proportional to sqrt(KE)
      const baseVelocity = Math.sqrt(ke * 100);

      // Generate electrons proportional to intensity and quantum efficiency
      const baseCount = (state.intensity / 100) * 2;
      const count = state.quantumEfficiency
        ? Math.max(1, Math.floor(baseCount * 0.8))
        : Math.max(1, Math.floor(baseCount));

      for (let i = 0; i < count; i++) {
        const x = 80 + Math.random() * 200;
        const y = mainCanvas.height - 40;
        const angle = Math.PI / 2 + (Math.random() - 0.5) * 0.6;
        const velocity = baseVelocity * (0.8 + Math.random() * 0.4);

        state.electrons.push({
          x: x,
          y: y,
          vx: Math.cos(angle) * velocity * 0.2,
          vy: Math.sin(angle) * velocity * 0.3,
          life: 100
        });
      }
    }

    // Update UI values
    function updateUI() {
      const photonEnergy = getPhotonEnergy(state.wavelength);
      const workFunction = metals[state.metal].phi;
      const ke = getMaxKE(photonEnergy, workFunction);
      const thresholdWavelength = getThresholdWavelength(workFunction);
      const stoppingVoltage = getStoppingVoltage(ke);

      // Update control displays
      document.getElementById('wavelengthValue').innerHTML =
        `${state.wavelength} nm <span class="color-preview" id="colorPreview" style="background: ${wavelengthToColor(state.wavelength)};"></span>`;
      document.getElementById('intensityValue').textContent = `${state.intensity}%`;
      document.getElementById('voltageValue').textContent = `${state.voltage.toFixed(1)} V`;

      // Update info grid
      document.getElementById('photonEnergy').textContent = `${photonEnergy.toFixed(2)} eV`;
      document.getElementById('workFunction').textContent = `${workFunction.toFixed(1)} eV`;
      document.getElementById('keMax').textContent = `${ke.toFixed(2)} eV`;
      document.getElementById('thresholdWavelength').textContent = `${thresholdWavelength.toFixed(0)} nm`;

      // Update ammeter
      const emitting = photonEnergy > workFunction;
      const baseCurrent = (state.intensity / 100) * 50; // µA
      const currentValue = emitting ? baseCurrent : 0;
      document.getElementById('currentValue').textContent = `${currentValue.toFixed(2)} µA`;
      document.getElementById('stoppingVoltageValue').textContent = `${stoppingVoltage.toFixed(2)} V`;

      // Update emission indicator
      const indicator = document.getElementById('emissionIndicator');
      if (emitting) {
        indicator.textContent = 'YES';
        indicator.className = 'emission-indicator yes';
      } else {
        indicator.textContent = 'NO';
        indicator.className = 'emission-indicator no';
      }

      // Generate electrons periodically
      if (emitting && Math.random() < 0.3) {
        generateElectrons();
      }
    }

    // Draw graphs
    function drawGraphs() {
      drawKEFrequencyGraph();
      drawCurrentIntensityGraph();
      drawCurrentVoltageGraph();
    }

    function drawKEFrequencyGraph() {
      const canvas = graphKECanvas;
      const ctx = getCanvasContext(canvas);
      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;
      const margin = 40;
      const graphWidth = width - 2 * margin;
      const graphHeight = height - 2 * margin;

      // Clear
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      // Draw axes
      ctx.strokeStyle = '#2a2a2a';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(margin, height - margin);
      ctx.lineTo(width - margin, height - margin);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(margin, margin);
      ctx.lineTo(margin, height - margin);
      ctx.stroke();

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('Frequency (×10¹⁴ Hz)', width / 2, height - 10);
      ctx.save();
      ctx.translate(15, height / 2);
      ctx.rotate(-Math.PI / 2);
      ctx.fillText('KE (max) [eV]');
      ctx.restore();

      // Draw lines for all metals
      const frequencies = [];
      for (let w = 250; w <= 800; w += 10) {
        frequencies.push(getFrequency(w) / 1e14); // in 10^14 Hz
      }

      // Reference metals (gray)
      for (const [name, data] of Object.entries(metals)) {
        if (name === state.metal) continue;

        ctx.strokeStyle = '#333333';
        ctx.lineWidth = 1;
        ctx.beginPath();

        for (let i = 0; i < frequencies.length; i++) {
          const w = 250 + i * 10;
          const freq = frequencies[i];
          const photonEnergy = getPhotonEnergy(w);
          const ke = getMaxKE(photonEnergy, data.phi);

          const x = margin + (freq / 12) * graphWidth;
          const y = height - margin - (ke / 8) * graphHeight;

          if (i === 0) ctx.moveTo(x, y);
          else ctx.lineTo(x, y);
        }
        ctx.stroke();
      }

      // Current metal (red)
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.beginPath();

      const workFunction = metals[state.metal].phi;
      for (let i = 0; i < frequencies.length; i++) {
        const w = 250 + i * 10;
        const freq = frequencies[i];
        const photonEnergy = getPhotonEnergy(w);
        const ke = getMaxKE(photonEnergy, workFunction);

        const x = margin + (freq / 12) * graphWidth;
        const y = height - margin - (ke / 8) * graphHeight;

        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      // Mark threshold
      const thresholdFreq = (workFunction * eV_to_J) / h / 1e14;
      const thresholdX = margin + (thresholdFreq / 12) * graphWidth;
      ctx.strokeStyle = '#ff5555';
      ctx.setLineDash([5, 5]);
      ctx.beginPath();
      ctx.moveTo(thresholdX, margin);
      ctx.lineTo(thresholdX, height - margin);
      ctx.stroke();
      ctx.setLineDash([]);

      // Mark current point
      const currentWavelength = state.wavelength;
      const currentFreq = getFrequency(currentWavelength) / 1e14;
      const currentPhotonEnergy = getPhotonEnergy(currentWavelength);
      const currentKE = getMaxKE(currentPhotonEnergy, workFunction);
      const currentX = margin + (currentFreq / 12) * graphWidth;
      const currentY = height - margin - (currentKE / 8) * graphHeight;

      ctx.fillStyle = '#ff2200';
      ctx.beginPath();
      ctx.arc(currentX, currentY, 5, 0, Math.PI * 2);
      ctx.fill();
    }

    function drawCurrentIntensityGraph() {
      const canvas = graphCurrentCanvas;
      const ctx = getCanvasContext(canvas);
      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;
      const margin = 40;
      const graphWidth = width - 2 * margin;
      const graphHeight = height - 2 * margin;

      // Clear
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      // Draw axes
      ctx.strokeStyle = '#2a2a2a';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(margin, height - margin);
      ctx.lineTo(width - margin, height - margin);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(margin, margin);
      ctx.lineTo(margin, height - margin);
      ctx.stroke();

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('Intensity (%)', width / 2, height - 10);
      ctx.save();
      ctx.translate(15, height / 2);
      ctx.rotate(-Math.PI / 2);
      ctx.fillText('Photocurrent (µA)');
      ctx.restore();

      const photonEnergy = getPhotonEnergy(state.wavelength);
      const workFunction = metals[state.metal].phi;
      const emitting = photonEnergy > workFunction;

      // Draw line
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.beginPath();

      for (let intensity = 0; intensity <= 100; intensity += 5) {
        const current = emitting ? (intensity / 100) * 50 : 0;
        const x = margin + (intensity / 100) * graphWidth;
        const y = height - margin - (current / 50) * graphHeight;

        if (intensity === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      // Mark current point
      const currentX = margin + (state.intensity / 100) * graphWidth;
      const currentCurrent = emitting ? (state.intensity / 100) * 50 : 0;
      const currentY = height - margin - (currentCurrent / 50) * graphHeight;

      ctx.fillStyle = '#ff2200';
      ctx.beginPath();
      ctx.arc(currentX, currentY, 5, 0, Math.PI * 2);
      ctx.fill();
    }

    function drawCurrentVoltageGraph() {
      const canvas = graphVoltageCanvas;
      const ctx = getCanvasContext(canvas);
      const width = canvas.width / window.devicePixelRatio;
      const height = canvas.height / window.devicePixelRatio;
      const margin = 40;
      const graphWidth = width - 2 * margin;
      const graphHeight = height - 2 * margin;

      // Clear
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      // Draw axes
      ctx.strokeStyle = '#2a2a2a';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(margin, height - margin);
      ctx.lineTo(width - margin, height - margin);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(margin, margin);
      ctx.lineTo(margin, height - margin);
      ctx.stroke();

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('Stopping Voltage (V)', width / 2, height - 10);
      ctx.save();
      ctx.translate(15, height / 2);
      ctx.rotate(-Math.PI / 2);
      ctx.fillText('Photocurrent (µA)');
      ctx.restore();

      const photonEnergy = getPhotonEnergy(state.wavelength);
      const workFunction = metals[state.metal].phi;
      const ke = getMaxKE(photonEnergy, workFunction);
      const stoppingVoltage = getStoppingVoltage(ke);
      const baseCurrent = (state.intensity / 100) * 50;

      // Draw line
      ctx.strokeStyle = '#ff2200';
      ctx.lineWidth = 2;
      ctx.beginPath();

      for (let v = -2; v <= 5; v += 0.2) {
        let current = baseCurrent;
        if (v > stoppingVoltage) {
          current = 0; // Above stopping voltage, no current
        }
        const x = margin + ((v + 2) / 7) * graphWidth;
        const y = height - margin - (current / 50) * graphHeight;

        if (v === -2) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      // Mark stopping voltage
      const stopX = margin + ((stoppingVoltage + 2) / 7) * graphWidth;
      ctx.strokeStyle = '#ff5555';
      ctx.setLineDash([5, 5]);
      ctx.beginPath();
      ctx.moveTo(stopX, margin);
      ctx.lineTo(stopX, height - margin);
      ctx.stroke();
      ctx.setLineDash([]);

      // Mark current point
      const currentX = margin + ((state.voltage + 2) / 7) * graphWidth;
      let currentCurrent = baseCurrent;
      if (state.voltage > stoppingVoltage) {
        currentCurrent = 0;
      }
      const currentY = height - margin - (currentCurrent / 50) * graphHeight;

      ctx.fillStyle = '#ff2200';
      ctx.beginPath();
      ctx.arc(currentX, currentY, 5, 0, Math.PI * 2);
      ctx.fill();
    }

    // Event listeners
    wavelengthSlider.addEventListener('input', (e) => {
      state.wavelength = parseInt(e.target.value);
      updateColorPreview();
    });

    intensitySlider.addEventListener('input', (e) => {
      state.intensity = parseInt(e.target.value);
    });

    voltageSlider.addEventListener('input', (e) => {
      state.voltage = parseFloat(e.target.value);
    });

    quantumEfficiencyCheckbox.addEventListener('change', (e) => {
      state.quantumEfficiency = e.target.checked;
    });

    metalButtons.forEach(btn => {
      btn.addEventListener('click', (e) => {
        metalButtons.forEach(b => b.classList.remove('active'));
        e.target.classList.add('active');
        state.metal = e.target.dataset.metal;
      });
    });

    // Initialize
    updateColorPreview();
    animate();