physics
Bohr Model
Interactive simulation of electron transitions, emission spectra, and energy levels in hydrogen and multi-electron atoms.
Atom Visualization
Electron Transition
Select shells and click to animate transitions
Statistics
1
Current Shell
-13.6
Energy (eV)
0.53
Radius (Å)
0.0
ΔE (eV)
—
λ (nm)
—
Series
Emission Spectrum
Visible light range (400–700 nm)
Bright lines show spectral emission from electron transitions. Line intensity increases when transition is animated.
Energy Level Diagram
Horizontal lines represent electron energy levels. Arrows show possible transitions (click to animate).
Visible Lines (Balmer Series)
Hα, Hβ, Hγ, Hδ are characteristic hydrogen emission lines visible in emission spectroscopy.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Constants
const h = 6.626e-34; // Planck's constant (J·s)
const c = 3e8; // Speed of light (m/s)
const hc = h * c; // hc in J·m
const eV_to_J = 1.602e-19; // 1 eV in Joules
const a0 = 0.529e-10; // Bohr radius in meters
const a0_angstrom = 0.529; // Bohr radius in Angstroms
const Ry = 13.6; // Rydberg energy for hydrogen (eV)
// Element data
const elements = {
H: { Z: 1, name: 'Hydrogen', electrons: 1 },
He: { Z: 2, name: 'Helium', electrons: 2 },
Li: { Z: 3, name: 'Lithium', electrons: 3 },
Na: { Z: 11, name: 'Sodium', electrons: 11 },
Ne: { Z: 10, name: 'Neon', electrons: 10 }
};
// State
let state = {
element: 'H',
Z: 1,
currentN: 1,
speedMult: 1,
zoomMult: 1,
showLabels: true,
showEnergy: true,
showAnimation: true,
nInitial: 1,
nFinal: 2,
animationFrame: 0,
isAnimating: false
};
// Canvas references
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const spectrumCanvas = document.getElementById('spectrumCanvas');
const spectrumCtx = spectrumCanvas.getContext('2d');
const energyCanvas = document.getElementById('energyCanvas');
const energyCtx = energyCanvas.getContext('2d');
// Resize canvases
function resizeCanvases() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
const specRect = spectrumCanvas.getBoundingClientRect();
spectrumCanvas.width = specRect.width;
spectrumCanvas.height = specRect.height;
const engRect = energyCanvas.getBoundingClientRect();
energyCanvas.width = engRect.width;
energyCanvas.height = engRect.height;
}
// Physics calculations
function getEnergyLevel(n, Z = 1) {
return -Ry * Math.pow(Z, 2) / Math.pow(n, 2);
}
function getBohrRadius(n, Z = 1) {
return (a0_angstrom * Math.pow(n, 2)) / Z;
}
function getPhotonEnergy(n1, n2, Z = 1) {
return Math.abs(getEnergyLevel(n2, Z) - getEnergyLevel(n1, Z));
}
function getPhotonWavelength(deltaE) {
if (deltaE <= 0) return null;
return (hc / eV_to_J) / deltaE / 1e-9; // Convert to nm
}
function wavelengthToColor(lambda) {
if (!lambda) return '#555555';
if (lambda < 380) return '#8B00FF'; // UV-ish purple
if (lambda < 420) return '#4B0082'; // Indigo
if (lambda < 450) return '#0000FF'; // Blue
if (lambda < 495) return '#00FFFF'; // Cyan
if (lambda < 570) return '#00FF00'; // Green
if (lambda < 590) return '#FFFF00'; // Yellow
if (lambda < 620) return '#FFA500'; // Orange
if (lambda < 750) return '#FF0000'; // Red
return '#8B0000'; // Dark red (IR-ish)
}
function getSeriesName(n1, n2) {
if (n1 === 1) return 'Lyman';
if (n1 === 2) return 'Balmer';
if (n1 === 3) return 'Paschen';
if (n1 === 4) return 'Brackett';
if (n1 === 5) return 'Pfund';
return 'Other';
}
function isVisibleWavelength(lambda) {
return lambda && lambda >= 380 && lambda <= 750;
}
// Spectral line data for Hydrogen
const hydrogenBalmer = [
{ n2: 6, n1: 2, name: 'Hε', lambda: 397.0 },
{ n2: 5, n1: 2, name: 'Hδ', lambda: 410.2 },
{ n2: 4, n1: 2, name: 'Hγ', lambda: 434.0 },
{ n2: 3, n1: 2, name: 'Hβ', lambda: 486.1 },
{ n2: 2, n1: 1, name: 'Hα', lambda: 656.3 }
];
// Draw atom visualization
function drawAtom() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const scale = 50 * state.zoomMult; // pixels per Angstrom
const Z = state.Z;
// Draw nucleus
const nucleusSize = 4 + Math.log(Z + 1) * 2;
ctx.fillStyle = '#ff2200';
ctx.shadowColor = 'rgba(255, 34, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(centerX, centerY, nucleusSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = 'transparent';
// Draw orbits (n=1 to 6)
for (let n = 1; n <= 6; n++) {
const r = getBohrRadius(n, Z) * scale;
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
ctx.stroke();
// Draw orbit labels
if (state.showLabels) {
ctx.fillStyle = '#555555';
ctx.font = 'bold 12px DM Mono';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(`n=${n}`, centerX + r + 5, centerY - 5);
}
// Draw energy labels
if (state.showEnergy) {
const En = getEnergyLevel(n, Z);
ctx.fillStyle = '#555555';
ctx.font = '11px DM Mono';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(`${En.toFixed(1)}eV`, centerX - r - 5, centerY);
}
}
// Draw animated electrons
if (state.showAnimation) {
const electronColor = '#00c896';
// Current electron on current shell
const rCurrent = getBohrRadius(state.currentN, Z) * scale;
const speed = (state.speedMult / state.currentN) * 0.005;
const angle = (state.animationFrame * speed) % (Math.PI * 2);
const ex = centerX + rCurrent * Math.cos(angle);
const ey = centerY + rCurrent * Math.sin(angle);
ctx.fillStyle = electronColor;
ctx.shadowColor = 'rgba(0, 200, 150, 0.6)';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(ex, ey, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = 'transparent';
}
}
// Draw emission spectrum
function drawSpectrum() {
spectrumCtx.clearRect(0, 0, spectrumCanvas.width, spectrumCanvas.height);
const width = spectrumCanvas.width;
const height = spectrumCanvas.height;
const margin = 20;
const specWidth = width - 2 * margin;
const specHeight = height - 2 * margin;
// Draw background gradient (rainbow)
for (let x = 0; x < specWidth; x++) {
const lambda = 400 + (x / specWidth) * 300; // 400nm to 700nm
const color = wavelengthToColor(lambda);
spectrumCtx.fillStyle = color;
spectrumCtx.fillRect(margin + x, margin, 1, specHeight);
}
// Draw wavelength scale
spectrumCtx.fillStyle = '#555555';
spectrumCtx.font = '11px DM Mono';
spectrumCtx.textAlign = 'center';
spectrumCtx.textBaseline = 'top';
for (let nm = 400; nm <= 700; nm += 100) {
const x = margin + ((nm - 400) / 300) * specWidth;
spectrumCtx.fillRect(x, margin + specHeight + 2, 1, 3);
spectrumCtx.fillText(`${nm}nm`, x, margin + specHeight + 8);
}
// Draw emission lines for current element
if (state.element === 'H') {
hydrogenBalmer.forEach((line) => {
const x = margin + ((line.lambda - 400) / 300) * specWidth;
if (x >= margin && x <= margin + specWidth) {
// Intensity based on whether this is the active transition
const isActive = (state.nInitial === line.n1 && state.nFinal === line.n2) ||
(state.nInitial === line.n2 && state.nFinal === line.n1);
const intensity = isActive ? 1 : 0.6;
spectrumCtx.strokeStyle = wavelengthToColor(line.lambda);
spectrumCtx.lineWidth = isActive ? 4 : 2;
spectrumCtx.globalAlpha = intensity;
spectrumCtx.beginPath();
spectrumCtx.moveTo(x, margin);
spectrumCtx.lineTo(x, margin + specHeight);
spectrumCtx.stroke();
spectrumCtx.globalAlpha = 1;
// Label active line
if (isActive) {
spectrumCtx.fillStyle = wavelengthToColor(line.lambda);
spectrumCtx.font = 'bold 12px DM Mono';
spectrumCtx.textAlign = 'center';
spectrumCtx.textBaseline = 'bottom';
spectrumCtx.fillText(line.name, x, margin - 5);
}
}
});
}
// Labels
spectrumCtx.fillStyle = '#555555';
spectrumCtx.font = '11px DM Mono';
spectrumCtx.textAlign = 'center';
spectrumCtx.fillText('Visible Spectrum', width / 2, 8);
}
// Draw energy level diagram
function drawEnergyDiagram() {
energyCtx.clearRect(0, 0, energyCanvas.width, energyCanvas.height);
const width = energyCanvas.width;
const height = energyCanvas.height;
const margin = 50;
const diagWidth = width - 2 * margin;
const diagHeight = height - 2 * margin;
// Draw Y-axis (energy)
energyCtx.strokeStyle = '#1e1e1e';
energyCtx.lineWidth = 1;
energyCtx.beginPath();
energyCtx.moveTo(margin, margin);
energyCtx.lineTo(margin, height - margin);
energyCtx.stroke();
// Draw X-axis
energyCtx.beginPath();
energyCtx.moveTo(margin, height - margin);
energyCtx.lineTo(width - margin, height - margin);
energyCtx.stroke();
// Labels
energyCtx.fillStyle = '#555555';
energyCtx.font = '11px DM Mono';
energyCtx.textAlign = 'right';
energyCtx.fillText('E (eV)', margin - 5, margin - 10);
energyCtx.textAlign = 'center';
energyCtx.fillText('n →', width - margin, height - margin + 20);
// Draw energy levels (n=1 to 6)
const levels = [];
for (let n = 1; n <= 6; n++) {
const En = getEnergyLevel(n, state.Z);
const yPos = height - margin - ((En + 15) / 15) * diagHeight; // Scale from -15 to 0
// Draw level line
energyCtx.strokeStyle = '#2a2a2a';
energyCtx.lineWidth = 2;
energyCtx.beginPath();
energyCtx.moveTo(margin + 10, yPos);
energyCtx.lineTo(margin + diagWidth - 10, yPos);
energyCtx.stroke();
// Label
energyCtx.fillStyle = '#555555';
energyCtx.font = '11px DM Mono';
energyCtx.textAlign = 'right';
energyCtx.textBaseline = 'middle';
energyCtx.fillText(`${En.toFixed(1)}`, margin - 8, yPos);
energyCtx.textAlign = 'center';
energyCtx.fillText(`n=${n}`, margin + diagWidth + 15, yPos);
levels.push({ n, En, yPos });
}
// Draw transition arrows
const n1 = parseInt(state.nInitial);
const n2 = parseInt(state.nFinal);
if (n1 !== n2) {
const level1 = levels.find(l => l.n === n1);
const level2 = levels.find(l => l.n === n2);
if (level1 && level2) {
const deltaE = Math.abs(level2.En - level1.En);
const lambda = getPhotonWavelength(deltaE);
const color = wavelengthToColor(lambda);
const yFrom = level1.yPos;
const yTo = level2.yPos;
const x = margin + 30 + Math.abs(n2 - n1) * 15;
// Draw arrow
energyCtx.strokeStyle = color;
energyCtx.lineWidth = 3;
energyCtx.beginPath();
energyCtx.moveTo(x, yFrom);
energyCtx.lineTo(x, yTo);
energyCtx.stroke();
// Arrowhead
const arrowSize = 6;
energyCtx.fillStyle = color;
if (yTo < yFrom) { // Going up (excitation)
energyCtx.beginPath();
energyCtx.moveTo(x, yTo);
energyCtx.lineTo(x - arrowSize, yTo + arrowSize);
energyCtx.lineTo(x + arrowSize, yTo + arrowSize);
energyCtx.fill();
} else { // Going down (emission)
energyCtx.beginPath();
energyCtx.moveTo(x, yTo);
energyCtx.lineTo(x - arrowSize, yTo - arrowSize);
energyCtx.lineTo(x + arrowSize, yTo - arrowSize);
energyCtx.fill();
}
}
}
}
// Update statistics
function updateStats() {
const n = state.currentN;
const En = getEnergyLevel(n, state.Z);
const rn = getBohrRadius(n, state.Z);
const n1 = parseInt(state.nInitial);
const n2 = parseInt(state.nFinal);
const deltaE = getPhotonEnergy(n1, n2, state.Z);
const lambda = getPhotonWavelength(deltaE);
document.getElementById('nCurrent').textContent = n;
document.getElementById('enValue').textContent = En.toFixed(2);
document.getElementById('rnValue').textContent = rn.toFixed(2);
document.getElementById('deltaEValue').textContent = deltaE.toFixed(2);
document.getElementById('wavelengthValue').textContent = lambda ? lambda.toFixed(1) : '—';
document.getElementById('seriesName').textContent = lambda ? getSeriesName(Math.min(n1, n2), Math.max(n1, n2)) : '—';
// Update transition info
const infoText = `n=${n1} → n=${n2}: ΔE = ${deltaE.toFixed(2)} eV${lambda ? `, λ = ${lambda.toFixed(1)} nm (${wavelengthToColor(lambda) === '#555555' ? 'IR/UV' : 'Visible'})` : ''}`;
document.getElementById('transitionInfo').textContent = infoText;
}
// Update transition list
function updateTransitionList() {
const listEl = document.getElementById('transitionList');
listEl.innerHTML = '';
if (state.element === 'H') {
hydrogenBalmer.forEach((line) => {
const item = document.createElement('div');
item.className = 'transition-item';
if (state.nInitial == line.n1 && state.nFinal == line.n2) {
item.classList.add('active');
}
const color = wavelengthToColor(line.lambda);
item.innerHTML = `<span class="color-swatch" style="background: ${color};"></span><strong>${line.name}</strong> (${line.lambda.toFixed(1)}nm) — n=${line.n2}→${line.n1}`;
item.addEventListener('click', () => {
state.nInitial = line.n1;
state.nFinal = line.n2;
updateUI();
animateTransition();
});
listEl.appendChild(item);
});
}
}
// Animate transition
function animateTransition() {
if (state.isAnimating) return;
state.isAnimating = true;
const n1 = parseInt(state.nInitial);
const n2 = parseInt(state.nFinal);
let steps = 0;
const totalSteps = 30;
function animateFrame() {
steps++;
const progress = steps / totalSteps;
state.currentN = n1 + (n2 - n1) * progress;
drawAtom();
if (steps < totalSteps) {
requestAnimationFrame(animateFrame);
} else {
state.currentN = n2;
state.isAnimating = false;
drawAtom();
}
}
animateFrame();
}
// Update all UI
function updateUI() {
updateStats();
updateTransitionList();
drawAtom();
drawSpectrum();
drawEnergyDiagram();
}
// Event listeners
document.getElementById('elementSelect').addEventListener('change', (e) => {
state.element = e.target.value;
state.Z = elements[state.element].Z;
state.currentN = 1;
updateUI();
});
document.getElementById('speedSlider').addEventListener('input', (e) => {
state.speedMult = parseFloat(e.target.value);
document.getElementById('speedValue').textContent = state.speedMult.toFixed(1) + 'x';
});
document.getElementById('zoomSlider').addEventListener('input', (e) => {
state.zoomMult = parseFloat(e.target.value);
document.getElementById('zoomValue').textContent = state.zoomMult.toFixed(1) + 'x';
drawAtom();
});
document.getElementById('showLabels').addEventListener('change', (e) => {
state.showLabels = e.target.checked;
drawAtom();
});
document.getElementById('showEnergy').addEventListener('change', (e) => {
state.showEnergy = e.target.checked;
drawAtom();
});
document.getElementById('showAnimation').addEventListener('change', (e) => {
state.showAnimation = e.target.checked;
drawAtom();
});
document.getElementById('nInitial').addEventListener('change', (e) => {
state.nInitial = parseInt(e.target.value);
updateUI();
});
document.getElementById('nFinal').addEventListener('change', (e) => {
state.nFinal = parseInt(e.target.value);
updateUI();
});
document.getElementById('exciteBtn').addEventListener('click', () => {
const n1 = parseInt(state.nInitial);
const n2 = parseInt(state.nFinal);
if (n1 < n2) {
state.nInitial = n1;
state.nFinal = n2;
} else {
state.nInitial = n2;
state.nFinal = n1;
}
document.getElementById('nInitial').value = state.nInitial;
document.getElementById('nFinal').value = state.nFinal;
updateUI();
animateTransition();
});
document.getElementById('emitBtn').addEventListener('click', () => {
const n1 = parseInt(state.nInitial);
const n2 = parseInt(state.nFinal);
if (n1 > n2) {
state.nInitial = n1;
state.nFinal = n2;
} else {
state.nInitial = n2;
state.nFinal = n1;
}
document.getElementById('nInitial').value = state.nInitial;
document.getElementById('nFinal').value = state.nFinal;
updateUI();
animateTransition();
});
// Animation loop
let animLoopRunning = true;
function animationLoop() {
if (animLoopRunning) {
state.animationFrame++;
if (state.showAnimation) {
drawAtom();
}
requestAnimationFrame(animationLoop);
}
}
// Initialize
resizeCanvases();
window.addEventListener('resize', resizeCanvases);
updateUI();
animationLoop();