Physics Lab
EM WAVE
Interactive electromagnetic wave propagation visualization. Explore electric and magnetic fields, polarization modes, and the Poynting vector.
470 nm
50 V/m
1.0x
20°
Wavelength (λ)
470
nm
Frequency (f)
6.38
×10¹⁴ Hz
Period (T)
1.57
×10⁻¹⁵ s
E₀
50
V/m
B₀ (E₀/c)
1.67
×10⁻⁷ T
Photon Energy
2.64
eV
Physics Fundamentals
E(x,t) = E₀ · sin(kx - ωt)
B(x,t) = B₀ · sin(kx - ωt) = (E₀/c) · sin(kx - ωt)
Poynting Vector: S = E × B / μ₀
Photon Energy: E = h·f = ℏ·ω
The electric field (red) and magnetic field (blue) oscillate perpendicular to each other and to the direction of propagation (x-axis). The wave travels at the speed of light c = 1/√(μ₀ε₀) ≈ 3×10⁸ m/s. The Poynting vector (yellow) shows the direction and intensity of energy flow.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Constants
const c = 3e8; // speed of light m/s
const h = 6.626e-34; // Planck constant
const mu0 = 4 * Math.PI * 1e-7; // permeability of free space
const eV = 1.602e-19; // electron volt
// Canvas setup
const canvas = document.getElementById('waveCanvas');
const ctx = canvas.getContext('2d');
// Responsive canvas
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width - 40;
canvas.height = 400;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// State
let state = {
wavelength: 470e-9, // m
amplitude: 50, // V/m
animationSpeed: 1,
viewRotation: 20, // degrees
polarization: 'linear',
showE: true,
showB: true,
showS: false,
time: 0
};
// Update wavelength from spectrum position
function updateFromWavelength(wl) {
state.wavelength = wl * 1e-9;
updateStats();
updateSpectrumMarker();
}
// Calculate derived values
function getWaveParams() {
const wavelength = state.wavelength;
const frequency = c / wavelength;
const omega = 2 * Math.PI * frequency;
const k = 2 * Math.PI / wavelength;
const period = 1 / frequency;
const B0 = state.amplitude / c;
const photonEnergy = h * frequency / eV;
return { frequency, omega, k, period, B0, photonEnergy, wavelength };
}
function updateStats() {
const p = getWaveParams();
const wlNm = p.wavelength * 1e9;
document.getElementById('statWavelength').textContent = wlNm.toFixed(0);
document.getElementById('statFrequency').textContent = (p.frequency / 1e14).toFixed(2);
document.getElementById('statPeriod').textContent = (p.period / 1e-15).toFixed(2);
document.getElementById('statE0').textContent = state.amplitude.toFixed(0);
document.getElementById('statB0').textContent = (p.B0 * 1e7).toFixed(2);
document.getElementById('statPhoton').textContent = p.photonEnergy.toFixed(2);
// Frequency display
const wl = state.wavelength * 1e9;
let region = 'Radio';
if (wl < 1e-12) region = 'Gamma';
else if (wl < 10e-9) region = 'X-ray';
else if (wl < 400) region = 'UV';
else if (wl < 700) region = 'Visible';
else if (wl < 1e6) region = 'IR';
else if (wl < 1e9) region = 'Micro';
const color = wl < 380 ? 'Violet' : wl < 450 ? 'Blue' : wl < 495 ? 'Cyan' :
wl < 570 ? 'Green' : wl < 590 ? 'Yellow' : wl < 620 ? 'Orange' : 'Red';
if (wl >= 380 && wl <= 700) {
document.getElementById('freqDisplay').textContent = `${wl.toFixed(0)} nm (${color})`;
} else {
document.getElementById('freqDisplay').textContent = `${wl.toFixed(1)} nm (${region})`;
}
}
function updateSpectrumMarker() {
const wl = state.wavelength * 1e9;
// Map wavelength to position (10 nm to 1000 nm)
const position = Math.max(0, Math.min(100, (Math.log10(wl) - 1) / 2 * 100));
document.getElementById('spectrumMarker').style.left = position + '%';
}
// Draw 3D wave
function drawWave() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const p = getWaveParams();
const centerY = canvas.height / 2;
const centerX = 50;
const amplitude = state.amplitude / 50; // Scale for display
const displayWavelength = canvas.width / 5; // Show ~3-4 wavelengths
// View angle in radians
const viewAngle = state.viewRotation * Math.PI / 180;
const scale = Math.cos(viewAngle) * 0.7 + 0.5;
state.time += state.animationSpeed * 0.05;
const phase = state.time;
// Draw propagation axis
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(20, centerY);
ctx.lineTo(canvas.width - 20, centerY);
ctx.stroke();
// Arrow at end
const arrowX = canvas.width - 20;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.moveTo(arrowX, centerY);
ctx.lineTo(arrowX - 10, centerY - 6);
ctx.lineTo(arrowX - 10, centerY + 6);
ctx.closePath();
ctx.fill();
// Label 'c'
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px DM Mono';
ctx.fillText('c', arrowX - 20, centerY - 15);
// Draw E field (red sine wave)
if (state.showE) {
ctx.strokeStyle = '#ff2200';
ctx.lineWidth = 3;
ctx.beginPath();
let first = true;
for (let x = 20; x < canvas.width - 20; x += 2) {
const normalizedX = (x - 20) / displayWavelength;
const y = Math.sin(normalizedX * 2 * Math.PI - phase) * amplitude * 40 * scale;
const screenY = centerY - y;
if (first) {
ctx.moveTo(x, screenY);
first = false;
} else {
ctx.lineTo(x, screenY);
}
}
ctx.stroke();
// E field label and arrows
ctx.fillStyle = '#ff2200';
ctx.font = '12px DM Mono';
ctx.fillText('E', 10, centerY - 50);
}
// Draw B field (blue sine wave, offset for isometric view)
if (state.showB) {
ctx.strokeStyle = '#4488ff';
ctx.lineWidth = 3;
ctx.beginPath();
let first = true;
const zOffset = 30 * Math.sin(viewAngle);
for (let x = 20; x < canvas.width - 20; x += 2) {
const normalizedX = (x - 20) / displayWavelength;
const y = Math.sin(normalizedX * 2 * Math.PI - phase) * amplitude * 40 * scale;
const screenY = centerY + zOffset - y * Math.sin(viewAngle * 0.5);
if (first) {
ctx.moveTo(x, screenY);
first = false;
} else {
ctx.lineTo(x, screenY);
}
}
ctx.stroke();
// B field label
ctx.fillStyle = '#4488ff';
ctx.font = '12px DM Mono';
ctx.fillText('B', 10, centerY + 50 + zOffset);
}
// Draw Poynting vector
if (state.showS) {
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 2;
const sx = canvas.width * 0.15;
const sy = 80;
ctx.beginPath();
ctx.moveTo(canvas.width * 0.05, sy);
ctx.lineTo(canvas.width * 0.05 + sx, sy);
ctx.stroke();
// Arrow
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.moveTo(canvas.width * 0.05 + sx, sy);
ctx.lineTo(canvas.width * 0.05 + sx - 8, sy - 5);
ctx.lineTo(canvas.width * 0.05 + sx - 8, sy + 5);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffff00';
ctx.font = '12px DM Mono';
ctx.fillText('S = E × B / μ₀', canvas.width * 0.05, sy - 10);
}
// Apply polarization effects
if (state.polarization === 'circular') {
ctx.strokeStyle = 'rgba(200, 100, 255, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
const radius = amplitude * 30;
ctx.arc(100, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
} else if (state.polarization === 'elliptical') {
ctx.strokeStyle = 'rgba(200, 100, 255, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.ellipse(100, centerY, amplitude * 35, amplitude * 20, 0, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
}
}
// Control listeners
document.getElementById('wavelengthSlider').addEventListener('input', (e) => {
const wl = parseFloat(e.target.value);
updateFromWavelength(wl);
document.getElementById('wavelengthValue').textContent = wl + ' nm';
});
document.getElementById('amplitudeSlider').addEventListener('input', (e) => {
state.amplitude = parseFloat(e.target.value);
document.getElementById('amplitudeValue').textContent = e.target.value + ' V/m';
updateStats();
});
document.getElementById('speedSlider').addEventListener('input', (e) => {
state.animationSpeed = parseFloat(e.target.value);
document.getElementById('speedValue').textContent = e.target.value + 'x';
});
document.getElementById('rotationSlider').addEventListener('input', (e) => {
state.viewRotation = parseFloat(e.target.value);
document.getElementById('rotationValue').textContent = e.target.value + '°';
});
// Polarization buttons
document.querySelectorAll('[data-polarization]').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('[data-polarization]').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
state.polarization = e.target.dataset.polarization;
});
});
// Field toggles
document.querySelectorAll('[data-field]').forEach(btn => {
btn.addEventListener('click', (e) => {
const field = e.target.dataset.field;
e.target.classList.toggle('active');
state['show' + field] = e.target.classList.contains('active');
});
});
// Animation loop
function animate() {
drawWave();
requestAnimationFrame(animate);
}
// Initialize
updateStats();
updateSpectrumMarker();
animate();