education

FOURIER SERIES

Interactive visualization of Fourier series decomposition with rotating epicycles. Build classic waveforms or draw your own.

Left: Rotating epicycles | Right: Waveform output | Click to set time = 0
Wave Type
Harmonics
10 terms
Speed
1.0×
Display
Show circles
Show trace line
Glow effect
10
Harmonics
0.0
Time (s)
f(t) = (4/π) × Σ sin((2k-1)ωt)/(2k-1)
Click on the epicycle area to reset time to 0. Watch the circles rotate and trace the waveform on the right.
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const canvas = document.getElementById('fourierCanvas');
    const ctx = canvas.getContext('2d');

    // Resize canvas to fill container
    function resizeCanvas() {
      const wrapper = canvas.parentElement;
      canvas.width = wrapper.clientWidth;
      canvas.height = Math.max(400, window.innerHeight * 0.6);
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    // State
    let state = {
      mode: 'square',
      harmonics: 10,
      speed: 1.0,
      time: 0,
      showCircles: true,
      showLine: true,
      glowEffect: false,
      waveformHistory: [],
      customCoefficients: []
    };

    const controls = {
      harmonicsSlider: document.getElementById('harmonicsSlider'),
      speedSlider: document.getElementById('speedSlider'),
      harmonicsValue: document.getElementById('harmonicsValue'),
      speedValue: document.getElementById('speedValue'),
      freqValue: document.getElementById('freqValue'),
      timeValue: document.getElementById('timeValue'),
      equationDisplay: document.getElementById('equationDisplay'),
      canvasInfo: document.getElementById('canvasInfo')
    };

    // Fourier coefficients for different waveforms
    function getCoefficient(n, mode) {
      if (mode === 'square') {
        // Square wave: 4/(π·n) for odd n, 0 for even
        return n % 2 === 1 ? (4 / (Math.PI * n)) : 0;
      } else if (mode === 'triangle') {
        // Triangle wave: 8/(π²·n²) · sin(π·n/2) for odd n
        return n % 2 === 1 ? (8 / (Math.PI * Math.PI * n * n)) * Math.sin(Math.PI * n / 2) : 0;
      } else if (mode === 'sawtooth') {
        // Sawtooth: 2/n · (-1)^(n+1) for all n
        return (2 / n) * Math.pow(-1, n + 1);
      }
      return 0;
    }

    // Generate equation display
    function updateEquationDisplay() {
      const mode = state.mode;
      let equation = '';

      if (mode === 'square') {
        equation = `f(t) = (4/π) × Σ sin((2k-1)ωt)/(2k-1), k=1 to ${state.harmonics}`;
      } else if (mode === 'triangle') {
        equation = `f(t) = (8/π²) × Σ sin(πk/2) × sin(kωt)/k², k=1 to ${state.harmonics}`;
      } else if (mode === 'sawtooth') {
        equation = `f(t) = 2 × Σ (-1)^(k+1) × sin(kωt)/k, k=1 to ${state.harmonics}`;
      } else if (mode === 'custom' && state.customCoefficients.length > 0) {
        equation = `f(t) = Σ aₖ × sin(kωt), k=1 to ${state.customCoefficients.length}`;
      }

      controls.equationDisplay.textContent = equation || 'Draw on canvas to create custom waveform';
    }

    // Calculate y position at time t for a given harmonic
    function evaluateFourier(t, mode) {
      let y = 0;
      for (let k = 1; k <= state.harmonics; k++) {
        const coeff = mode === 'custom' && state.customCoefficients[k - 1]
          ? state.customCoefficients[k - 1]
          : getCoefficient(k, mode);
        y += coeff * Math.sin(k * t);
      }
      return y;
    }

    // Get all harmonics for epicycle visualization
    function getHarmonics(t, mode) {
      const harmonics = [];
      for (let k = 1; k <= state.harmonics; k++) {
        const coeff = mode === 'custom' && state.customCoefficients[k - 1]
          ? state.customCoefficients[k - 1]
          : getCoefficient(k, mode);
        harmonics.push({
          index: k,
          amplitude: coeff,
          phase: k * t,
          x: coeff * Math.cos(k * t),
          y: coeff * Math.sin(k * t)
        });
      }
      return harmonics;
    }

    // Draw the epicycles
    function drawEpicycles(x, y) {
      const harmonics = getHarmonics(state.time, state.mode);
      let posX = x;
      let posY = y;

      // Draw each circle and arm
      harmonics.forEach((h, idx) => {
        if (state.showCircles) {
          // Circle
          ctx.strokeStyle = `rgba(255, 34, 0, ${0.6 - idx * 0.02})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.arc(posX, posY, Math.abs(h.amplitude), 0, Math.PI * 2);
          ctx.stroke();

          // Arm (line from center to edge)
          const armX = posX + h.x;
          const armY = posY + h.y;
          ctx.strokeStyle = `rgba(255, 34, 0, ${0.4 - idx * 0.01})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(posX, posY);
          ctx.lineTo(armX, armY);
          ctx.stroke();

          // Update position for next circle
          posX = armX;
          posY = armY;
        }
      });

      return { posX, posY };
    }

    // Draw waveform
    function drawWaveform(startX, centerY, width) {
      if (state.waveformHistory.length < 2) return;

      ctx.strokeStyle = state.glowEffect ? 'rgba(255, 34, 0, 0.8)' : 'rgba(255, 34, 0, 1)';
      ctx.lineWidth = 2;
      if (state.glowEffect) {
        ctx.shadowColor = 'rgba(255, 34, 0, 0.6)';
        ctx.shadowBlur = 8;
      }

      ctx.beginPath();
      const pixelPerSample = width / state.waveformHistory.length;
      state.waveformHistory.forEach((y, i) => {
        const x = startX + i * pixelPerSample;
        const displayY = centerY - y * 30;
        if (i === 0) {
          ctx.moveTo(x, displayY);
        } else {
          ctx.lineTo(x, displayY);
        }
      });
      ctx.stroke();

      ctx.shadowColor = 'transparent';
    }

    // Main animation loop
    function animate() {
      const centerX = canvas.width / 3;
      const centerY = canvas.height / 2;
      const rightX = (canvas.width * 2) / 3;
      const waveformWidth = (canvas.width / 3) - 20;

      // Clear canvas
      ctx.fillStyle = 'rgba(17, 17, 17, 1)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // Draw divider
      ctx.strokeStyle = 'rgba(30, 30, 30, 0.5)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(canvas.width / 2, 0);
      ctx.lineTo(canvas.width / 2, canvas.height);
      ctx.stroke();

      // Draw labels
      ctx.fillStyle = 'rgba(85, 85, 85, 0.8)';
      ctx.font = '11px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('EPICYCLES', centerX, 20);
      ctx.fillText('WAVEFORM', rightX, 20);

      // Draw epicycles on left
      const { posX, posY } = drawEpicycles(centerX, centerY);

      // Draw trace line from epicycle tip to waveform
      if (state.showLine) {
        ctx.strokeStyle = 'rgba(255, 34, 0, 0.3)';
        ctx.lineWidth = 1;
        ctx.setLineDash([4, 4]);
        ctx.beginPath();
        ctx.moveTo(posX, posY);
        ctx.lineTo(rightX, centerY - (state.waveformHistory[state.waveformHistory.length - 1] || 0) * 30);
        ctx.stroke();
        ctx.setLineDash([]);
      }

      // Draw waveform on right
      drawWaveform(rightX - waveformWidth, centerY, waveformWidth);

      // Add new point to waveform history
      const currentY = evaluateFourier(state.time, state.mode);
      state.waveformHistory.push(currentY);
      const maxHistory = Math.floor((waveformWidth / 3) * 2);
      if (state.waveformHistory.length > maxHistory) {
        state.waveformHistory.shift();
      }

      // Update time
      state.time += 0.05 * state.speed;
      if (state.time > Math.PI * 2) {
        state.time = 0;
        state.waveformHistory = [];
      }

      // Update display
      controls.timeValue.textContent = state.time.toFixed(2);

      requestAnimationFrame(animate);
    }

    // Event listeners
    controls.harmonicsSlider.addEventListener('input', (e) => {
      state.harmonics = parseInt(e.target.value);
      controls.harmonicsValue.textContent = state.harmonics;
      controls.freqValue.textContent = state.harmonics;
      updateEquationDisplay();
      state.waveformHistory = [];
    });

    controls.speedSlider.addEventListener('input', (e) => {
      state.speed = parseFloat(e.target.value);
      controls.speedValue.textContent = state.speed.toFixed(1);
    });

    document.querySelectorAll('.btn-mode').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.btn-mode').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        state.mode = btn.dataset.mode;
        state.time = 0;
        state.waveformHistory = [];
        updateEquationDisplay();
        if (state.mode === 'custom') {
          controls.canvasInfo.textContent = 'Draw your custom waveform on the LEFT epicycle area. Click the Square button to reset.';
          canvas.style.cursor = 'crosshair';
        } else {
          controls.canvasInfo.textContent = 'Left: Rotating epicycles | Right: Waveform output | Click to set time = 0';
          canvas.style.cursor = 'crosshair';
        }
      });
    });

    document.querySelectorAll('.toggle-item').forEach(toggle => {
      toggle.addEventListener('click', () => {
        toggle.classList.toggle('active');
        const toggleName = toggle.dataset.toggle;
        state[toggleName] = toggle.classList.contains('active');
      });
    });

    canvas.addEventListener('click', (e) => {
      const rect = canvas.getBoundingClientRect();
      const clickX = e.clientX - rect.left;
      const centerX = canvas.width / 3;

      if (clickX < canvas.width / 2) {
        state.time = 0;
        state.waveformHistory = [];
      }
    });

    updateEquationDisplay();
    animate();