Physics Lab
DOUBLE SLIT DIFFRACTION
Interactive simulation of wave interference and diffraction patterns. Explore Huygens principle, constructive/destructive interference, and how light behaves when passing through single or multiple slits.
How it works: Light waves passing through narrow slits diffract and create an interference pattern on a distant screen. The bright and dark bands represent constructive and destructive interference — where waves add up or cancel out.
Wave Visualization
Fringe Spacing (Δy):
—
1st Minimum at θ:
—
Screen Intensity:
—
Controls
Slit Mode
Wave Properties
Animation
Display
Central Peak
100%
Visibility
High
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// State
const state = {
mode: 'double',
wavelength: 550, // nm
separation: 2.0, // μm
slitWidth: 0.5, // μm
distance: 0.5, // m
numSlits: 2,
waveSpeed: 1.0,
showIndividualWaves: true,
showEnvelope: true,
showPathDifference: false,
animationTime: 0
};
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
// DPI scaling for sharp canvas
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const displayWidth = rect.width;
const displayHeight = rect.height;
// Physics calculations
function calculateIntensity(sinTheta, wavelength, separation, slitWidth, numSlits) {
wavelength *= 1e-9; // Convert nm to m
separation *= 1e-6; // Convert μm to m
slitWidth *= 1e-6; // Convert μm to m
if (Math.abs(sinTheta) > 1) return 0;
// Single slit diffraction envelope
const alpha = Math.PI * slitWidth * sinTheta / wavelength;
let singleSlitFactor = 1;
if (Math.abs(alpha) > 1e-6) {
singleSlitFactor = Math.sin(alpha) / alpha;
}
// Multi-slit interference
let intensity = 1;
if (numSlits > 1) {
const beta = Math.PI * separation * sinTheta / wavelength;
const numerator = Math.sin(numSlits * beta);
const denominator = Math.sin(beta);
if (Math.abs(denominator) > 1e-6) {
intensity = (numerator / denominator) ** 2;
} else {
intensity = numSlits * numSlits;
}
}
const totalIntensity = singleSlitFactor * singleSlitFactor * intensity;
return Math.max(0, Math.min(1, totalIntensity));
}
function drawWave(x, y, amplitude, frequency, phase, color, alpha) {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.beginPath();
for (let i = 0; i < 100; i++) {
const px = x + i * 3;
const py = y + amplitude * Math.sin(frequency * i - phase);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.stroke();
ctx.globalAlpha = 1;
}
function drawCircularWave(cx, cy, radius, color, alpha) {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
}
function hueToRgb(hue) {
// Hue: 400nm (violet) to 700nm (red)
let h = (700 - hue) / (700 - 400) * 240; // 240=violet, 0=red
h = Math.max(0, Math.min(240, h));
// Simple HSL to RGB approximation
if (h < 60) return { r: 255, g: h * 4.25, b: 0 };
if (h < 120) return { r: 255 - (h - 60) * 4.25, g: 255, b: 0 };
if (h < 180) return { r: 0, g: 255, b: (h - 120) * 4.25 };
if (h < 240) return { r: 0, g: 255 - (h - 180) * 4.25, b: 255 };
return { r: (h - 240) * 4.25, g: 0, b: 255 };
}
function drawSimulation() {
ctx.clearRect(0, 0, displayWidth, displayHeight);
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, displayWidth, displayHeight);
const centerY = displayHeight / 2;
const margin = 30;
const slitAreaX = displayWidth * 0.3;
const screenX = displayWidth - margin - 20;
// Draw wave source (left side)
const sourceX = margin;
const sourceY = centerY;
ctx.fillStyle = '#ff2200';
ctx.fillRect(sourceX - 5, sourceY - 15, 10, 30);
// Animated ripples from source
const rippleRadius = (state.animationTime * state.waveSpeed) % 60 + 10;
ctx.strokeStyle = 'rgba(255, 34, 0, 0.3)';
ctx.lineWidth = 1;
for (let r = rippleRadius; r > 0; r -= 15) {
ctx.beginPath();
ctx.arc(sourceX, sourceY, r, 0, Math.PI * 2);
ctx.stroke();
}
// Draw barrier and slits
const slitHeight = 80;
const slitSpacing = 50;
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(slitAreaX - 10, 0, 20, centerY - slitHeight / 2 - slitSpacing / 2);
ctx.fillRect(slitAreaX - 10, centerY + slitHeight / 2 + slitSpacing / 2, 20, displayHeight - centerY - slitHeight / 2 - slitSpacing / 2);
// Draw slits
let slits = [];
if (state.mode === 'single') {
slits = [{ y: centerY, width: 30 }];
} else if (state.mode === 'double') {
slits = [
{ y: centerY - slitSpacing / 2, width: 30 },
{ y: centerY + slitSpacing / 2, width: 30 }
];
} else if (state.mode === 'multiple') {
const totalSpacing = (state.numSlits - 1) * slitSpacing;
for (let i = 0; i < state.numSlits; i++) {
slits.push({
y: centerY - totalSpacing / 2 + i * slitSpacing,
width: Math.max(20, 40 - state.numSlits * 5)
});
}
}
// Draw diffracted waves from slits
slits.forEach((slit, idx) => {
const waveX = slitAreaX + 20;
const waveY = slit.y;
const animPhase = (state.animationTime * state.waveSpeed) % 100;
if (state.showIndividualWaves) {
for (let d = 0; d < 150; d += 15) {
const r = d + animPhase * 1.5;
const alpha = Math.max(0, 0.3 - r / 150);
ctx.strokeStyle = `rgba(255, 100, 0, ${alpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(waveX, waveY, r, 0, Math.PI * 2);
ctx.stroke();
}
}
});
// Draw intensity pattern on screen
const patternHeight = displayHeight * 0.8;
const patternTop = (displayHeight - patternHeight) / 2;
const pixelWidth = 2;
// Calculate and draw intensity pattern
for (let py = patternTop; py < patternTop + patternHeight; py += pixelWidth) {
const y = (py - patternTop - patternHeight / 2) / (displayHeight * 0.4); // normalized
const sinTheta = y / Math.sqrt(1 + y * y);
let intensity = 0;
if (state.mode === 'single') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.slitWidth * 10, state.slitWidth, 1);
} else if (state.mode === 'double') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, 2);
} else if (state.mode === 'multiple') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, state.numSlits);
}
const brightness = Math.floor(intensity * 200) + 55;
ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;
ctx.fillRect(screenX - 30, py, 25, pixelWidth);
}
// Draw intensity curve overlay
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 2;
ctx.beginPath();
let firstPoint = true;
for (let py = patternTop; py < patternTop + patternHeight; py += 2) {
const y = (py - patternTop - patternHeight / 2) / (displayHeight * 0.4);
const sinTheta = y / Math.sqrt(1 + y * y);
let intensity = 0;
if (state.mode === 'single') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.slitWidth * 10, state.slitWidth, 1);
} else if (state.mode === 'double') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, 2);
} else if (state.mode === 'multiple') {
intensity = calculateIntensity(sinTheta, state.wavelength, state.separation, state.slitWidth, state.numSlits);
}
const px = screenX - 5 - intensity * 25;
if (firstPoint) {
ctx.moveTo(px, py);
firstPoint = false;
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
// Draw screen label
ctx.fillStyle = '#555555';
ctx.font = '12px DM Mono';
ctx.save();
ctx.translate(screenX + 5, centerY);
ctx.rotate(Math.PI / 2);
ctx.fillText('SCREEN', 0, 0);
ctx.restore();
// Draw path difference lines if enabled
if (state.showPathDifference && state.mode === 'double') {
const slit1Y = centerY - 25;
const slit2Y = centerY + 25;
const observationY = centerY + 50;
const observationX = slitAreaX + 150;
ctx.strokeStyle = 'rgba(255, 200, 0, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(slitAreaX + 10, slit1Y);
ctx.lineTo(observationX, observationY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(slitAreaX + 10, slit2Y);
ctx.lineTo(observationX, observationY);
ctx.stroke();
ctx.setLineDash([]);
}
// Info text
ctx.fillStyle = '#555555';
ctx.font = '11px DM Mono';
ctx.fillText(`λ: ${state.wavelength} nm`, margin, margin + 20);
ctx.fillText(`d: ${state.separation} μm`, margin, margin + 35);
ctx.fillText(`L: ${state.distance} m`, margin, margin + 50);
state.animationTime += state.waveSpeed;
requestAnimationFrame(drawSimulation);
}
// Update calculations and labels
function updateCalculations() {
// Fringe spacing: Δy = λL/d
const fringeSpacing = (state.wavelength * state.distance * 1e-6) / (state.separation * 1e-6);
document.getElementById('fringeSpacing').textContent = fringeSpacing.toFixed(3) + ' mm';
// First minimum: sinθ = λ/a
const sinFirstMin = state.wavelength / (state.slitWidth * 1000);
const firstMinAngle = Math.asin(Math.min(1, sinFirstMin)) * 180 / Math.PI;
document.getElementById('firstMinimum').textContent = firstMinAngle.toFixed(2) + '°';
// Central peak intensity
const centerIntensity = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
document.getElementById('centralPeakIntensity').textContent = Math.round(centerIntensity * 100) + '%';
// Visibility (contrast)
const maxIntensity = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
const minIntensity = calculateIntensity(0.01, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
const visibility = (maxIntensity - minIntensity) / (maxIntensity + minIntensity);
document.getElementById('visibility').textContent = visibility > 0.7 ? 'High' : (visibility > 0.4 ? 'Medium' : 'Low');
// Screen intensity at center
const centerIntensity2 = calculateIntensity(0, state.wavelength, state.separation, state.slitWidth, state.mode === 'single' ? 1 : (state.mode === 'double' ? 2 : state.numSlits));
document.getElementById('screenIntensity').textContent = Math.round(centerIntensity2 * 100) + '%';
}
// Event listeners
document.getElementById('wavelength').addEventListener('input', (e) => {
state.wavelength = parseInt(e.target.value);
document.getElementById('wavelengthLabel').textContent = state.wavelength + ' nm';
updateCalculations();
});
document.getElementById('separation').addEventListener('input', (e) => {
state.separation = parseFloat(e.target.value);
document.getElementById('separationLabel').textContent = state.separation.toFixed(1) + ' μm';
updateCalculations();
});
document.getElementById('slitWidth').addEventListener('input', (e) => {
state.slitWidth = parseFloat(e.target.value);
document.getElementById('slitWidthLabel').textContent = state.slitWidth.toFixed(2) + ' μm';
updateCalculations();
});
document.getElementById('distance').addEventListener('input', (e) => {
state.distance = parseFloat(e.target.value);
document.getElementById('distanceLabel').textContent = state.distance.toFixed(1) + ' m';
updateCalculations();
});
document.getElementById('numSlits').addEventListener('input', (e) => {
state.numSlits = parseInt(e.target.value);
document.getElementById('numSlitsLabel').textContent = state.numSlits;
updateCalculations();
});
document.getElementById('waveSpeed').addEventListener('input', (e) => {
state.waveSpeed = parseFloat(e.target.value);
document.getElementById('speedLabel').textContent = (state.waveSpeed).toFixed(1) + '×';
});
document.getElementById('showIndividualWaves').addEventListener('change', (e) => {
state.showIndividualWaves = e.target.checked;
});
document.getElementById('showEnvelope').addEventListener('change', (e) => {
state.showEnvelope = e.target.checked;
});
document.getElementById('showPathDifference').addEventListener('change', (e) => {
state.showPathDifference = e.target.checked;
});
// Mode toggle
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
state.mode = e.target.dataset.mode;
// Show/hide numSlits control
if (state.mode === 'multiple') {
document.getElementById('numSlitsControl').style.display = 'block';
} else {
document.getElementById('numSlitsControl').style.display = 'none';
}
updateCalculations();
});
});
// Initial setup
updateCalculations();
drawSimulation();