8

Photoelectric Effect: Why Intensity Isn't Enough

drag the frequency and intensity sliders

A photoelectric effect simulation of Einstein's 1905 result that won the 1921 Nobel Prize: electrons leave a sodium plate only if each photon individually carries enough energy, with eV. Crank the intensity with red light and nothing comes out no matter how bright — gray puffs mark photons absorbed below the threshold frequency — then slide past green into blue and watch electrons eject, the current needle swing, and the dot climb the straight vs line whose x-intercept is . Drag the spectrum slider to set photon frequency and the lower slider to set photon rate.

idle
162 lines · vanilla
view source
const PHI = 2.28, HEV = 0.41357; // sodium work fn (eV); eV per 1e14 Hz
const FMIN = 2.7, FMAX = 12, F0 = PHI / HEV;
const NP = 90, NE = 90;
let W, H, f14, inten, spawnAcc, emaI, T;
let pX, pY, pVX, pVY, pF, pA, eX, eY, eVX, eVY, eA, flX, flY, flAge;

function init({ width, height }) {
  W = width; H = height;
  f14 = 7.5; inten = 16; spawnAcc = 0; emaI = 0; T = 0;
  pX = new Float32Array(NP); pY = new Float32Array(NP); pVX = new Float32Array(NP);
  pVY = new Float32Array(NP); pF = new Float32Array(NP); pA = new Uint8Array(NP);
  eX = new Float32Array(NE); eY = new Float32Array(NE); eVX = new Float32Array(NE);
  eVY = new Float32Array(NE); eA = new Uint8Array(NE);
  flX = new Float32Array(16); flY = new Float32Array(16); flAge = new Float32Array(16).fill(9);
  for (let i = 0; i < 80; i++) update(1 / 60); // pre-seed so frame 1 is alive
}

function lay() {
  return { px: W * 0.10, py0: H * 0.16, py1: H - 130, cx: W * 0.88,
    s1y: H - 96, s2y: H - 48, sx0: 14, sx1: W - 14 };
}

function freqColor(f) {
  const L = 2998 / f; let r = 0, g = 0, b = 0;
  if (L < 380) { r = 0.5; g = 0.25; b = 1; }
  else if (L < 440) { r = (440 - L) / 60 * 0.5; b = 1; }
  else if (L < 490) { g = (L - 440) / 50; b = 1; }
  else if (L < 510) { g = 1; b = (510 - L) / 20; }
  else if (L < 580) { r = (L - 510) / 70; g = 1; }
  else if (L < 645) { r = 1; g = (645 - L) / 65; }
  else if (L < 780) { r = 1; }
  else { r = 0.55; g = 0.1; b = 0.1; }
  return `rgb(${r * 255 | 0},${g * 255 | 0},${b * 255 | 0})`;
}

function update(dt) {
  const L = lay();
  T += dt;
  spawnAcc += inten * dt;
  while (spawnAcc >= 1) {
    spawnAcc -= 1;
    for (let i = 0; i < NP; i++) if (!pA[i]) {
      pA[i] = 1; pF[i] = f14;
      pX[i] = W * (0.3 + 0.55 * Math.random()); pY[i] = -10;
      const tx = L.px + 8 - pX[i], ty = L.py0 + Math.random() * (L.py1 - L.py0) - pY[i];
      const m = Math.sqrt(tx * tx + ty * ty);
      pVX[i] = tx / m * 230; pVY[i] = ty / m * 230;
      break;
    }
  }
  let arrivals = 0;
  for (let i = 0; i < NP; i++) {
    if (!pA[i]) continue;
    pX[i] += pVX[i] * dt; pY[i] += pVY[i] * dt;
    if (pX[i] <= L.px + 12) { // hits the plate
      pA[i] = 0;
      const KE = HEV * pF[i] - PHI;
      if (KE > 0) {
        for (let j = 0; j < NE; j++) if (!eA[j]) {
          eA[j] = 1; eX[j] = L.px + 16; eY[j] = pY[i];
          eVX[j] = 55 + 115 * KE; eVY[j] = (Math.random() - 0.5) * 24;
          break;
        }
      } else {
        for (let j = 0; j < 16; j++) if (flAge[j] > 0.5) { flAge[j] = 0; flX[j] = L.px + 14; flY[j] = pY[i]; break; }
      }
    }
  }
  for (let j = 0; j < NE; j++) {
    if (!eA[j]) continue;
    eX[j] += eVX[j] * dt; eY[j] += eVY[j] * dt;
    if (eX[j] >= L.cx) { eA[j] = 0; arrivals++; }
    else if (eY[j] < 0 || eY[j] > H) eA[j] = 0;
  }
  for (let j = 0; j < 16; j++) flAge[j] += dt;
  emaI += (arrivals / dt - emaI) * Math.min(1, dt * 1.6);
}

function slider(ctx, x0, x1, y, frac, label, spectrum) {
  ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(x0 - 4, y, x1 - x0 + 8, 44);
  if (spectrum) {
    const g = ctx.createLinearGradient(x0, 0, x1, 0);
    for (let k = 0; k <= 12; k++) g.addColorStop(k / 12, freqColor(FMIN + (k / 12) * (FMAX - FMIN)));
    ctx.fillStyle = g;
  } else ctx.fillStyle = '#3a4a8a';
  ctx.fillRect(x0, y + 18, x1 - x0, 12);
  if (!spectrum) { ctx.fillStyle = '#ff9a3c'; ctx.fillRect(x0, y + 18, (x1 - x0) * frac, 12); }
  ctx.fillStyle = '#fff';
  ctx.fillRect(x0 + (x1 - x0) * frac - 3, y + 8, 6, 32);
  ctx.font = '11px monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
  ctx.fillText(label, x0, y + 12);
}

function tick({ ctx, dt, width, height, input }) {
  if (width !== W || height !== H) { W = width; H = height; }
  const L = lay();
  if (input.mouseDown) {
    const fr = Math.max(0, Math.min(1, (input.mouseX - L.sx0) / (L.sx1 - L.sx0)));
    if (input.mouseY >= L.s1y && input.mouseY < L.s1y + 46) f14 = FMIN + fr * (FMAX - FMIN);
    else if (input.mouseY >= L.s2y && input.mouseY < L.s2y + 46) inten = 2 + fr * 48;
  }
  input.consumeClicks();
  update(Math.min(dt, 0.05));

  ctx.fillStyle = '#0a0d14'; ctx.fillRect(0, 0, W, H);
  const KE = HEV * f14 - PHI;
  // plate + collector
  ctx.fillStyle = '#8a93a5'; ctx.fillRect(L.px, L.py0, 12, L.py1 - L.py0);
  ctx.fillStyle = '#6a7385'; ctx.fillRect(L.cx, L.py0, 12, L.py1 - L.py0);
  ctx.fillStyle = '#9aa5b8'; ctx.font = '11px monospace'; ctx.textAlign = 'center';
  ctx.fillText('Na', L.px + 6, L.py0 - 6);
  ctx.fillText('collector', L.cx + 6, L.py0 - 6);
  // photons: wavy trails, squiggle wavelength ~ 1/f
  ctx.lineWidth = 1.6;
  for (let i = 0; i < NP; i++) {
    if (!pA[i]) continue;
    const ux = pVX[i] / 230, uy = pVY[i] / 230, k = 14 / pF[i];
    ctx.strokeStyle = freqColor(pF[i]);
    ctx.beginPath();
    for (let s = 0; s <= 7; s++) {
      const d = s * 4, w = Math.sin(T * 24 + i + d * 0.9 / k) * 3;
      const x = pX[i] - ux * d - uy * w, y = pY[i] - uy * d + ux * w;
      if (s === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.stroke();
  }
  // no-emission puffs
  for (let j = 0; j < 16; j++) {
    if (flAge[j] > 0.45) continue;
    ctx.strokeStyle = `rgba(180,180,180,${(0.6 * (1 - flAge[j] / 0.45)).toFixed(2)})`;
    ctx.beginPath(); ctx.arc(flX[j], flY[j], 3 + flAge[j] * 30, 0, Math.PI * 2); ctx.stroke();
  }
  // electrons
  ctx.fillStyle = '#43e8ff';
  for (let j = 0; j < NE; j++) {
    if (!eA[j]) continue;
    ctx.beginPath(); ctx.arc(eX[j], eY[j], 2.6, 0, Math.PI * 2); ctx.fill();
  }
  // current meter
  const mcx = 58, mcy = H - 122, mr = 34;
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.beginPath(); ctx.arc(mcx, mcy, mr + 6, Math.PI, 0); ctx.fill();
  ctx.strokeStyle = '#888'; ctx.beginPath(); ctx.arc(mcx, mcy, mr, Math.PI, 0); ctx.stroke();
  const ia = Math.PI + Math.min(1, emaI / 14) * Math.PI;
  ctx.strokeStyle = '#ffd17a'; ctx.lineWidth = 2.5;
  ctx.beginPath(); ctx.moveTo(mcx, mcy); ctx.lineTo(mcx + Math.cos(ia) * mr, mcy + Math.sin(ia) * mr); ctx.stroke();
  ctx.lineWidth = 1;
  ctx.fillStyle = '#cfe0ff'; ctx.font = '11px monospace'; ctx.textAlign = 'center';
  ctx.fillText('current', mcx, mcy + 14);
  // KE_max vs f graph
  const gw = Math.min(150, W - 196), gx = W - 12 - gw, gy = 12, gh = 86;
  ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(gx, gy, gw, gh);
  const X = f => gx + 6 + (f - FMIN) / (FMAX - FMIN) * (gw - 12);
  const Y = ke => gy + gh - 14 - ke / 3 * (gh - 24);
  ctx.strokeStyle = '#666'; ctx.beginPath(); ctx.moveTo(gx + 6, Y(0)); ctx.lineTo(gx + gw - 6, Y(0)); ctx.stroke();
  ctx.strokeStyle = '#ffd17a'; ctx.beginPath();
  ctx.moveTo(X(FMIN), Y(0)); ctx.lineTo(X(F0), Y(0)); ctx.lineTo(X(FMAX), Y(HEV * FMAX - PHI)); ctx.stroke();
  ctx.fillStyle = freqColor(f14);
  ctx.beginPath(); ctx.arc(X(f14), Y(Math.max(0, KE)), 4, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#9aa5b8'; ctx.font = '10px monospace'; ctx.textAlign = 'left';
  ctx.fillText('KEmax vs f', gx + 6, gy + 12);
  ctx.fillText('f0', X(F0) - 5, Y(0) + 11);
  // HUD
  ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(12, 12, 172, 80);
  ctx.font = '13px monospace'; ctx.textAlign = 'left';
  ctx.fillStyle = freqColor(f14);
  ctx.fillText(`E = ${(HEV * f14).toFixed(2)} eV`, 22, 32);
  ctx.fillStyle = '#cfe0ff';
  ctx.fillText(`phi = ${PHI.toFixed(2)} eV (Na)`, 22, 50);
  ctx.fillStyle = KE > 0 ? '#43e8ff' : '#ff7a6a';
  ctx.fillText(KE > 0 ? `KEmax = ${KE.toFixed(2)} eV` : 'below f0: no e-', 22, 68);
  ctx.fillStyle = '#cfe0ff';
  ctx.fillText(`I = ${emaI.toFixed(1)} e/s`, 22, 86);
  // sliders
  slider(ctx, L.sx0, L.sx1, L.s1y, (f14 - FMIN) / (FMAX - FMIN), `frequency  ${(2998 / f14).toFixed(0)} nm`, true);
  slider(ctx, L.sx0, L.sx1, L.s2y, (inten - 2) / 48, `intensity  ${inten.toFixed(0)} photons/s`, false);
}

Comments (0)

Log in to comment.