physics

Bohr Model

Interactive simulation of electron transitions, emission spectra, and energy levels in hydrogen and multi-electron atoms.

Atom Visualization

1.0x
1.0x

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