Physics
Maxwell-Boltzmann Distribution
Interactive simulation of molecular speed distribution at different temperatures. Explore how gas molecules move in thermal equilibrium.
Equation: f(v) = 4π·(m/2πkT)^(3/2) · v² · e^(−mv²/2kT)
— where m is molecular mass, k is Boltzmann's constant, T is absolute temperature.
Most Probable Speed
—
v_p = √(2kT/m)
Mean Speed
—
⟨v⟩ = √(8kT/πm)
RMS Speed
—
v_rms = √(3kT/m)
Mean Kinetic Energy
—
⟨E_k⟩ = (3/2)kT
Peak Distribution Value
—
Escape Velocity Fraction
—
Above 11.2 km/s
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Constants
const k_B = 1.380649e-23; // Boltzmann constant (J/K)
const u = 1.66053906660e-27; // Atomic mass unit (kg)
const c = 299792458; // Speed of light (m/s)
// State
let state = {
temperature: 300,
molecularMass: 32, // O2
showVp: true,
showVavg: true,
showVrms: true,
showEnergy: false,
comparisonCount: 1,
useLogScale: false,
showParticleSim: false,
showHistogram: false,
showGrid: true,
particleSpeedMult: 1
};
let particles = [];
let speedHistogram = new Map();
// Gas presets
const gasPresets = {
'2': { name: 'H₂', mass: 2 },
'4': { name: 'He', mass: 4 },
'20': { name: 'Ne', mass: 20 },
'28': { name: 'N₂', mass: 28 },
'32': { name: 'O₂', mass: 32 },
'44': { name: 'CO₂', mass: 44 },
'131': { name: 'Xe', mass: 131 }
};
// Calculate characteristic speeds
function calculateSpeeds(T, mass) {
const m = mass * u;
const vp = Math.sqrt(2 * k_B * T / m);
const vavg = Math.sqrt(8 * k_B * T / (Math.PI * m));
const vrms = Math.sqrt(3 * k_B * T / m);
return { vp, vavg, vrms };
}
// Maxwell-Boltzmann distribution f(v)
function maxwellBoltzmann(v, T, mass) {
const m = mass * u;
const coeff = 4 * Math.PI * Math.pow(m / (2 * Math.PI * k_B * T), 1.5);
return coeff * v * v * Math.exp(-m * v * v / (2 * k_B * T));
}
// Maxwell-Boltzmann energy distribution f(E)
function energyDistribution(E, T) {
const coeff = 2 * Math.PI * Math.pow(1 / (Math.PI * k_B * T), 1.5);
return coeff * Math.sqrt(E) * Math.exp(-E / (k_B * T));
}
// Get legend data
function getLegendData() {
const colors = [
'#001a4d', '#0033cc', '#0066ff', '#0099ff',
'#00ccff', '#00ffcc', '#00ff99', '#00ff66',
'#66ff00', '#99ff00', '#ccff00', '#ffff00',
'#ffcc00', '#ff9900', '#ff6600', '#ff3300'
];
const temps = [];
const baseTemp = state.temperature;
for (let i = 0; i < state.comparisonCount; i++) {
if (state.useLogScale) {
const ratio = Math.pow(10, (Math.log10(10000) - Math.log10(50)) * (i / (state.comparisonCount - 1 || 1)));
temps.push(Math.round(50 * ratio));
} else {
const ratio = i / (state.comparisonCount - 1 || 1);
temps.push(Math.round(50 + (10000 - 50) * ratio));
}
}
const html = temps.map((t, i) => `
<div class="legend-item">
<div class="legend-color" style="background: ${colors[i % colors.length]};"></div>
<span>${t} K</span>
</div>
`).join('');
document.getElementById('legend').innerHTML = html;
}
// Draw main distribution canvas
function drawMainCanvas() {
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const width = canvas.parentElement.clientWidth - 40;
const height = 400;
canvas.width = width;
canvas.height = height;
const margin = { top: 40, right: 40, bottom: 60, left: 80 };
const plotWidth = width - margin.left - margin.right;
const plotHeight = height - margin.top - margin.bottom;
// Find max velocity for plotting
let maxV = 0;
let maxF = 0;
const temps = [];
const baseTemp = state.temperature;
for (let i = 0; i < state.comparisonCount; i++) {
const ratio = i / (state.comparisonCount - 1 || 1);
temps.push(50 + (10000 - 50) * ratio);
}
temps.forEach(T => {
const speeds = calculateSpeeds(T, state.molecularMass);
maxV = Math.max(maxV, speeds.vrms * 2.5);
for (let v = 0; v <= speeds.vrms * 2.5; v += speeds.vrms / 100) {
const f = state.showEnergy ?
energyDistribution(0.5 * state.molecularMass * u * v * v, T) :
maxwellBoltzmann(v, T, state.molecularMass);
maxF = Math.max(maxF, f);
}
});
// Background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, width, height);
// Grid
if (state.showGrid) {
ctx.strokeStyle = '#1e1e1e';
ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
const x = margin.left + (plotWidth * i / 10);
const y = margin.top + (plotHeight * i / 10);
ctx.beginPath();
ctx.moveTo(x, margin.top);
ctx.lineTo(x, margin.top + plotHeight);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + plotWidth, y);
ctx.stroke();
}
}
// Plot distribution curves
const colors = [
'#001a4d', '#0033cc', '#0066ff', '#0099ff',
'#00ccff', '#00ffcc', '#00ff99', '#00ff66',
'#66ff00', '#99ff00', '#ccff00', '#ffff00',
'#ffcc00', '#ff9900', '#ff6600', '#ff3300'
];
temps.forEach((T, idx) => {
ctx.strokeStyle = colors[idx % colors.length];
ctx.lineWidth = 2.5;
ctx.beginPath();
let first = true;
const step = maxV / 200;
for (let v = 0; v <= maxV; v += step) {
const f = state.showEnergy ?
energyDistribution(0.5 * state.molecularMass * u * v * v, T) :
maxwellBoltzmann(v, T, state.molecularMass);
const x = margin.left + (v / maxV) * plotWidth;
const y = margin.top + plotHeight - (f / maxF) * plotHeight;
if (first) {
ctx.moveTo(x, y);
first = false;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
});
// Draw characteristic speeds for primary temperature
const speeds = calculateSpeeds(state.temperature, state.molecularMass);
const speedsToShow = [
{ v: speeds.vp, label: 'v_p', color: '#f5c518', show: state.showVp },
{ v: speeds.vavg, label: '⟨v⟩', color: '#00c896', show: state.showVavg },
{ v: speeds.vrms, label: 'v_rms', color: '#ff2200', show: state.showVrms }
];
speedsToShow.forEach(({ v, label, color, show }) => {
if (show && v <= maxV) {
const x = margin.left + (v / maxV) * plotWidth;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(x, margin.top);
ctx.lineTo(x, margin.top + plotHeight);
ctx.stroke();
ctx.setLineDash([]);
// Label
ctx.fillStyle = color;
ctx.font = 'bold 12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText(label, x, margin.top - 15);
}
});
// Axes
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + plotHeight);
ctx.lineTo(margin.left + plotWidth, margin.top + plotHeight);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText('Velocity (m/s)', margin.left + plotWidth / 2, height - 15);
ctx.save();
ctx.translate(15, margin.top + plotHeight / 2);
ctx.rotate(-Math.PI / 2);
const yLabel = state.showEnergy ? 'Probability (1/m)' : 'Probability (s/m)';
ctx.fillText(yLabel, 0, 0);
ctx.restore();
// Axis values
ctx.textAlign = 'right';
for (let i = 0; i <= 10; i++) {
const v = (maxV * i / 10).toFixed(0);
const x = margin.left + (plotWidth * i / 10);
ctx.fillText(v, x, margin.top + plotHeight + 20);
}
ctx.textAlign = 'left';
for (let i = 0; i <= 5; i++) {
const f = (maxF * i / 5).toExponential(1);
const y = margin.top + plotHeight - (plotHeight * i / 5);
ctx.fillText(f, 5, y + 4);
}
}
// Initialize particles
function initializeParticles() {
particles = [];
speedHistogram.clear();
const boxSize = 200; // pixels
const particleCount = 100;
const speeds = calculateSpeeds(state.temperature, state.molecularMass);
for (let i = 0; i < particleCount; i++) {
// Sample from Maxwell-Boltzmann distribution
let v;
do {
v = Math.random() * speeds.vrms * 3;
} while (Math.random() > maxwellBoltzmann(v, state.temperature, state.molecularMass) /
maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass));
const angle = Math.random() * 2 * Math.PI;
particles.push({
x: Math.random() * boxSize,
y: Math.random() * boxSize,
vx: Math.cos(angle) * v * state.particleSpeedMult / 100,
vy: Math.sin(angle) * v * state.particleSpeedMult / 100,
speed: v
});
}
}
// Update particle positions
function updateParticles() {
const boxSize = 200;
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
// Boundary collisions
if (p.x < 0 || p.x > boxSize) p.vx *= -1;
if (p.y < 0 || p.y > boxSize) p.vy *= -1;
p.x = Math.max(0, Math.min(boxSize, p.x));
p.y = Math.max(0, Math.min(boxSize, p.y));
});
// Update histogram
speedHistogram.clear();
const speeds = calculateSpeeds(state.temperature, state.molecularMass);
const binSize = speeds.vrms / 10;
particles.forEach(p => {
const bin = Math.floor(p.speed / binSize);
speedHistogram.set(bin, (speedHistogram.get(bin) || 0) + 1);
});
}
// Draw particle simulation
function drawParticleCanvas() {
const canvas = document.getElementById('particle-canvas');
const ctx = canvas.getContext('2d');
const width = 300;
const height = 300;
canvas.width = width;
canvas.height = height;
// Background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, width, height);
// Border
ctx.strokeStyle = '#1e1e1e';
ctx.lineWidth = 2;
ctx.strokeRect(0, 0, width, height);
// Draw particles
const scale = width / 200;
particles.forEach(p => {
ctx.fillStyle = '#ff2200';
ctx.beginPath();
ctx.arc(p.x * scale, p.y * scale, 2, 0, 2 * Math.PI);
ctx.fill();
});
// Update stats
const avgSpeed = particles.reduce((sum, p) => sum + p.speed, 0) / particles.length;
document.getElementById('particle-count').textContent = particles.length;
document.getElementById('particle-avg-speed').textContent = avgSpeed.toFixed(0);
}
// Draw histogram
function drawHistogramCanvas() {
const canvas = document.getElementById('histogram-canvas');
const ctx = canvas.getContext('2d');
const width = canvas.parentElement.clientWidth - 40;
const height = 300;
canvas.width = width;
canvas.height = height;
const margin = { top: 40, right: 40, bottom: 60, left: 80 };
const plotWidth = width - margin.left - margin.right;
const plotHeight = height - margin.top - margin.bottom;
// Background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, width, height);
const speeds = calculateSpeeds(state.temperature, state.molecularMass);
const maxV = speeds.vrms * 2.5;
const binSize = maxV / 20;
// Find max bin count
let maxCount = Math.max(...speedHistogram.values());
maxCount = Math.max(maxCount, particles.length / 20);
// Draw histogram bars
ctx.fillStyle = '#ff220055';
for (let i = 0; i < 20; i++) {
const count = speedHistogram.get(i) || 0;
const barHeight = (count / maxCount) * plotHeight;
const x = margin.left + (i * plotWidth / 20);
const y = margin.top + plotHeight - barHeight;
const barWidth = plotWidth / 20 - 2;
ctx.fillRect(x, y, barWidth, barHeight);
}
// Draw theoretical curve
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 2;
ctx.beginPath();
let first = true;
for (let v = 0; v <= maxV; v += maxV / 100) {
const f = maxwellBoltzmann(v, state.temperature, state.molecularMass);
const normalized = (f / maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass)) * maxCount;
const x = margin.left + (v / maxV) * plotWidth;
const y = margin.top + plotHeight - (normalized / maxCount) * plotHeight;
if (first) {
ctx.moveTo(x, y);
first = false;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
// Axes
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + plotHeight);
ctx.lineTo(margin.left + plotWidth, margin.top + plotHeight);
ctx.stroke();
// Labels
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText('Velocity (m/s)', margin.left + plotWidth / 2, height - 15);
}
// Update statistics
function updateStats() {
const speeds = calculateSpeeds(state.temperature, state.molecularMass);
const m = state.molecularMass * u;
const meanKE = 1.5 * k_B * state.temperature;
// Escape velocity is 11.2 km/s
const escapeV = 11200;
const escapeTemp = 2 * m * escapeV * escapeV / (3 * k_B);
const escapeFraction = 1 - 0.5 * (1 + Math.erf((Math.sqrt(3 * state.temperature / escapeTemp))));
document.getElementById('stat-vp').textContent = speeds.vp.toFixed(0) + ' m/s';
document.getElementById('stat-vavg').textContent = speeds.vavg.toFixed(0) + ' m/s';
document.getElementById('stat-vrms').textContent = speeds.vrms.toFixed(0) + ' m/s';
document.getElementById('stat-ke').textContent = (meanKE * 1e22).toFixed(2) + '×10⁻²² J';
const peak = maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass);
document.getElementById('stat-peak').textContent = peak.toExponential(2);
document.getElementById('stat-escape').textContent = (escapeFraction * 100).toFixed(2) + '%';
}
// Render loop
function render() {
getLegendData();
drawMainCanvas();
updateStats();
if (state.showParticleSim) {
updateParticles();
drawParticleCanvas();
}
if (state.showHistogram && state.showParticleSim) {
drawHistogramCanvas();
}
}
// Event listeners
document.getElementById('temp-slider').addEventListener('input', (e) => {
if (state.useLogScale) {
state.temperature = Math.round(50 * Math.pow(200, e.target.value / 10000));
} else {
state.temperature = parseInt(e.target.value);
}
document.getElementById('temp-value').textContent = state.temperature;
if (state.showParticleSim) initializeParticles();
render();
});
document.getElementById('log-scale').addEventListener('change', (e) => {
state.useLogScale = e.target.checked;
});
document.getElementById('gas-select').addEventListener('change', (e) => {
if (e.target.value === 'custom') {
document.getElementById('custom-mass-field').style.display = 'flex';
} else {
document.getElementById('custom-mass-field').style.display = 'none';
state.molecularMass = parseInt(e.target.value);
if (state.showParticleSim) initializeParticles();
render();
}
});
document.getElementById('custom-mass').addEventListener('change', (e) => {
state.molecularMass = parseInt(e.target.value) || 32;
if (state.showParticleSim) initializeParticles();
render();
});
document.getElementById('comparison-count').addEventListener('input', (e) => {
state.comparisonCount = parseInt(e.target.value);
document.getElementById('comparison-value').textContent = state.comparisonCount;
render();
});
document.getElementById('particle-speed-mult').addEventListener('input', (e) => {
state.particleSpeedMult = parseFloat(e.target.value);
document.getElementById('particle-mult-value').textContent = state.particleSpeedMult.toFixed(1);
if (state.showParticleSim) initializeParticles();
});
// Toggle buttons
document.querySelectorAll('[data-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
const toggle = btn.dataset.toggle;
if (toggle === 'vp') {
state.showVp = !state.showVp;
} else if (toggle === 'vavg') {
state.showVavg = !state.showVavg;
} else if (toggle === 'vrms') {
state.showVrms = !state.showVrms;
} else if (toggle === 'energy') {
state.showEnergy = !state.showEnergy;
} else if (toggle === 'particle-sim') {
state.showParticleSim = !state.showParticleSim;
document.getElementById('particle-sim-container').style.display =
state.showParticleSim ? 'grid' : 'none';
if (state.showParticleSim) initializeParticles();
} else if (toggle === 'histogram') {
state.showHistogram = !state.showHistogram;
document.getElementById('histogram-container').style.display =
state.showHistogram ? 'block' : 'none';
} else if (toggle === 'grid') {
state.showGrid = !state.showGrid;
}
btn.classList.toggle('active');
render();
});
});
// Initialize
render();
// Animation loop for particles
let lastUpdate = Date.now();
function animationLoop() {
const now = Date.now();
if (now - lastUpdate > 50) { // Update every 50ms
render();
lastUpdate = now;
}
requestAnimationFrame(animationLoop);
}
animationLoop();
// Responsive resize
window.addEventListener('resize', render);