Light: Wave or Particle?
by UltraViper34487 lines15.8 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Light: Wave or Particle?</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: #0e1018;
background-image:
radial-gradient(1px 1px at 12% 18%, rgba(255,247,220,0.55), transparent 50%),
radial-gradient(1px 1px at 78% 22%, rgba(255,247,220,0.35), transparent 50%),
radial-gradient(1px 1px at 32% 78%, rgba(255,247,220,0.45), transparent 50%),
radial-gradient(1px 1px at 88% 88%, rgba(255,247,220,0.3), transparent 50%),
radial-gradient(1px 1px at 55% 42%, rgba(255,247,220,0.25), transparent 50%),
radial-gradient(1px 1px at 8% 65%, rgba(255,247,220,0.35), transparent 50%),
radial-gradient(1px 1px at 92% 48%, rgba(255,247,220,0.4), transparent 50%),
radial-gradient(1px 1px at 45% 92%, rgba(255,247,220,0.22), transparent 50%),
radial-gradient(1px 1px at 22% 38%, rgba(255,247,220,0.18), transparent 50%),
radial-gradient(1px 1px at 65% 58%, rgba(255,247,220,0.3), transparent 50%);
background-attachment: fixed;
color: #e8e4d8;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
min-height: 100vh;
font-size: 16px;
}
.page { max-width: 760px; margin: 0 auto; padding: 3rem 1.5rem 4rem; }
h1 {
font-family: 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
font-weight: 400;
font-size: clamp(1.85rem, 5.5vw, 2.75rem);
line-height: 1.1;
margin: 0 0 0.5rem;
color: #f7eed3;
letter-spacing: -0.015em;
}
h1 em { font-style: italic; color: #f4b942; }
.subtitle {
color: #8a8678;
margin: 0 0 2rem;
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.intro {
font-size: 1.05rem;
color: #c8c2b3;
margin: 0 0 2rem;
max-width: 620px;
}
.stage {
background: #161922;
border: 1px solid #2a2e3c;
border-radius: 12px;
padding: 10px;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 680 / 320;
border-radius: 6px;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 1rem 0;
}
.stat {
background: #161922;
border: 1px solid #2a2e3c;
border-radius: 8px;
padding: 12px 16px;
}
.stat-lbl {
font-size: 11px;
color: #8a8678;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.stat-val {
font-size: 22px;
font-weight: 500;
margin: 4px 0 0;
font-variant-numeric: tabular-nums;
color: #f4b942;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
.stat-val.txt {
font-size: 14px;
font-family: inherit;
color: #e8e4d8;
}
.btns { display: flex; gap: 8px; flex-wrap: wrap; margin: 1rem 0 1.5rem; }
button {
background: transparent;
color: #e8e4d8;
border: 1px solid #3a3e4c;
padding: 8px 14px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, transform 0.05s ease;
}
button:hover { border-color: #f4b942; color: #f7eed3; background: rgba(244, 185, 66, 0.08); }
button:active { transform: scale(0.97); }
.ctrl {
display: flex;
align-items: center;
gap: 12px;
margin: 0 0 0.75rem;
font-size: 14px;
color: #a8a394;
flex-wrap: wrap;
}
.ctrl > label:first-child { min-width: 130px; }
.ctrl input[type="range"] { flex: 1; min-width: 140px; accent-color: #f4b942; }
.ctrl .v { min-width: 36px; text-align: right; font-weight: 500; color: #f4b942; font-variant-numeric: tabular-nums; font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
.tog { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px 0; font-size: 14px; color: #a8a394; user-select: none; }
.tog input { margin: 0; accent-color: #f4b942; cursor: pointer; }
.toggles { display: flex; gap: 24px; flex-wrap: wrap; margin: 1rem 0 2rem; }
.explainer { border-top: 1px solid #2a2e3c; margin-top: 3rem; padding-top: 2rem; }
.explainer h2 {
font-family: 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
font-weight: 400;
font-size: 1.5rem;
color: #f7eed3;
margin: 0 0 1rem;
letter-spacing: -0.01em;
}
.explainer p { color: #c8c2b3; margin: 0 0 1rem; }
.explainer strong { color: #f4b942; font-weight: 500; }
.foot {
margin-top: 3rem;
font-size: 12px;
color: #5a5648;
text-align: center;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<div class="page">
<h1>Light is a <em>wave</em>. Light is a <em>particle</em>.</h1>
<p class="subtitle">The double-slit experiment, interactive</p>
<p class="intro">
Light is the strangest thing in physics. It arrives as discrete particles called photons that land at a single point. But while it travels, it behaves like a wave that ripples through space. This experiment lets you watch both happen at the same time. Fire one photon. Then a thousand. See what they paint together.
</p>
<div class="stage">
<canvas id="cv"></canvas>
</div>
<div class="stats">
<div class="stat"><p class="stat-lbl">Photons fired</p><p class="stat-val" id="count">0</p></div>
<div class="stat"><p class="stat-lbl">Hit the screen</p><p class="stat-val" id="hits">0</p></div>
<div class="stat"><p class="stat-lbl">Pattern</p><p class="stat-val txt" id="status">no data yet</p></div>
</div>
<div class="btns">
<button type="button" id="b1">Fire 1 photon</button>
<button type="button" id="b50">Fire 50</button>
<button type="button" id="b500">Fire 500</button>
<button type="button" id="b2000">Fire 2,000</button>
<button type="button" id="bReset">Reset</button>
</div>
<div class="ctrl">
<label>Slit separation</label>
<input type="range" id="sep" min="35" max="100" value="55" step="1">
<span class="v" id="sepV">55</span>
</div>
<div class="toggles">
<label class="tog"><input type="checkbox" id="waveT" checked> Show wave behavior</label>
<label class="tog"><input type="checkbox" id="curveT"> Show predicted pattern</label>
</div>
<div class="explainer">
<h2>What's happening?</h2>
<p>
Each amber dot leaving the source on the left is one <strong>photon</strong>: a single, indivisible quantum of light. It arrives at the detector screen as a single point. Particles do that. Waves don't.
</p>
<p>
The faint blue stripes in the middle region show the <strong>probability wave</strong> โ what light "looks like" while it's passing through both slits at once. Bright stripes are where the two wave paths reinforce each other. Dark stripes are where they cancel out. That's pure wave behavior.
</p>
<p>
Here's the weird part: each photon is just one tiny dot, but where it lands is decided by the wave pattern. Fire one photon and the location seems random. Fire 2,000 and the dots pile up exactly into the stripes the wave predicts. Each photon, traveling alone, somehow already "knew" about both slits.
</p>
<p>
Nobody fully understands why. The math works perfectly. The intuition does not. That's wave-particle duality.
</p>
</div>
<p class="foot">Made with Claude ยท Published on ArcadeLab</p>
</div>
<script>
(function () {
try {
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const W = 680, H = 320;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.scale(dpr, dpr);
const SRC_X = 60, SLIT_X = 320, SCREEN_X = 580;
const CY = H / 2;
const SCR_HALF = 120;
const COL = {
text: '#9c9a92',
barrier: '#a8a59a',
screen: '#888780',
source: '#d3d1c7',
amber: '#f4b942',
amberFaint: 'rgba(244, 185, 66, 0.4)',
impact: 'rgba(250, 199, 117, 0.8)',
waveLine: 'rgba(133, 183, 235, 0.35)',
curve: '#85b7eb',
curveFill: 'rgba(133, 183, 235, 0.18)',
bg: '#161922'
};
const fieldRGB = [133, 183, 235];
let slitSep = 55;
const impacts = [];
const inFlight = [];
let showWave = true;
let showCurve = false;
let totalPhotons = 0;
let pendingSpawn = 0;
let field = null;
function P(y) {
const ny = y / SCR_HALF;
const fringes = slitSep / 9;
return Math.cos(ny * fringes * Math.PI) ** 2 * Math.exp(-ny * ny * 2.2);
}
let maxP = 1;
function recalcMaxP() {
maxP = 0;
for (let i = -200; i <= 200; i++) {
const v = P(i * SCR_HALF / 200);
if (v > maxP) maxP = v;
}
}
function sampleY() {
for (let i = 0; i < 500; i++) {
const y = (Math.random() - 0.5) * 2 * SCR_HALF;
if (Math.random() * maxP < P(y)) return y;
}
return 0;
}
function computeField() {
const fw = SCREEN_X - SLIT_X + 20;
const fh = H;
const off = document.createElement('canvas');
off.width = fw;
off.height = fh;
const offCtx = off.getContext('2d');
const img = offCtx.createImageData(fw, fh);
const d = img.data;
const lambda = slitSep * 0.34;
const s1y = CY - slitSep / 2;
const s2y = CY + slitSep / 2;
for (let py = 0; py < fh; py++) {
for (let px = 0; px < fw; px++) {
const dy1 = py - s1y, dy2 = py - s2y;
const d1 = Math.sqrt(px * px + dy1 * dy1);
const d2 = Math.sqrt(px * px + dy2 * dy2);
const intensity = (1 + Math.cos(2 * Math.PI * (d1 - d2) / lambda)) / 2;
const att = Math.max(0, 1 - ((d1 + d2) / 2) / 380);
const I = intensity * att * att;
const i = (py * fw + px) * 4;
d[i] = fieldRGB[0]; d[i+1] = fieldRGB[1]; d[i+2] = fieldRGB[2];
d[i+3] = Math.min(140, I * 200);
}
}
offCtx.putImageData(img, 0, 0);
field = off;
}
recalcMaxP();
computeField();
function spawn() {
const ty = CY + sampleY();
inFlight.push({ x: SRC_X, y: CY, targetY: ty, t: 0 });
totalPhotons++;
}
function fire(n) { if (n === 1) spawn(); else pendingSpawn += n; }
function reset() {
impacts.length = 0; inFlight.length = 0;
totalPhotons = 0; pendingSpawn = 0;
updateUI();
}
const elCount = document.getElementById('count');
const elHits = document.getElementById('hits');
const elStatus = document.getElementById('status');
function updateUI() {
elCount.textContent = totalPhotons.toLocaleString();
elHits.textContent = impacts.length.toLocaleString();
const n = impacts.length;
let s;
if (n === 0) s = 'no data yet';
else if (n < 8) s = 'discrete hits';
else if (n < 80) s = 'bands forming...';
else if (n < 400) s = 'interference!';
else s = 'wave pattern';
elStatus.textContent = s;
}
document.getElementById('b1').addEventListener('click', () => fire(1));
document.getElementById('b50').addEventListener('click', () => fire(50));
document.getElementById('b500').addEventListener('click', () => fire(500));
document.getElementById('b2000').addEventListener('click', () => fire(2000));
document.getElementById('bReset').addEventListener('click', reset);
document.getElementById('sep').addEventListener('input', e => {
slitSep = parseInt(e.target.value);
document.getElementById('sepV').textContent = slitSep;
recalcMaxP();
computeField();
reset();
});
document.getElementById('waveT').addEventListener('change', e => { showWave = e.target.checked; });
document.getElementById('curveT').addEventListener('change', e => { showCurve = e.target.checked; });
let last = performance.now();
let spawnAcc = 0;
function tick(now) {
const dt = Math.min(60, now - last) / 1000;
last = now;
if (pendingSpawn > 0) {
let rate;
if (pendingSpawn > 1000) rate = 600;
else if (pendingSpawn > 200) rate = 150;
else if (pendingSpawn > 30) rate = 30;
else rate = 8;
spawnAcc += rate * dt;
while (spawnAcc >= 1 && pendingSpawn > 0) {
spawn(); spawnAcc -= 1; pendingSpawn--;
}
} else spawnAcc = 0;
for (let i = inFlight.length - 1; i >= 0; i--) {
const p = inFlight[i];
p.t += dt / 0.85;
if (p.t >= 1) {
impacts.push({ y: p.targetY, jx: (Math.random() - 0.5) * 3 });
inFlight.splice(i, 1);
} else {
p.x = SRC_X + (SCREEN_X - SRC_X) * p.t;
p.y = CY + (p.targetY - CY) * p.t;
}
}
updateUI();
draw(now);
requestAnimationFrame(tick);
}
function draw(now) {
ctx.fillStyle = COL.bg;
ctx.fillRect(0, 0, W, H);
const t = now / 1000;
if (showWave) {
ctx.drawImage(field, SLIT_X, 0);
ctx.strokeStyle = COL.waveLine;
ctx.lineWidth = 1;
const wl = 30;
for (let i = 0; i < 12; i++) {
const x = SRC_X + 10 + (((t * 26) + i * wl) % (SLIT_X - SRC_X - 22));
ctx.beginPath();
ctx.moveTo(x, CY - 60);
ctx.lineTo(x, CY + 60);
ctx.stroke();
}
}
ctx.fillStyle = COL.barrier;
const gap = 9;
const s1t = CY - slitSep/2 - gap/2;
const s1b = CY - slitSep/2 + gap/2;
const s2t = CY + slitSep/2 - gap/2;
const s2b = CY + slitSep/2 + gap/2;
ctx.fillRect(SLIT_X - 4, 30, 8, s1t - 30);
ctx.fillRect(SLIT_X - 4, s1b, 8, s2t - s1b);
ctx.fillRect(SLIT_X - 4, s2b, 8, H - 30 - s2b);
ctx.fillStyle = COL.source;
ctx.beginPath();
ctx.arc(SRC_X, CY, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = COL.amberFaint;
const pulse = 6 + 3.5 * (0.5 + 0.5 * Math.sin(t * 3));
ctx.beginPath();
ctx.arc(SRC_X, CY, pulse, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = COL.screen;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(SCREEN_X, CY - SCR_HALF);
ctx.lineTo(SCREEN_X, CY + SCR_HALF);
ctx.stroke();
for (const off of [-SCR_HALF, SCR_HALF]) {
ctx.beginPath();
ctx.moveTo(SCREEN_X - 4, CY + off);
ctx.lineTo(SCREEN_X + 4, CY + off);
ctx.stroke();
}
if (showCurve) {
const cx = SCREEN_X + 10;
const amp = 60;
ctx.fillStyle = COL.curveFill;
ctx.beginPath();
ctx.moveTo(cx, CY - SCR_HALF);
for (let i = -SCR_HALF; i <= SCR_HALF; i++) {
ctx.lineTo(cx + (P(i) / maxP) * amp, CY + i);
}
ctx.lineTo(cx, CY + SCR_HALF);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = COL.curve;
ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = -SCR_HALF; i <= SCR_HALF; i++) {
const x = cx + (P(i) / maxP) * amp;
if (i === -SCR_HALF) ctx.moveTo(x, CY + i);
else ctx.lineTo(x, CY + i);
}
ctx.stroke();
}
ctx.fillStyle = COL.impact;
for (const imp of impacts) {
ctx.beginPath();
ctx.arc(SCREEN_X + imp.jx, imp.y, 1.6, 0, Math.PI * 2);
ctx.fill();
}
for (const p of inFlight) {
const dy = (p.targetY - CY);
ctx.fillStyle = COL.amberFaint;
for (let k = 1; k <= 4; k++) {
const tt = Math.max(0, p.t - k * 0.025);
const tx = SRC_X + (SCREEN_X - SRC_X) * tt;
const ty = CY + dy * tt;
ctx.globalAlpha = 0.55 - k * 0.12;
ctx.beginPath();
ctx.arc(tx, ty, 2.6 - k * 0.3, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.fillStyle = COL.amber;
ctx.beginPath();
ctx.arc(p.x, p.y, 3.2, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = COL.text;
ctx.font = '11px ui-sans-serif, system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('source', SRC_X, CY + 22);
ctx.fillText('double slit', SLIT_X, 22);
ctx.fillText('detector screen', SCREEN_X, 22);
}
updateUI();
requestAnimationFrame(tick);
} catch (err) {
console.error('widget error:', err);
}
})();
</script>
</body>
</html>Game Source: Light: Wave or Particle?
Creator: UltraViper34
Libraries: none
Complexity: complex (487 lines, 15.8 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: light-wave-or-particle-ultraviper34" to link back to the original. Then publish at arcadelab.ai/publish.