8

Rutherford Scattering: Gold Foil Experiment

drag up/down to aim the white alpha, +/- sets energy

A Rutherford scattering simulation of the 1909 gold foil experiment that killed the plum-pudding model: alpha particles stream past a gold nucleus under the Coulomb repulsion , tracing a family of hyperbolas. Most sail nearly straight through empty atom — but about 1 in 8000 strikes close enough to whip backwards, which is exactly what stunned Rutherford into proposing a tiny dense nucleus. Trails are colored by deflection angle (cool blue for grazes, hot red for backscatter) while a live log-scale histogram and the fraction deflected past 90° build up; drag vertically to aim the highlighted white alpha's impact parameter and use +/- to change the beam 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.