physics
POLARIZATION OF LIGHT
Explore Malus's Law, linear & circular polarization, and the effects of birefringent wave plates.
Final Intensity
100%
θ = 0°
Malus's Law
I = I₀ × cos²(θ)
Current angle θ and intensity ratio shown above.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Colors
const colors = {
bg: '#0a0a0a',
surface: '#111111',
card: '#161616',
border: '#1e1e1e',
text: '#e8e0d5',
muted: '#555555',
accent: '#ff2200',
success: '#00c896',
warning: '#f5c518'
};
// State
let state = {
sourceType: 'unpolarized',
pol1Angle: 0,
pol2Angle: 0,
waveplate: 'none',
wpAngle: 0,
numPolarizers: 0,
intermediatePols: [0, 45, 90]
};
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
e.target.classList.add('active');
document.querySelector(`[data-tab="${e.target.dataset.tab}"]`).style.display = 'block';
if (e.target.dataset.tab === 'graph') {
setTimeout(() => drawMalusGraph(), 0);
} else if (e.target.dataset.tab === 'cascade') {
setTimeout(() => drawCascade(), 0);
}
});
});
// Control listeners
document.getElementById('sourceType').addEventListener('change', (e) => {
state.sourceType = e.target.value;
update();
});
document.getElementById('pol1Angle').addEventListener('input', (e) => {
state.pol1Angle = parseFloat(e.target.value);
document.getElementById('pol1Val').textContent = Math.round(state.pol1Angle) + '°';
update();
});
document.getElementById('pol2Angle').addEventListener('input', (e) => {
state.pol2Angle = parseFloat(e.target.value);
document.getElementById('pol2Val').textContent = Math.round(state.pol2Angle) + '°';
update();
});
document.getElementById('waveplate').addEventListener('change', (e) => {
state.waveplate = e.target.value;
update();
});
document.getElementById('waveplate-angle').addEventListener('input', (e) => {
state.wpAngle = parseFloat(e.target.value);
document.getElementById('wpVal').textContent = Math.round(state.wpAngle) + '°';
update();
});
document.getElementById('numPolarizers').addEventListener('change', (e) => {
state.numPolarizers = parseInt(e.target.value);
updatePolarizersUI();
});
document.addEventListener('change', (e) => {
if (e.target.classList.contains('intermediate-pol')) {
const idx = parseInt(e.target.dataset.index);
state.intermediatePols[idx] = parseFloat(e.target.value);
document.querySelector(`[data-index="${idx}"].polarizer-angle`).textContent = Math.round(e.target.value) + '°';
drawCascade();
}
});
function updatePolarizersUI() {
const stack = document.getElementById('polarizerStack');
stack.innerHTML = `
<div class="polarizer-item">
<div class="polarizer-label">Pol 1:</div>
<div class="polarizer-slider">
<input type="range" class="intermediate-pol" data-index="0" min="0" max="180" value="${state.intermediatePols[0]}" step="1">
</div>
<div class="polarizer-angle" data-index="0">${Math.round(state.intermediatePols[0])}°</div>
</div>
`;
for (let i = 0; i < state.numPolarizers; i++) {
const item = document.createElement('div');
item.className = 'polarizer-item';
item.innerHTML = `
<div class="polarizer-label">Pol ${i + 2}:</div>
<div class="polarizer-slider">
<input type="range" class="intermediate-pol" data-index="${i + 1}" min="0" max="180" value="${state.intermediatePols[i + 1]}" step="1">
</div>
<div class="polarizer-angle" data-index="${i + 1}">${Math.round(state.intermediatePols[i + 1])}°</div>
`;
stack.appendChild(item);
}
const analyzerItem = document.createElement('div');
analyzerItem.className = 'polarizer-item';
analyzerItem.innerHTML = `
<div class="polarizer-label">Analyzer:</div>
<div class="polarizer-slider">
<input type="range" class="intermediate-pol" data-index="${state.numPolarizers + 1}" min="0" max="180" value="${state.intermediatePols[state.numPolarizers + 1]}" step="1">
</div>
<div class="polarizer-angle" data-index="${state.numPolarizers + 1}">${Math.round(state.intermediatePols[state.numPolarizers + 1])}°</div>
`;
stack.appendChild(analyzerItem);
drawCascade();
}
function calculateIntensity(angle1, angle2) {
const diff = Math.abs(angle2 - angle1);
const theta = Math.min(diff, 180 - diff);
return Math.pow(Math.cos(theta * Math.PI / 180), 2);
}
function update() {
drawSideView();
drawFrontView();
updateIntensityDisplay();
}
function updateIntensityDisplay() {
let intensity = 0.5; // Unpolarized through first polarizer
if (state.sourceType === 'linear') intensity = 1;
if (state.sourceType === 'circular') intensity = 0.5;
if (state.sourceType === 'elliptical') intensity = 0.6;
if (state.waveplate !== 'none') {
intensity *= calculateIntensity(state.pol1Angle, state.wpAngle) * 0.5;
intensity *= calculateIntensity(state.wpAngle, state.pol2Angle);
} else {
intensity *= calculateIntensity(state.pol1Angle, state.pol2Angle);
}
const percent = Math.round(intensity * 100);
document.getElementById('intensityValue').textContent = percent + '%';
document.getElementById('intensityBar').style.width = percent + '%';
const angleDiff = Math.abs(state.pol2Angle - state.pol1Angle);
const displayAngle = Math.min(angleDiff, 180 - angleDiff);
document.getElementById('angleInfo').textContent = `θ = ${Math.round(displayAngle)}°`;
}
function drawSideView() {
const canvas = document.getElementById('sideCanvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, w, h);
const stages = [
{ x: w * 0.15, label: 'Source', intensity: 1 },
{ x: w * 0.5, label: 'After Pol 1', intensity: 0.5 },
{ x: w * 0.85, label: 'After Pol 2', intensity: calculateIntensity(state.pol1Angle, state.pol2Angle) * 0.5 }
];
stages.forEach((stage, idx) => {
const x = stage.x;
// Draw label
ctx.fillStyle = colors.muted;
ctx.font = '12px DM Mono';
ctx.textAlign = 'center';
ctx.fillText(stage.label, x, 30);
// Draw beam center line
ctx.strokeStyle = colors.border;
ctx.beginPath();
ctx.moveTo(x - 50, h / 2);
ctx.lineTo(x + 50, h / 2);
ctx.stroke();
// Draw E-field oscillation
const amplitude = 40 * Math.sqrt(stage.intensity);
const color = `rgb(${Math.round(255 * stage.intensity)}, ${Math.round(34 * stage.intensity)}, 0)`;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = -50; i <= 50; i += 2) {
const t = (i + 50) / 100 * Math.PI * 2;
const y = h / 2 + Math.sin(t) * amplitude;
if (i === -50) ctx.moveTo(x + i, y);
else ctx.lineTo(x + i, y);
}
ctx.stroke();
// Draw polarizer (if applicable)
if (idx === 1 || idx === 2) {
const polAngle = idx === 1 ? state.pol1Angle : state.pol2Angle;
drawPolarizer(ctx, x, h / 2, polAngle, 50);
}
});
}
function drawPolarizer(ctx, x, y, angle, size) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle * Math.PI / 180);
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.lineTo(0, size);
ctx.stroke();
// Grid pattern
for (let i = -size; i <= size; i += 8) {
ctx.beginPath();
ctx.moveTo(-4, i);
ctx.lineTo(4, i);
ctx.stroke();
}
ctx.restore();
}
function drawFrontView() {
const canvas = document.getElementById('frontCanvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, w, h);
const centerX = w / 2;
const centerY = h / 2;
const radius = 60;
// Draw circle for reference
ctx.strokeStyle = colors.border;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
// Draw axes
ctx.strokeStyle = colors.muted;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerX - radius, centerY);
ctx.lineTo(centerX + radius, centerY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY - radius);
ctx.lineTo(centerX, centerY + radius);
ctx.stroke();
// Draw E-field vector based on polarization state
let ex, ey;
const time = Date.now() / 1000;
if (state.sourceType === 'linear') {
ex = Math.cos((state.pol1Angle - 90) * Math.PI / 180) * Math.cos(time * 4) * radius;
ey = Math.sin((state.pol1Angle - 90) * Math.PI / 180) * Math.cos(time * 4) * radius;
} else if (state.sourceType === 'circular') {
ex = Math.cos(time * 4) * radius;
ey = Math.sin(time * 4) * radius;
} else if (state.sourceType === 'elliptical') {
ex = Math.cos(time * 4) * radius * 0.7;
ey = Math.sin(time * 4 + Math.PI / 4) * radius;
} else {
ex = Math.cos(time * 4) * radius * 0.5;
ey = Math.sin(time * 4) * radius * 0.5;
}
// Apply intensity reduction from polarizers
const intensity = calculateIntensity(state.pol1Angle, state.pol2Angle) * 0.5;
ex *= Math.sqrt(intensity);
ey *= Math.sqrt(intensity);
// Draw E-field vector
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + ex, centerY + ey);
ctx.stroke();
// Draw arrowhead
const angle = Math.atan2(ey, ex);
const arrowSize = 10;
ctx.fillStyle = colors.accent;
ctx.beginPath();
ctx.moveTo(centerX + ex, centerY + ey);
ctx.lineTo(centerX + ex - arrowSize * Math.cos(angle - Math.PI / 6), centerY + ey - arrowSize * Math.sin(angle - Math.PI / 6));
ctx.lineTo(centerX + ex - arrowSize * Math.cos(angle + Math.PI / 6), centerY + ey - arrowSize * Math.sin(angle + Math.PI / 6));
ctx.fill();
// Draw polarizer axis (analyzer)
ctx.strokeStyle = colors.warning;
ctx.lineWidth = 2;
const polLength = radius + 10;
const polAngle = state.pol2Angle * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(centerX - polLength * Math.cos(polAngle), centerY - polLength * Math.sin(polAngle));
ctx.lineTo(centerX + polLength * Math.cos(polAngle), centerY + polLength * Math.sin(polAngle));
ctx.stroke();
// Labels
ctx.fillStyle = colors.muted;
ctx.font = '11px DM Mono';
ctx.textAlign = 'center';
ctx.fillText('E-field (red arrow)', centerX, h - 20);
ctx.fillStyle = colors.warning;
ctx.fillText('Analyzer axis (yellow)', centerX, h - 5);
requestAnimationFrame(drawFrontView);
}
function drawMalusGraph() {
const canvas = document.getElementById('graphCanvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const padding = 60;
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, w, h);
// Draw grid
ctx.strokeStyle = colors.border;
ctx.lineWidth = 0.5;
for (let i = 0; i <= 10; i++) {
// Horizontal
const y = padding + (h - 2 * padding) * (i / 10);
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(w - padding, y);
ctx.stroke();
// Vertical
const x = padding + (w - 2 * padding) * (i / 10);
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, h - padding);
ctx.stroke();
}
// Draw axes
ctx.strokeStyle = colors.text;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, h - padding);
ctx.lineTo(w - padding, h - padding);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, h - padding);
ctx.stroke();
// Draw Malus curve
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 3;
ctx.beginPath();
for (let angle = 0; angle <= 180; angle += 1) {
const intensity = Math.pow(Math.cos(angle * Math.PI / 180), 2);
const x = padding + (w - 2 * padding) * (angle / 180);
const y = h - padding - (h - 2 * padding) * intensity;
if (angle === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Draw current point
const intensity = Math.pow(Math.cos(state.pol2Angle * Math.PI / 180), 2);
const x = padding + (w - 2 * padding) * (state.pol2Angle / 180);
const y = h - padding - (h - 2 * padding) * intensity;
ctx.fillStyle = colors.accent;
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
// Labels
ctx.fillStyle = colors.text;
ctx.font = 'bold 14px DM Mono';
ctx.textAlign = 'center';
ctx.fillText('Angle (degrees)', w / 2, h - 10);
ctx.save();
ctx.translate(15, h / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Intensity (I/I₀)', 0, 0);
ctx.restore();
// Tick labels
ctx.font = '12px DM Mono';
for (let i = 0; i <= 180; i += 30) {
const x = padding + (w - 2 * padding) * (i / 180);
ctx.fillText(i + '°', x, h - padding + 20);
}
for (let i = 0; i <= 1; i += 0.2) {
const y = h - padding - (h - 2 * padding) * i;
ctx.textAlign = 'right';
ctx.fillText((i * 100).toFixed(0) + '%', padding - 10, y + 4);
}
}
function drawCascade() {
const canvas = document.getElementById('cascadeCanvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, w, h);
const numPols = state.numPolarizers + 2;
const spacing = (w - 100) / (numPols + 1);
let intensity = 0.5;
const progression = [100];
for (let i = 0; i < numPols; i++) {
const x = 50 + (i + 1) * spacing;
const angle = state.intermediatePols[i];
// Draw light beam
const beamIntensity = intensity;
const color = `rgb(${Math.round(255 * beamIntensity)}, ${Math.round(34 * beamIntensity)}, 0)`;
ctx.fillStyle = color;
ctx.fillRect(x - 15, h / 2 - 20, 30, 40);
// Draw polarizer
ctx.save();
ctx.translate(x, h / 2);
ctx.rotate(angle * Math.PI / 180);
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, -30);
ctx.lineTo(0, 30);
ctx.stroke();
ctx.restore();
// Calculate intensity after this polarizer
if (i === 0) {
intensity = 0.5; // Unpolarized source
} else {
const prevAngle = state.intermediatePols[i - 1];
const diff = Math.abs(angle - prevAngle);
const theta = Math.min(diff, 180 - diff);
intensity *= Math.pow(Math.cos(theta * Math.PI / 180), 2);
}
progression.push(Math.round(intensity * 100));
// Draw intensity value
ctx.fillStyle = colors.text;
ctx.font = 'bold 14px Bebas Neue';
ctx.textAlign = 'center';
ctx.fillText(Math.round(intensity * 100) + '%', x, h - 20);
ctx.font = '11px DM Mono';
ctx.fillStyle = colors.muted;
const label = i === 0 ? 'Pol 1' : i === numPols - 1 ? 'Analyzer' : `Pol ${i + 1}`;
ctx.fillText(label, x, 25);
}
// Update progression display
const prog1 = document.getElementById('prog1');
const prog2 = document.getElementById('prog2');
if (state.numPolarizers === 0) {
prog1.textContent = Math.round(0.5 * Math.pow(Math.cos((state.intermediatePols[1] - state.intermediatePols[0]) * Math.PI / 180), 2) * 100) + '%';
prog2.textContent = '';
} else {
prog1.textContent = progression[1] + '%';
prog2.textContent = progression[progression.length - 1] + '%';
}
}
// Initial draw
update();
requestAnimationFrame(drawFrontView);