Double Pendulum: How to Create Mesmerizing Chaos Art With Code
A double pendulum is one of the simplest physical systems that produces genuine chaos. Take a pendulum. Attach another pendulum to its end. Let go. What happens next is deterministic — governed by rigid Newtonian mechanics — yet practically unpredictable. The second bob traces wild, looping arcs that never repeat. Change the starting angle by a thousandth of a degree and within seconds the trajectory looks completely different.
This makes the double pendulum a perfect subject for creative coding. The physics are elegant enough to simulate in a few dozen lines of JavaScript, but the visual output is endlessly complex and strikingly beautiful. Every run produces a unique pattern — organic curves that look hand-drawn, spirals that evoke galaxies, and sudden whip-crack reversals that catch the eye. It is deterministic art generated by chaos.
In this guide we will build eight interactive simulations, starting from a basic double pendulum and ending with gallery-worthy generative chaos art. Each example is self-contained, runs on a plain HTML Canvas, and uses no external libraries. Along the way you will learn about Lagrangian mechanics, Runge-Kutta integration, phase space portraits, Lyapunov exponents, and the butterfly effect — all through code you can run in your browser right now.
The physics of the double pendulum
A double pendulum has two arms of lengths L1 and L2, with masses m1 and m2 at their ends. The state is described by two angles θ1 and θ2 (measured from vertical) and their angular velocities ω1 and ω2. The equations of motion come from Lagrangian mechanics — instead of tracking forces directly, we express the system's kinetic and potential energy, then derive the accelerations from the Lagrangian L = T - V. The resulting equations are coupled second-order differential equations that cannot be solved analytically.
To simulate the system numerically, we use the Runge-Kutta 4th order method (RK4). This is a standard technique for integrating differential equations: instead of a naive Euler step (which drifts badly), RK4 evaluates the derivatives at four carefully chosen points within each time step and takes a weighted average. The result is far more accurate — good enough to conserve energy over thousands of frames, which is essential for a convincing simulation. Our state vector is [θ1, θ2, ω1, ω2], and at each frame we advance it by a small time step dt.
Example 1: Basic double pendulum simulation
Our first example draws a simple double pendulum — two arms, two bobs, animated in real time. Click the canvas to reset with random initial conditions and watch how different starting angles produce wildly different motion.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 120, l2 = 100;
var state = [Math.PI / 2, Math.PI / 2, 0, 0];
var ox = c.width / 2, oy = 160;
function derivs(s) {
var t1 = s[0], t2 = s[1], w1 = s[2], w2 = s[3];
var dt = t1 - t2;
var den = 2 * m1 + m2 - m2 * Math.cos(2 * dt);
var a1 = (-g * (2 * m1 + m2) * Math.sin(t1) - m2 * g * Math.sin(t1 - 2 * t2)
- 2 * Math.sin(dt) * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * Math.cos(dt))) / (l1 * den);
var a2 = (2 * Math.sin(dt) * (w1 * w1 * l1 * (m1 + m2) + g * (m1 + m2) * Math.cos(t1)
+ w2 * w2 * l2 * m2 * Math.cos(dt))) / (l2 * den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1 = derivs(s);
var s2 = s.map(function(v, i) { return v + k1[i] * dt / 2; });
var k2 = derivs(s2);
var s3 = s.map(function(v, i) { return v + k2[i] * dt / 2; });
var k3 = derivs(s3);
var s4 = s.map(function(v, i) { return v + k3[i] * dt; });
var k4 = derivs(s4);
return s.map(function(v, i) { return v + (k1[i] + 2*k2[i] + 2*k3[i] + k4[i]) * dt / 6; });
}
function animate() {
for (var step = 0; step < 3; step++) state = rk4(state, 0.05);
var x1 = ox + l1 * Math.sin(state[0]);
var y1 = oy + l1 * Math.cos(state[0]);
var x2 = x1 + l2 * Math.sin(state[1]);
var y2 = y1 + l2 * Math.cos(state[1]);
ctx.fillStyle = 'rgba(10, 10, 26, 1)';
ctx.fillRect(0, 0, c.width, c.height);
// Arms
ctx.strokeStyle = '#556';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
// Pivot
ctx.fillStyle = '#888';
ctx.beginPath(); ctx.arc(ox, oy, 5, 0, Math.PI * 2); ctx.fill();
// Bob 1
ctx.fillStyle = '#ff6b6b';
ctx.shadowColor = '#ff6b6b'; ctx.shadowBlur = 15;
ctx.beginPath(); ctx.arc(x1, y1, 10, 0, Math.PI * 2); ctx.fill();
// Bob 2
ctx.fillStyle = '#4ecdc4';
ctx.shadowColor = '#4ecdc4'; ctx.shadowBlur = 15;
ctx.beginPath(); ctx.arc(x2, y2, 8, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
// Label
ctx.fillStyle = '#445';
ctx.font = '12px monospace';
ctx.fillText('click to reset with random angles', 10, c.height - 12);
requestAnimationFrame(animate);
}
c.addEventListener('click', function() {
state = [Math.PI * (0.3 + Math.random() * 1.4), Math.PI * (0.3 + Math.random() * 1.4), 0, 0];
});
animate();
The simulation runs three RK4 steps per frame for stability. The pivot is fixed at the top center of the canvas. Bob 1 (red) hangs from the pivot, and bob 2 (teal) hangs from bob 1. At low energy the motion is relatively tame — almost periodic. But increase the starting angles and the second bob begins to swing wildly, flipping over the top and reversing direction unpredictably. Click a few times and notice how similar starting angles can produce very different motions — that is chaos in action.
Example 2: Trail painting (chaos traces)
Now we add a fading trail behind the second bob. Instead of clearing the canvas each frame, we draw a semi-transparent background, letting previous positions fade gradually. The trail color cycles through the HSL spectrum based on time, creating organic rainbow ribbons.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 120, l2 = 100;
var state = [Math.PI * 0.75, Math.PI * 0.9, 0, 0];
var ox = c.width / 2, oy = 150;
var frame = 0, prevX = 0, prevY = 0;
function derivs(s) {
var t1 = s[0], t2 = s[1], w1 = s[2], w2 = s[3];
var dt = t1 - t2, den = 2 * m1 + m2 - m2 * Math.cos(2 * dt);
var a1 = (-g * (2*m1+m2) * Math.sin(t1) - m2*g*Math.sin(t1-2*t2)
- 2*Math.sin(dt)*m2*(w2*w2*l2 + w1*w1*l1*Math.cos(dt))) / (l1*den);
var a2 = (2*Math.sin(dt)*(w1*w1*l1*(m1+m2) + g*(m1+m2)*Math.cos(t1)
+ w2*w2*l2*m2*Math.cos(dt))) / (l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1 = derivs(s);
var s2 = s.map(function(v,i){ return v+k1[i]*dt/2; });
var k2 = derivs(s2);
var s3 = s.map(function(v,i){ return v+k2[i]*dt/2; });
var k3 = derivs(s3);
var s4 = s.map(function(v,i){ return v+k3[i]*dt; });
var k4 = derivs(s4);
return s.map(function(v,i){ return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6; });
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
function animate() {
for (var step = 0; step < 4; step++) state = rk4(state, 0.04);
var x1 = ox + l1 * Math.sin(state[0]);
var y1 = oy + l1 * Math.cos(state[0]);
var x2 = x1 + l2 * Math.sin(state[1]);
var y2 = y1 + l2 * Math.cos(state[1]);
ctx.fillStyle = 'rgba(10, 10, 26, 0.03)';
ctx.fillRect(0, 0, c.width, c.height);
// Trail line
if (frame > 0) {
var hue = (frame * 0.5) % 360;
ctx.strokeStyle = 'hsl(' + hue + ', 80%, 60%)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(x2, y2);
ctx.stroke();
}
prevX = x2; prevY = y2;
frame++;
requestAnimationFrame(animate);
}
c.addEventListener('click', function() {
state = [Math.PI * (0.4 + Math.random() * 1.2), Math.PI * (0.4 + Math.random() * 1.2), 0, 0];
frame = 0;
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
});
animate();
The key technique here is the semi-transparent overlay: each frame we paint rgba(10, 10, 26, 0.03) over the entire canvas. Old trail segments slowly darken and fade, while recent ones glow brightly. The color cycles through the full hue spectrum at 0.5 degrees per frame, so a complete color cycle takes 720 frames — about 12 seconds. The result is a flowing ribbon that reveals the chaotic attractor's structure. Click to restart with new angles and watch a completely different pattern emerge.
Example 3: Energy visualization
A great way to verify your simulation is correct: track the total energy. In a frictionless double pendulum, total energy (kinetic + potential) must be conserved. We color the pendulum based on the ratio of kinetic to potential energy and draw an energy bar at the bottom.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 110, l2 = 90;
var state = [Math.PI * 0.8, Math.PI * 0.6, 0, 0];
var ox = c.width / 2, oy = 140;
var initE = null;
function derivs(s) {
var t1 = s[0], t2 = s[1], w1 = s[2], w2 = s[3];
var dt = t1 - t2, den = 2*m1+m2-m2*Math.cos(2*dt);
var a1 = (-g*(2*m1+m2)*Math.sin(t1)-m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*m2*(w2*w2*l2+w1*w1*l1*Math.cos(dt)))/(l1*den);
var a2 = (2*Math.sin(dt)*(w1*w1*l1*(m1+m2)+g*(m1+m2)*Math.cos(t1)
+w2*w2*l2*m2*Math.cos(dt)))/(l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1 = derivs(s);
var s2 = s.map(function(v,i){return v+k1[i]*dt/2;});
var k2 = derivs(s2);
var s3 = s.map(function(v,i){return v+k2[i]*dt/2;});
var k3 = derivs(s3);
var s4 = s.map(function(v,i){return v+k3[i]*dt;});
var k4 = derivs(s4);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
function energy(s) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var KE = 0.5*m1*l1*l1*w1*w1 + 0.5*m2*(l1*l1*w1*w1 + l2*l2*w2*w2
+ 2*l1*l2*w1*w2*Math.cos(t1-t2));
var PE = -(m1+m2)*g*l1*Math.cos(t1) - m2*g*l2*Math.cos(t2);
return {ke: KE, pe: PE, total: KE + PE};
}
function animate() {
for (var step = 0; step < 3; step++) state = rk4(state, 0.05);
var e = energy(state);
if (initE === null) initE = e.total;
var keRatio = Math.max(0, Math.min(1, e.ke / (Math.abs(e.ke) + Math.abs(e.pe) + 0.001)));
var x1 = ox + l1*Math.sin(state[0]);
var y1 = oy + l1*Math.cos(state[0]);
var x2 = x1 + l2*Math.sin(state[1]);
var y2 = y1 + l2*Math.cos(state[1]);
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0, 0, c.width, c.height);
// Arms colored by energy
var armHue = keRatio * 30 + (1 - keRatio) * 220;
ctx.strokeStyle = 'hsl(' + armHue + ', 70%, 50%)';
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
// Bobs
ctx.fillStyle = 'hsl(' + (keRatio * 30) + ', 90%, 60%)';
ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur = 12;
ctx.beginPath(); ctx.arc(x1, y1, 9, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'hsl(' + ((1-keRatio) * 220) + ', 90%, 60%)';
ctx.shadowColor = ctx.fillStyle;
ctx.beginPath(); ctx.arc(x2, y2, 7, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
// Energy bar background
var barY = c.height - 50, barW = c.width - 80, barH = 16;
ctx.fillStyle = '#1a1a2e'; ctx.fillRect(40, barY, barW, barH);
// KE portion (orange)
ctx.fillStyle = '#ff6b35';
ctx.fillRect(40, barY, barW * keRatio, barH);
// PE portion (blue)
ctx.fillStyle = '#4a9eff';
ctx.fillRect(40 + barW * keRatio, barY, barW * (1 - keRatio), barH);
// Labels
ctx.font = '11px monospace'; ctx.fillStyle = '#888';
ctx.fillText('KE (kinetic)', 40, barY - 5);
ctx.fillText('PE (potential)', 40 + barW - 90, barY - 5);
// Energy drift indicator
var drift = Math.abs((e.total - initE) / Math.abs(initE)) * 100;
ctx.fillStyle = drift < 0.1 ? '#4ecdc4' : '#ff6b6b';
ctx.fillText('Energy drift: ' + drift.toFixed(4) + '%', 40, c.height - 14);
requestAnimationFrame(animate);
}
animate();
Watch the energy bar oscillate between kinetic (orange) and potential (blue). When the bobs are high and slow, potential energy dominates. When they swing fast through the bottom, kinetic energy peaks. The total energy drift indicator at the bottom shows how well RK4 conserves energy — typically under 0.01% even after thousands of frames. If you switch to simple Euler integration, you would see that number climb rapidly, which is why RK4 matters.
Example 4: Phase space portrait
Phase space is a powerful tool from dynamical systems theory. Instead of plotting positions over time, we plot angle vs. angular velocity. For a chaotic system, the trajectory fills a region of phase space without repeating — unlike periodic systems that trace closed loops. This is the visual fingerprint of chaos.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 120, l2 = 100;
var state = [Math.PI * 0.99, Math.PI * 0.99, 0, 0];
var frame = 0;
function derivs(s) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var dt=t1-t2, den=2*m1+m2-m2*Math.cos(2*dt);
var a1=(-g*(2*m1+m2)*Math.sin(t1)-m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*m2*(w2*w2*l2+w1*w1*l1*Math.cos(dt)))/(l1*den);
var a2=(2*Math.sin(dt)*(w1*w1*l1*(m1+m2)+g*(m1+m2)*Math.cos(t1)
+w2*w2*l2*m2*Math.cos(dt)))/(l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1=derivs(s);
var s2=s.map(function(v,i){return v+k1[i]*dt/2;});
var k2=derivs(s2);
var s3=s.map(function(v,i){return v+k2[i]*dt/2;});
var k3=derivs(s3);
var s4=s.map(function(v,i){return v+k3[i]*dt;});
var k4=derivs(s4);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
// Draw axes
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0,0,c.width,c.height);
ctx.strokeStyle = '#223'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(c.width/2,20); ctx.lineTo(c.width/2,c.height-20); ctx.stroke();
ctx.beginPath(); ctx.moveTo(20,c.height/2); ctx.lineTo(c.width-20,c.height/2); ctx.stroke();
ctx.fillStyle = '#445'; ctx.font = '11px monospace';
ctx.fillText('theta2', c.width/2 + 5, 30);
ctx.fillText('omega2 (angular velocity)', c.width - 200, c.height/2 - 8);
function animate() {
for (var step = 0; step < 5; step++) {
state = rk4(state, 0.03);
frame++;
// Normalize angle to [-PI, PI]
var angle = ((state[1] % (2*Math.PI)) + 3*Math.PI) % (2*Math.PI) - Math.PI;
var omega = state[3];
var px = c.width/2 + angle * (c.width / (2 * Math.PI)) * 0.8;
var py = c.height/2 - omega * 12;
var hue = (frame * 0.04) % 360;
ctx.fillStyle = 'hsla(' + hue + ', 85%, 60%, 0.6)';
ctx.fillRect(px - 0.8, py - 0.8, 1.6, 1.6);
}
requestAnimationFrame(animate);
}
c.addEventListener('click', function() {
state = [Math.PI * (0.5+Math.random()), Math.PI * (0.5+Math.random()), 0, 0];
frame = 0;
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0,0,c.width,c.height);
ctx.strokeStyle = '#223'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(c.width/2,20); ctx.lineTo(c.width/2,c.height-20); ctx.stroke();
ctx.beginPath(); ctx.moveTo(20,c.height/2); ctx.lineTo(c.width-20,c.height/2); ctx.stroke();
ctx.fillStyle = '#445'; ctx.font = '11px monospace';
ctx.fillText('theta2', c.width/2+5, 30);
ctx.fillText('omega2 (angular velocity)', c.width-200, c.height/2-8);
});
animate();
The horizontal axis represents θ2 (the angle of the second pendulum, normalized to [-π, π]), and the vertical axis represents ω2 (its angular velocity). For high-energy chaotic motion, the trajectory fills a complex region — the hallmark of a strange attractor. The color progression from cool to warm shows the time evolution. If you start with low energy (small angles), you will see a more regular, elliptical pattern instead. Click to try different initial conditions and compare the phase portraits.
Example 5: Butterfly effect — two pendulums side by side
The butterfly effect is the defining property of chaos: tiny differences in initial conditions grow exponentially over time. This example runs two double pendulums simultaneously, starting with angles that differ by just 0.0001 radians (about 0.006 degrees). Watch them begin in perfect sync and then rapidly diverge.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 100, l2 = 80;
var stateA = [Math.PI * 0.8, Math.PI * 0.7, 0, 0];
var stateB = [Math.PI * 0.8 + 0.0001, Math.PI * 0.7, 0, 0];
var ox = c.width / 2, oy = 130;
var frame = 0;
function derivs(s) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var dt=t1-t2, den=2*m1+m2-m2*Math.cos(2*dt);
var a1=(-g*(2*m1+m2)*Math.sin(t1)-m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*m2*(w2*w2*l2+w1*w1*l1*Math.cos(dt)))/(l1*den);
var a2=(2*Math.sin(dt)*(w1*w1*l1*(m1+m2)+g*(m1+m2)*Math.cos(t1)
+w2*w2*l2*m2*Math.cos(dt)))/(l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1=derivs(s);
var s2=s.map(function(v,i){return v+k1[i]*dt/2;});
var k2=derivs(s2);
var s3=s.map(function(v,i){return v+k2[i]*dt/2;});
var k3=derivs(s3);
var s4=s.map(function(v,i){return v+k3[i]*dt;});
var k4=derivs(s4);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
function drawPendulum(s, color1, color2, alpha) {
var x1 = ox + l1*Math.sin(s[0]), y1 = oy + l1*Math.cos(s[0]);
var x2 = x1 + l2*Math.sin(s[1]), y2 = y1 + l2*Math.cos(s[1]);
ctx.globalAlpha = alpha;
ctx.strokeStyle = '#334'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
ctx.fillStyle = color1;
ctx.beginPath(); ctx.arc(x1, y1, 7, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = color2;
ctx.beginPath(); ctx.arc(x2, y2, 6, 0, Math.PI*2); ctx.fill();
ctx.globalAlpha = 1;
}
function animate() {
for (var step = 0; step < 3; step++) {
stateA = rk4(stateA, 0.05);
stateB = rk4(stateB, 0.05);
frame++;
}
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0, 0, c.width, c.height);
drawPendulum(stateA, '#ff6b6b', '#ff9999', 0.9);
drawPendulum(stateB, '#4ecdc4', '#7eddd6', 0.9);
// Angle difference
var diff = Math.abs(stateA[1] - stateB[1]);
var seconds = (frame * 0.05).toFixed(1);
ctx.fillStyle = '#666'; ctx.font = '12px monospace';
ctx.fillText('Red: theta1 = ' + (stateA[0] * 180 / Math.PI).toFixed(4) + ' deg', 15, c.height - 65);
ctx.fillText('Teal: theta1 = ' + (stateB[0] * 180 / Math.PI).toFixed(4) + ' deg', 15, c.height - 48);
ctx.fillStyle = diff > 0.1 ? '#ff6b6b' : '#4ecdc4';
ctx.fillText('Angle difference: ' + diff.toFixed(6) + ' rad', 15, c.height - 28);
ctx.fillStyle = '#445';
ctx.fillText('Time: ' + seconds + 's | Initial diff: 0.0001 rad', 15, c.height - 10);
// Pivot
ctx.fillStyle = '#888';
ctx.beginPath(); ctx.arc(ox, oy, 4, 0, Math.PI*2); ctx.fill();
requestAnimationFrame(animate);
}
c.addEventListener('click', function() {
var a = Math.PI * (0.4 + Math.random() * 1.2);
var b = Math.PI * (0.4 + Math.random() * 1.2);
stateA = [a, b, 0, 0];
stateB = [a + 0.0001, b, 0, 0];
frame = 0;
});
animate();
For the first few seconds, both pendulums appear identical — the red and teal bobs overlap perfectly. Then tiny numerical differences accumulate exponentially. Within 10-20 seconds, the two pendulums are moving in completely unrelated patterns. The angle difference display at the bottom shows this divergence quantitatively: it starts at 0.0001 radians and grows to several radians. This exponential divergence rate is characterized by the Lyapunov exponent — a positive value confirms the system is chaotic.
Example 6: Pendulum array
One of the most mesmerizing visualizations in physics: launch 16 pendulums with initial angles spaced by tiny increments. They start in near-perfect sync, creating a wave-like pattern, then gradually break apart into individual chaotic trajectories. It is a visual poem about determinism and divergence.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var N = 16, g = 9.81, m1 = 2, m2 = 1, l1 = 100, l2 = 80;
var ox = c.width / 2, oy = 120;
var pendulums = [];
function initPendulums() {
pendulums = [];
var baseAngle = Math.PI * (0.5 + Math.random() * 0.8);
for (var i = 0; i < N; i++) {
pendulums.push([baseAngle + i * 0.002, Math.PI * 0.7, 0, 0]);
}
}
initPendulums();
function derivs(s) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var dt=t1-t2, den=2*m1+m2-m2*Math.cos(2*dt);
var a1=(-g*(2*m1+m2)*Math.sin(t1)-m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*m2*(w2*w2*l2+w1*w1*l1*Math.cos(dt)))/(l1*den);
var a2=(2*Math.sin(dt)*(w1*w1*l1*(m1+m2)+g*(m1+m2)*Math.cos(t1)
+w2*w2*l2*m2*Math.cos(dt)))/(l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1=derivs(s);
var s2=s.map(function(v,i){return v+k1[i]*dt/2;});
var k2=derivs(s2);
var s3=s.map(function(v,i){return v+k2[i]*dt/2;});
var k3=derivs(s3);
var s4=s.map(function(v,i){return v+k3[i]*dt;});
var k4=derivs(s4);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
function animate() {
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0, 0, c.width, c.height);
for (var i = 0; i < N; i++) {
for (var step = 0; step < 3; step++) pendulums[i] = rk4(pendulums[i], 0.05);
var s = pendulums[i];
var x1 = ox + l1*Math.sin(s[0]), y1 = oy + l1*Math.cos(s[0]);
var x2 = x1 + l2*Math.sin(s[1]), y2 = y1 + l2*Math.cos(s[1]);
var hue = (i / N) * 300;
ctx.globalAlpha = 0.6;
ctx.strokeStyle = 'hsl(' + hue + ', 60%, 40%)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
ctx.globalAlpha = 0.85;
ctx.fillStyle = 'hsl(' + hue + ', 80%, 60%)';
ctx.beginPath(); ctx.arc(x2, y2, 4, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
// Pivot
ctx.fillStyle = '#aaa';
ctx.beginPath(); ctx.arc(ox, oy, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#445'; ctx.font = '11px monospace';
ctx.fillText(N + ' pendulums | spacing: 0.002 rad | click to reset', 12, c.height - 10);
requestAnimationFrame(animate);
}
c.addEventListener('click', function() { initPendulums(); });
animate();
In the first moments, all 16 pendulums swing together like a colorful wave — a rainbow of bobs tracing nearly identical arcs. But chaos is patient, not absent. The 0.002-radian spacing between each pendulum's starting angle is amplified exponentially, and within 15-30 seconds the array dissolves into 16 independent chaotic trajectories. The moment of transition — when order breaks into chaos — is hauntingly beautiful every time. Click to restart and notice that the duration of the coherent phase varies with the starting angle.
Example 7: Damped double pendulum with spring
Real pendulums have friction. This example adds damping (proportional to angular velocity) and a weak spring force pulling back to vertical. The pendulum starts chaotic, but gradually loses energy and spirals toward rest. The trail reveals this transition from chaos to order — wide chaotic loops that tighten into a decaying spiral.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, m1 = 2, m2 = 1, l1 = 110, l2 = 90;
var damping = 0.15, spring = 0.3;
var state = [Math.PI * 0.95, Math.PI * 0.85, 0, 0];
var ox = c.width / 2, oy = 140;
var frame = 0, prevX = 0, prevY = 0;
function derivs(s) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var dt=t1-t2, den=2*m1+m2-m2*Math.cos(2*dt);
var a1=(-g*(2*m1+m2)*Math.sin(t1)-m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*m2*(w2*w2*l2+w1*w1*l1*Math.cos(dt)))/(l1*den);
var a2=(2*Math.sin(dt)*(w1*w1*l1*(m1+m2)+g*(m1+m2)*Math.cos(t1)
+w2*w2*l2*m2*Math.cos(dt)))/(l2*den);
// Add damping and spring
a1 -= damping * w1 + spring * t1;
a2 -= damping * w2 + spring * t2;
return [w1, w2, a1, a2];
}
function rk4(s, dt) {
var k1=derivs(s);
var s2=s.map(function(v,i){return v+k1[i]*dt/2;});
var k2=derivs(s2);
var s3=s.map(function(v,i){return v+k2[i]*dt/2;});
var k3=derivs(s3);
var s4=s.map(function(v,i){return v+k3[i]*dt;});
var k4=derivs(s4);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0,0,c.width,c.height);
function animate() {
for (var step = 0; step < 3; step++) state = rk4(state, 0.05);
var x1 = ox + l1*Math.sin(state[0]), y1 = oy + l1*Math.cos(state[0]);
var x2 = x1 + l2*Math.sin(state[1]), y2 = y1 + l2*Math.cos(state[1]);
// Fading background
ctx.fillStyle = 'rgba(10, 10, 26, 0.04)';
ctx.fillRect(0, 0, c.width, c.height);
// Trail
if (frame > 0) {
var speed = Math.sqrt(state[2]*state[2] + state[3]*state[3]);
var hue = Math.min(speed * 15, 1) * 60 + 180;
ctx.strokeStyle = 'hsla(' + hue + ', 80%, 55%, 0.7)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(prevX, prevY); ctx.lineTo(x2, y2); ctx.stroke();
}
prevX = x2; prevY = y2;
// Draw pendulum (on top of trail every 10 frames)
if (frame % 10 === 0) {
ctx.strokeStyle = '#445'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
ctx.fillStyle = '#ff6b6b';
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#4ecdc4';
ctx.beginPath(); ctx.arc(x2, y2, 4, 0, Math.PI*2); ctx.fill();
}
frame++;
requestAnimationFrame(animate);
}
c.addEventListener('click', function() {
state = [Math.PI*(0.5+Math.random()*0.8), Math.PI*(0.5+Math.random()*0.8), 0, 0];
frame = 0;
ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0,0,c.width,c.height);
});
animate();
The trail color is mapped to speed: fast motion glows warm (cyan through green), while slow motion shifts toward blue and violet. Early in the simulation you see wide, saturated arcs — the pendulum is energetic and chaotic. As damping steals energy, the arcs tighten and the colors cool. Eventually the trail converges to a tiny spiral at the rest position. This single image captures the entire journey from chaos to equilibrium — a natural transition that happens in every real-world pendulum.
Example 8: Generative chaos art
Our final example combines everything into a generative art piece. Multiple double pendulums with different parameters paint trails using additive blending (the "lighter" composite operation), creating luminous interference patterns where paths overlap. Click to generate a new random composition.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#000';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var g = 9.81, numPendulums = 5;
var pends = [], prevPos = [];
var ox = c.width / 2, oy = c.height / 2;
var frame = 0;
function init() {
pends = []; prevPos = []; frame = 0;
ctx.fillStyle = '#050510'; ctx.fillRect(0, 0, c.width, c.height);
for (var i = 0; i < numPendulums; i++) {
pends.push({
state: [Math.PI*(0.3+Math.random()*1.4), Math.PI*(0.3+Math.random()*1.4),
(Math.random()-0.5)*2, (Math.random()-0.5)*2],
m1: 1.5 + Math.random(), m2: 0.5 + Math.random(),
l1: 60 + Math.random() * 60, l2: 50 + Math.random() * 50,
hue: (i / numPendulums) * 360
});
prevPos.push({x: 0, y: 0});
}
}
init();
function derivs(s, p) {
var t1=s[0],t2=s[1],w1=s[2],w2=s[3];
var dt=t1-t2, den=2*p.m1+p.m2-p.m2*Math.cos(2*dt);
var a1=(-g*(2*p.m1+p.m2)*Math.sin(t1)-p.m2*g*Math.sin(t1-2*t2)
-2*Math.sin(dt)*p.m2*(w2*w2*p.l2+w1*w1*p.l1*Math.cos(dt)))/(p.l1*den);
var a2=(2*Math.sin(dt)*(w1*w1*p.l1*(p.m1+p.m2)+g*(p.m1+p.m2)*Math.cos(t1)
+w2*w2*p.l2*p.m2*Math.cos(dt)))/(p.l2*den);
return [w1, w2, a1, a2];
}
function rk4(s, dt, p) {
var k1=derivs(s,p);
var s2=s.map(function(v,i){return v+k1[i]*dt/2;});
var k2=derivs(s2,p);
var s3=s.map(function(v,i){return v+k2[i]*dt/2;});
var k3=derivs(s3,p);
var s4=s.map(function(v,i){return v+k3[i]*dt;});
var k4=derivs(s4,p);
return s.map(function(v,i){return v+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*dt/6;});
}
function animate() {
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < numPendulums; i++) {
var p = pends[i];
for (var step = 0; step < 4; step++) p.state = rk4(p.state, 0.04, p);
var x1 = ox + p.l1*Math.sin(p.state[0]);
var y1 = oy + p.l1*Math.cos(p.state[0]);
var x2 = x1 + p.l2*Math.sin(p.state[1]);
var y2 = y1 + p.l2*Math.cos(p.state[1]);
if (frame > 0) {
var hue = (p.hue + frame * 0.15) % 360;
ctx.strokeStyle = 'hsla(' + hue + ', 90%, 50%, 0.15)';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(prevPos[i].x, prevPos[i].y);
ctx.lineTo(x2, y2);
ctx.stroke();
}
prevPos[i] = {x: x2, y: y2};
}
ctx.globalCompositeOperation = 'source-over';
frame++;
if (frame < 3000) requestAnimationFrame(animate);
}
c.addEventListener('click', function() { init(); animate(); });
animate();
Five pendulums paint simultaneously with additive blending — where trails cross, the colors add together, creating bright white hotspots and unexpected color combinations. Each pendulum has randomized masses, arm lengths, initial angles, and initial velocities, so every click produces a unique artwork. The hue slowly rotates over time, giving each trail a gradient quality. After 3000 frames the animation stops, leaving a finished composition. These pieces have the quality of abstract expressionist paintings — organic, layered, and full of motion energy frozen in place.
The mathematics of chaos
What makes the double pendulum chaotic in the mathematical sense? The key concept is the Lyapunov exponent, which measures the rate at which nearby trajectories in phase space diverge. If two initial conditions differ by a tiny amount δ, after time t the separation grows approximately as δ · eλt, where λ is the largest Lyapunov exponent. When λ is positive, the system is chaotic. For a double pendulum at high energy, λ is typically around 5-10 per second — meaning differences amplify by a factor of e (about 2.7) every fraction of a second.
Crucially, chaos is not randomness. The double pendulum is completely deterministic — given exact initial conditions, the trajectory is uniquely determined for all time. The unpredictability comes from our inability to measure initial conditions with infinite precision. Any finite measurement error, no matter how small, eventually grows large enough to make prediction impossible. This is "deterministic chaos": the equations know exactly what will happen, but we cannot extract that knowledge from approximate initial data.
This insight was first understood by Henri Poincaré in the 1890s while studying the three-body problem, and rediscovered by Edward Lorenz in 1963 when he noticed that rounding a number from 0.506127 to 0.506 in his weather simulation produced a completely different forecast. Lorenz's famous question — "Does the flap of a butterfly's wings in Brazil set off a tornado in Texas?" — gave the butterfly effect its name. The double pendulum is one of the simplest physical systems that exhibits this phenomenon, which is why it has become the standard demonstration of chaos in physics education.
Double pendulums in nature and art
The mathematics of coupled pendulums appears far beyond the physics classroom:
- Robotic arms: industrial robots are essentially chains of linked pendulums with motors at each joint — understanding chaotic dynamics helps engineers design stable control systems
- Human biomechanics: your leg during walking is a double pendulum (thigh + shin), and the passive dynamics of gait exploit pendulum-like energy transfer to minimize muscle effort
- Crane dynamics: a load hanging from a crane on a cable behaves as a pendulum, and if the crane arm also swings, you get double-pendulum dynamics that operators must carefully manage
- Juggling: the arcs of clubs during a juggling pattern follow multi-pendulum trajectories, and skilled jugglers intuitively exploit the sensitivity to initial conditions
- Bacterial flagella: the whip-like motion of bacterial flagellum motors creates coupled rotational dynamics analogous to linked pendulums
- Poincaré and the three-body problem: the discovery that three gravitationally interacting bodies produce chaotic orbits — essentially the same mathematics as multi-pendulum systems — led to the birth of chaos theory itself
- Kinetic sculpture: artists like Alexander Calder created mobiles that are multi-pendulum systems, and modern kinetic art installations deliberately exploit chaotic dynamics for visual complexity
Where to go from here
- Explore gravity simulation to see chaos in orbital mechanics — the three-body problem is the gravitational cousin of the double pendulum, with equally beautiful and unpredictable trajectories
- Apply these pendulum principles to kinetic art — build swinging, spinning compositions that harness pendulum physics for organic motion
- Combine chaos mathematics with mathematical art techniques like fractals, golden spirals, and L-systems to create hybrid generative pieces
- Simulate the Lorenz attractor — the original chaotic system discovered by Edward Lorenz, which produces the iconic butterfly-shaped strange attractor
- Add double pendulums to a fluid simulation environment and watch chaotic pendulum motion create turbulent flow patterns in surrounding fluid
- Use particle systems to emit particles from the pendulum bobs — each particle inherits the bob's chaotic velocity, creating explosion-like trails that map the attractor's structure
- On Lumitree, chaos blooms are generated by double pendulums with randomized parameters — each seed grows a unique chaotic flower that never repeats, living proof that simple rules create infinite variety