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