8
Photoelectric Effect: Why Intensity Isn't Enough
drag the frequency and intensity sliders
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.