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();