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();