BLACKBODY RADIATION
Interactive simulation of Planck's law. Explore how temperature affects spectral radiance, color, and Wien's displacement law.
Temperature Control
Star Types
Display Options
Blackbody Color
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!
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();
});