8
Rutherford Scattering: Gold Foil Experiment
drag up/down to aim the white alpha, +/- sets energy
idle
145 lines · vanilla
view source
const N = 130, BTN = 44, PAD = 12, BINS = 18;
const VREF = 268, DREF = 26, A = 0.5 * DREF * VREF * VREF; // closest approach 26px @ 5 MeV
let xs, ys, vxs, vys, act;
let W, H, nx, ny, EMeV, bins, total, over90, bSel, hx, hy, hvx, hvy, hAct, hAng, spawnAcc;
function v0() { return VREF * Math.sqrt(EMeV / 5); }
function init({ ctx, width, height }) {
W = width; H = height; nx = W * 0.62; ny = H * 0.5;
xs = new Float32Array(N); ys = new Float32Array(N);
vxs = new Float32Array(N); vys = new Float32Array(N);
act = new Uint8Array(N);
bins = new Float32Array(BINS); total = 0; over90 = 0;
EMeV = 5; bSel = 36; hAct = 0; hAng = 0; spawnAcc = 0;
for (let i = 0; i < 100; i++) physics(1 / 60); // warm start: stream already mid-flight
ctx.fillStyle = '#08080e'; ctx.fillRect(0, 0, W, H);
}
function accel(x, y, out) {
const dx = x - nx, dy = y - ny;
const r2 = dx * dx + dy * dy;
const r3 = r2 * Math.sqrt(r2) + 1200;
out[0] = A * dx / r3; out[1] = A * dy / r3;
}
const acc = [0, 0];
function integrate(i, dt) {
for (let s = 0; s < 4; s++) {
const h = dt / 4;
accel(xs[i], ys[i], acc);
vxs[i] += acc[0] * h; vys[i] += acc[1] * h;
xs[i] += vxs[i] * h; ys[i] += vys[i] * h;
}
}
function exitAngle(i) {
const ang = Math.abs(Math.atan2(vys[i], vxs[i]));
const b = Math.min(BINS - 1, (ang / Math.PI * BINS) | 0);
bins[b]++; total++;
if (ang > Math.PI / 2) over90++;
}
function physics(dt) {
spawnAcc += 45 * dt;
while (spawnAcc >= 1) {
spawnAcc -= 1;
for (let i = 0; i < N; i++) if (!act[i]) {
act[i] = 1; xs[i] = -6; ys[i] = ny + (Math.random() * 2 - 1) * H * 0.46;
vxs[i] = v0(); vys[i] = 0;
break;
}
}
for (let i = 0; i < N; i++) {
if (!act[i]) continue;
integrate(i, dt);
if (xs[i] > W + 12 || xs[i] < -30 || ys[i] < -30 || ys[i] > H + 30) { act[i] = 0; exitAngle(i); }
}
// highlighted alpha at chosen impact parameter
if (!hAct) { hAct = 1; hx = -6; hy = ny + bSel; hvx = v0(); hvy = 0; }
for (let s = 0; s < 4; s++) {
const h = dt / 4;
accel(hx, hy, acc);
hvx += acc[0] * h; hvy += acc[1] * h;
hx += hvx * h; hy += hvy * h;
}
hAng = Math.abs(Math.atan2(hvy, hvx)) * 180 / Math.PI;
if (hx > W + 12 || hx < -30 || hy < -30 || hy > H + 30) hAct = 0;
}
function inRect(x, y, rx, ry, rw, rh) { return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh; }
function drawBtn(ctx, x, y, label, hot) {
ctx.fillStyle = hot ? 'rgba(255,160,60,0.85)' : 'rgba(0,0,0,0.6)';
ctx.fillRect(x, y, BTN, BTN);
ctx.strokeStyle = 'rgba(255,255,255,0.45)';
ctx.strokeRect(x + 0.5, y + 0.5, BTN - 1, BTN - 1);
ctx.fillStyle = '#fff';
ctx.font = 'bold 26px ui-sans-serif, system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(label, x + BTN / 2, y + BTN / 2);
}
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; nx = W * 0.62; ny = H * 0.5; }
const plusX = W - PAD - BTN, minusX = plusX - BTN - 8, btnY = H - PAD - BTN;
const overBtns = input.mouseY > btnY - 8 && input.mouseX > minusX - 8;
for (const c of input.consumeClicks()) {
if (inRect(c.x, c.y, minusX, btnY, BTN, BTN)) EMeV = Math.max(1, EMeV - 1);
else if (inRect(c.x, c.y, plusX, btnY, BTN, BTN)) EMeV = Math.min(12, EMeV + 1);
}
if (input.mouseDown && !overBtns) {
const nb = Math.max(-H * 0.45, Math.min(H * 0.45, input.mouseY - ny));
if (Math.abs(nb - bSel) > 2) { bSel = nb; hAct = 0; } // relaunch at new b
}
const step = Math.min(dt, 0.04);
physics(step);
ctx.fillStyle = 'rgba(8,8,14,0.10)'; ctx.fillRect(0, 0, W, H);
// gold nucleus glow
const g = ctx.createRadialGradient(nx, ny, 1, nx, ny, 26);
g.addColorStop(0, 'rgba(255,215,90,0.95)');
g.addColorStop(0.4, 'rgba(255,170,40,0.35)');
g.addColorStop(1, 'rgba(255,170,40,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(nx, ny, 26, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#ffe9a8';
ctx.beginPath(); ctx.arc(nx, ny, 4, 0, Math.PI * 2); ctx.fill();
// alpha trails: short velocity segments, colored by deflection angle
ctx.lineWidth = 1.5; ctx.lineCap = 'round';
for (let i = 0; i < N; i++) {
if (!act[i]) continue;
const ang = Math.abs(Math.atan2(vys[i], vxs[i])) / Math.PI;
ctx.strokeStyle = `hsl(${(200 - 200 * ang).toFixed(0)},95%,${(55 + ang * 15).toFixed(0)}%)`;
ctx.beginPath();
ctx.moveTo(xs[i] - vxs[i] * step * 1.6, ys[i] - vys[i] * step * 1.6);
ctx.lineTo(xs[i], ys[i]);
ctx.stroke();
}
// highlighted alpha + aim marker
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.setLineDash([4, 6]);
ctx.beginPath(); ctx.moveTo(0, ny + bSel); ctx.lineTo(nx, ny + bSel); ctx.stroke();
ctx.setLineDash([]);
if (hAct) {
ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(hx - hvx * step * 2, hy - hvy * step * 2);
ctx.lineTo(hx, hy); ctx.stroke();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(hx, hy, 3, 0, Math.PI * 2); ctx.fill();
}
ctx.lineWidth = 1;
// histogram of scattering angles (log scale)
const hw = Math.min(190, W - 150), hh = 64, hx0 = PAD, hy0 = H - PAD - hh;
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(hx0, hy0, hw, hh);
let mx = 1;
for (let b = 0; b < BINS; b++) mx = Math.max(mx, bins[b]);
const lmx = Math.log(1 + mx);
for (let b = 0; b < BINS; b++) {
const f = Math.log(1 + bins[b]) / lmx;
ctx.fillStyle = `hsl(${200 - 200 * (b / (BINS - 1))},90%,55%)`;
const bw = (hw - 12) / BINS;
ctx.fillRect(hx0 + 6 + b * bw, hy0 + hh - 16 - f * (hh - 28), bw - 1, f * (hh - 28));
}
ctx.fillStyle = '#cfe0ff'; ctx.font = '10px monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
ctx.fillText('0°', hx0 + 6, hy0 + hh - 5);
ctx.textAlign = 'right'; ctx.fillText('180°', hx0 + hw - 6, hy0 + hh - 5);
ctx.textAlign = 'left'; ctx.fillText('log N(θ)', hx0 + 6, hy0 + 11);
// HUD
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(PAD, PAD, 198, 78);
ctx.fillStyle = '#ffd17a'; ctx.font = '13px monospace';
ctx.fillText(`E = ${EMeV.toFixed(0)} MeV (+/−)`, PAD + 10, PAD + 20);
ctx.fillStyle = '#cfe0ff';
ctx.fillText(`θ > 90°: ${total ? (100 * over90 / total).toFixed(2) : '0.00'}%`, PAD + 10, PAD + 38);
ctx.fillText(`aimed b=${bSel.toFixed(0)}px θ=${hAng.toFixed(0)}°`, PAD + 10, PAD + 56);
ctx.fillStyle = 'rgba(207,224,255,0.7)'; ctx.font = '11px monospace';
ctx.fillText('drag up/down to aim the white alpha', PAD + 10, PAD + 72);
drawBtn(ctx, minusX, btnY, '−', input.mouseDown && inRect(input.mouseX, input.mouseY, minusX, btnY, BTN, BTN));
drawBtn(ctx, plusX, btnY, '+', input.mouseDown && inRect(input.mouseX, input.mouseY, plusX, btnY, BTN, BTN));
}
Comments (0)
Log in to comment.