physics

BLACKBODY RADIATION

Interactive simulation of Planck's law. Explore how temperature affects spectral radiance, color, and Wien's displacement law.

Spectral Radiance Plot: Wavelength vs. intensity. Color gradient represents visible spectrum. Dashed line shows Wien's peak.

Temperature Control

Temperature
5778
K (Kelvin)
300 K — 40000 K

Star Types

Display Options

Blackbody Color

Star Type: Sun (G-type)
Color Temp: 5778 K
Wien's Peak
501
nm
Peak Frequency
597
THz
Radiant Power
6.37e7
W/m² (Stefan-Boltzmann)
Peak Photon Energy
2.48
eV

About This Simulation

Planck's Law describes the spectral radiance of electromagnetic radiation emitted by a blackbody in thermal equilibrium at temperature T.

B(λ, T) = (2hc² / λ⁵) · 1 / (e^(hc/λkT) − 1)

Wien's Displacement Law tells us the wavelength of peak emission:

λ_max = b / T, where b = 2.898 × 10⁻³ m·K

Stefan-Boltzmann Law gives total radiant power per unit area:

P = σT⁴, where σ = 5.67 × 10⁻⁸ W/(m²·K⁴)

The Ultraviolet Catastrophe: Classical physics (Rayleigh-Jeans) predicts infinite energy in short wavelengths. Quantum mechanics (Planck) fixes this by quantizing energy. Toggle the Rayleigh-Jeans curve to see the dramatic difference!

Developer Reference

Core Algorithm & Standalone Script

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

// Physical constants
    const h = 6.62607015e-34;      // Planck constant (J·s)
    const c = 299792458;           // Speed of light (m/s)
    const k = 1.380649e-23;        // Boltzmann constant (J/K)
    const sigma = 5.670374419e-8;  // Stefan-Boltzmann constant
    const b_wien = 2.897771955e-3; // Wien's displacement constant (m·K)

    // Star type data
    const starTypes = {
      3000: { name: 'M-type (Red Dwarf)', color: '#FF3000' },
      5778: { name: 'G-type (Sun)', color: '#FFEB3B' },
      9000: { name: 'A-type (Sirius A)', color: '#FFFFFF' },
      25000: { name: 'B-type (Rigel)', color: '#0099FF' },
      40000: { name: 'O-type (Blue Supergiant)', color: '#0050FF' }
    };

    let temperature = 5778;
    let showPlanck = true;
    let showRayleigh = false;
    let showWien = false;
    let showColor = true;
    let comparisonTemps = [3000, 5778, 9000];

    const canvas = document.getElementById('spectralCanvas');
    const ctx = canvas.getContext('2d');
    const tempSlider = document.getElementById('tempSlider');
    const tempValue = document.getElementById('tempValue');
    const glowSphere = document.getElementById('glowSphere');
    const colorPreview = document.getElementById('colorPreview');

    // Wavelength to RGB color mapping
    function wavelengthToColor(wavelength) {
      let r, g, b;
      wavelength = Math.round(wavelength);

      if (wavelength < 380 || wavelength > 750) {
        r = g = b = 100; // Gray for IR/UV
      } else if (wavelength < 450) {
        // Violet to Blue
        r = Math.floor(-(wavelength - 450) / 70 * 255);
        g = 0;
        b = 255;
      } else if (wavelength < 495) {
        // Blue to Cyan
        r = 0;
        g = Math.floor((wavelength - 450) / 45 * 255);
        b = 255;
      } else if (wavelength < 570) {
        // Cyan to Green to Yellow
        r = Math.floor((wavelength - 495) / 75 * 255);
        g = 255;
        b = Math.floor(-(wavelength - 570) / 75 * 255);
      } else if (wavelength < 590) {
        // Yellow to Orange
        r = 255;
        g = Math.floor(255 - (wavelength - 570) / 20 * 100);
        b = 0;
      } else if (wavelength < 650) {
        // Orange to Red
        r = 255;
        g = Math.floor(140 - (wavelength - 590) / 60 * 140);
        b = 0;
      } else {
        // Deep Red
        r = 255;
        g = 0;
        b = 0;
      }

      return { r: Math.max(0, Math.min(255, r)), g: Math.max(0, Math.min(255, g)), b: Math.max(0, Math.min(255, b)) };
    }

    // Planck's law
    function plancksLaw(wavelength, temp) {
      wavelength = wavelength * 1e-9; // Convert nm to meters
      const numerator = 2 * h * c * c;
      const denominator = Math.pow(wavelength, 5);
      const exponent = (h * c) / (wavelength * k * temp);
      const result = (numerator / denominator) / (Math.exp(exponent) - 1);
      return result;
    }

    // Rayleigh-Jeans (classical, fails at short wavelengths)
    function rayleighJeans(wavelength, temp) {
      wavelength = wavelength * 1e-9;
      return (2 * c * k * temp) / Math.pow(wavelength, 4);
    }

    // Wien's approximation
    function wienApprox(wavelength, temp) {
      wavelength = wavelength * 1e-9;
      const exponent = (h * c) / (wavelength * k * temp);
      return (2 * h * c * c) / (Math.pow(wavelength, 5) * Math.exp(exponent));
    }

    // Draw spectrum
    function drawSpectrum() {
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const margin = 60;
      const graphWidth = canvas.width - margin - 20;
      const graphHeight = canvas.height - margin - 20;
      const graphX = margin;
      const graphY = 20;

      // Draw axes
      ctx.strokeStyle = '#1e1e1e';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(graphX, graphY + graphHeight);
      ctx.lineTo(graphX + graphWidth, graphY + graphHeight);
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(graphX, graphY);
      ctx.lineTo(graphX, graphY + graphHeight);
      ctx.stroke();

      // Draw grid lines
      ctx.strokeStyle = '#161616';
      ctx.lineWidth = 0.5;
      for (let i = 0; i <= 10; i++) {
        const y = graphY + graphHeight - (graphHeight / 10) * i;
        ctx.beginPath();
        ctx.moveTo(graphX, y);
        ctx.lineTo(graphX + graphWidth, y);
        ctx.stroke();

        const x = graphX + (graphWidth / 10) * i;
        ctx.beginPath();
        ctx.moveTo(x, graphY + graphHeight);
        ctx.lineTo(x, graphY + graphHeight + 5);
        ctx.stroke();
      }

      // Wavelength range: 100-3000 nm
      const minWL = 100;
      const maxWL = 3000;

      // Find max intensity for scaling
      let maxIntensity = 0;
      for (let i = 0; i < 1000; i++) {
        const wl = minWL + (maxWL - minWL) * (i / 1000);
        const intensity = plancksLaw(wl, temperature);
        if (intensity > maxIntensity) maxIntensity = intensity;
      }

      // Draw colored fill under Planck curve
      if (showColor) {
        for (let i = 0; i < graphWidth; i++) {
          const wl = minWL + (maxWL - minWL) * (i / graphWidth);
          const intensity = plancksLaw(wl, temperature);
          const normalizedIntensity = intensity / maxIntensity;
          const fillHeight = normalizedIntensity * graphHeight;

          const color = wavelengthToColor(wl);
          ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 0.3)`;
          ctx.fillRect(graphX + i, graphY + graphHeight - fillHeight, 1, fillHeight);
        }
      }

      // Draw Planck curve
      if (showPlanck) {
        ctx.strokeStyle = '#ff2200';
        ctx.lineWidth = 2;
        ctx.beginPath();
        for (let i = 0; i < graphWidth; i++) {
          const wl = minWL + (maxWL - minWL) * (i / graphWidth);
          const intensity = plancksLaw(wl, temperature);
          const normalizedIntensity = intensity / maxIntensity;
          const y = graphY + graphHeight - normalizedIntensity * graphHeight;

          if (i === 0) ctx.moveTo(graphX + i, y);
          else ctx.lineTo(graphX + i, y);
        }
        ctx.stroke();
      }

      // Draw Rayleigh-Jeans curve
      if (showRayleigh) {
        ctx.strokeStyle = '#ff8800';
        ctx.lineWidth = 2;
        ctx.setLineDash([5, 5]);
        ctx.beginPath();
        for (let i = 0; i < graphWidth; i++) {
          const wl = minWL + (maxWL - minWL) * (i / graphWidth);
          const intensity = rayleighJeans(wl, temperature);
          const normalizedIntensity = Math.min(intensity / maxIntensity, 10); // Cap for visibility
          const y = graphY + graphHeight - normalizedIntensity * graphHeight;

          if (i === 0) ctx.moveTo(graphX + i, y);
          else ctx.lineTo(graphX + i, y);
        }
        ctx.stroke();
        ctx.setLineDash([]);
      }

      // Draw Wien's approximation
      if (showWien) {
        ctx.strokeStyle = '#00c896';
        ctx.lineWidth = 2;
        ctx.setLineDash([3, 3]);
        ctx.beginPath();
        for (let i = 0; i < graphWidth; i++) {
          const wl = minWL + (maxWL - minWL) * (i / graphWidth);
          const intensity = wienApprox(wl, temperature);
          const normalizedIntensity = intensity / maxIntensity;
          const y = graphY + graphHeight - normalizedIntensity * graphHeight;

          if (i === 0) ctx.moveTo(graphX + i, y);
          else ctx.lineTo(graphX + i, y);
        }
        ctx.stroke();
        ctx.setLineDash([]);
      }

      // Draw Wien's peak line
      const wienPeakWL = b_wien / temperature * 1e9; // Convert to nm
      if (wienPeakWL >= minWL && wienPeakWL <= maxWL) {
        const peakX = graphX + ((wienPeakWL - minWL) / (maxWL - minWL)) * graphWidth;
        ctx.strokeStyle = '#ffaa00';
        ctx.lineWidth = 2;
        ctx.setLineDash([3, 3]);
        ctx.beginPath();
        ctx.moveTo(peakX, graphY);
        ctx.lineTo(peakX, graphY + graphHeight);
        ctx.stroke();
        ctx.setLineDash([]);

        // Label peak
        ctx.fillStyle = '#ffaa00';
        ctx.font = 'bold 12px DM Mono, monospace';
        ctx.textAlign = 'center';
        ctx.fillText(`λ_max: ${wienPeakWL.toFixed(0)} nm`, peakX, graphY + 20);
      }

      // Draw axes labels
      ctx.fillStyle = '#e8e0d5';
      ctx.font = '12px DM Mono, monospace';
      ctx.textAlign = 'center';
      ctx.fillText('Wavelength (nm)', canvas.width / 2, canvas.height - 5);

      ctx.save();
      ctx.translate(20, canvas.height / 2);
      ctx.rotate(-Math.PI / 2);
      ctx.textAlign = 'center';
      ctx.fillText('Spectral Radiance', 0, 0);
      ctx.restore();

      // Wavelength ticks
      ctx.textAlign = 'center';
      ctx.fillStyle = '#555555';
      ctx.font = '10px DM Mono, monospace';
      for (let i = 0; i <= 10; i++) {
        const wl = minWL + (maxWL - minWL) * (i / 10);
        const x = graphX + (graphWidth / 10) * i;
        ctx.fillText(Math.round(wl), x, canvas.height - margin + 15);
      }
    }

    // Update color preview
    function updateColorPreview() {
      const wienPeakWL = b_wien / temperature * 1e9;
      const color = wavelengthToColor(wienPeakWL);
      const hexColor = `#${((color.r << 16) | (color.g << 8) | color.b).toString(16).padStart(6, '0')}`;

      glowSphere.style.backgroundColor = hexColor;
      glowSphere.style.boxShadow = `0 0 30px ${hexColor}, 0 0 60px ${hexColor}80`;

      // Find closest star type
      let closestTemp = 5778;
      let minDiff = Math.abs(temperature - closestTemp);
      for (const temp in starTypes) {
        const diff = Math.abs(temperature - temp);
        if (diff < minDiff) {
          minDiff = diff;
          closestTemp = parseInt(temp);
        }
      }

      document.getElementById('starType').textContent = starTypes[closestTemp].name;
      document.getElementById('colorTemp').textContent = `${temperature} K`;
    }

    // Update stats
    function updateStats() {
      const wienPeak = b_wien / temperature * 1e9;
      const peakFreq = c / (wienPeak * 1e-9) / 1e12;
      const radiantPower = sigma * Math.pow(temperature, 4);
      const photonEnergy = (h * c) / (wienPeak * 1e-9) / 1.60217663e-19; // Convert to eV

      document.getElementById('wienPeak').textContent = wienPeak.toFixed(0);
      document.getElementById('peakFreq').textContent = peakFreq.toFixed(0);
      document.getElementById('radiantPower').textContent = radiantPower.toExponential(2);
      document.getElementById('photonEnergy').textContent = photonEnergy.toFixed(2);

      document.getElementById('tempValue').textContent = temperature;
    }

    // Event listeners
    tempSlider.addEventListener('input', (e) => {
      temperature = parseInt(e.target.value);
      updateStats();
      updateColorPreview();
      drawSpectrum();
    });

    // Star type buttons
    document.querySelectorAll('.star-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.star-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        temperature = parseInt(btn.dataset.temp);
        tempSlider.value = temperature;
        updateStats();
        updateColorPreview();
        drawSpectrum();
      });
    });

    // Toggle buttons
    document.querySelectorAll('.toggle-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        const toggle = btn.dataset.toggle;
        if (toggle === 'planck') showPlanck = !showPlanck;
        else if (toggle === 'rayleigh') showRayleigh = !showRayleigh;
        else if (toggle === 'wien') showWien = !showWien;
        else if (toggle === 'color') showColor = !showColor;

        btn.classList.toggle('active');
        drawSpectrum();
      });
    });

    // Initialize
    updateStats();
    updateColorPreview();
    drawSpectrum();

    // Responsive canvas
    window.addEventListener('resize', () => {
      drawSpectrum();
    });