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);