<
<
<
<Chaotic Double Pendulum
<
body {
margin: 0;
overflow: hidden;
background-color: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: sans-serif;
}
canvas {
display: block;
}
#ui {
position: absolute;
top: 20px;
left: 20px;
color: rgba(255, 255, 255, 0.7);
pointer-events: none;
user-select: none;
}
<
Double Pendulum Simulation<
<Chaotic motion via Runge-Kutta integration
<
<
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width, height;
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
}
window.addEventListener('resize', resize);
resize();
// Physics Constants
const g = 0.98; // gravity (scaled for simulation speed)
const m1 = 10; // mass 1
const m2 = 10; // mass 2
const l1 = 150; // length 1
const l2 = 150; // length 2
// State: [theta1, theta2, omega1, omega2]
let state = [
Math.PI / 2, // theta1
Math.PI / 2, // theta2
0, // omega1
0 // omega2
];
// Trail history
const trail = [];
const MAX_TRAIL = 500;
// Derivatives function for RK4
// Equations of motion for double pendulum:
// Using Lagrangian mechanics
function derivatives(s) {
const [t1, t2, w1, w2] = s;
// Denominator term common to many parts
const delta = t1 - t2;
const den1 = (2 * m1 + m2 - m2 * Math.cos(2 * t1 - 2 * t2));
// Angular acceleration for theta 1
const dw1 = (
-g * (2 * m1 + m2) * Math.sin(t1)
- m2 * g * Math.sin(t1 - 2 * t2)
- 2 * Math.sin(delta) * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * Math.cos(delta))
) / (l1 * den1);
// Angular acceleration for theta 2
const dw2 = (
2 * Math.sin(delta) * (
w1 * w1 * l1 * (m1 + m2)
+ g * (m1 + m2) * Math.cos(t1)
+ w2 * w2 * l2 * m2 * Math.cos(delta)
)
) / (l2 * den1);
// Note: Simplified/Standard forms of equations are actually quite complex.
// Let's use the standard precise form for double pendulum:
const num1 = -g * (2 * m1 + m2) * Math.sin(t1);
const num2 = -m2 * g * Math.sin(t1 - 2 * t2);
const num3 = -2 * Math.sin(t1 - t2) * m2;
const num4 = w2 * w2 * l2 + w1 * w1 * l1 * Math.cos(t1 - t2);
const den = l1 * (2 * m1 + m2 - m2 * Math.cos(2 * t1 - 2 * t2));
const d_w1 = (num1 + num2 + num3 * num4) / den;
const num5 = 2 * Math.sin(t1 - t2);
const num6 = w1 * w1 * l1 * (m1 + m2);
const num7 = g * (m1 + m2) * Math.cos(t1);
const num8 = w2 * w2 * l2 * m2 * Math.cos(t1 - t2);
const den2 = l2 * (2 * m1 + m2 - m2 * Math.cos(2 * t1 - 2 * t2));
const d_w2 = (num5 * (num6 + num7 + num8)) / den2;
return [w1, w2, d_w1, d_w2];
}
// RK4 Integration step
function rk4Step(s, dt) {
const k1 = derivatives(s);
const s2 = s.map((val, i) => val + k1[i] * dt / 2);
const k2 = derivatives(s2);
const s3 = s.map((val, i) => val + k2[i] * dt / 2);
const k3 = derivatives(s3);
const s4 = s.map((val, i) => val + k3[i] * dt);
const k4 = derivatives(s4);
return s.map((val, i) => val + (dt / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]));
}
function update() {
// Perform multiple sub-steps for stability
const subSteps = 5;
const dt = 0.15 / subSteps;
for (let i = 0; i << sub subSteps; i++) {
state = rk4Step(state, dt);
}
const [t1, t2] = state;
const x1 = l1 * Math.sin(t1);
const y1 = l1 * Math.cos(t1);
const x2 = x1 + l2 * Math.sin(t2);
const y2 = y1 + l2 * Math.cos(t2);
trail.push({ x: x2, y: y2, hue: (Date.now() / 20) % 360 });
if (trail.length > MAX_TRAIL) {
trail.shift();
}
}
function draw() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, width, height);
const cx = width / 2;
const cy = height / 3;
const [t1, t2] = state;
const x1 = cx + l1 * Math.sin(t1);
const y1 = cy + l1 * Math.cos(t1);
const x2 = x1 + l2 * Math.sin(t2);
const y2 = y1 + l2 * Math.cos(t2);
// Draw trail
if (trail.length > 1) {
ctx.beginPath();
ctx.lineWidth = 2;
for (let i = 0; i << trail trail.length; i++) {
const p = trail[i];
const opacity = i / trail.length;
// Map trail relative coordinates to screen
const tx = cx + p.x;
const ty = cy + p.y;
if (i === 0) ctx.moveTo(tx, ty);
else ctx.lineTo(tx, ty);
// To get individual colors per segment, we can't use one moveTo/lineTo easily
// without multiple stroke calls, but for a single fading trail:
}
// Actually, for a colorful fading trail, we should draw segments
for (let i = 1; i << trail trail.length; i++) {
const p1 = trail[i-1];
const p2 = trail[i];
const opacity = i / trail.length;
ctx.strokeStyle = `hsla(${p2.hue}, 100%, 50%, ${opacity})`;
ctx.lineWidth = opacity * 4;
ctx.beginPath();
ctx.moveTo(cx + p1.x, cy + p1.y);
ctx.lineTo(cx + p2.x, cy + p2.y);
ctx.stroke();
}
}
// Draw arms
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// Draw masses
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x1, y1, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x2, y2, 8, 0, Math.PI * 2);
ctx.fill();
// Draw origin
ctx.fillStyle = '#555';
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fill();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();