physics

NUCLEAR DECAY

Interactive radioactive decay simulation. Watch atoms decay in real-time, visualize half-lives, decay chains, and carbon dating.

Atom Grid Visualization
Decay Curve & Activity
Simulation Controls
Atoms: 300
Speed: 100×
Carbon-14 Dating
Estimate the age of an artifact based on remaining C-14.
Live Statistics
Atoms Remaining
300
Atoms Decayed
0
Fraction Remaining
1.00
Half-Lives Elapsed
0.00
Activity (Bq)
0
Time Elapsed
0 s
Daughter Atoms
0
Estimated Age
Current Isotope Info
Name: Carbon-14
Half-Life: 5,730 years
Decay Type: β⁻
Daughter: Nitrogen-14
λ = 1.21e-4 yr⁻¹
How It Works

Left Panel: Glowing green atoms decay randomly. Each decays at rate λ. Flash when decaying, then turn gray.

Right Panel: Real-time decay curve (dots) vs theoretical (line). Shows N(t) = N₀e^(−λt).

Activity: Rate of decay in Becquerels. A(t) = λN(t).

Half-Life: Time for 50% of atoms to decay. Mark on graph with vertical lines.

Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// Isotope data: { t_half_years, decay_type, daughter, lambda_per_second }
    const isotopes = {
      c14: {
        name: "Carbon-14",
        t_half_years: 5730,
        decay_type: "β⁻",
        daughter: "Nitrogen-14",
        defaultN0: 300,
      },
      u238: {
        name: "Uranium-238",
        t_half_years: 4.468e9,
        decay_type: "α",
        daughter: "Thorium-234",
        defaultN0: 300,
      },
      ra226: {
        name: "Radium-226",
        t_half_years: 1600,
        decay_type: "α",
        daughter: "Radon-222",
        defaultN0: 300,
      },
      rn222: {
        name: "Radon-222",
        t_half_years: 3.82 / 365.25,
        decay_type: "α",
        daughter: "Polonium-218",
        defaultN0: 300,
      },
      po210: {
        name: "Polonium-210",
        t_half_years: 138 / 365.25,
        decay_type: "α",
        daughter: "Lead-206",
        defaultN0: 300,
      },
      co60: {
        name: "Cobalt-60",
        t_half_years: 5.27,
        decay_type: "β⁻",
        daughter: "Nickel-60",
        defaultN0: 300,
      },
      i131: {
        name: "Iodine-131",
        t_half_years: 8.02 / 365.25,
        decay_type: "β⁻",
        daughter: "Xenon-131",
        defaultN0: 300,
      },
      cs137: {
        name: "Cesium-137",
        t_half_years: 30.1,
        decay_type: "β⁻",
        daughter: "Barium-137",
        defaultN0: 300,
      },
    };

    // Decay chains for display
    const decayChains = {
      c14: [
        { parent: "C-14", decay: "β⁻", daughter: "N-14", t_half: "5,730 yr" },
      ],
      u238: [
        { parent: "U-238", decay: "α", daughter: "Th-234", t_half: "4.468 Gy" },
        { parent: "Th-234", decay: "β⁻", daughter: "Pa-234", t_half: "24 d" },
        { parent: "Pa-234", decay: "β⁻", daughter: "U-234", t_half: "1.17 min" },
      ],
      ra226: [
        { parent: "Ra-226", decay: "α", daughter: "Rn-222", t_half: "1,600 yr" },
        { parent: "Rn-222", decay: "α", daughter: "Po-218", t_half: "3.82 d" },
      ],
    };

    // State
    let state = {
      mode: "standard",
      currentIsotope: "c14",
      N0: 300,
      N: 300,
      N_daughter: 0,
      decayed: 0,
      timeElapsed: 0, // in seconds
      isRunning: false,
      isPaused: false,
      lambda: 0, // decay constant in seconds^-1
      t_half_seconds: 0,
      speed: 100,
      showTheoretical: true,
      showActivity: false,
      showDaughter: false,
      dataPoints: [],
      activityPoints: [],
      daughterPoints: [],
      decayingAtoms: new Set(), // indices of atoms currently decaying (flash animation)
      particles: [], // flying particles { x, y, vx, vy, type, age }
    };

    const canvas = document.getElementById("atomCanvas");
    const ctx = canvas.getContext("2d");
    const graphCanvas = document.getElementById("graphCanvas");
    const graphCtx = graphCanvas.getContext("2d");

    // Update canvas sizes on resize
    function resizeCanvases() {
      const rect = canvas.parentElement.getBoundingClientRect();
      canvas.width = rect.width;
      canvas.height = 400;
      graphCanvas.width = rect.width;
      graphCanvas.height = 400;
    }
    window.addEventListener("resize", resizeCanvases);
    resizeCanvases();

    // Setup controls
    document.getElementById("isotopeSelect").addEventListener("change", (e) => {
      state.currentIsotope = e.target.value;
      if (e.target.value === "custom") {
        document.getElementById("customHalflifeGroup").classList.remove("hidden");
      } else {
        document.getElementById("customHalflifeGroup").classList.add("hidden");
      }
      updateIsotopeInfo();
      reset();
    });

    document.getElementById("initialAtomsSlider").addEventListener("input", (e) => {
      state.N0 = parseInt(e.target.value);
      document.getElementById("initialAtomsValue").textContent = state.N0;
    });

    document.getElementById("speedSlider").addEventListener("input", (e) => {
      state.speed = parseInt(e.target.value);
      document.getElementById("speedValue").textContent = state.speed + "×";
    });

    // Mode buttons
    document.querySelectorAll(".mode-btn").forEach((btn) => {
      btn.addEventListener("click", () => {
        document.querySelectorAll(".mode-btn").forEach((b) => b.classList.remove("active"));
        btn.classList.add("active");
        state.mode = btn.dataset.mode;
        updateModeUI();
        reset();
      });
    });

    // Display checkboxes
    document.getElementById("showTheoretical").addEventListener("change", (e) => {
      state.showTheoretical = e.target.checked;
    });
    document.getElementById("showActivity").addEventListener("change", (e) => {
      state.showActivity = e.target.checked;
    });
    document.getElementById("showDaughter").addEventListener("change", (e) => {
      state.showDaughter = e.target.checked;
    });

    // Start/Pause/Reset buttons
    document.getElementById("startBtn").addEventListener("click", start);
    document.getElementById("pauseBtn").addEventListener("click", pause);
    document.getElementById("resetBtn").addEventListener("click", reset);

    // Carbon dating
    document.getElementById("calculateAgeBtn").addEventListener("click", () => {
      const percentage = parseFloat(document.getElementById("c14Percentage").value);
      const c14_half_life = 5730;
      const n_fraction = percentage / 100;
      const age = (c14_half_life / Math.LN2) * Math.log(1 / n_fraction);
      const ageResult = document.getElementById("ageResult");
      ageResult.innerHTML = `<div class="age-result"><span class="age-value">${age.toFixed(0)}</span> years old</div>`;
    });

    function updateIsotopeInfo() {
      const iso = state.currentIsotope === "custom"
        ? { name: "Custom", t_half_years: parseFloat(document.getElementById("customHalflife").value), decay_type: "?", daughter: "?" }
        : isotopes[state.currentIsotope];

      const t_half_s = iso.t_half_years * 365.25 * 24 * 3600;
      const lambda = Math.LN2 / t_half_s;

      document.getElementById("isotopeName").textContent = iso.name;
      document.getElementById("isotopeHalflife").textContent =
        iso.t_half_years < 1
          ? (iso.t_half_years * 365.25).toFixed(1) + " days"
          : iso.t_half_years < 1000
          ? iso.t_half_years.toFixed(1) + " years"
          : (iso.t_half_years / 1e9).toFixed(3) + " billion years";
      document.getElementById("isotopeDecayType").innerHTML = `<span class="decay-type ${getDecayTypeClass(iso.decay_type)}">${iso.decay_type}</span>`;
      document.getElementById("isotopeDaughter").textContent = iso.daughter;
      document.getElementById("decayConstant").textContent = lambda.toExponential(2);

      state.lambda = lambda;
      state.t_half_seconds = t_half_s;
    }

    function getDecayTypeClass(type) {
      if (type.includes("α")) return "alpha";
      if (type.includes("β")) return "beta";
      if (type.includes("γ")) return "gamma";
      return "";
    }

    function updateModeUI() {
      document.getElementById("chainInfo").classList.toggle("hidden", state.mode !== "chain");
      document.getElementById("carbonDatingPanel").classList.toggle("active", state.mode === "carbon");
      document.getElementById("daughterStatsBox").classList.toggle("hidden", state.mode !== "chain");
      document.getElementById("ageStatsBox").classList.toggle("hidden", state.mode !== "carbon");

      if (state.mode === "chain") {
        const chain = decayChains[state.currentIsotope] || [];
        const chainDisplay = document.getElementById("decayChainDisplay");
        if (chain.length > 0) {
          chainDisplay.innerHTML = chain
            .map(
              (step) =>
                `<div class="decay-step">
              <span class="isotope">${step.parent}</span>
              <span class="arrow">→</span>
              <span class="particle">${step.decay}</span>
              <span class="arrow">→</span>
              <span class="isotope">${step.daughter}</span>
              <span class="halflife">${step.t_half}</span>
            </div>`
            )
            .join("");
        } else {
          chainDisplay.innerHTML = `<div class="decay-step">No chain data available</div>`;
        }
      }
    }

    function reset() {
      state.N = state.N0;
      state.N_daughter = 0;
      state.decayed = 0;
      state.timeElapsed = 0;
      state.isRunning = false;
      state.isPaused = false;
      state.dataPoints = [];
      state.activityPoints = [];
      state.daughterPoints = [];
      state.decayingAtoms.clear();
      state.particles = [];
      updateUI();
      draw();
      document.getElementById("startBtn").textContent = "Start";
    }

    function start() {
      updateIsotopeInfo();
      if (!state.isRunning && !state.isPaused) {
        state.N = state.N0;
        state.N_daughter = 0;
        state.decayed = 0;
        state.timeElapsed = 0;
        state.dataPoints = [];
        state.activityPoints = [];
        state.daughterPoints = [];
        state.decayingAtoms.clear();
        state.particles = [];
      }
      state.isRunning = true;
      state.isPaused = false;
      document.getElementById("startBtn").textContent = "Running...";
      document.getElementById("startBtn").disabled = true;
      animate();
    }

    function pause() {
      state.isRunning = false;
      document.getElementById("startBtn").textContent = "Resume";
      document.getElementById("startBtn").disabled = false;
    }

    // Main animation loop
    function animate() {
      if (!state.isRunning) return;

      const dt = (1 / 60) * (state.speed / 100); // time step in seconds, adjusted by speed
      const lambda = state.lambda;

      // Decay step: for each remaining atom, P(decay) = 1 - e^(-λ*dt)
      const decayProb = 1 - Math.exp(-lambda * dt);
      let newDecays = 0;
      for (let i = 0; i < state.N; i++) {
        if (Math.random() < decayProb) {
          newDecays++;
        }
      }

      state.N -= newDecays;
      state.decayed += newDecays;
      if (state.mode === "chain") {
        state.N_daughter += newDecays * 0.7; // decay chain daughter buildup
      }
      state.timeElapsed += dt;

      // Add decaying atoms to flash set
      for (let i = 0; i < newDecays && i < 5; i++) {
        const idx = Math.floor(Math.random() * Math.floor(Math.sqrt(state.N0)));
        state.decayingAtoms.add(idx);
        setTimeout(() => state.decayingAtoms.delete(idx), 150);
      }

      // Emit particles
      for (let i = 0; i < newDecays; i++) {
        const angle = Math.random() * Math.PI * 2;
        const speed = 2 + Math.random() * 3;
        state.particles.push({
          x: Math.random() * canvas.width,
          y: Math.random() * canvas.height,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          type: ["α", "β", "γ"][Math.floor(Math.random() * 3)],
          age: 0,
          maxAge: 60,
        });
      }

      // Update particle positions
      state.particles = state.particles.filter((p) => {
        p.x += p.vx;
        p.y += p.vy;
        p.age++;
        return p.age < p.maxAge;
      });

      // Record data point
      state.dataPoints.push({ t: state.timeElapsed, N: state.N });
      const activity = lambda * state.N;
      state.activityPoints.push({ t: state.timeElapsed, A: activity });
      if (state.mode === "chain") {
        state.daughterPoints.push({ t: state.timeElapsed, N: state.N_daughter });
      }

      updateUI();
      draw();

      // Stop if all atoms decayed
      if (state.N <= 0) {
        state.isRunning = false;
        document.getElementById("startBtn").textContent = "Complete";
      } else {
        requestAnimationFrame(animate);
      }
    }

    function updateUI() {
      document.getElementById("atomsRemaining").textContent = Math.max(0, Math.floor(state.N));
      document.getElementById("atomsDecayed").textContent = state.decayed;
      const fraction = state.N0 > 0 ? state.N / state.N0 : 0;
      document.getElementById("fractionRemaining").textContent = fraction.toFixed(3);
      const halflives = state.timeElapsed / state.t_half_seconds;
      document.getElementById("halflives").textContent = halflives.toFixed(2);

      const activity = state.lambda * state.N;
      document.getElementById("activity").textContent = activity.toFixed(0);

      const hours = state.timeElapsed / 3600;
      const days = hours / 24;
      const years = days / 365.25;
      let timeStr = "";
      if (state.timeElapsed < 60) {
        timeStr = state.timeElapsed.toFixed(0) + " s";
      } else if (hours < 24) {
        timeStr = hours.toFixed(2) + " h";
      } else if (days < 365) {
        timeStr = days.toFixed(1) + " d";
      } else {
        timeStr = years.toFixed(2) + " yr";
      }
      document.getElementById("timeElapsed").textContent = timeStr;

      if (state.mode === "chain") {
        document.getElementById("daughterAtoms").textContent = Math.max(0, Math.floor(state.N_daughter));
      }

      if (state.mode === "carbon") {
        const c14_half_life = 5730;
        const fraction_remaining = state.N / state.N0;
        if (fraction_remaining > 0) {
          const age = (c14_half_life / Math.LN2) * Math.log(1 / fraction_remaining);
          document.getElementById("estimatedAge").textContent = age.toFixed(0) + " yr";
        }
      }
    }

    function draw() {
      // Draw atom grid
      ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--surface").trim();
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const gridSize = Math.ceil(Math.sqrt(state.N0));
      const atomSize = Math.min(canvas.width, canvas.height) / (gridSize + 2);

      for (let i = 0; i < state.N0; i++) {
        const col = i % gridSize;
        const row = Math.floor(i / gridSize);
        const x = (col + 1) * (canvas.width / (gridSize + 1));
        const y = (row + 1) * (canvas.height / (gridSize + 1));

        if (i < state.N) {
          // Alive atom
          ctx.fillStyle = state.decayingAtoms.has(i) ? "#ffffff" : "#00c896";
          ctx.beginPath();
          ctx.arc(x, y, atomSize * 0.35, 0, Math.PI * 2);
          ctx.fill();

          // Glow
          ctx.strokeStyle = "rgba(0, 200, 150, 0.3)";
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.arc(x, y, atomSize * 0.5, 0, Math.PI * 2);
          ctx.stroke();
        } else {
          // Decayed atom
          ctx.fillStyle = "#555555";
          ctx.beginPath();
          ctx.arc(x, y, atomSize * 0.25, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      // Draw particles
      state.particles.forEach((p) => {
        const alpha = 1 - p.age / p.maxAge;
        const size = (3 - p.age / p.maxAge * 2);

        ctx.fillStyle =
          p.type === "α"
            ? `rgba(255, 34, 0, ${alpha})`
            : p.type === "β"
            ? `rgba(34, 100, 255, ${alpha})`
            : `rgba(255, 197, 24, ${alpha})`;

        ctx.beginPath();
        ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
        ctx.fill();
      });

      // Draw decay graph
      graphCtx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--surface").trim();
      graphCtx.fillRect(0, 0, graphCanvas.width, graphCanvas.height);

      // Get max time and N for scaling
      const maxTime = Math.max(state.timeElapsed * 1.1, state.t_half_seconds * 3);
      const maxN = state.N0;

      // Draw grid
      graphCtx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue("--border").trim();
      graphCtx.lineWidth = 0.5;
      for (let i = 0; i <= 5; i++) {
        const y = (i / 5) * graphCanvas.height;
        graphCtx.beginPath();
        graphCtx.moveTo(0, y);
        graphCtx.lineTo(graphCanvas.width, y);
        graphCtx.stroke();
      }

      // Draw half-life markers
      graphCtx.strokeStyle = "rgba(255, 34, 0, 0.3)";
      graphCtx.lineWidth = 1;
      graphCtx.setLineDash([5, 5]);
      for (let i = 1; i <= 3; i++) {
        const x = (i * state.t_half_seconds) / maxTime * graphCanvas.width;
        if (x < graphCanvas.width) {
          graphCtx.beginPath();
          graphCtx.moveTo(x, 0);
          graphCtx.lineTo(x, graphCanvas.height);
          graphCtx.stroke();
        }
      }
      graphCtx.setLineDash([]);

      // Draw theoretical curve
      if (state.showTheoretical) {
        graphCtx.strokeStyle = "#ff2200";
        graphCtx.lineWidth = 2;
        graphCtx.beginPath();
        for (let i = 0; i < graphCanvas.width; i++) {
          const t = (i / graphCanvas.width) * maxTime;
          const N_theory = state.N0 * Math.exp(-state.lambda * t);
          const y = graphCanvas.height - (N_theory / maxN) * graphCanvas.height;
          if (i === 0) graphCtx.moveTo(i, y);
          else graphCtx.lineTo(i, y);
        }
        graphCtx.stroke();
      }

      // Draw simulation data points
      graphCtx.fillStyle = "rgba(232, 224, 213, 0.5)";
      graphCtx.beginPath();
      state.dataPoints.forEach((p, idx) => {
        const x = (p.t / maxTime) * graphCanvas.width;
        const y = graphCanvas.height - (p.N / maxN) * graphCanvas.height;
        if (idx === 0) graphCtx.moveTo(x, y);
        graphCtx.linePath(x, y);
      });

      state.dataPoints.forEach((p) => {
        const x = (p.t / maxTime) * graphCanvas.width;
        const y = graphCanvas.height - (p.N / maxN) * graphCanvas.height;
        graphCtx.beginPath();
        graphCtx.arc(x, y, 3, 0, Math.PI * 2);
        graphCtx.fill();
      });

      // Draw activity curve if enabled
      if (state.showActivity && state.activityPoints.length > 0) {
        const maxActivity = state.N0 * state.lambda;
        graphCtx.strokeStyle = "#00c896";
        graphCtx.lineWidth = 1.5;
        graphCtx.beginPath();
        state.activityPoints.forEach((p, idx) => {
          const x = (p.t / maxTime) * graphCanvas.width;
          const y = graphCanvas.height - (p.A / maxActivity) * graphCanvas.height * 0.5;
          if (idx === 0) graphCtx.moveTo(x, y);
          else graphCtx.lineTo(x, y);
        });
        graphCtx.stroke();
      }

      // Draw daughter buildup if enabled
      if (state.showDaughter && state.daughterPoints.length > 0) {
        const maxDaughter = state.N0;
        graphCtx.strokeStyle = "#2264ff";
        graphCtx.lineWidth = 1.5;
        graphCtx.beginPath();
        state.daughterPoints.forEach((p, idx) => {
          const x = (p.t / maxTime) * graphCanvas.width;
          const y = graphCanvas.height - (p.N / maxDaughter) * graphCanvas.height;
          if (idx === 0) graphCtx.moveTo(x, y);
          else graphCtx.lineTo(x, y);
        });
        graphCtx.stroke();
      }
    }

    // Initialize
    updateIsotopeInfo();
    updateModeUI();
    draw();