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