Quantum Mechanics
Particle in a BOX
Interactive visualization of a quantum particle confined in an infinite square well. Explore wave functions, energy levels, superposition, and time evolution.
Wave Function Visualization
Eₙ / E₁
1.00
Nodes
0
λₙ / L
2.00
⟨x⟩ / L
0.50
Superposition Mode
Normalization: Σ|cₙ|² = 1.00
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Physical constants (in atomic units)
const PI = Math.PI;
const HBAR = 1; // ℏ = 1 in atomic units
// State
let state = {
n: 1,
L: 1,
m: 1,
mode: '1d',
displayModes: { psi: true, psi2: true, re: false, im: false },
nx: 1,
ny: 1,
c: [1, 0, 0, 0],
speed: 1,
time: 0,
isSuperposition: false,
animationId: null
};
// DOM elements
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const nSlider = document.getElementById('nSlider');
const lSlider = document.getElementById('lSlider');
const mSlider = document.getElementById('mSlider');
const speedSlider = document.getElementById('speedSlider');
const nxSlider = document.getElementById('nxSlider');
const nySlider = document.getElementById('nySlider');
const nValue = document.getElementById('nValue');
const lValue = document.getElementById('lValue');
const mValue = document.getElementById('mValue');
const speedValue = document.getElementById('speedValue');
const nxValue = document.getElementById('nxValue');
const nyValue = document.getElementById('nyValue');
const energyRatio = document.getElementById('energyRatio');
const nodesDisplay = document.getElementById('nodes');
const wavelengthDisplay = document.getElementById('wavelength');
// Quantum functions
function psi_n(x, n, L) {
// ψₙ(x) = √(2/L) * sin(nπx/L)
return Math.sqrt(2 / L) * Math.sin(n * PI * x / L);
}
function psi_n_2d(x, y, nx, ny, L) {
// ψ(x,y) = (2/L) * sin(nₓπx/L) * sin(nᵧπy/L)
return (2 / L) * Math.sin(nx * PI * x / L) * Math.sin(ny * PI * y / L);
}
function energy_n(n, L, m) {
// Eₙ = n²π²ℏ²/(2mL²)
// In atomic units with ℏ=1: Eₙ = n²π²/(2mL²)
return (n * n * PI * PI) / (2 * m * L * L);
}
function wavefunction_time(x, n, t, L, m) {
// Ψₙ(x,t) = ψₙ(x) * e^(-iEₙt/ℏ)
const psi = psi_n(x, n, L);
const E = energy_n(n, L, m);
const phase = E * t;
return {
re: psi * Math.cos(phase),
im: psi * Math.sin(phase)
};
}
function wavefunction_superposition(x, t, L, m, c, maxN = 4) {
// Ψ(x,t) = Σ cₙ ψₙ(x) e^(-iEₙt/ℏ)
let re = 0, im = 0;
for (let n = 1; n <= maxN; n++) {
if (c[n - 1] > 0.001) {
const psi = psi_n(x, n, L);
const E = energy_n(n, L, m);
const phase = E * t;
const cos_p = Math.cos(phase);
const sin_p = Math.sin(phase);
re += c[n - 1] * psi * cos_p;
im += c[n - 1] * psi * sin_p;
}
}
return { re, im };
}
// Update stats
function updateStats() {
const E = energy_n(state.n, state.L, state.m);
const E1 = energy_n(1, state.L, state.m);
energyRatio.textContent = (E / E1).toFixed(2);
nodesDisplay.textContent = state.n - 1;
const lambda = (2 * state.L) / state.n;
wavelengthDisplay.textContent = (lambda / state.L).toFixed(2);
// Normalization check
let norm = 0;
for (let i = 0; i < 4; i++) {
norm += state.c[i] * state.c[i];
}
document.getElementById('normValue').textContent = norm.toFixed(2);
}
// Draw the visualization
function draw() {
const w = canvas.width;
const h = canvas.height;
// Clear canvas
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, w, h);
if (state.mode === '1d') {
draw1D();
} else {
draw2D();
}
}
function draw1D() {
const w = canvas.width;
const h = canvas.height;
const margin = 80;
const plotWidth = w - 2 * margin;
const plotHeight = h - 2 * margin;
const x0 = margin;
const y0 = margin + plotHeight / 2;
// Draw box walls (infinite potential)
ctx.strokeStyle = '#555555';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x0, y0 - plotHeight / 3);
ctx.lineTo(x0, y0 + plotHeight / 3);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x0 + plotWidth, y0 - plotHeight / 3);
ctx.lineTo(x0 + plotWidth, y0 + plotHeight / 3);
ctx.stroke();
// Draw axis line
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x0 + plotWidth, y0);
ctx.stroke();
// Draw energy level diagram on left
drawEnergyLevels(x0 - 50, y0, 30);
// Sample the wave function
const samples = 400;
const maxVal = Math.sqrt(2 / state.L) * 1.5;
const isSuperposition = state.c[1] > 0.001 || state.c[2] > 0.001 || state.c[3] > 0.001;
// Draw probability density |ψ|²
if (state.displayModes.psi2) {
ctx.fillStyle = 'rgba(255, 34, 0, 0.2)';
ctx.beginPath();
ctx.moveTo(x0, y0);
for (let i = 0; i <= samples; i++) {
const xNorm = i / samples;
const x = xNorm * state.L;
let psi2;
if (isSuperposition) {
const wf = wavefunction_superposition(x, state.time, state.L, state.m, state.c);
psi2 = wf.re * wf.re + wf.im * wf.im;
} else {
const psi = psi_n(x, state.n, state.L);
psi2 = psi * psi;
}
const canvasX = x0 + xNorm * plotWidth;
const canvasY = y0 - (psi2 / (2 / state.L)) * plotHeight / 3;
ctx.lineTo(canvasX, canvasY);
}
ctx.lineTo(x0 + plotWidth, y0);
ctx.closePath();
ctx.fill();
}
// Draw wave function ψ(x) - above zero
if (state.displayModes.psi && !isSuperposition) {
ctx.strokeStyle = '#00c896';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i <= samples; i++) {
const xNorm = i / samples;
const x = xNorm * state.L;
const psi = psi_n(x, state.n, state.L);
const canvasX = x0 + xNorm * plotWidth;
const canvasY = y0 - (psi / maxVal) * plotHeight / 3;
if (i === 0) ctx.moveTo(canvasX, canvasY);
else ctx.lineTo(canvasX, canvasY);
}
ctx.stroke();
}
// Draw Re(Ψ) and Im(Ψ) for superposition
if (state.displayModes.re || state.displayModes.im || isSuperposition) {
if (state.displayModes.re) {
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i <= samples; i++) {
const xNorm = i / samples;
const x = xNorm * state.L;
const wf = isSuperposition ? wavefunction_superposition(x, state.time, state.L, state.m, state.c) : wavefunction_time(x, state.n, state.time, state.L, state.m);
const canvasX = x0 + xNorm * plotWidth;
const canvasY = y0 - (wf.re / maxVal) * plotHeight / 3;
if (i === 0) ctx.moveTo(canvasX, canvasY);
else ctx.lineTo(canvasX, canvasY);
}
ctx.stroke();
}
if (state.displayModes.im) {
ctx.strokeStyle = '#4488ff';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i <= samples; i++) {
const xNorm = i / samples;
const x = xNorm * state.L;
const wf = wavefunction_superposition(x, state.time, state.L, state.m, state.c);
const canvasX = x0 + xNorm * plotWidth;
const canvasY = y0 - (wf.im / maxVal) * plotHeight / 3;
if (i === 0) ctx.moveTo(canvasX, canvasY);
else ctx.lineTo(canvasX, canvasY);
}
ctx.stroke();
}
}
// Draw axes labels
ctx.fillStyle = '#555555';
ctx.font = '12px "DM Mono"';
ctx.textAlign = 'center';
ctx.fillText('x', x0 + plotWidth / 2, h - 30);
ctx.textAlign = 'right';
ctx.fillText('ψ(x)', 40, y0 - 20);
// Draw x-axis ticks and labels
for (let i = 0; i <= 4; i++) {
const xNorm = i / 4;
const canvasX = x0 + xNorm * plotWidth;
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(canvasX, y0 - 5);
ctx.lineTo(canvasX, y0 + 5);
ctx.stroke();
ctx.fillStyle = '#555555';
ctx.font = '11px "DM Mono"';
ctx.textAlign = 'center';
const xLabel = (xNorm * state.L).toFixed(2);
ctx.fillText(xLabel, canvasX, y0 + 20);
}
}
function drawEnergyLevels(x, yCenter, width) {
// Draw energy level diagram
const levelHeight = 40;
const maxLevel = 8;
ctx.strokeStyle = '#1e1e1e';
ctx.lineWidth = 1;
for (let i = 1; i <= maxLevel; i++) {
const y = yCenter - (maxLevel / 2 - i) * levelHeight;
ctx.strokeStyle = i === state.n ? '#ff2200' : '#1e1e1e';
ctx.lineWidth = i === state.n ? 2 : 1;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.stroke();
ctx.fillStyle = i === state.n ? '#ff2200' : '#555555';
ctx.font = i === state.n ? 'bold 11px "DM Mono"' : '11px "DM Mono"';
ctx.textAlign = 'right';
const E = energy_n(i, state.L, state.m);
const E1 = energy_n(1, state.L, state.m);
const label = 'E' + i + '=' + (i * i).toFixed(0) + 'E₁';
ctx.fillText(label, x - 5, y + 4);
}
}
function draw2D() {
const w = canvas.width;
const h = canvas.height;
const margin = 60;
const size = Math.min(w, h) - 2 * margin;
const x0 = (w - size) / 2;
const y0 = (h - size) / 2;
// Draw 2D heatmap
const resolution = 100;
const pixelSize = size / resolution;
const imageData = ctx.createImageData(resolution, resolution);
const data = imageData.data;
let maxPsi2 = 0;
const psi2Data = [];
for (let py = 0; py < resolution; py++) {
for (let px = 0; px < resolution; px++) {
const x = (px / resolution) * state.L;
const y = (py / resolution) * state.L;
const psi = psi_n_2d(x, y, state.nx, state.ny, state.L);
const psi2 = psi * psi;
psi2Data.push(psi2);
maxPsi2 = Math.max(maxPsi2, psi2);
}
}
for (let i = 0; i < psi2Data.length; i++) {
const val = psi2Data[i] / maxPsi2;
const hue = (1 - val) * 240; // Blue for low, red for high
const rgb = hslToRgb(hue / 360, 0.7, 0.5);
data[i * 4] = rgb[0];
data[i * 4 + 1] = rgb[1];
data[i * 4 + 2] = rgb[2];
data[i * 4 + 3] = 255;
}
ctx.putImageData(imageData, x0, y0);
// Draw border
ctx.strokeStyle = '#555555';
ctx.lineWidth = 2;
ctx.strokeRect(x0, y0, size, size);
// Labels
ctx.fillStyle = '#555555';
ctx.font = '12px "DM Mono"';
ctx.textAlign = 'center';
ctx.fillText('x', x0 + size / 2, h - 20);
ctx.save();
ctx.translate(20, y0 + size / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('y', 0, 0);
ctx.restore();
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// Animation loop
function animate() {
state.time += 0.01 * state.speed;
draw();
state.animationId = requestAnimationFrame(animate);
}
// Event listeners
nSlider.addEventListener('input', (e) => {
state.n = parseInt(e.target.value);
nValue.textContent = state.n;
updateStats();
});
lSlider.addEventListener('input', (e) => {
state.L = parseFloat(e.target.value);
lValue.textContent = state.L.toFixed(1);
updateStats();
});
mSlider.addEventListener('input', (e) => {
state.m = parseFloat(e.target.value);
mValue.textContent = state.m.toFixed(1);
updateStats();
});
speedSlider.addEventListener('input', (e) => {
state.speed = parseFloat(e.target.value);
speedValue.textContent = state.speed.toFixed(1) + 'x';
});
nxSlider.addEventListener('input', (e) => {
state.nx = parseInt(e.target.value);
nxValue.textContent = state.nx;
});
nySlider.addEventListener('input', (e) => {
state.ny = parseInt(e.target.value);
nyValue.textContent = state.ny;
});
// Mode toggle
document.querySelectorAll('[data-mode]').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-mode]').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
state.mode = btn.dataset.mode;
document.getElementById('twoDBtnsSection').style.display = state.mode === '2d' ? 'block' : 'none';
});
});
// Display mode toggle
document.querySelectorAll('[data-display]').forEach((btn) => {
btn.addEventListener('click', () => {
btn.classList.toggle('active');
state.displayModes[btn.dataset.display] = btn.classList.contains('active');
});
});
// Superposition sliders
for (let i = 1; i <= 4; i++) {
document.getElementById(`c${i}Slider`).addEventListener('input', (e) => {
state.c[i - 1] = parseFloat(e.target.value);
document.getElementById(`c${i}Value`).textContent = state.c[i - 1].toFixed(2);
state.isSuperposition = state.c.some(c => c > 0.001);
updateStats();
});
}
// Initialize
updateStats();
animate();