Spaces:
Running
Running
Download index.html from SMKBK1/wave_particle_duality: direct link, hf CLI and curl.
- Browser
- Download file 54.6 kB
-
https://huggingface.co/spaces/SMKBK1/wave_particle_duality/resolve/main/index.html
- Command line
-
hf download hf://spaces/SMKBK1/wave_particle_duality/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SMKBK1/wave_particle_duality/resolve/main/index.html
54.6 kB
| <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> | |
| </script> | |
| </body> | |
| </html> |