Pendulum Wave: How to Create Mesmerizing Phase Animations With Code
A pendulum wave is one of the most hypnotic demonstrations in physics. A row of pendulums, each slightly longer than the last, swing together in perfect unison — then slowly drift apart, creating traveling waves, snaking curves, and apparent chaos, before magically returning to synchrony. The effect is mesmerizing, and the math behind it is beautifully simple.
In this article, we build 8 pendulum wave visualizations from scratch using JavaScript and Canvas. Every example runs live in your browser with no dependencies. We start with the classic linear pendulum wave, then explore circular arrays, rainbow phase gradients, Lissajous combinations, 3D perspective, interactive frequency tuning, coupled pendulums with energy transfer, and finish with a generative pendulum art composition.
The Physics of Pendulum Waves
A simple pendulum's period depends only on its length: T = 2π√(L/g), where L is the length and g is gravitational acceleration (9.81 m/s²). A pendulum wave exploits this: by choosing lengths so that each pendulum completes a different integer number of oscillations in the same overall cycle time, you get predictable phase relationships that produce stunning visual patterns.
If we want pendulum i to complete N+i oscillations in cycle time Tcycle, its period must be Ti = Tcycle / (N+i), and its length Li = g · (Ti / 2π)².
1. Basic Pendulum Wave
The classic setup: 15 pendulums in a row, each tuned so that in 60 seconds they complete 51, 52, 53... 65 oscillations. Watch them start in phase, create traveling waves, appear random, then snap back together.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 15;
const cycleTime = 32;
const baseOsc = 51;
const pivotY = 60;
const rodLength = 300;
const bobRadius = 10;
const maxAngle = Math.PI / 6;
const pendulums = Array.from({ length: N }, (_, i) => {
const oscillations = baseOsc + i;
const frequency = oscillations / cycleTime;
return { frequency, x: 80 + i * ((canvas.width - 160) / (N - 1)) };
});
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// pivot bar
ctx.strokeStyle = '#1a3a2a';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(40, pivotY);
ctx.lineTo(canvas.width - 40, pivotY);
ctx.stroke();
for (const p of pendulums) {
const angle = maxAngle * Math.sin(2 * Math.PI * p.frequency * t);
const bobX = p.x + rodLength * Math.sin(angle);
const bobY = pivotY + rodLength * Math.cos(angle);
// rod
ctx.strokeStyle = 'rgba(110,230,180,0.3)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(p.x, pivotY);
ctx.lineTo(bobX, bobY);
ctx.stroke();
// bob
const phase = (Math.sin(2 * Math.PI * p.frequency * t) + 1) / 2;
const hue = 140 + phase * 60;
ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
ctx.beginPath();
ctx.arc(bobX, bobY, bobRadius, 0, Math.PI * 2);
ctx.fill();
// pivot
ctx.fillStyle = '#2a5a3a';
ctx.beginPath();
ctx.arc(p.x, pivotY, 3, 0, Math.PI * 2);
ctx.fill();
}
// time + cycle info
ctx.fillStyle = 'rgba(110,230,180,0.4)';
ctx.font = '12px monospace';
ctx.fillText(`t = ${t.toFixed(1)}s | cycle = ${cycleTime}s | click to reset`, 20, canvas.height - 15);
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => { startTime = performance.now(); });
requestAnimationFrame(draw);
2. Circular Pendulum Array
Instead of a line, arrange pendulums in a circle. Each pendulum swings radially outward from the center. The phase pattern creates mesmerizing rotational waves — spirals, pulsing rings, and flower-like blooms.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 24;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const ringRadius = 80;
const armLength = 160;
const cycleTime = 30;
const baseOsc = 40;
const maxAngle = 0.35;
const pendulums = Array.from({ length: N }, (_, i) => {
const theta = (i / N) * Math.PI * 2;
const oscillations = baseOsc + i;
return { theta, frequency: oscillations / cycleTime };
});
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.fillStyle = 'rgba(10, 14, 23, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// center ring
ctx.strokeStyle = 'rgba(110,230,180,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, ringRadius, 0, Math.PI * 2);
ctx.stroke();
for (const p of pendulums) {
const pivotX = cx + ringRadius * Math.cos(p.theta);
const pivotY = cy + ringRadius * Math.sin(p.theta);
const swing = maxAngle * Math.sin(2 * Math.PI * p.frequency * t);
const armAngle = p.theta + swing;
const bobX = pivotX + armLength * Math.cos(armAngle);
const bobY = pivotY + armLength * Math.sin(armAngle);
// arm
ctx.strokeStyle = 'rgba(110,230,180,0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pivotX, pivotY);
ctx.lineTo(bobX, bobY);
ctx.stroke();
// bob
const phase = (Math.sin(2 * Math.PI * p.frequency * t) + 1) / 2;
const hue = (p.theta / (Math.PI * 2)) * 360;
ctx.fillStyle = `hsla(${hue}, 70%, ${40 + phase * 30}%, 0.9)`;
ctx.beginPath();
ctx.arc(bobX, bobY, 6, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => { startTime = performance.now(); });
requestAnimationFrame(draw);
3. Rainbow Phase Gradient
Map each pendulum's phase to a full rainbow spectrum, and draw trails. The result is a cascading ribbon of color that reveals the wave pattern as a continuously flowing gradient. Trails fade slowly to create a luminous afterglow.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 30;
const cycleTime = 40;
const baseOsc = 30;
const amplitude = 160;
const centerY = canvas.height / 2;
const pendulums = Array.from({ length: N }, (_, i) => ({
x: 30 + i * ((canvas.width - 60) / (N - 1)),
frequency: (baseOsc + i) / cycleTime,
trail: [],
}));
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.fillStyle = 'rgba(10, 14, 23, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < N; i++) {
const p = pendulums[i];
const y = centerY + amplitude * Math.sin(2 * Math.PI * p.frequency * t);
const hue = (i / N) * 360;
// trail
p.trail.push(y);
if (p.trail.length > 50) p.trail.shift();
for (let j = 0; j < p.trail.length; j++) {
const alpha = j / p.trail.length;
ctx.fillStyle = `hsla(${hue}, 80%, 55%, ${alpha * 0.4})`;
ctx.beginPath();
ctx.arc(p.x, p.trail[j], 3 + alpha * 4, 0, Math.PI * 2);
ctx.fill();
}
// main bob
ctx.fillStyle = `hsl(${hue}, 85%, 60%)`;
ctx.beginPath();
ctx.arc(p.x, y, 8, 0, Math.PI * 2);
ctx.fill();
// glow
const grad = ctx.createRadialGradient(p.x, y, 0, p.x, y, 20);
grad.addColorStop(0, `hsla(${hue}, 85%, 60%, 0.3)`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(p.x, y, 20, 0, Math.PI * 2);
ctx.fill();
}
// connecting curve
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < N; i++) {
const p = pendulums[i];
const y = centerY + amplitude * Math.sin(2 * Math.PI * p.frequency * t);
if (i === 0) ctx.moveTo(p.x, y);
else ctx.lineTo(p.x, y);
}
ctx.stroke();
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => {
startTime = performance.now();
pendulums.forEach(p => p.trail = []);
});
requestAnimationFrame(draw);
4. Lissajous Pendulums
Combine two pendulum waves at right angles. Each pendulum swings in both X and Y with different frequency ratios, tracing Lissajous curves. The array of overlapping figures creates a dancing constellation of curves.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const grid = 4;
const N = grid * grid;
const cellW = canvas.width / grid;
const cellH = canvas.height / grid;
const amplitude = cellW * 0.35;
const cycleTime = 24;
const baseOsc = 20;
const pendulums = [];
for (let row = 0; row < grid; row++) {
for (let col = 0; col < grid; col++) {
const cx = (col + 0.5) * cellW;
const cy = (row + 0.5) * cellH;
const freqX = (baseOsc + col) / cycleTime;
const freqY = (baseOsc + row) / cycleTime;
pendulums.push({ cx, cy, freqX, freqY, trail: [] });
}
}
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.fillStyle = 'rgba(10, 14, 23, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// grid lines
ctx.strokeStyle = 'rgba(110,230,180,0.06)';
ctx.lineWidth = 1;
for (let i = 1; i < grid; i++) {
ctx.beginPath();
ctx.moveTo(i * cellW, 0); ctx.lineTo(i * cellW, canvas.height);
ctx.moveTo(0, i * cellH); ctx.lineTo(canvas.width, i * cellH);
ctx.stroke();
}
for (const p of pendulums) {
const x = p.cx + amplitude * Math.sin(2 * Math.PI * p.freqX * t);
const y = p.cy + amplitude * Math.sin(2 * Math.PI * p.freqY * t);
p.trail.push({ x, y });
if (p.trail.length > 200) p.trail.shift();
// trail
ctx.strokeStyle = 'rgba(110,230,180,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < p.trail.length; i++) {
if (i === 0) ctx.moveTo(p.trail[i].x, p.trail[i].y);
else ctx.lineTo(p.trail[i].x, p.trail[i].y);
}
ctx.stroke();
// bob
ctx.fillStyle = '#6ee6b4';
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => {
startTime = performance.now();
pendulums.forEach(p => p.trail = []);
});
requestAnimationFrame(draw);
5. 3D Perspective Pendulum Wave
Add depth: arrange pendulums in a line receding into the screen. A simple perspective projection makes closer pendulums larger and farther ones smaller, creating a striking 3D tunnel effect as the wave propagates.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 20;
const cycleTime = 36;
const baseOsc = 40;
const maxAngle = Math.PI / 5;
const focalLength = 400;
const depthSpacing = 40;
function project(x3d, y3d, z3d) {
const scale = focalLength / (focalLength + z3d);
return {
x: canvas.width / 2 + x3d * scale,
y: 160 + y3d * scale,
scale,
};
}
const pendulums = Array.from({ length: N }, (_, i) => ({
z: i * depthSpacing,
frequency: (baseOsc + i) / cycleTime,
}));
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw back to front
for (let i = N - 1; i >= 0; i--) {
const p = pendulums[i];
const angle = maxAngle * Math.sin(2 * Math.PI * p.frequency * t);
const armLen = 250;
const pivotProj = project(0, 0, p.z);
const bobX3d = armLen * Math.sin(angle);
const bobY3d = armLen * Math.cos(angle);
const bobProj = project(bobX3d, bobY3d, p.z);
const alpha = 0.3 + 0.7 * (1 - i / N);
// rod
ctx.strokeStyle = `rgba(110,230,180,${alpha * 0.4})`;
ctx.lineWidth = 2 * pivotProj.scale;
ctx.beginPath();
ctx.moveTo(pivotProj.x, pivotProj.y);
ctx.lineTo(bobProj.x, bobProj.y);
ctx.stroke();
// bob
const hue = 140 + (i / N) * 80;
const radius = 10 * bobProj.scale;
ctx.fillStyle = `hsla(${hue}, 70%, 55%, ${alpha})`;
ctx.beginPath();
ctx.arc(bobProj.x, bobProj.y, radius, 0, Math.PI * 2);
ctx.fill();
// glow
if (bobProj.scale > 0.5) {
const grad = ctx.createRadialGradient(bobProj.x, bobProj.y, 0, bobProj.x, bobProj.y, radius * 3);
grad.addColorStop(0, `hsla(${hue}, 70%, 55%, ${alpha * 0.3})`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(bobProj.x, bobProj.y, radius * 3, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.fillStyle = 'rgba(110,230,180,0.3)';
ctx.font = '12px monospace';
ctx.fillText('3D perspective pendulum wave — click to reset', 20, canvas.height - 15);
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => { startTime = performance.now(); });
requestAnimationFrame(draw);
6. Interactive Frequency Tuner
Drag the mouse left/right to change the base frequency, and up/down to change the frequency spread between pendulums. This lets you explore the parameter space in real time — find settings that produce tight traveling waves, wide chaos, or rapid resynchronization.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 20;
const centerY = canvas.height / 2;
const amplitude = 180;
let baseFreq = 1.5;
let freqSpread = 0.08;
let mouseDown = false;
canvas.addEventListener('mousedown', () => mouseDown = true);
canvas.addEventListener('mouseup', () => mouseDown = false);
canvas.addEventListener('mousemove', (e) => {
if (!mouseDown) return;
const rect = canvas.getBoundingClientRect();
baseFreq = 0.5 + (e.clientX - rect.left) / rect.width * 3;
freqSpread = 0.01 + (e.clientY - rect.top) / rect.height * 0.2;
});
let startTime = performance.now();
function draw(now) {
const t = (now - startTime) / 1000;
ctx.fillStyle = 'rgba(10, 14, 23, 0.12)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const positions = [];
for (let i = 0; i < N; i++) {
const x = 40 + i * ((canvas.width - 80) / (N - 1));
const freq = baseFreq + i * freqSpread;
const y = centerY + amplitude * Math.sin(2 * Math.PI * freq * t);
positions.push({ x, y, freq });
const hue = 140 + (i / N) * 80;
ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2);
ctx.fill();
}
// connecting curve
ctx.strokeStyle = 'rgba(110,230,180,0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < positions.length; i++) {
if (i === 0) ctx.moveTo(positions[i].x, positions[i].y);
else ctx.lineTo(positions[i].x, positions[i].y);
}
ctx.stroke();
// info
ctx.fillStyle = 'rgba(110,230,180,0.5)';
ctx.font = '13px monospace';
ctx.fillText(`base freq: ${baseFreq.toFixed(2)} Hz | spread: ${freqSpread.toFixed(3)} | drag to tune`, 20, 25);
ctx.fillText(`freq range: ${baseFreq.toFixed(2)} – ${(baseFreq + (N-1) * freqSpread).toFixed(2)} Hz`, 20, 45);
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => { startTime = performance.now(); });
requestAnimationFrame(draw);
7. Coupled Pendulums With Energy Transfer
Connect adjacent pendulums with springs, so energy transfers between them. Unlike independent pendulum waves, coupled pendulums exhibit beat frequencies — energy sloshes back and forth between neighbors, creating a different kind of visual rhythm.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const N = 12;
const pivotY = 50;
const armLength = 300;
const bobRadius = 10;
const coupling = 0.3;
const damping = 0.9995;
const dt = 0.02;
const pendulums = Array.from({ length: N }, (_, i) => ({
x: 60 + i * ((canvas.width - 120) / (N - 1)),
angle: i === 0 ? 0.5 : 0,
velocity: 0,
naturalFreq: 3 + i * 0.15,
}));
function step() {
for (let i = 0; i < N; i++) {
const p = pendulums[i];
let acc = -p.naturalFreq * p.naturalFreq * p.angle;
// coupling to neighbors
if (i > 0) acc += coupling * (pendulums[i - 1].angle - p.angle);
if (i < N - 1) acc += coupling * (pendulums[i + 1].angle - p.angle);
p.velocity += acc * dt;
p.velocity *= damping;
p.angle += p.velocity * dt;
}
}
function draw() {
for (let s = 0; s < 4; s++) step();
ctx.fillStyle = 'rgba(10, 14, 23, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// pivot bar
ctx.strokeStyle = '#1a3a2a';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(30, pivotY);
ctx.lineTo(canvas.width - 30, pivotY);
ctx.stroke();
// coupling springs
for (let i = 0; i < N - 1; i++) {
const p1 = pendulums[i];
const p2 = pendulums[i + 1];
const midY = pivotY + armLength * 0.3;
const x1 = p1.x + armLength * 0.3 * Math.sin(p1.angle);
const x2 = p2.x + armLength * 0.3 * Math.sin(p2.angle);
ctx.strokeStyle = 'rgba(230,180,110,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x1, midY);
// zigzag spring
const segs = 8;
const dx = (x2 - x1) / segs;
for (let s = 1; s < segs; s++) {
const sx = x1 + s * dx;
const sy = midY + (s % 2 === 0 ? -6 : 6);
ctx.lineTo(sx, sy);
}
ctx.lineTo(x2, midY);
ctx.stroke();
}
for (let i = 0; i < N; i++) {
const p = pendulums[i];
const bobX = p.x + armLength * Math.sin(p.angle);
const bobY = pivotY + armLength * Math.cos(p.angle);
// rod
ctx.strokeStyle = 'rgba(110,230,180,0.3)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(p.x, pivotY);
ctx.lineTo(bobX, bobY);
ctx.stroke();
// energy = 0.5 * v^2 (kinetic)
const energy = Math.min(1, Math.abs(p.velocity) * 2);
const hue = 140 - energy * 100;
ctx.fillStyle = `hsl(${hue}, 70%, ${45 + energy * 25}%)`;
ctx.beginPath();
ctx.arc(bobX, bobY, bobRadius, 0, Math.PI * 2);
ctx.fill();
// pivot
ctx.fillStyle = '#2a5a3a';
ctx.beginPath();
ctx.arc(p.x, pivotY, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = 'rgba(110,230,180,0.4)';
ctx.font = '12px monospace';
ctx.fillText('coupled pendulums — energy transfers through springs — click to kick first pendulum', 20, canvas.height - 15);
requestAnimationFrame(draw);
}
canvas.addEventListener('click', () => {
pendulums.forEach(p => { p.angle = 0; p.velocity = 0; });
pendulums[0].angle = 0.5;
});
requestAnimationFrame(draw);
8. Generative Pendulum Art
Combine multiple pendulum waves at different scales, map positions to color and size, and let trails accumulate into a generative artwork. Each click seeds a new composition by randomizing wave parameters. The result is a unique abstract painting drawn by physics.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 700;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let waves = [];
let startTime;
function randomize() {
ctx.fillStyle = '#0a0e17';
ctx.fillRect(0, 0, canvas.width, canvas.height);
startTime = performance.now();
const numWaves = 3 + Math.floor(Math.random() * 3);
waves = Array.from({ length: numWaves }, () => {
const N = 8 + Math.floor(Math.random() * 20);
const baseFreq = 0.5 + Math.random() * 2;
const spread = 0.02 + Math.random() * 0.15;
const hueBase = Math.random() * 360;
const amplitudeX = 100 + Math.random() * 200;
const amplitudeY = 100 + Math.random() * 200;
const offsetX = canvas.width / 2 + (Math.random() - 0.5) * 100;
const offsetY = canvas.height / 2 + (Math.random() - 0.5) * 100;
const phaseOffset = Math.random() * Math.PI * 2;
return {
pendulums: Array.from({ length: N }, (_, i) => ({
freqX: baseFreq + i * spread,
freqY: baseFreq + i * spread * 1.3,
})),
hueBase, amplitudeX, amplitudeY, offsetX, offsetY, phaseOffset,
};
});
}
randomize();
function draw(now) {
const t = (now - startTime) / 1000;
if (t > 60) { randomize(); requestAnimationFrame(draw); return; }
ctx.globalCompositeOperation = 'screen';
for (const wave of waves) {
for (let i = 0; i < wave.pendulums.length; i++) {
const p = wave.pendulums[i];
const x = wave.offsetX + wave.amplitudeX * Math.sin(2 * Math.PI * p.freqX * t + wave.phaseOffset);
const y = wave.offsetY + wave.amplitudeY * Math.sin(2 * Math.PI * p.freqY * t);
const hue = (wave.hueBase + i * 10 + t * 5) % 360;
const size = 1.5 + Math.sin(t * 2 + i) * 1;
ctx.fillStyle = `hsla(${hue}, 70%, 50%, 0.06)`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.globalCompositeOperation = 'source-over';
// info
ctx.fillStyle = 'rgba(110,230,180,0.3)';
ctx.font = '11px monospace';
ctx.fillText(`${waves.length} wave layers | ${waves.reduce((s, w) => s + w.pendulums.length, 0)} pendulums | click for new composition`, 15, canvas.height - 12);
requestAnimationFrame(draw);
}
canvas.addEventListener('click', randomize);
requestAnimationFrame(draw);
The Mathematics of Re-synchronization
The most magical moment in a pendulum wave is when all pendulums return to alignment. This happens because we chose integer frequency ratios. If pendulum i has frequency fi = (N+i)/Tcycle, then after time Tcycle, each pendulum has completed exactly N+i full oscillations — and since oscillation is periodic, they all return to their starting position simultaneously.
During the cycle, you see predictable stages: unison → traveling wave → standing wave → figure-eight patterns → apparent chaos → standing wave → traveling wave (reversed) → unison. These stages correspond to specific phase relationships between adjacent pendulums, and they repeat perfectly every cycle.
Building Physical Pendulum Waves
If you want to build a real pendulum wave, the key challenge is precision. A 1% error in pendulum length causes a 0.5% error in period, which compounds over multiple cycles. Tips:
- Use the formula Li = g · (Tcycle / (2π(N+i)))² to calculate exact lengths
- Fine-tune by timing — measure 10 oscillations with a stopwatch and adjust length
- Minimize air resistance — use dense, compact bobs (steel ball bearings work well)
- Keep amplitudes small — the simple pendulum formula assumes small angles (<15°)
- Use rigid, lightweight rods instead of strings to prevent twisting
Performance Tips
- Pendulum waves are cheap to simulate — each bob is just a sine function, no differential equations needed (unless you add coupling or nonlinear effects)
- Use requestAnimationFrame time directly rather than accumulating dt to avoid drift
- For trails, use a semi-transparent rectangle overlay instead of storing point arrays for smoother fade effects
- Coupled pendulums need sub-stepping (multiple physics steps per frame) for stability — 4 sub-steps at dt=0.02 is a good starting point
The pendulum wave is proof that simple rules create extraordinary beauty. Each bob follows the same equation — angle = A·sin(2πft) — but a small frequency difference between neighbors produces an infinite variety of visual patterns. It's the same principle that drives chaos in double pendulums, elegance in Lissajous curves, and complexity in wave simulations. Explore more creative coding tutorials on the Lumitree blog, or visit the tree to discover generative micro-worlds grown from visitor seeds.