wave_particle_duality / index.html
SMKBK1's picture
Update index.html
27f16ca verified
Raw History Blame Contribute Delete
54.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wave-Particle Duality & Photoelectric Effect</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #0f172a;
color: #f8fafc;
overflow-x: hidden;
}
canvas {
touch-action: none;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
</style>
</head>
<body class="min-h-screen flex flex-col bg-slate-950 text-slate-100">
<header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-50 px-6 py-4 flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="flex items-center space-x-3">
<div class="p-2 bg-indigo-600 rounded-lg shadow-lg shadow-indigo-500/30">
<i class="fa-solid fa-atom text-xl text-white"></i>
</div>
<div>
<h1 class="font-bold text-lg leading-tight">Wave-Particle Duality Lab</h1>
<p class="text-xs text-slate-400">Double-Slit Interference & Photoelectric Effect</p>
<p class="text-[11px] text-slate-500">Made by Group 8 — JUN FIZIK SARAWAK 2026</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="preset-red" class="px-3 py-1.5 rounded-full text-xs font-medium border border-red-500/40 bg-red-950/40 text-red-300 hover:bg-red-900/60 transition">
Red Light (700 nm)
</button>
<button id="preset-green" class="px-3 py-1.5 rounded-full text-xs font-medium border border-emerald-500/40 bg-emerald-950/40 text-emerald-300 hover:bg-emerald-900/60 transition">
Green Light (530 nm)
</button>
<button id="preset-violet" class="px-3 py-1.5 rounded-full text-xs font-medium border border-violet-500/40 bg-violet-950/40 text-violet-300 hover:bg-violet-900/60 transition">
Violet Light (400 nm)
</button>
<button id="preset-uv" class="px-3 py-1.5 rounded-full text-xs font-medium border border-purple-500/40 bg-purple-950/40 text-purple-300 hover:bg-purple-900/60 transition">
UV Light (300 nm)
</button>
</div>
</header>
<section class="border-b border-slate-800 bg-slate-900/60 px-4 lg:px-6 py-3">
<div class="max-w-[1600px] mx-auto flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<span class="text-[11px] uppercase tracking-wider text-slate-500 mr-1">5E Inquiry</span>
<button type="button" data-phase="engage" class="e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border border-amber-500/50 bg-amber-950/50 text-amber-200">1 Engage</button>
<button type="button" data-phase="explore" class="e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border border-sky-500/40 bg-slate-950 text-sky-300">2 Explore</button>
<button type="button" data-phase="explain" class="e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border border-indigo-500/40 bg-slate-950 text-indigo-300">3 Explain</button>
<button type="button" data-phase="elaborate" class="e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border border-emerald-500/40 bg-slate-950 text-emerald-300">4 Elaborate</button>
<button type="button" data-phase="evaluate" class="e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border border-rose-500/40 bg-slate-950 text-rose-300">5 Evaluate</button>
<button type="button" id="e5-hide" class="ml-auto text-[11px] text-slate-500 hover:text-slate-300">Hide activity</button>
</div>
<div id="e5-panel" class="rounded-xl border border-slate-800 bg-slate-950/80 p-4 text-sm text-slate-200"></div>
</div>
</section>
<main class="flex-1 grid grid-cols-1 lg:grid-cols-4 gap-4 p-4 lg:p-6 max-w-[1600px] w-full mx-auto">
<div class="lg:col-span-3 flex flex-col gap-4">
<div class="relative w-full bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-2xl flex-1 min-h-[500px] flex flex-col">
<div class="absolute top-4 left-4 right-4 flex justify-between items-center pointer-events-none z-10 flex-wrap gap-2">
<div class="bg-slate-950/80 backdrop-blur border border-slate-800 rounded-lg px-3 py-1.5 text-xs flex items-center gap-3 pointer-events-auto">
<span class="flex items-center gap-1.5">
<span class="w-2.5 h-2.5 rounded-full" id="color-indicator"></span>
<span id="wave-length-display" class="font-mono font-semibold">530 nm</span>
</span>
<span class="text-slate-600">|</span>
<span class="text-slate-300">Photon Energy: <span id="photon-energy-display" class="font-mono font-semibold text-amber-400">2.34 eV</span></span>
</div>
<div class="flex gap-2 pointer-events-auto">
<button id="btn-pause-source" class="bg-slate-950/80 hover:bg-slate-800 border border-slate-700 text-xs px-3 py-1.5 rounded-lg font-medium transition flex items-center gap-1.5 text-rose-300">
<i class="fa-solid fa-pause"></i> Pause Source
</button>
<button id="btn-toggle-waves" class="bg-slate-950/80 hover:bg-slate-800 border border-slate-700 text-xs px-3 py-1.5 rounded-lg font-medium transition flex items-center gap-1.5 text-indigo-300">
<i class="fa-solid fa-lines-leaning"></i> Light Rays
</button>
<button id="btn-toggle-photons" class="bg-slate-950/80 hover:bg-slate-800 border border-slate-700 text-xs px-3 py-1.5 rounded-lg font-medium transition flex items-center gap-1.5 text-amber-300">
<i class="fa-solid fa-circle-dot"></i> Photons
</button>
<button id="btn-clear-screen" class="bg-slate-950/80 hover:bg-slate-800 border border-slate-700 text-xs px-3 py-1.5 rounded-lg font-medium transition text-slate-300">
<i class="fa-solid fa-rotate-right"></i> Reset Hits
</button>
</div>
</div>
<canvas id="simCanvas" class="w-full h-full flex-1 block cursor-crosshair"></canvas>
<div class="absolute bottom-3 left-4 right-4 flex justify-between items-end pointer-events-none">
<div class="bg-slate-950/80 backdrop-blur border border-slate-800 text-slate-400 text-xs px-3 py-2 rounded-lg pointer-events-auto max-w-md">
<i class="fa-solid fa-hand-pointer text-amber-400 mr-1"></i> Drag the <strong class="text-slate-200">3D Metal Target</strong> vertically across bright & dark fringes to see individual photon hits & photoelectron emission!
</div>
</div>
</div>
<div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col">
<div class="flex justify-between items-center mb-2">
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-chart-area text-indigo-400"></i> Screen Interference Intensity Profile & Photon Hits Accumulation
</h3>
<span class="text-xs text-slate-500">I ∝ cos²(π d y / λD)</span>
</div>
<div class="flex flex-wrap gap-x-4 gap-y-1 mb-2 text-[11px]">
<span class="text-slate-300">— Theoretical wave intensity (continuous)</span>
<span id="graph-legend-bars" class="text-emerald-400">■ Accumulated photon impacts</span>
</div>
<div class="h-32 w-full relative">
<canvas id="graphCanvas" class="w-full h-full block rounded-lg bg-slate-950 border border-slate-800/80"></canvas>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="bg-slate-900 border border-slate-800 rounded-xl p-5 flex flex-col gap-4">
<h2 class="text-sm font-bold text-slate-200 flex items-center gap-2 border-b border-slate-800 pb-3">
<i class="fa-solid fa-sliders text-indigo-400"></i> Light Source Controls
</h2>
<div class="space-y-1.5">
<div class="flex justify-between text-xs">
<label class="text-slate-400 font-medium">Wavelength (λ) / Color</label>
<span id="val-wavelength" class="font-mono text-indigo-400 font-semibold">530 nm</span>
</div>
<input type="range" id="slider-wavelength" min="200" max="750" value="530" step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500">
<div class="flex justify-between text-[10px] text-slate-500 font-mono">
<span>200nm (UV)</span>
<span>400nm</span>
<span>550nm</span>
<span>750nm (IR)</span>
</div>
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-xs">
<label class="text-slate-400 font-medium">Beam Intensity (Photon Flux)</label>
<span id="val-intensity" class="font-mono text-indigo-400 font-semibold">Medium</span>
</div>
<input type="range" id="slider-intensity" min="1" max="10" value="5" step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500">
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-xs">
<label class="text-slate-400 font-medium">Slit Separation (d)</label>
<span id="val-slit" class="font-mono text-indigo-400 font-semibold">25 µm</span>
</div>
<input type="range" id="slider-slit" min="10" max="50" value="25" step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500">
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-xs">
<label class="text-slate-400 font-medium">Slit-to-Screen Distance (D)</label>
<span id="val-distance" class="font-mono text-indigo-400 font-semibold">40 cm</span>
</div>
<input type="range" id="slider-distance" min="20" max="80" value="40" step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500">
<div class="flex justify-between text-[10px] text-slate-500 font-mono">
<span>20 cm (closer)</span>
<span>80 cm (farther)</span>
</div>
</div>
</div>
<div class="bg-slate-900 border border-slate-800 rounded-xl p-5 flex flex-col gap-4 flex-1">
<h2 class="text-sm font-bold text-slate-200 flex items-center gap-2 border-b border-slate-800 pb-3">
<i class="fa-solid fa-bolt text-amber-400"></i> Photoelectric Detector Metal
</h2>
<div class="space-y-1.5">
<label class="text-xs text-slate-400 font-medium">Target Metal Material</label>
<select id="select-metal" class="w-full bg-slate-950 border border-slate-800 text-slate-200 text-xs rounded-lg p-2.5 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<option value="2.1" selected>Cesium (Cs) — Work Function: 2.10 eV</option>
<option value="2.3">Potassium (K) — Work Function: 2.30 eV</option>
<option value="2.7">Sodium (Na) — Work Function: 2.70 eV</option>
<option value="4.3">Zinc (Zn) — Work Function: 4.30 eV</option>
<option value="4.7">Copper (Cu) — Work Function: 4.70 eV</option>
</select>
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-xs">
<label class="text-slate-400 font-medium">Metal Plate Y-Position</label>
<span id="val-metal-pos" class="font-mono text-amber-400 font-semibold">Center</span>
</div>
<input type="range" id="slider-metal-pos" min="0" max="100" value="50" step="1"
class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-amber-500">
</div>
<div class="bg-slate-950 rounded-xl p-3.5 border border-slate-800/80 space-y-3 mt-auto">
<div class="flex justify-between items-center text-xs">
<span class="text-slate-400">Work Function (W):</span>
<span id="stat-workfunction" class="font-mono font-medium text-slate-200">2.10 eV</span>
</div>
<div class="flex justify-between items-center text-xs">
<span class="text-slate-400">Photon Energy (hν):</span>
<span id="stat-photonenergy" class="font-mono font-medium text-indigo-300">2.34 eV</span>
</div>
<div class="flex justify-between items-center text-xs">
<span class="text-slate-400">Max Electron KE:</span>
<span id="stat-ke" class="font-mono font-bold text-emerald-400">0.24 eV</span>
</div>
<div class="flex justify-between items-center text-xs">
<span class="text-slate-400">Photocurrent:</span>
<span id="stat-current" class="font-mono font-bold text-cyan-400">0.00 µA</span>
</div>
<div class="flex justify-between items-center text-xs pt-2 border-t border-slate-800">
<span class="text-slate-400">Status:</span>
<span id="stat-status" class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
Emitting Electrons
</span>
</div>
</div>
<div class="p-3 bg-indigo-950/30 border border-indigo-800/30 rounded-lg text-[11px] text-indigo-300/80 leading-relaxed">
<strong class="text-indigo-200">Wave-Particle Duality Concept:</strong>
Light travels as continuous interference <em>waves</em> through the slits, but hits the detector as discrete <em>quanta (photons)</em>. Photoelectrons are ejected <strong>instantly</strong> only if the photon energy <span class="font-mono text-indigo-100">E = hν</span> exceeds the metal work function <span class="font-mono text-indigo-100">W</span> (so <span class="font-mono text-indigo-100">KE<sub>max</sub> = hν − W</span>), regardless of wave intensity. Photocurrent scales with intensity and local fringe brightness.
</div>
</div>
</div>
</main>
<script>
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const graphCanvas = document.getElementById('graphCanvas');
const gctx = graphCanvas.getContext('2d');
const sliderWavelength = document.getElementById('slider-wavelength');
const valWavelength = document.getElementById('val-wavelength');
const sliderIntensity = document.getElementById('slider-intensity');
const valIntensity = document.getElementById('val-intensity');
const sliderSlit = document.getElementById('slider-slit');
const valSlit = document.getElementById('val-slit');
const sliderDistance = document.getElementById('slider-distance');
const valDistance = document.getElementById('val-distance');
const selectMetal = document.getElementById('select-metal');
const sliderMetalPos = document.getElementById('slider-metal-pos');
const valMetalPos = document.getElementById('val-metal-pos');
document.getElementById('preset-red').addEventListener('click', () => setWavelength(700));
document.getElementById('preset-green').addEventListener('click', () => setWavelength(530));
document.getElementById('preset-violet').addEventListener('click', () => setWavelength(400));
document.getElementById('preset-uv').addEventListener('click', () => setWavelength(300));
const waveLengthDisplay = document.getElementById('wave-length-display');
const photonEnergyDisplay = document.getElementById('photon-energy-display');
const colorIndicator = document.getElementById('color-indicator');
const statWorkFunction = document.getElementById('stat-workfunction');
const statPhotonEnergy = document.getElementById('stat-photonenergy');
const statKE = document.getElementById('stat-ke');
const statStatus = document.getElementById('stat-status');
const statCurrent = document.getElementById('stat-current');
let showRays = true;
let showPhotons = true;
let sourcePaused = false;
let patternReveal = 1;
document.getElementById('btn-pause-source').addEventListener('click', (e) => {
sourcePaused = !sourcePaused;
if (sourcePaused) {
photons = [];
screenImpactSpots = [];
screenAccumulation.fill(0);
patternReveal = 0;
}
const icon = e.currentTarget.querySelector('i');
icon.className = sourcePaused ? 'fa-solid fa-play' : 'fa-solid fa-pause';
e.currentTarget.lastChild.textContent = sourcePaused ? ' Resume Source' : ' Pause Source';
e.currentTarget.classList.toggle('text-emerald-300', sourcePaused);
e.currentTarget.classList.toggle('text-rose-300', !sourcePaused);
});
document.getElementById('btn-toggle-waves').addEventListener('click', (e) => {
showRays = !showRays;
e.currentTarget.classList.toggle('opacity-50', !showRays);
});
document.getElementById('btn-toggle-photons').addEventListener('click', (e) => {
showPhotons = !showPhotons;
e.currentTarget.classList.toggle('opacity-50', !showPhotons);
});
document.getElementById('btn-clear-screen').addEventListener('click', () => {
screenAccumulation.fill(0);
screenImpactSpots = [];
});
let wavelength = 530;
let intensity = 5;
let slitSeparation = 25;
let screenDistance = 40;
let metalWorkFunction = 2.1;
let metalNormalizedY = 0.5;
let screenAccumulation = [];
const ACCUMULATION_BINS = 300;
screenAccumulation = new Array(ACCUMULATION_BINS).fill(0);
let screenImpactSpots = [];
const MAX_IMPACT_SPOTS = 350;
let photons = [];
let photoelectrons = [];
let rayPhase = 0;
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
const gRect = graphCanvas.parentElement.getBoundingClientRect();
graphCanvas.width = gRect.width;
graphCanvas.height = gRect.height;
}
window.addEventListener('resize', resizeCanvas);
function wavelengthToColor(wavelength) {
let r, g, b;
if (wavelength >= 380 && wavelength < 440) {
r = -1 * (wavelength - 440) / (440 - 380);
g = 0;
b = 1;
} else if (wavelength >= 440 && wavelength < 490) {
r = 0;
g = (wavelength - 440) / (490 - 440);
b = 1;
} else if (wavelength >= 490 && wavelength < 510) {
r = 0;
g = 1;
b = -1 * (wavelength - 510) / (510 - 490);
} else if (wavelength >= 510 && wavelength < 580) {
r = (wavelength - 510) / (580 - 510);
g = 1;
b = 0;
} else if (wavelength >= 580 && wavelength < 645) {
r = 1;
g = -1 * (wavelength - 645) / (645 - 580);
b = 0;
} else if (wavelength >= 645 && wavelength <= 780) {
r = 1;
g = 0;
b = 0;
} else if (wavelength < 380) {
r = 0.6;
g = 0.1;
b = 0.9;
} else {
r = 0.5;
g = 0.0;
b = 0.0;
}
let factor = 1.0;
if (wavelength >= 380 && wavelength < 420) {
factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380);
} else if (wavelength >= 420 && wavelength <= 700) {
factor = 1.0;
} else if (wavelength > 700 && wavelength <= 780) {
factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700);
} else if (wavelength < 380) {
factor = 0.8;
}
const R = Math.round(r * factor * 255);
const G = Math.round(g * factor * 255);
const B = Math.round(b * factor * 255);
return {
r: R, g: G, b: B,
rgbStr: `rgb(${R}, ${G}, ${B})`,
rgbaStr: (alpha) => `rgba(${R}, ${G}, ${B}, ${alpha})`
};
}
function calculatePhotonEnergy(nm) {
return 1239.84 / nm;
}
function setWavelength(val) {
wavelength = val;
sliderWavelength.value = val;
updateParams();
}
function getInterferenceIntensity(y, screenHeight, slitSep, waveLen) {
const centerY = screenHeight / 2;
const dy = y - centerY;
const L = screenDistance * 15;
const d = slitSep * 7.5;
const lambda = waveLen * 0.08;
const pathDiffAngle = (Math.PI * d * dy) / (lambda * L);
const intensityVal = Math.pow(Math.cos(pathDiffAngle), 2);
const singleSlitBeta = (Math.PI * (d * 0.008) * dy) / (lambda * L);
const diffractionEnvelope = singleSlitBeta === 0 ? 1 : Math.pow(Math.sin(singleSlitBeta) / singleSlitBeta, 2);
return intensityVal * diffractionEnvelope;
}
function updateParams() {
wavelength = parseFloat(sliderWavelength.value);
intensity = parseFloat(sliderIntensity.value);
slitSeparation = parseFloat(sliderSlit.value);
screenDistance = parseFloat(sliderDistance.value);
metalWorkFunction = parseFloat(selectMetal.value);
metalNormalizedY = parseFloat(sliderMetalPos.value) / 100;
valWavelength.textContent = `${wavelength} nm`;
waveLengthDisplay.textContent = `${wavelength} nm`;
const energy = calculatePhotonEnergy(wavelength);
photonEnergyDisplay.textContent = `${energy.toFixed(2)} eV`;
statPhotonEnergy.textContent = `${energy.toFixed(2)} eV`;
statWorkFunction.textContent = `${metalWorkFunction.toFixed(2)} eV`;
const colorObj = wavelengthToColor(wavelength);
colorIndicator.style.backgroundColor = colorObj.rgbStr;
const graphLegend = document.getElementById('graph-legend-bars');
if (graphLegend) graphLegend.style.color = colorObj.rgbStr;
if (intensity < 4) valIntensity.textContent = "Low";
else if (intensity < 8) valIntensity.textContent = "Medium";
else valIntensity.textContent = "High";
valSlit.textContent = `${slitSeparation} µm`;
valDistance.textContent = `${screenDistance} cm`;
if (metalNormalizedY === 0.5) valMetalPos.textContent = "Center";
else valMetalPos.textContent = `${Math.round(metalNormalizedY * 100)}%`;
const maxKE = energy - metalWorkFunction;
if (maxKE > 0) {
statKE.textContent = `${maxKE.toFixed(2)} eV`;
statStatus.textContent = "Emitting Photoelectrons!";
statStatus.className = "px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-emerald-500/20 text-emerald-400 border border-emerald-500/30";
const h = canvas.height || 500;
const localI = getInterferenceIntensity(metalNormalizedY * h, h, slitSeparation, wavelength);
const currentuA = intensity * localI * 1.5;
statCurrent.textContent = `${currentuA.toFixed(2)} µA`;
} else {
statKE.textContent = "0.00 eV";
statStatus.textContent = "No Emission (E < W)";
statStatus.className = "px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-red-500/20 text-red-400 border border-red-500/30";
statCurrent.textContent = "0.00 µA";
}
}
sliderWavelength.addEventListener('input', updateParams);
sliderIntensity.addEventListener('input', updateParams);
sliderSlit.addEventListener('input', updateParams);
sliderDistance.addEventListener('input', updateParams);
selectMetal.addEventListener('change', updateParams);
sliderMetalPos.addEventListener('input', updateParams);
let isDraggingMetal = false;
function handlePointerMove(e) {
if (!isDraggingMetal) return;
const rect = canvas.getBoundingClientRect();
const y = e.clientY - rect.top;
let normY = y / canvas.height;
normY = Math.max(0.05, Math.min(0.95, normY));
metalNormalizedY = normY;
sliderMetalPos.value = Math.round(normY * 100);
updateParams();
}
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const screenBaseX = currentScreenBaseX();
if (x >= screenBaseX - 20 && x <= screenBaseX + 90) {
isDraggingMetal = true;
handlePointerMove(e);
}
});
window.addEventListener('mousemove', handlePointerMove);
window.addEventListener('mouseup', () => { isDraggingMetal = false; });
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const screenBaseX = canvas.width * 0.55;
if (x >= screenBaseX - 20 && x <= screenBaseX + 80) {
isDraggingMetal = true;
}
}
});
window.addEventListener('touchmove', (e) => {
if (isDraggingMetal && e.touches.length === 1) {
const rect = canvas.getBoundingClientRect();
const y = e.touches[0].clientY - rect.top;
let normY = y / canvas.height;
normY = Math.max(0.05, Math.min(0.95, normY));
metalNormalizedY = normY;
sliderMetalPos.value = Math.round(normY * 100);
updateParams();
}
});
window.addEventListener('touchend', () => { isDraggingMetal = false; });
function currentScreenBaseX() {
const screenSpan = (screenDistance - 20) / 60;
return canvas.width * (0.46 + screenSpan * 0.16);
}
function project3DScreenPoint(screenBaseX, y, zOffset, depthWidth) {
const projX = screenBaseX + (zOffset * depthWidth);
const projY = y + (zOffset * 15) - 7.5;
return { x: projX, y: projY };
}
function spawnPhotons(sourcePoint, slit1, slit2, barrierX) {
const rate = intensity * 2;
const colorObj = wavelengthToColor(wavelength);
const topBoundaryY = slit1.y - 30;
const bottomBoundaryY = slit2.y + 30;
for (let i = 0; i < rate; i++) {
if (Math.random() < 0.4) {
const targetYOnBarrier = topBoundaryY + Math.random() * (bottomBoundaryY - topBoundaryY);
const angle = Math.atan2(targetYOnBarrier - sourcePoint.y, barrierX - sourcePoint.x);
const speed = 4 + Math.random() * 2;
photons.push({
x: sourcePoint.x,
y: sourcePoint.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: colorObj.rgbStr,
stage: 'source-to-slit',
targetSlit: Math.abs(targetYOnBarrier - slit1.y) < Math.abs(targetYOnBarrier - slit2.y) ? slit1 : slit2,
size: 2.5 + Math.random() * 1.5,
phase: Math.random() * Math.PI * 2,
zOffset: Math.random()
});
}
}
}
function tryEmitPhotoelectron(impactX, impactY) {
const photonE = calculatePhotonEnergy(wavelength);
const maxKE = photonE - metalWorkFunction;
if (maxKE > 0) {
const speed = Math.sqrt(maxKE) * 3 + 1.5;
const angle = (Math.random() - 0.5) * (Math.PI * 0.7);
photoelectrons.push({
x: impactX + 10,
y: impactY,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
size: 3 + Math.random() * 2
});
}
}
function animate() {
rayPhase += 0.15;
if (!sourcePaused) patternReveal = Math.min(1, patternReveal + 0.004);
else patternReveal = 0;
ctx.fillStyle = '#090d16';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const lightSourceX = canvas.width * 0.05;
const lightSourceY = canvas.height / 2;
const barrierX = canvas.width * 0.25;
const screenBaseX = currentScreenBaseX();
const screenDepthWidth = 45;
const slit1Y = lightSourceY - slitSeparation;
const slit2Y = lightSourceY + slitSeparation;
const colorObj = wavelengthToColor(wavelength);
if (showRays && !sourcePaused) {
ctx.save();
const coneGrad = ctx.createLinearGradient(lightSourceX, lightSourceY, barrierX, lightSourceY);
coneGrad.addColorStop(0, colorObj.rgbaStr(0.4));
coneGrad.addColorStop(1, colorObj.rgbaStr(0.08));
const coneTopY = slit1Y - 30;
const coneBottomY = slit2Y + 30;
ctx.fillStyle = coneGrad;
ctx.beginPath();
ctx.moveTo(lightSourceX, lightSourceY);
ctx.lineTo(barrierX, coneTopY);
ctx.lineTo(barrierX, coneBottomY);
ctx.closePath();
ctx.fill();
const numDivergingRays = 11;
for (let i = 0; i < numDivergingRays; i++) {
const rayTargetY = coneTopY + (i / (numDivergingRays - 1)) * (coneBottomY - coneTopY);
const rayGrad = ctx.createLinearGradient(lightSourceX, lightSourceY, barrierX, rayTargetY);
rayGrad.addColorStop(0, colorObj.rgbaStr(0.7));
rayGrad.addColorStop(1, colorObj.rgbaStr(0.2));
ctx.lineWidth = 1.5;
ctx.strokeStyle = rayGrad;
ctx.beginPath();
ctx.moveTo(lightSourceX, lightSourceY);
ctx.lineTo(barrierX, rayTargetY);
ctx.stroke();
}
const numRaysPerSlit = 22;
ctx.lineWidth = 1;
for (let i = 0; i <= numRaysPerSlit; i++) {
const targetY = (i / numRaysPerSlit) * canvas.height;
const I = getInterferenceIntensity(targetY, canvas.height, slitSeparation, wavelength);
const targetPoint = project3DScreenPoint(screenBaseX, targetY, 0.5, screenDepthWidth);
ctx.strokeStyle = colorObj.rgbaStr(I * 0.25);
ctx.beginPath();
ctx.moveTo(barrierX, slit1Y);
ctx.lineTo(targetPoint.x, targetPoint.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(barrierX, slit2Y);
ctx.lineTo(targetPoint.x, targetPoint.y);
ctx.stroke();
}
ctx.restore();
}
ctx.save();
ctx.shadowColor = sourcePaused ? '#334155' : colorObj.rgbStr;
ctx.shadowBlur = sourcePaused ? 0 : 20;
ctx.fillStyle = sourcePaused ? '#334155' : colorObj.rgbStr;
ctx.beginPath();
ctx.arc(lightSourceX, lightSourceY, 14, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#94a3b8';
ctx.font = '10px Inter';
ctx.fillText("Source", lightSourceX - 15, lightSourceY + 28);
ctx.fillStyle = '#334155';
ctx.shadowBlur = 0;
ctx.fillRect(barrierX - 4, 0, 8, slit1Y - 12);
ctx.fillRect(barrierX - 4, slit1Y + 12, 8, (slit2Y - 12) - (slit1Y + 12));
ctx.fillRect(barrierX - 4, slit2Y + 12, 8, canvas.height - (slit2Y + 12));
ctx.fillStyle = '#cbd5e1';
ctx.font = '10px Inter';
ctx.fillText("Double Slit Barrier", barrierX - 35, 25);
if (!sourcePaused) {
spawnPhotons({ x: lightSourceX, y: lightSourceY }, { x: barrierX, y: slit1Y }, { x: barrierX, y: slit2Y }, barrierX);
}
if (showPhotons) {
for (let i = photons.length - 1; i >= 0; i--) {
const p = photons[i];
p.x += p.vx;
p.y += p.vy;
ctx.save();
ctx.shadowColor = p.color;
ctx.shadowBlur = 8;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = colorObj.rgbaStr(0.4);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - p.vx * 3, p.y - p.vy * 3 + Math.sin(p.x * 0.15 + p.phase) * 3);
ctx.stroke();
ctx.restore();
if (p.stage === 'source-to-slit' && p.x >= barrierX) {
const hitSlit1 = Math.abs(p.y - slit1Y) <= 12;
const hitSlit2 = Math.abs(p.y - slit2Y) <= 12;
if (hitSlit1 || hitSlit2) {
p.stage = 'slit-to-screen';
p.x = barrierX;
p.y = hitSlit1 ? slit1Y : slit2Y;
let targetY = Math.random() * canvas.height;
let intensityAtY = getInterferenceIntensity(targetY, canvas.height, slitSeparation, wavelength);
while (Math.random() > intensityAtY) {
targetY = Math.random() * canvas.height;
intensityAtY = getInterferenceIntensity(targetY, canvas.height, slitSeparation, wavelength);
}
const targetPoint = project3DScreenPoint(screenBaseX, targetY, p.zOffset, screenDepthWidth);
const angleToScreen = Math.atan2(targetPoint.y - p.y, targetPoint.x - barrierX);
const speed = 4 + Math.random() * 2;
p.vx = Math.cos(angleToScreen) * speed;
p.vy = Math.sin(angleToScreen) * speed;
p.targetPoint = targetPoint;
p.targetY = targetY;
} else {
photons.splice(i, 1);
continue;
}
}
if (p.targetPoint && p.x >= p.targetPoint.x) {
const bin = Math.floor((p.targetY / canvas.height) * ACCUMULATION_BINS);
if (bin >= 0 && bin < ACCUMULATION_BINS) {
screenAccumulation[bin] += 1;
}
const intensityAtHit = getInterferenceIntensity(p.targetY, canvas.height, slitSeparation, wavelength);
if (intensityAtHit > 0.05) {
screenImpactSpots.push({
x: p.targetPoint.x,
y: p.targetPoint.y,
color: colorObj.rgbStr,
radius: 1.5 + Math.random() * 2,
opacity: 0.9,
intensity: intensityAtHit
});
if (screenImpactSpots.length > MAX_IMPACT_SPOTS) {
screenImpactSpots.shift();
}
}
const metalHeight = 40;
const metalY = metalNormalizedY * canvas.height;
const metalTop = metalY - metalHeight / 2;
const metalBottom = metalY + metalHeight / 2;
if (p.targetY >= metalTop && p.targetY <= metalBottom) {
tryEmitPhotoelectron(p.targetPoint.x, p.targetPoint.y);
}
photons.splice(i, 1);
}
}
}
ctx.save();
const numSteps = 120;
const stepH = canvas.height / numSteps;
for (let i = 0; i < numSteps; i++) {
const y1 = i * stepH;
const y2 = (i + 1) * stepH;
const midY = (y1 + y2) / 2;
const I = sourcePaused ? 0 : getInterferenceIntensity(midY, canvas.height, slitSeparation, wavelength);
const p1Left = project3DScreenPoint(screenBaseX, y1, 0, screenDepthWidth);
const p2Left = project3DScreenPoint(screenBaseX, y2, 0, screenDepthWidth);
const p1Right = project3DScreenPoint(screenBaseX, y1, 1, screenDepthWidth);
const p2Right = project3DScreenPoint(screenBaseX, y2, 1, screenDepthWidth);
ctx.fillStyle = sourcePaused ? 'rgba(15, 23, 42, 0.95)' : colorObj.rgbaStr(I * 0.85 * patternReveal);
ctx.beginPath();
ctx.moveTo(p1Left.x, p1Left.y);
ctx.lineTo(p1Right.x, p1Right.y);
ctx.lineTo(p2Right.x, p2Right.y);
ctx.lineTo(p2Left.x, p2Left.y);
ctx.closePath();
ctx.fill();
}
const topLeft = project3DScreenPoint(screenBaseX, 0, 0, screenDepthWidth);
const topRight = project3DScreenPoint(screenBaseX, 0, 1, screenDepthWidth);
const bottomLeft = project3DScreenPoint(screenBaseX, canvas.height, 0, screenDepthWidth);
const bottomRight = project3DScreenPoint(screenBaseX, canvas.height, 1, screenDepthWidth);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(topLeft.x, topLeft.y);
ctx.lineTo(topRight.x, topRight.y);
ctx.lineTo(bottomRight.x, bottomRight.y);
ctx.lineTo(bottomLeft.x, bottomLeft.y);
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 0.8;
for (let z = 0.25; z < 1; z += 0.25) {
const pTop = project3DScreenPoint(screenBaseX, 0, z, screenDepthWidth);
const pBot = project3DScreenPoint(screenBaseX, canvas.height, z, screenDepthWidth);
ctx.beginPath();
ctx.moveTo(pTop.x, pTop.y);
ctx.lineTo(pBot.x, pBot.y);
ctx.stroke();
}
if (!sourcePaused) {
for (let i = 0; i < screenImpactSpots.length; i++) {
const spot = screenImpactSpots[i];
ctx.save();
ctx.shadowColor = spot.color;
ctx.shadowBlur = 6;
ctx.fillStyle = spot.color;
ctx.globalAlpha = spot.opacity;
ctx.beginPath();
ctx.arc(spot.x, spot.y, spot.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
ctx.fillStyle = '#cbd5e1';
ctx.font = '10px Inter';
ctx.fillText("3D Detection Screen", topLeft.x - 5, canvas.height - 12);
const metalHeight = 40;
const metalY = metalNormalizedY * canvas.height;
const metalTopY = metalY - metalHeight / 2;
const metalBotY = metalY + metalHeight / 2;
const mP1Left = project3DScreenPoint(screenBaseX - 3, metalTopY, -0.1, screenDepthWidth);
const mP2Left = project3DScreenPoint(screenBaseX - 3, metalBotY, -0.1, screenDepthWidth);
const mP1Right = project3DScreenPoint(screenBaseX - 3, metalTopY, 1.1, screenDepthWidth);
const mP2Right = project3DScreenPoint(screenBaseX - 3, metalBotY, 1.1, screenDepthWidth);
const localIntensity = sourcePaused ? 0 : getInterferenceIntensity(metalY, canvas.height, slitSeparation, wavelength);
const inBrightFringe = localIntensity > 0.25;
ctx.save();
const metalGrad = ctx.createLinearGradient(mP1Left.x, mP1Left.y, mP1Right.x, mP1Right.y);
metalGrad.addColorStop(0, '#94a3b8');
metalGrad.addColorStop(0.5, '#e2e8f0');
metalGrad.addColorStop(1, '#64748b');
ctx.fillStyle = metalGrad;
ctx.strokeStyle = isDraggingMetal ? '#f59e0b' : (inBrightFringe ? '#34d399' : '#f87171');
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(mP1Left.x, mP1Left.y);
ctx.lineTo(mP1Right.x, mP1Right.y);
ctx.lineTo(mP2Right.x, mP2Right.y);
ctx.lineTo(mP2Left.x, mP2Left.y);
ctx.closePath();
ctx.fill();
ctx.stroke();
const centerHandle = project3DScreenPoint(screenBaseX - 3, metalY, 0.5, screenDepthWidth);
ctx.fillStyle = isDraggingMetal ? '#f59e0b' : '#0f172a';
ctx.beginPath();
ctx.arc(centerHandle.x, centerHandle.y, 4, 0, Math.PI * 2);
ctx.fill();
const textX = mP1Right.x + 25;
ctx.fillStyle = '#fbbf24';
ctx.font = 'bold 11px Inter';
ctx.fillText("Photosensitive Metal Target", textX, centerHandle.y - 10);
const pEnergy = calculatePhotonEnergy(wavelength);
const meetsThreshold = (pEnergy > metalWorkFunction);
ctx.font = '10px Inter';
if (!inBrightFringe) {
ctx.fillStyle = '#f87171';
ctx.fillText("Dark Fringe (No Photons → No Emission)", textX, centerHandle.y + 8);
} else if (meetsThreshold) {
ctx.fillStyle = '#34d399';
ctx.fillText("Bright Fringe (Photon Hits → Photoelectrons Emitted!)", textX, centerHandle.y + 8);
} else {
ctx.fillStyle = '#f59e0b';
ctx.fillText("Bright Fringe (Photons Hit, but E < W)", textX, centerHandle.y + 8);
}
ctx.restore();
for (let i = photoelectrons.length - 1; i >= 0; i--) {
const e = photoelectrons[i];
e.x += e.vx;
e.y += e.vy;
e.life -= 0.015;
if (e.life <= 0 || e.x > canvas.width || e.y < 0 || e.y > canvas.height) {
photoelectrons.splice(i, 1);
continue;
}
ctx.save();
ctx.shadowColor = '#60a5fa';
ctx.shadowBlur = 10;
ctx.fillStyle = `rgba(96, 165, 250, ${e.life})`;
ctx.beginPath();
ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(15, 23, 42, ${e.life})`;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(e.x - 2, e.y);
ctx.lineTo(e.x + 2, e.y);
ctx.stroke();
ctx.restore();
}
renderGraph();
requestAnimationFrame(animate);
}
function renderGraph() {
const gWidth = graphCanvas.width;
const gHeight = graphCanvas.height;
gctx.clearRect(0, 0, gWidth, gHeight);
gctx.strokeStyle = '#1e293b';
gctx.lineWidth = 1;
gctx.beginPath();
gctx.moveTo(0, gHeight / 2);
gctx.lineTo(gWidth, gHeight / 2);
gctx.stroke();
const colorObj = wavelengthToColor(wavelength);
gctx.beginPath();
gctx.strokeStyle = colorObj.rgbaStr(0.8);
gctx.lineWidth = 2;
for (let x = 0; x < gWidth; x += 2) {
const canvasY = (x / gWidth) * canvas.height;
const I = sourcePaused ? 0 : getInterferenceIntensity(canvasY, canvas.height, slitSeparation, wavelength) * patternReveal;
const graphY = gHeight - (I * (gHeight - 12) + 6);
if (x === 0) gctx.moveTo(x, graphY);
else gctx.lineTo(x, graphY);
}
gctx.stroke();
if (!sourcePaused) {
const maxHits = Math.max(1, ...screenAccumulation);
const binWidth = gWidth / ACCUMULATION_BINS;
gctx.fillStyle = colorObj.rgbaStr(0.35);
for (let i = 0; i < ACCUMULATION_BINS; i++) {
const hits = screenAccumulation[i];
if (hits === 0) continue;
const barHeight = (hits / maxHits) * (gHeight - 16);
const x = i * binWidth;
const y = gHeight - barHeight;
gctx.fillRect(x, y, binWidth + 0.5, barHeight);
}
}
}
window.onload = () => {
resizeCanvas();
updateParams();
animate();
};
</script>
<script>
const e5Panel = document.getElementById('e5-panel');
const e5Buttons = document.querySelectorAll('.e5-btn');
let e5Choice = null;
const e5Active = {
engage: 'border-amber-400 bg-amber-900/70 text-amber-50',
explore: 'border-sky-400 bg-sky-900/70 text-sky-50',
explain: 'border-indigo-400 bg-indigo-900/70 text-indigo-50',
elaborate: 'border-emerald-400 bg-emerald-900/70 text-emerald-50',
evaluate: 'border-rose-400 bg-rose-900/70 text-rose-50'
};
const e5Idle = {
engage: 'border-amber-500/50 bg-slate-950 text-amber-200',
explore: 'border-sky-500/40 bg-slate-950 text-sky-300',
explain: 'border-indigo-500/40 bg-slate-950 text-indigo-300',
elaborate: 'border-emerald-500/40 bg-slate-950 text-emerald-300',
evaluate: 'border-rose-500/40 bg-slate-950 text-rose-300'
};
function e5SetActive(phase) {
e5Buttons.forEach((btn) => {
const key = btn.dataset.phase;
btn.className = 'e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border ' + (key === phase ? e5Active[key] : e5Idle[key]);
});
}
function renderEngage() {
e5Panel.innerHTML = `
<p class="text-xs uppercase tracking-wider text-amber-300/80 mb-1">Engage · enquiry</p>
<h3 class="font-semibold text-base mb-2">What is light? Pick one side and defend it.</h3>
<p class="text-slate-400 text-xs mb-3">Do not allow “both”. Students write one word, then stand with their side. Stop the debate while both sides still think they are right.</p>
<div class="flex flex-wrap gap-2 mb-3">
<button type="button" id="pick-wave" class="px-3 py-1.5 rounded-lg text-xs font-semibold border border-sky-500/50 bg-sky-950/40 text-sky-200">I say light is a wave</button>
<button type="button" id="pick-particle" class="px-3 py-1.5 rounded-lg text-xs font-semibold border border-amber-500/50 bg-amber-950/40 text-amber-200">I say light is a particle</button>
</div>
<div id="engage-brief" class="text-xs leading-relaxed text-slate-300">Choose a side to unlock the defence task and the prediction you must lock before using the simulation.</div>`;
document.getElementById('pick-wave').onclick = () => showSide('wave');
document.getElementById('pick-particle').onclick = () => showSide('particle');
if (e5Choice) showSide(e5Choice);
}
function showSide(side) {
e5Choice = side;
const brief = document.getElementById('engage-brief');
if (!brief) return;
document.getElementById('pick-wave').classList.toggle('ring-2', side === 'wave');
document.getElementById('pick-particle').classList.toggle('ring-2', side === 'particle');
if (side === 'wave') {
brief.innerHTML = `
<p class="font-medium text-sky-200 mb-1">Wave side</p>
<ol class="list-decimal ml-4 space-y-1">
<li>Give one everyday example that light spreads, bends, or makes a pattern (rainbow, ripple-like colours on a CD, sharp shadow edges).</li>
<li>Attack the particle side: if light is only bullets, why do two openings make bright and dark bands instead of two bright strips?</li>
<li><strong>Lock this prediction</strong> before Explore: the screen will be smooth bands, and very bright red light will still knock electrons out because “more light” means more energy.</li>
</ol>`;
} else {
brief.innerHTML = `
<p class="font-medium text-amber-200 mb-1">Particle side</p>
<ol class="list-decimal ml-4 space-y-1">
<li>Give one everyday example that light arrives in hits (camera pixels, a solar cell, a light sensor clicking).</li>
<li>Attack the wave side: a wave should wash over the metal. Why would a colour change matter more than brightness?</li>
<li><strong>Lock this prediction</strong> before Explore: hits on the screen will be random with no stable bands, and only “strong” light ejects electrons. Brighter light means faster electrons.</li>
</ol>`;
}
}
function renderExplore() {
e5Panel.innerHTML = `
<p class="text-xs uppercase tracking-wider text-sky-300/80 mb-1">Explore · use the lab</p>
<h3 class="font-semibold text-base mb-2">Test your side. Tick each result you actually see.</h3>
<ul class="space-y-2 text-xs">
<li><label class="flex gap-2 items-start"><input type="checkbox" class="mt-0.5"> <span><strong>Low intensity, green 530 nm.</strong> Press Reset Hits. Do dots slowly build the same bright and dark bands, or do the bands vanish?</span></label></li>
<li><label class="flex gap-2 items-start"><input type="checkbox" class="mt-0.5"> <span><strong>Drag the metal plate</strong> from a bright fringe into a dark fringe. What happens to the photoelectrons and the photocurrent (µA)?</span></label></li>
<li><label class="flex gap-2 items-start"><input type="checkbox" class="mt-0.5"> <span><strong>Zinc or copper.</strong> Red 700 nm at High intensity, then UV 300 nm at Low intensity. Record photon energy, max KE, and photocurrent for both.</span></label></li>
</ul>
<p class="text-[11px] text-slate-500 mt-3">Stay on your Engage side until all three boxes are ticked. Then decide whether your locked prediction survived.</p>`;
}
function renderExplain() {
e5Panel.innerHTML = `
<p class="text-xs uppercase tracking-wider text-indigo-300/80 mb-1">Explain · after the tests</p>
<h3 class="font-semibold text-base mb-2">Say it in physics, using what you just saw.</h3>
<div class="grid sm:grid-cols-3 gap-2 text-xs">
<div class="rounded-lg border border-slate-800 p-3">
<p class="font-semibold text-indigo-200 mb-1">Wave</p>
Bright and dark fringes are formed. The pattern is already in the wave before any photon is detected.
</div>
<div class="rounded-lg border border-slate-800 p-3">
<p class="font-semibold text-indigo-200 mb-1">Particle</p>
Each spot is one photon. Dim light does not blur the pattern. It only slows down how fast the same pattern is built.
</div>
<div class="rounded-lg border border-slate-800 p-3">
<p class="font-semibold text-indigo-200 mb-1">Energy</p>
E = hν = hc/λ. Electrons leave only if E > W, and KE<sub>max</sub> = hν − W. Intensity changes the current, not KE.
</div>
</div>
<p class="text-[11px] text-slate-400 mt-3">Now “both” is allowed. One sentence: light travels so that it interferes like a wave, and it is absorbed as a quantum.</p>`;
}
function renderElaborate() {
e5Panel.innerHTML = `
<p class="text-xs uppercase tracking-wider text-emerald-300/80 mb-1">Elaborate · a new case</p>
<h3 class="font-semibold text-base mb-2">Predict first. Then run it.</h3>
<p class="text-xs text-slate-300 mb-2">Metal: <strong>sodium</strong> (W = 2.70 eV). Compare <strong>violet 400 nm</strong> and <strong>green 530 nm</strong> at the same intensity, with the plate on a bright fringe.</p>
<div class="grid sm:grid-cols-2 gap-2 text-xs mb-3">
<label class="block">Violet: will electrons be emitted?
<select id="elab-v" class="mt-1 w-full bg-slate-900 border border-slate-700 rounded-lg p-2">
<option value="">Choose…</option><option>Yes, KE > 0</option><option>No, E < W</option>
</select>
</label>
<label class="block">Green: will electrons be emitted?
<select id="elab-g" class="mt-1 w-full bg-slate-900 border border-slate-700 rounded-lg p-2">
<option value="">Choose…</option><option>Yes, KE > 0</option><option>No, E < W</option>
</select>
</label>
</div>
<button type="button" id="elab-check" class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-700 text-white">Check prediction</button>
<p id="elab-result" class="text-xs mt-2 text-slate-300"></p>
<p class="text-[11px] text-slate-500 mt-2">Extra: why is the photocurrent almost zero in a dark fringe even when E > W?</p>`;
document.getElementById('elab-check').onclick = () => {
const v = document.getElementById('elab-v').value;
const g = document.getElementById('elab-g').value;
const out = document.getElementById('elab-result');
if (!v || !g) { out.textContent = 'Choose both predictions first.'; return; }
const vOk = v.startsWith('Yes');
const gOk = g.startsWith('No');
out.innerHTML = (vOk && gOk)
? 'Matched. Violet photons are about 3.10 eV, above 2.70 eV, so KE is about 0.40 eV. Green photons are about 2.34 eV, below W, so KE and current stay zero. Confirm it on the metal panel.'
: 'Not yet. Calculate E = 1240 / λ(nm). Sodium needs more than 2.70 eV. Then set the metal and the two colours in the lab.';
};
}
function renderEvaluate() {
e5Panel.innerHTML = `
<p class="text-xs uppercase tracking-wider text-rose-300/80 mb-1">Evaluate · exit ticket</p>
<h3 class="font-semibold text-base mb-2">1. Why can dim UV eject electrons when very bright red light cannot?</h3>
<textarea id="eval-answer" rows="3" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-xs" placeholder="Write your answer. Mention energy and intensity separately."></textarea>
<div class="flex gap-2 mt-2">
<button type="button" id="eval-show" class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-rose-700 text-white">Show a model answer</button>
</div>
<p id="eval-model" class="hidden text-xs mt-2 text-slate-300 leading-relaxed"></p>
<h3 class="font-semibold text-base mb-2 mt-5">2. Will particles exhibit the properties of waves?</h3>
<p class="text-xs text-slate-400 mb-2">If light can behave as both a wave and a particle, what about matter such as electrons?</p>
<textarea id="eval-answer2" rows="3" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-xs" placeholder="Yes or no, plus a short reason. This leads into the next unit."></textarea>
<div class="flex gap-2 mt-2">
<button type="button" id="eval-show2" class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-rose-700 text-white">Show a model answer & next unit teaser</button>
</div>
<p id="eval-model2" class="hidden text-xs mt-2 text-slate-300 leading-relaxed"></p>`;
document.getElementById('eval-show').onclick = () => {
const box = document.getElementById('eval-model');
box.classList.remove('hidden');
box.textContent = 'Each photon’s energy is E = hν = hc/λ, so UV photons each carry more energy than red photons. Emission needs E > W. Brightness only increases how many photons arrive, so it raises the photocurrent after emission has started. It does not raise the energy of a red photon, so red light stays below W no matter how intense it is.';
};
document.getElementById('eval-show2').onclick = () => {
const box = document.getElementById('eval-model2');
box.classList.remove('hidden');
box.innerHTML = 'Yes. Particles of matter can also show wave behaviour. Louis de Broglie proposed that any moving particle has a wavelength <span class="font-mono text-rose-200">λ = h / p</span>, where p is its momentum. Electrons can form interference and diffraction patterns, just as light does. That is the start of the next unit: the de Broglie wavelength.';
};
}
const e5Views = { engage: renderEngage, explore: renderExplore, explain: renderExplain, elaborate: renderElaborate, evaluate: renderEvaluate };
e5Buttons.forEach((btn) => {
btn.addEventListener('click', () => {
e5Panel.classList.remove('hidden');
e5SetActive(btn.dataset.phase);
e5Views[btn.dataset.phase]();
});
});
document.getElementById('e5-hide').addEventListener('click', () => {
e5Panel.classList.add('hidden');
e5Buttons.forEach((btn) => {
const key = btn.dataset.phase;
btn.className = 'e5-btn px-3 py-1.5 rounded-full text-xs font-semibold border ' + e5Idle[key];
});
});
e5SetActive('engage');
renderEngage();
</script>
</body>
</html>