
Double Pendulum Simulation
Prompt
Develop an interactive double pendulum simulation for browser execution.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Interactive Double Pendulum</title> <style> html,body{ margin:0; height:100%; background:#111; color:#eee; font-family:Arial,Helvetica,sans-serif; user-select:none; } #ui{ position:absolute; top:10px; left:10px; background:rgba(0,0,0,.7); padding:10px 14px; border-radius:6px; font-size:14px; } #ui input[type=range]{ width:120px; } #ui button{ margin:4px 2px; padding:4px 10px; background:#444; color:#eee; border:none; border-radius:4px; cursor:pointer; } #ui button:hover{background:#666;} canvas{ display:block; width:100%; height:100%; } a{color:#9cf} </style> </head> <body> <div id="ui"> <div> θ₁: <input id="theta1" type="range" min="-180" max="180" step="1" value="120"> <span id="theta1Val">120°</span> </div> <div> θ₂: <input id="theta2" type="range" min="-180" max="180" step="1" value=" -10"> <span id="theta2Val">-10°</span> </div> <div> L₁: <input id="len1" type="range" min="50" max="250" step="1" value="160"> <span id="len1Val">160 px</span> </div> <div> L₂: <input id="len2" type="range" min="50" max="250" step="1" value="160"> <span id="len2Val">160 px</span> </div> <div> m₁: <input id="mass1" type="range" min="1" max="10" step="1" value="4"> <span id="mass1Val">4 kg</span> </div> <div> m₂: <input id="mass2" type="range" min="1" max="10" step="1" value="4"> <span id="mass2Val">4 kg</span> </div> <div style="margin-top:6px;"> <button id="startBtn">Start</button> <button id="pauseBtn">Pause</button> <button id="resetBtn">Reset</button> <label style="margin-left:8px;cursor:pointer;"> <input id="traceChk" type="checkbox" checked> Trace </label> </div> </div> <canvas id="c"></canvas> <script> (function(){ const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); let W, H, originX, originY; function resize(){ W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; originX = W/2; originY = H/4; } window.addEventListener('resize',resize); resize(); // UI elements const uiElements = {}; ['theta1','theta2','len1','len2','mass1','mass2'].forEach(id=>{ uiElements[id] = document.getElementById(id); uiElements[id+'Val'] = document.getElementById(id+'Val'); }); const traceChk = document.getElementById('traceChk'); const startBtn = document.getElementById('startBtn'); const pauseBtn = document.getElementById('pauseBtn'); const resetBtn = document.getElementById('resetBtn'); // Simulation state let params = {}; let running = false; let theta1,theta2,omega1,omega2,l1,l2,m1,m2; const g = 9.81; const dt = 1/60; // simulation time step (s) const scaleTime = 1; // simulation speed multiplier let tracePoints = []; function updateParamsFromUI(){ theta1 = toRad(+uiElements.theta1.value); theta2 = toRad(+uiElements.theta2.value); omega1 = omega2 = 0; l1 = +uiElements.len1.value; l2 = +uiElements.len2.value; m1 = +uiElements.mass1.value; m2 = +uiElements.mass2.value; } function refreshLabels(){ uiElements.theta1Val.textContent = uiElements.theta1.value + '°'; uiElements.theta2Val.textContent = uiElements.theta2.value + '°'; uiElements.len1Val.textContent = uiElements.len1.value + ' px'; uiElements.len2Val.textContent = uiElements.len2.value + ' px'; uiElements.mass1Val.textContent = uiElements.mass1.value + ' kg'; uiElements.mass2Val.textContent = uiElements.mass2.value + ' kg'; } Object.values(uiElements).forEach(el=>{ if(el.tagName==='INPUT' && el.type==='range'){ el.addEventListener('input',()=>{ refreshLabels(); if(!running) updateParamsFromUI(); }); } }); startBtn.onclick = ()=>{ running = true; }; pauseBtn.onclick = ()=>{ running = false; }; resetBtn.onclick = ()=>{ running = false; updateParamsFromUI(); tracePoints = []; clearCanvas(); drawPendulum(); }; function toRad(deg){ return deg*Math.PI/180; } function clearCanvas(){ ctx.fillStyle = '#111'; ctx.fillRect(0,0,W,H); } function drawPendulum(){ clearCanvas(); const x1 = originX + l1*Math.sin(theta1); const y1 = originY + l1*Math.cos(theta1); const x2 = x1 + l2*Math.sin(theta2); const y2 = y1 + l2*Math.cos(theta2); if(traceChk.checked){ tracePoints.push([x2,y2]); ctx.strokeStyle = 'rgba(180,180,255,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); for(let i=0;i<tracePoints.length;i++){ const [tx,ty] = tracePoints[i]; if(i===0) ctx.moveTo(tx,ty); else ctx.lineTo(tx,ty); } ctx.stroke(); }else{ tracePoints=[]; } ctx.lineWidth = 2; ctx.strokeStyle = '#eee'; ctx.fillStyle = '#eee'; // Rod 1 ctx.beginPath(); ctx.moveTo(originX,originY); ctx.lineTo(x1,y1); ctx.stroke(); // Rod 2 ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); // Mass 1 ctx.beginPath(); ctx.arc(x1,y1,Math.max(8, m1*2),0,Math.PI*2); ctx.fill(); // Mass 2 ctx.beginPath(); ctx.arc(x2,y2,Math.max(8, m2*2),0,Math.PI*2); ctx.fill(); } function rk4Step(){ const state = [theta1,theta2,omega1,omega2]; const derivs = (st)=>{ const [th1,th2,om1,om2] = st; // Equations from https://www.myphysicslab.com/pendulum/double-pendulum-en.html const delta = th2 - th1; const denom1 = (m1 + m2)*l1 - m2*l1*Math.cos(delta)*Math.cos(delta); const denom2 = (l2/l1)*denom1; const a1 = (m2*l1*om1*om1*Math.sin(delta)*Math.cos(delta) + m2*g*Math.sin(th2)*Math.cos(delta) + m2*l2*om2*om2*Math.sin(delta) - (m1 + m2)*g*Math.sin(th1)) / denom1; const a2 = (-m2*l2*om2*om2*Math.sin(delta)*Math.cos(delta) + (m1 + m2)*(g*Math.sin(th1)*Math.cos(delta) - l1*om1*om1*Math.sin(delta) - g*Math.sin(th2))) / denom2; return [om1, om2, a1, a2]; }; function addVec(a,b,dtScale=1){ return a.map((v,i)=> v + b[i]*dtScale); } const k1 = derivs(state); const k2 = derivs(addVec(state,k1,dt*0.5)); const k3 = derivs(addVec(state,k2,dt*0.5)); const k4 = derivs(addVec(state,k3,dt)); const ds = state.map((v,i)=> (dt/6)*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])); theta1 += ds[0]*scaleTime; theta2 += ds[1]*scaleTime; omega1 += ds[2]*scaleTime; omega2 += ds[3]*scaleTime; } function loop(){ if(running) rk4Step(); drawPendulum(); requestAnimationFrame(loop); } // Init refreshLabels(); updateParamsFromUI(); drawPendulum(); loop(); })(); </script> </body> </html> ```
Drag to resize
Here is an interactive double pendulum simulation. You can click and drag either of the bobs to set a new starting position and then release to see its chaotic motion. The path of the lower bob is traced to visualize the intricate patterns it creates. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Double Pendulum Simulation</title> <style> body { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f0f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #333; text-align: center; } #simulation-container { border: 2px solid #ccc; box-shadow: 0 4px 8px rgba(0,0,0,0.1); background-color: #fff; } p { margin: 0.5em 0; max-width: 600px; } </style> </head> <body> <h1>Interactive Double Pendulum</h1> <p>Click and drag either bob to set a new starting position. Release the mouse to start the simulation.</p> <div id="simulation-container"> <canvas id="pendulumCanvas" width="600" height="600"></canvas> </div> <script> document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('pendulumCanvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const pivotX = width / 2; const pivotY = height / 2.5; // --- Simulation Parameters --- let g = 1; // Gravity let l1 = 120; // Length of the first arm let l2 = 120; // Length of the second arm let m1 = 15; // Mass of the first bob let m2 = 15; // Mass of the second bob // --- State Variables --- let theta1 = Math.PI / 2; // Angle of the first arm let theta2 = Math.PI / 1.5; // Angle of the second arm let omega1 = 0; // Angular velocity of the first arm let omega2 = 0; // Angular velocity of the second arm // --- RK4 Integrator Time Step --- // A smaller h increases accuracy but decreases performance. const h = 0.1; // --- Interaction Variables --- let isDragging = false; let draggedBob = null; // 1 for the first bob, 2 for the second // --- Trace Path --- let tracePoints = []; const maxTracePoints = 800; // --- Helper Functions --- function getCanvasMousePos(event) { const rect = canvas.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; } // --- Physics Core (Runge-Kutta 4th Order Integration) --- function getDerivatives(t1, t2, w1, w2) { const d = t2 - t1; // Equations of motion derived from the Lagrangian const den1 = (m1 + m2) * l1 - m2 * l1 * Math.cos(d) * Math.cos(d); const den2 = (l2 / l1) * den1; const alpha1 = (m2 * l1 * w1 * w1 * Math.sin(d) * Math.cos(d) + m2 * g * Math.sin(t2) * Math.cos(d) + m2 * l2 * w2 * w2 * Math.sin(d) - (m1 + m2) * g * Math.sin(t1)) / den1; const alpha2 = (-m2 * l2 * w2 * w2 * Math.sin(d) * Math.cos(d) + (m1 + m2) * g * Math.sin(t1) * Math.cos(d) - (m1 + m2) * l1 * w1 * w1 * Math.sin(d) - (m1 + m2) * g * Math.sin(t2)) / den2; return { alpha1, alpha2 }; } function update() { if (isDragging) return; // RK4 integration for a more stable and accurate simulation // Step 1 let k1 = getDerivatives(theta1, theta2, omega1, omega2); // Step 2 let k2 = getDerivatives(theta1 + 0.5 * h * omega1, theta2 + 0.5 * h * omega2, omega1 + 0.5 * h * k1.alpha1, omega2 + 0.5 * h * k1.alpha2); // Step 3 let k3 = getDerivatives(theta1 + 0.5 * h * (omega1 + 0.5 * h * k1.alpha1), theta2 + 0.5 * h * (omega2 + 0.5 * h * k1.alpha2), omega1 + 0.5 * h * k2.alpha1, omega2 + 0.5 * h * k2.alpha2); // Step 4 let k4 = getDerivatives(theta1 + h * (omega1 + h * k2.alpha1), theta2 + h * (omega2 + h * k2.alpha2), omega1 + h * k3.alpha1, omega2 + h * k3.alpha2); // Update angular velocities and positions omega1 += (h / 6) * (k1.alpha1 + 2 * k2.alpha1 + 2 * k3.alpha1 + k4.alpha1); omega2 += (h / 6) * (k1.alpha2 + 2 * k2.alpha2 + 2 * k3.alpha2 + k4.alpha2); theta1 += h * omega1; theta2 += h * omega2; } // --- Rendering Core --- function draw() { // Clear canvas ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); // Calculate bob positions const x1 = pivotX + l1 * Math.sin(theta1); const y1 = pivotY + l1 * Math.cos(theta1); const x2 = x1 + l2 * Math.sin(theta2); const y2 = y1 + l2 * Math.cos(theta2); // Add current position to trace path if (!isDragging) { tracePoints.push({ x: x2, y: y2 }); if (tracePoints.length > maxTracePoints) { tracePoints.shift(); } } // Draw trace path ctx.beginPath(); ctx.moveTo(tracePoints[0]?.x, tracePoints[0]?.y); for (let i = 1; i < tracePoints.length; i++) { const p = tracePoints[i]; ctx.lineTo(p.x, p.y); } ctx.strokeStyle = 'rgba(0, 150, 255, 0.4)'; ctx.lineWidth = 1; ctx.stroke(); // Draw arms ctx.beginPath(); ctx.moveTo(pivotX, pivotY); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#333'; ctx.lineWidth = 3; ctx.stroke(); // Draw pivot ctx.beginPath(); ctx.arc(pivotX, pivotY, 6, 0, 2 * Math.PI); ctx.fillStyle = '#333'; ctx.fill(); // Draw bobs ctx.beginPath(); ctx.arc(x1, y1, m1, 0, 2 * Math.PI); ctx.fillStyle = '#c0392b'; // Red ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(x2, y2, m2, 0, 2 * Math.PI); ctx.fillStyle = '#2980b9'; // Blue ctx.fill(); ctx.stroke(); } // --- Main Animation Loop --- function animate() { update(); draw(); requestAnimationFrame(animate); } // --- Event Handlers for Interactivity --- canvas.addEventListener('mousedown', (e) => { const pos = getCanvasMousePos(e); const x1 = pivotX + l1 * Math.sin(theta1); const y1 = pivotY + l1 * Math.cos(theta1); const x2 = x1 + l2 * Math.sin(theta2); const y2 = y1 + l2 * Math.cos(theta2); const distToBob1 = Math.sqrt((pos.x - x1) ** 2 + (pos.y - y1) ** 2); const distToBob2 = Math.sqrt((pos.x - x2) ** 2 + (pos.y - y2) ** 2); if (distToBob2 < m2 + 5) { // Check bob 2 first as it's "on top" isDragging = true; draggedBob = 2; } else if (distToBob1 < m1 + 5) { isDragging = true; draggedBob = 1; } if (isDragging) { tracePoints = []; // Clear trace on drag start } }); canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const pos = getCanvasMousePos(e); let x1, y1; if (draggedBob === 1) { // Update theta1 based on mouse position relative to pivot const dx = pos.x - pivotX; const dy = pos.y - pivotY; theta1 = Math.atan2(dx, dy); // atan2(x,y) gives angle from positive y-axis // Reset velocities omega1 = 0; omega2 = 0; } else if (draggedBob === 2) { // Update theta2 based on mouse position relative to bob 1 x1 = pivotX + l1 * Math.sin(theta1); y1 = pivotY + l1 * Math.cos(theta1); const dx = pos.x - x1; const dy = pos.y - y1; theta2 = Math.atan2(dx, dy); // Reset velocities omega1 = 0; omega2 = 0; } }); window.addEventListener('mouseup', () => { isDragging = false; draggedBob = null; }); // Start the simulation animate(); }); </script> </body> </html> ```
Drag to resize
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Interactive Double Pendulum Simulation</title> <style> body { background: #1e1e1e; color: #f8f8f2; font-family: sans-serif; margin: 0; display: flex; flex-direction: column; align-items: center; min-height: 100vh; user-select: none; } #controls { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; margin-bottom: 10px; background: #282c34cc; padding: 14px 20px; border-radius: 10px; margin-top: 18px; } label, button { font-size: 1rem; color: #fafafa; } input[type=range] { width: 90px; } canvas { background: #222; border: 2px solid #333; border-radius: 8px; margin-bottom: 24px; box-shadow: 0 2px 32px #000a; display: block; } #info { text-align: center; color: #aaa; font-size: 0.97em; margin-top: 10px; } button { background: #4f7cff; color: #fff; border: none; padding: 7px 19px; border-radius: 5px; cursor: pointer; transition: background .22s; } button:hover { background: #3050c7; } </style> </head> <body> <div id="controls"> <label>Length 1: <input id="l1" type="range" min="50" max="220" value="150"> <span id="l1_val">150</span> </label> <label>Length 2: <input id="l2" type="range" min="50" max="220" value="100"> <span id="l2_val">100</span> </label> <label>Mass 1: <input id="m1" type="range" min="5" max="50" value="20"> <span id="m1_val">20</span> </label> <label>Mass 2: <input id="m2" type="range" min="5" max="50" value="20"> <span id="m2_val">20</span> </label> <label>θ1: <input id="a1" type="range" min="-180" max="180" value="120"> <span id="a1_val">120</span>° </label> <label>θ2: <input id="a2" type="range" min="-180" max="180" value="-10"> <span id="a2_val">-10</span>° </label> <button id="reset">Reset</button> <button id="pause">Pause</button> </div> <canvas id="pendulum" width="700" height="540"></canvas> <div id="info"> <p>Interactive Double Pendulum. Drag the bobs to reposition.<br> Adjust parameters to experiment with different behaviors!</p> </div> <script> // Canvas and context const canvas = document.getElementById('pendulum'); const ctx = canvas.getContext('2d'); const w = canvas.width; const h = canvas.height; const origin = {x: w/2, y: 180}; // Controls const l1Slider = document.getElementById("l1"); const l2Slider = document.getElementById("l2"); const m1Slider = document.getElementById("m1"); const m2Slider = document.getElementById("m2"); const a1Slider = document.getElementById("a1"); const a2Slider = document.getElementById("a2"); const l1Val = document.getElementById("l1_val"); const l2Val = document.getElementById("l2_val"); const m1Val = document.getElementById("m1_val"); const m2Val = document.getElementById("m2_val"); const a1Val = document.getElementById("a1_val"); const a2Val = document.getElementById("a2_val"); // Initial parameters function deg(r) { return r * (180/Math.PI);} function rad(d) { return d * (Math.PI/180);} let params = { l1: parseFloat(l1Slider.value), l2: parseFloat(l2Slider.value), m1: parseFloat(m1Slider.value), m2: parseFloat(m2Slider.value), a1: rad(parseFloat(a1Slider.value)), a2: rad(parseFloat(a2Slider.value)), a1_v: 0, a2_v: 0, g: 1.0 }; let trail = []; const maxTrail = 700; // Controls - setup display and events function updateSliderText() { l1Val.textContent = l1Slider.value; l2Val.textContent = l2Slider.value; m1Val.textContent = m1Slider.value; m2Val.textContent = m2Slider.value; a1Val.textContent = a1Slider.value; a2Val.textContent = a2Slider.value; } updateSliderText(); l1Slider.oninput = () => { l1Val.textContent = l1Slider.value; params.l1 = parseFloat(l1Slider.value); }; l2Slider.oninput = () => { l2Val.textContent = l2Slider.value; params.l2 = parseFloat(l2Slider.value); }; m1Slider.oninput = () => { m1Val.textContent = m1Slider.value; params.m1 = parseFloat(m1Slider.value); }; m2Slider.oninput = () => { m2Val.textContent = m2Slider.value; params.m2 = parseFloat(m2Slider.value); }; a1Slider.oninput = () => { a1Val.textContent = a1Slider.value; }; a2Slider.oninput = () => { a2Val.textContent = a2Slider.value; }; document.getElementById('reset').onclick = function() { params.l1 = parseFloat(l1Slider.value); params.l2 = parseFloat(l2Slider.value); params.m1 = parseFloat(m1Slider.value); params.m2 = parseFloat(m2Slider.value); params.a1 = rad(parseFloat(a1Slider.value)); params.a2 = rad(parseFloat(a2Slider.value)); params.a1_v = 0; params.a2_v = 0; trail = []; paused = false; pauseBtn.textContent = 'Pause'; }; let paused = false; const pauseBtn = document.getElementById('pause'); pauseBtn.onclick = function() { paused = !paused; pauseBtn.textContent = paused ? 'Resume' : 'Pause'; }; // Drag to set initial conditions let dragging = null; let dragPartial = false; function getMousePos(e) { let rect = canvas.getBoundingClientRect(); let x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; let y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top; return {x, y}; } function checkDist(x, y, cx, cy, r=17) { return (x-cx)*(x-cx) + (y-cy)*(y-cy) <= r*r; } function mouseDown(e) { let m = getMousePos(e); // project bobs let x1 = origin.x + params.l1 * Math.sin(params.a1); let y1 = origin.y + params.l1 * Math.cos(params.a1); let x2 = x1 + params.l2 * Math.sin(params.a2); let y2 = y1 + params.l2 * Math.cos(params.a2); if (checkDist(m.x, m.y, x2, y2, 22)) { dragging = 'bob2'; paused = true; pauseBtn.textContent = "Resume"; } else if (checkDist(m.x, m.y, x1, y1, 22)) { dragging = 'bob1'; paused = true; pauseBtn.textContent = "Resume"; } } function mouseMove(e) { if (!dragging) return; let m = getMousePos(e); if (dragging === 'bob1') { let dx = m.x - origin.x; let dy = m.y - origin.y; params.a1 = Math.atan2(dx, dy); params.a1_v = 0; params.a2_v = 0; a1Slider.value = Math.round(deg(params.a1)); a1Val.textContent = a1Slider.value; // Fix a2 to keep bob2 radial let x1 = origin.x + params.l1 * Math.sin(params.a1); let y1 = origin.y + params.l1 * Math.cos(params.a1); let dx2 = m.x - x1; let dy2 = m.y - y1; params.a2 = Math.atan2(dx2, dy2); a2Slider.value = Math.round(deg(params.a2)); a2Val.textContent = a2Slider.value; } else if (dragging === 'bob2') { // Fix θ2 only let x1 = origin.x + params.l1 * Math.sin(params.a1); let y1 = origin.y + params.l1 * Math.cos(params.a1); let dx = m.x - x1; let dy = m.y - y1; params.a2 = Math.atan2(dx, dy); params.a1_v = 0; params.a2_v = 0; a2Slider.value = Math.round(deg(params.a2)); a2Val.textContent = a2Slider.value; } trail = []; } function mouseUp(e) { dragging = null; } canvas.addEventListener('mousedown', mouseDown); canvas.addEventListener('mousemove', mouseMove); document.addEventListener('mouseup', mouseUp); canvas.addEventListener('touchstart', mouseDown, {passive: false}); canvas.addEventListener('touchmove', mouseMove, {passive: false}); document.addEventListener('touchend', mouseUp); // Pendulum dynamics function step(p, dt) { // Unpack let {a1, a2, a1_v, a2_v, m1, m2, l1, l2, g} = p; // Equations (https://www.myphysicslab.com/pendulum/double-pendulum-en.html) let num1 = -g * (2 * m1 + m2) * Math.sin(a1); let num2 = -m2 * g * Math.sin(a1 - 2 * a2); let num3 = -2 * Math.sin(a1 - a2) * m2; let num4 = (a2_v * a2_v * l2) + (a1_v * a1_v * l1 * Math.cos(a1 - a2)); let den = l1 * (2 * m1 + m2 - m2 * Math.cos(2 * a1 - 2 * a2)); let a1_a = (num1 + num2 + num3 * num4) / den; num1 = 2 * Math.sin(a1 - a2); num2 = (a1_v * a1_v * l1 * (m1 + m2)); num3 = g * (m1 + m2) * Math.cos(a1); num4 = a2_v * a2_v * l2 * m2 * Math.cos(a1 - a2); den = l2 * (2 * m1 + m2 - m2 * Math.cos(2 * a1 - 2 * a2)); let a2_a = (num1 * (num2 + num3 + num4)) / den; // Integrate a1_v += a1_a * dt; a2_v += a2_a * dt; a1 += a1_v * dt; a2 += a2_v * dt; // Slight friction (optional, keeps things from running wild) a1_v *= 0.999; a2_v *= 0.999; return {...p, a1, a2, a1_v, a2_v}; } // Animation let lastTime = performance.now(); function animate() { let now = performance.now(); let dt = Math.min(1, (now - lastTime)/13) * 0.07; lastTime = now; // If unpaused and not dragging, advance dynamics if (!paused && !dragging) { params = step(params, dt); // New trail point? Store tip let x1 = origin.x + params.l1 * Math.sin(params.a1); let y1 = origin.y + params.l1 * Math.cos(params.a1); let x2 = x1 + params.l2 * Math.sin(params.a2); let y2 = y1 + params.l2 * Math.cos(params.a2); trail.push({x:x2, y:y2}); if (trail.length > maxTrail) trail.shift(); } drawPendulum(); requestAnimationFrame(animate); } // Draw function drawPendulum() { ctx.clearRect(0,0, w, h); // trail if (trail.length > 2) { ctx.save(); for (let i=1; i<trail.length; ++i) { ctx.beginPath(); ctx.moveTo(trail[i-1].x, trail[i-1].y); ctx.lineTo(trail[i].x, trail[i].y); ctx.strokeStyle = `rgba(60,150,250,${0.09 + 0.22 * i/trail.length})`; ctx.lineWidth = 2.1; ctx.stroke(); } ctx.restore(); } // compute bobs let x1 = origin.x + params.l1 * Math.sin(params.a1); let y1 = origin.y + params.l1 * Math.cos(params.a1); let x2 = x1 + params.l2 * Math.sin(params.a2); let y2 = y1 + params.l2 * Math.cos(params.a2); // rod 1 ctx.save(); ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(x1, y1); ctx.lineWidth = 5.8; ctx.strokeStyle = "#ddd"; ctx.shadowBlur = 7; ctx.shadowColor = "#1555ff44"; ctx.stroke(); ctx.restore(); // rod 2 ctx.save(); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineWidth = 5.2; ctx.strokeStyle = "#9cf"; ctx.shadowBlur = 7; ctx.shadowColor = "#25aaff66"; ctx.stroke(); ctx.restore(); // joint 1 ctx.save(); ctx.beginPath(); ctx.arc(origin.x, origin.y, 13, 0, 2*Math.PI); ctx.fillStyle = "#234"; ctx.shadowBlur = 3; ctx.shadowColor = "#333"; ctx.globalAlpha = 0.64; ctx.fill(); ctx.restore(); // bob 1 ctx.save(); ctx.beginPath(); ctx.arc(x1, y1, 18 + 0.13 * params.m1, 0, 2 * Math.PI); ctx.fillStyle = dragging === 'bob1' ? "#f90" : "#2d7"; ctx.fill(); ctx.strokeStyle = "#003"; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); // bob 2 ctx.save(); ctx.beginPath(); ctx.arc(x2, y2, 18 + 0.13 * params.m2, 0, 2 * Math.PI); ctx.fillStyle = dragging === 'bob2' ? "#f90" : "#36afff"; ctx.shadowBlur = 9; ctx.shadowColor = "#0af7"; ctx.fill(); ctx.strokeStyle = "#003"; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); // theta text ctx.save(); ctx.font = "14px monospace"; ctx.fillStyle = "#ccc"; ctx.globalAlpha = 0.86; ctx.fillText("θ₁: " + deg(params.a1).toFixed(1) + "°", x1-40, y1-10); ctx.fillText("θ₂: " + deg(params.a2).toFixed(1) + "°", x2+20, y2-5); ctx.restore(); } // Start animation animate(); </script> </body> </html> ```
Drag to resize
# Interactive Double Pendulum Simulation I'll create a physics-accurate double pendulum simulation with interactive controls and visualization. The simulation will use the Runge-Kutta method for accurate physics calculations and include a trail to visualize the chaotic motion. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Double Pendulum Simulation</title> <style> body { margin: 0; padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); color: white; min-height: 100vh; display: flex; flex-direction: column; align-items: center; } .container { max-width: 1000px; width: 100%; display: flex; flex-direction: column; align-items: center; } h1 { text-align: center; margin-bottom: 5px; text-shadow: 0 0 10px rgba(0,0,0,0.5); font-size: 2.5rem; } .subtitle { text-align: center; margin-bottom: 20px; opacity: 0.8; } .simulation-container { display: flex; flex-wrap: wrap; gap: 20px; width: 100%; margin-bottom: 20px; } .canvas-container { flex: 1; min-width: 500px; background: rgba(0, 0, 0, 0.3); border-radius: 10px; padding: 15px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(4px); } canvas { background: rgba(0, 0, 0, 0.2); border-radius: 8px; width: 100%; display: block; } .controls { flex: 1; min-width: 300px; background: rgba(0, 0, 0, 0.3); border-radius: 10px; padding: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(4px); } .control-group { margin-bottom: 20px; } .control-group h3 { margin-top: 0; margin-bottom: 15px; border-bottom: 1px solid rgba(255, 255, 255, 0.2); padding-bottom: 5px; } .slider-container { display: flex; flex-direction: column; margin-bottom: 12px; } label { display: flex; justify-content: space-between; margin-bottom: 5px; } input[type="range"] { width: 100%; height: 8px; border-radius: 4px; background: rgba(255, 255, 255, 0.1); outline: none; } .value-display { background: rgba(0, 0, 0, 0.3); padding: 2px 8px; border-radius: 4px; min-width: 50px; text-align: center; } .buttons { display: flex; gap: 10px; margin-top: 20px; } button { flex: 1; padding: 12px; border: none; border-radius: 5px; background: linear-gradient(to right, #4A00E0, #8E2DE2); color: white; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } button:active { transform: translateY(0); } .info { background: rgba(0, 0, 0, 0.3); border-radius: 10px; padding: 20px; margin-top: 20px; width: 100%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(4px); } .info h2 { margin-top: 0; } @media (max-width: 900px) { .simulation-container { flex-direction: column; } .canvas-container, .controls { min-width: 100%; } } </style> </head> <body> <div class="container"> <h1>Double Pendulum Simulation</h1> <p class="subtitle">A chaotic system with mesmerizing motion</p> <div class="simulation-container"> <div class="canvas-container"> <canvas id="pendulumCanvas" width="600" height="500"></canvas> </div> <div class="controls"> <div class="control-group"> <h3>Pendulum Parameters</h3> <div class="slider-container"> <label> <span>Length 1:</span> <span class="value-display" id="length1Value">100</span> </label> <input type="range" id="length1" min="50" max="200" value="100"> </div> <div class="slider-container"> <label> <span>Length 2:</span> <span class="value-display" id="length2Value">100</span> </label> <input type="range" id="length2" min="50" max="200" value="100"> </div> <div class="slider-container"> <label> <span>Mass 1:</span> <span class="value-display" id="mass1Value">20</span> </label> <input type="range" id="mass1" min="5" max="40" value="20"> </div> <div class="slider-container"> <label> <span>Mass 2:</span> <span class="value-display" id="mass2Value">20</span> </label> <input type="range" id="mass2" min="5" max="40" value="20"> </div> <div class="slider-container"> <label> <span>Gravity:</span> <span class="value-display" id="gravityValue">0.5</span> </label> <input type="range" id="gravity" min="0.1" max="1" step="0.01" value="0.5"> </div> </div> <div class="control-group"> <h3>Initial Conditions</h3> <div class="slider-container"> <label> <span>Angle 1:</span> <span class="value-display" id="angle1Value">90°</span> </label> <input type="range" id="angle1" min="0" max="180" value="90"> </div> <div class="slider-container"> <label> <span>Angle 2:</span> <span class="value-display" id="angle2Value">180°</span> </label> <input type="range" id="angle2" min="0" max="360" value="180"> </div> </div> <div class="buttons"> <button id="resetBtn">Reset Simulation</button> <button id="randomBtn">Randomize</button> </div> </div> </div> <div class="info"> <h2>About Double Pendulums</h2> <p>A double pendulum is a simple physical system that exhibits chaotic behavior with a strong sensitivity to initial conditions. This simulation uses the Runge-Kutta method (RK4) for accurate physics calculations. Even tiny differences in starting positions can lead to completely different motion patterns over time.</p> <p>Adjust the parameters to see how they affect the pendulum's motion. The trail shows the path of the second pendulum bob, creating beautiful patterns as it moves.</p> </div> </div> <script> // Get canvas and context const canvas = document.getElementById('pendulumCanvas'); const ctx = canvas.getContext('2d'); // Set canvas dimensions canvas.width = 600; canvas.height = 500; // Pendulum parameters let params = { length1: 100, // Length of first pendulum arm length2: 100, // Length of second pendulum arm mass1: 20, // Mass of first pendulum bob mass2: 20, // Mass of second pendulum bob gravity: 0.5, // Gravity constant angle1: Math.PI / 2, // Initial angle of first pendulum (90°) angle2: Math.PI, // Initial angle of second pendulum (180°) damping: 0.999 // Energy damping factor }; // Pendulum state let state = { theta1: params.angle1, // Current angle of first pendulum theta2: params.angle2, // Current angle of second pendulum omega1: 0, // Angular velocity of first pendulum omega2: 0 // Angular velocity of second pendulum }; // Trail for the second pendulum bob let trail = []; const maxTrailLength = 500; // Animation control let animationId = null; let isRunning = true; // Physics constants const timeStep = 0.1; // Update value displays function updateDisplays() { document.getElementById('length1Value').textContent = params.length1; document.getElementById('length2Value').textContent = params.length2; document.getElementById('mass1Value').textContent = params.mass1; document.getElementById('mass2Value').textContent = params.mass2; document.getElementById('gravityValue').textContent = params.gravity.toFixed(2); document.getElementById('angle1Value').textContent = Math.round(params.angle1 * 180 / Math.PI) + '°'; document.getElementById('angle2Value').textContent = Math.round(params.angle2 * 180 / Math.PI) + '°'; } // Calculate derivatives for the state (theta1, omega1, theta2, omega2) function derivatives(state) { const { theta1, theta2, omega1, omega2 } = state; const { length1, length2, mass1, mass2, gravity, damping } = params; // Calculate angular accelerations const cos12 = Math.cos(theta1 - theta2); const sin12 = Math.sin(theta1 - theta2); const sin1 = Math.sin(theta1); const sin2 = Math.sin(theta2); const denom = 2 * mass1 + mass2 - mass2 * Math.cos(2 * (theta1 - theta2)); const alpha1 = (-gravity * (2 * mass1 + mass2) * sin1 - mass2 * gravity * sin1 * Math.cos(2 * (theta1 - theta2)) - 2 * sin12 * mass2 * (omega2 * omega2 * length2 + omega1 * omega1 * length1 * cos12)) / (length1 * denom); const alpha2 = (2 * sin12 * (omega1 * omega1 * length1 * (mass1 + mass2) + gravity * (mass1 + mass2) * Math.cos(theta1) + omega2 * omega2 * length2 * mass2 * cos12)) / (length2 * denom); return { dtheta1: omega1, domega1: alpha1 * damping, dtheta2: omega2, domega2: alpha2 * damping }; } // Runge-Kutta 4th order integration function rk4Integration(state, dt) { const s = state; // Step 1 const k1 = derivatives(s); // Step 2 const s2 = { theta1: s.theta1 + k1.dtheta1 * dt / 2, omega1: s.omega1 + k1.domega1 * dt / 2, theta2: s.theta2 + k1.dtheta2 * dt / 2, omega2: s.omega2 + k1.domega2 * dt / 2 }; const k2 = derivatives(s2); // Step 3 const s3 = { theta1: s.theta1 + k2.dtheta1 * dt / 2, omega1: s.omega1 + k2.domega1 * dt / 2, theta2: s.theta2 + k2.dtheta2 * dt / 2, omega2: s.omega2 + k2.domega2 * dt / 2 }; const k3 = derivatives(s3); // Step 4 const s4 = { theta1: s.theta1 + k3.dtheta1 * dt, omega1: s.omega1 + k3.domega1 * dt, theta2: s.theta2 + k3.dtheta2 * dt, omega2: s.omega2 + k3.domega2 * dt }; const k4 = derivatives(s4); // Combine return { theta1: s.theta1 + (k1.dtheta1 + 2*k2.dtheta1 + 2*k3.dtheta1 + k4.dtheta1) * dt / 6, omega1: s.omega1 + (k1.domega1 + 2*k2.domega1 + 2*k3.domega1 + k4.domega1) * dt / 6, theta2: s.theta2 + (k1.dtheta2 + 2*k2.dtheta2 + 2*k3.dtheta2 + k4.dtheta2) * dt / 6, omega2: s.omega2 + (k1.domega2 + 2*k2.domega2 + 2*k3.domega2 + k4.domega2) * dt / 6 }; } // Update pendulum state function updatePendulum() { state = rk4Integration(state, timeStep); // Calculate positions const x1 = params.length1 * Math.sin(state.theta1); const y1 = params.length1 * Math.cos(state.theta1); const x2 = x1 + params.length2 * Math.sin(state.theta2); const y2 = y1 + params.length2 * Math.cos(state.theta2); // Add to trail trail.push({x: x2, y: y2}); if (trail.length > maxTrailLength) { trail.shift(); } } // Draw the pendulum function drawPendulum() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Set origin to center bottom ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2 + 100); // Draw trail ctx.beginPath(); ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(100, 200, 255, 0.1)'; for (let i = 1; i < trail.length; i++) { ctx.moveTo(trail[i-1].x, trail[i-1].y); ctx.lineTo(trail[i].x, trail[i].y); } ctx.stroke(); // Calculate positions const x1 = params.length1 * Math.sin(state.theta1); const y1 = params.length1 * Math.cos(state.theta1); const x2 = x1 + params.length2 * Math.sin(state.theta2); const y2 = y1 + params.length2 * Math.cos(state.theta2); // Draw pendulum arms ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 3; ctx.stroke(); // Draw pendulum bobs ctx.beginPath(); ctx.arc(x1, y1, params.mass1 / 2, 0, Math.PI * 2); ctx.fillStyle = '#FF6B6B'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(x2, y2, params.mass2 / 2, 0, Math.PI * 2); ctx.fillStyle = '#4ECDC4'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); // Draw pivot point ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.restore(); } // Animation loop function animate() { if (isRunning) { updatePendulum(); drawPendulum(); } animationId = requestAnimationFrame(animate); } // Reset simulation function resetSimulation() { state = { theta1: params.angle1, theta2: params.angle2, omega1: 0, omega2: 0 }; trail = []; isRunning = true; } // Randomize parameters function randomize() { params.length1 = Math.floor(Math.random() * 150) + 50; params.length2 = Math.floor(Math.random() * 150) + 50; params.mass1 = Math.floor(Math.random() * 35) + 5; params.mass2 = Math.floor(Math.random() * 35) + 5; params.gravity = (Math.random() * 0.9 + 0.1).toFixed(2); params.angle1 = (Math.random() * Math.PI); params.angle2 = (Math.random() * Math.PI * 2); // Update sliders document.getElementById('length1').value = params.length1; document.getElementById('length2').value = params.length2; document.getElementById('mass1').value = params.mass1; document.getElementById('mass2').value = params.mass2; document.getElementById('gravity').value = params.gravity; document.getElementById('angle1').value = Math.round(params.angle1 * 180 / Math.PI); document.getElementById('angle2').value = Math.round(params.angle2 * 180 / Math.PI); updateDisplays(); resetSimulation(); } // Event listeners document.getElementById('length1').addEventListener('input', (e) => { params.length1 = parseInt(e.target.value); updateDisplays(); }); document.getElementById('length2').addEventListener('input', (e) => { params.length2 = parseInt(e.target.value); updateDisplays(); }); document.getElementById('mass1').addEventListener('input', (e) => { params.mass1 = parseInt(e.target.value); updateDisplays(); }); document.getElementById('mass2').addEventListener('input', (e) => { params.mass2 = parseInt(e.target.value); updateDisplays(); }); document.getElementById('gravity').addEventListener('input', (e) => { params.gravity = parseFloat(e.target.value); updateDisplays(); }); document.getElementById('angle1').addEventListener('input', (e) => { params.angle1 = parseInt(e.target.value) * Math.PI / 180; updateDisplays(); }); document.getElementById('angle2').addEventListener('input', (e) => { params.angle2 = parseInt(e.target.value) * Math.PI / 180; updateDisplays(); }); document.getElementById('resetBtn').addEventListener('click', resetSimulation); document.getElementById('randomBtn').addEventListener('click', randomize); // Initialize updateDisplays(); animate(); </script> </body> </html> ``` ## Features of this Simulation 1. **Physics-Accurate Simulation**: - Uses the Runge-Kutta 4th order method for precise physics calculations - Models the chaotic behavior of a double pendulum system 2. **Interactive Controls**: - Adjust pendulum lengths, masses, and gravity - Set initial angles for both pendulums - Reset or randomize the simulation 3. **Visual Elements**: - Color-coded pendulum bobs with trails - Smooth animation showing the complex motion - Gradient trail that visualizes the path of the second bob 4. **Responsive Design**: - Works on both desktop and mobile devices - Clean, modern interface with intuitive controls 5. **Educational Information**: - Explanation of double pendulum physics - Notes about chaotic systems and sensitivity to initial conditions The simulation demonstrates how small changes in initial conditions can lead to dramatically different outcomes, showcasing the fascinating nature of chaotic systems.
Drag to resize
Here's an interactive double pendulum simulation that runs in the browser: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Double Pendulum Simulation</title> <style> body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: linear-gradient(135deg, #1e3c72, #2a5298); color: white; overflow-x: hidden; } .container { display: flex; gap: 20px; align-items: flex-start; } canvas { border: 2px solid #fff; border-radius: 10px; background: #000; cursor: crosshair; } .controls { background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px); min-width: 250px; } .control-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-size: 14px; } input[type="range"] { width: 100%; margin-bottom: 5px; } input[type="number"] { width: 80px; padding: 5px; border: none; border-radius: 5px; background: rgba(255, 255, 255, 0.2); color: white; } button { background: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; margin: 5px 0; width: 100%; } button:hover { background: #45a049; } .reset-btn { background: #f44336; } .reset-btn:hover { background: #da190b; } .info { font-size: 12px; opacity: 0.8; margin-top: 10px; } </style> </head> <body> <h1>Interactive Double Pendulum Simulation</h1> <div class="container"> <canvas id="canvas" width="800" height="600"></canvas> <div class="controls"> <div class="control-group"> <label>Mass 1: <span id="m1-value">1.0</span> kg</label> <input type="range" id="m1" min="0.5" max="3" step="0.1" value="1"> </div> <div class="control-group"> <label>Mass 2: <span id="m2-value">1.0</span> kg</label> <input type="range" id="m2" min="0.5" max="3" step="0.1" value="1"> </div> <div class="control-group"> <label>Length 1: <span id="l1-value">150</span> px</label> <input type="range" id="l1" min="50" max="250" step="10" value="150"> </div> <div class="control-group"> <label>Length 2: <span id="l2-value">150</span> px</label> <input type="range" id="l2" min="50" max="250" step="10" value="150"> </div> <div class="control-group"> <label>Gravity: <span id="g-value">0.5</span></label> <input type="range" id="g" min="0.1" max="1" step="0.1" value="0.5"> </div> <div class="control-group"> <label>Damping: <span id="damping-value">0.999</span></label> <input type="range" id="damping" min="0.99" max="1" step="0.001" value="0.999"> </div> <button onclick="toggleSimulation()">⏸️ Pause/Play</button> <button class="reset-btn" onclick="resetPendulum()">🔄 Reset</button> <button onclick="toggleTrail()">👁️ Toggle Trail</button> <button onclick="clearTrail()">🧹 Clear Trail</button> <div class="info"> <p><strong>Instructions:</strong></p> <p>• Click and drag to set initial positions</p> <p>• Adjust parameters with sliders</p> <p>• Watch the chaotic motion!</p> <p><strong>Energy:</strong> <span id="energy">0</span> J</p> </div> </div> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Pendulum state let pendulum = { // Position of pivot point originX: canvas.width / 2, originY: 100, // Angles (in radians) angle1: Math.PI / 2, angle2: Math.PI / 2, // Angular velocities angleVel1: 0, angleVel2: 0, // Angular accelerations angleAcc1: 0, angleAcc2: 0, // Masses m1: 1, m2: 1, // Lengths l1: 150, l2: 150, // Physics g: 0.5, damping: 0.999 }; // Simulation state let isRunning = true; let showTrail = true; let trail = []; let isDragging = false; let dragTarget = null; // Control references const controls = { m1: document.getElementById('m1'), m2: document.getElementById('m2'), l1: document.getElementById('l1'), l2: document.getElementById('l2'), g: document.getElementById('g'), damping: document.getElementById('damping') }; // Initialize controls function initControls() { Object.keys(controls).forEach(key => { const control = controls[key]; const valueSpan = document.getElementById(key + '-value'); control.addEventListener('input', () => { pendulum[key] = parseFloat(control.value); valueSpan.textContent = control.value; }); // Set initial values pendulum[key] = parseFloat(control.value); valueSpan.textContent = control.value; }); } // Calculate positions of pendulum bobs function calculatePositions() { const x1 = pendulum.originX + pendulum.l1 * Math.sin(pendulum.angle1); const y1 = pendulum.originY + pendulum.l1 * Math.cos(pendulum.angle1); const x2 = x1 + pendulum.l2 * Math.sin(pendulum.angle2); const y2 = y1 + pendulum.l2 * Math.cos(pendulum.angle2); return { x1, y1, x2, y2 }; } // Physics update using Lagrangian mechanics function updatePhysics() { if (!isRunning) return; const { m1, m2, l1, l2, g, angle1, angle2, angleVel1, angleVel2 } = pendulum; // Differences const deltaAngle = angle1 - angle2; const den1 = (m1 + m2) * l1 - m2 * l1 * Math.cos(deltaAngle) * Math.cos(deltaAngle); const den2 = (l2 / l1) * den1; // Angular acceleration for first pendulum const num1 = (-m2 * l1 * angleVel1 * angleVel1 * Math.sin(deltaAngle) * Math.cos(deltaAngle) + m2 * g * Math.sin(angle2) * Math.cos(deltaAngle) + m2 * l2 * angleVel2 * angleVel2 * Math.sin(deltaAngle) - (m1 + m2) * g * Math.sin(angle1)); pendulum.angleAcc1 = num1 / den1; // Angular acceleration for second pendulum const num2 = (-m2 * l2 * angleVel2 * angleVel2 * Math.sin(deltaAngle) * Math.cos(deltaAngle) + (m1 + m2) * (g * Math.sin(angle1) * Math.cos(deltaAngle) - l1 * angleVel1 * angleVel1 * Math.sin(deltaAngle) - g * Math.sin(angle2))); pendulum.angleAcc2 = num2 / den2; // Update velocities and positions pendulum.angleVel1 += pendulum.angleAcc1; pendulum.angleVel2 += pendulum.angleAcc2; // Apply damping pendulum.angleVel1 *= pendulum.damping; pendulum.angleVel2 *= pendulum.damping; pendulum.angle1 += pendulum.angleVel1; pendulum.angle2 += pendulum.angleVel2; } // Calculate system energy function calculateEnergy() { const { m1, m2, l1, l2, g, angle1, angle2, angleVel1, angleVel2 } = pendulum; const { x1, y1, x2, y2 } = calculatePositions(); // Kinetic energy const v1x = l1 * angleVel1 * Math.cos(angle1); const v1y = -l1 * angleVel1 * Math.sin(angle1); const v2x = v1x + l2 * angleVel2 * Math.cos(angle2); const v2y = v1y - l2 * angleVel2 * Math.sin(angle2); const ke = 0.5 * m1 * (v1x * v1x + v1y * v1y) + 0.5 * m2 * (v2x * v2x + v2y * v2y); // Potential energy (using canvas coordinates) const pe = m1 * g * (y1 - pendulum.originY) + m2 * g * (y2 - pendulum.originY); return ke + pe; } // Render the pendulum function render() { if (showTrail) { // Fade trail ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } else { // Clear canvas ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); } const { x1, y1, x2, y2 } = calculatePositions(); // Add to trail if (showTrail) { trail.push({ x: x2, y: y2 }); if (trail.length > 500) { trail.shift(); } // Draw trail ctx.strokeStyle = 'rgba(255, 100, 100, 0.6)'; ctx.lineWidth = 1; ctx.beginPath(); for (let i = 1; i < trail.length; i++) { const alpha = i / trail.length; ctx.globalAlpha = alpha; ctx.beginPath(); ctx.moveTo(trail[i - 1].x, trail[i - 1].y); ctx.lineTo(trail[i].x, trail[i].y); ctx.stroke(); } ctx.globalAlpha = 1; } // Draw pendulum arms ctx.strokeStyle = 'white'; ctx.lineWidth = 3; ctx.beginPath(); // First arm ctx.moveTo(pendulum.originX, pendulum.originY); ctx.lineTo(x1, y1); // Second arm ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); // Draw pivot point ctx.fillStyle = 'gray'; ctx.beginPath(); ctx.arc(pendulum.originX, pendulum.originY, 8, 0, 2 * Math.PI); ctx.fill(); // Draw masses const radius1 = Math.max(10, pendulum.m1 * 10); const radius2 = Math.max(10, pendulum.m2 * 10); // First mass ctx.fillStyle = '#4CAF50'; ctx.beginPath(); ctx.arc(x1, y1, radius1, 0, 2 * Math.PI); ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); // Second mass ctx.fillStyle = '#FF5722'; ctx.beginPath(); ctx.arc(x2, y2, radius2, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); // Update energy display document.getElementById('energy').textContent = calculateEnergy().toFixed(2); } // Mouse interaction function getMousePos(e) { const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } function getDistance(p1, p2) { return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); } canvas.addEventListener('mousedown', (e) => { const mouse = getMousePos(e); const { x1, y1, x2, y2 } = calculatePositions(); const dist1 = getDistance(mouse, { x: x1, y: y1 }); const dist2 = getDistance(mouse, { x: x2, y: y2 }); if (dist1 < 30) { isDragging = true; dragTarget = 1; } else if (dist2 < 30) { isDragging = true; dragTarget = 2; } }); canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const mouse = getMousePos(e); if (dragTarget === 1) { const dx = mouse.x - pendulum.originX; const dy = mouse.y - pendulum.originY; pendulum.angle1 = Math.atan2(dx, dy); pendulum.angleVel1 = 0; } else if (dragTarget === 2) { const { x1, y1 } = calculatePositions(); const dx = mouse.x - x1; const dy = mouse.y - y1; pendulum.angle2 = Math.atan2(dx, dy); pendulum.angleVel2 = 0; } }); canvas.addEventListener('mouseup', () => { isDragging = false; dragTarget = null; }); // Control functions function toggleSimulation() { isRunning = !isRunning; } function resetPendulum() { pendulum.angle1 = Math.PI / 2; pendulum.angle2 = Math.PI / 2; pendulum.angleVel1 = 0; pendulum.angleVel2 = 0; trail = []; } function toggleTrail() { showTrail = !showTrail; if (!showTrail) { trail = []; } } function clearTrail() { trail = []; } // Animation loop function animate() { updatePhysics(); render(); requestAnimationFrame(animate); } // Initialize initControls(); animate(); </script> </body> </html> ``` This interactive double pendulum simulation includes: **Features:** - Real-time physics simulation using Lagrangian mechanics - Interactive controls for mass, length, gravity, and damping - Mouse interaction - click and drag the pendulum bobs to set initial positions - Trail visualization showing the chaotic motion path - Energy conservation display - Pause/play, reset, and trail toggle controls **Physics:** - Accurate double pendulum equations of motion - Adjustable parameters for experimentation - Energy calculation and display - Damping factor to simulate air resistance **Interactivity:** - Drag pendulum masses to set starting positions - Real-time parameter adjustment with sliders - Visual feedback with color-coded masses and trails - Responsive design that works on different screen sizes The simulation demonstrates the chaotic nature of double pendulums - small changes in initial conditions lead to drastically different outcomes, making it a great example of deterministic chaos.
Drag to resize
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Double Pendulum Simulation</title> <style> body { margin: 0; background: #f0f0f0; font-family: Arial, sans-serif; } #container { display: flex; flex-direction: column; align-items: center; padding: 20px; } #canvas { background: #ffffff; border: 1px solid #ccc; } #controls { margin-top: 20px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; max-width: 400px; } label { display: block; margin-bottom: 5px; } button { grid-column: span 2; padding: 10px; background: #4CAF50; color: white; border: none; cursor: pointer; } button:hover { background: #45a049; } </style> </head> <body> <div id="container"> <canvas id="canvas" width="600" height="600"></canvas> <div id="controls"> <div> <label>Length 1 (m): <input type="range" id="l1" min="0.5" max="2" step="0.1" value="1"></label> <span id="l1-value">1</span> </div> <div> <label>Length 2 (m): <input type="range" id="l2" min="0.5" max="2" step="0.1" value="1"></label> <span id="l2-value">1</span> </div> <div> <label>Mass 1 (kg): <input type="range" id="m1" min="0.5" max="2" step="0.1" value="1"></label> <span id="m1-value">1</span> </div> <div> <label>Mass 2 (kg): <input type="range" id="m2" min="0.5" max="2" step="0.1" value="1"></label> <span id="m2-value">1</span> </div> <div> <label>Initial θ1 (°): <input type="range" id="theta1" min="0" max="360" value="120"></label> <span id="theta1-value">120</span> </div> <div> <label>Initial θ2 (°): <input type="range" id="theta2" min="0" max="360" value="180"></label> <span id="theta2-value">180</span> </div> <button id="reset">Reset Simulation</button> </div> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const originX = width / 2; const originY = height / 4; const g = 9.81; const dt = 0.01; const trailLength = 100; const trail = []; let l1 = 1, l2 = 1, m1 = 1, m2 = 1; let theta1 = 120 * Math.PI / 180, theta2 = 180 * Math.PI / 180; let omega1 = 0, omega2 = 0; let running = true; function updateValues() { document.getElementById('l1-value').textContent = l1; document.getElementById('l2-value').textContent = l2; document.getElementById('m1-value').textContent = m1; document.getElementById('m2-value').textContent = m2; document.getElementById('theta1-value').textContent = (theta1 * 180 / Math.PI).toFixed(0); document.getElementById('theta2-value').textContent = (theta2 * 180 / Math.PI).toFixed(0); } function reset() { l1 = parseFloat(document.getElementById('l1').value); l2 = parseFloat(document.getElementById('l2').value); m1 = parseFloat(document.getElementById('m1').value); m2 = parseFloat(document.getElementById('m2').value); theta1 = parseFloat(document.getElementById('theta1').value) * Math.PI / 180; theta2 = parseFloat(document.getElementById('theta2').value) * Math.PI / 180; omega1 = 0; omega2 = 0; trail.length = 0; updateValues(); } document.getElementById('reset').addEventListener('click', reset); document.getElementById('l1').addEventListener('input', () => { l1 = parseFloat(document.getElementById('l1').value); updateValues(); }); document.getElementById('l2').addEventListener('input', () => { l2 = parseFloat(document.getElementById('l2').value); updateValues(); }); document.getElementById('m1').addEventListener('input', () => { m1 = parseFloat(document.getElementById('m1').value); updateValues(); }); document.getElementById('m2').addEventListener('input', () => { m2 = parseFloat(document.getElementById('m2').value); updateValues(); }); document.getElementById('theta1').addEventListener('input', () => { theta1 = parseFloat(document.getElementById('theta1').value) * Math.PI / 180; updateValues(); }); document.getElementById('theta2').addEventListener('input', () => { theta2 = parseFloat(document.getElementById('theta2').value) * Math.PI / 180; updateValues(); }); function simulate() { const sin = Math.sin, cos = Math.cos; const num1 = -g * (2 * m1 + m2) * sin(theta1); const num2 = -m2 * g * sin(theta1 - 2 * theta2); const num3 = -2 * sin(theta1 - theta2) * m2 * (omega2 * omega2 * l2 + omega1 * omega1 * l1 * cos(theta1 - theta2)); const den = l1 * (2 * m1 + m2 - m2 * cos(2 * theta1 - 2 * theta2)); const alpha1 = (num1 + num2 + num3) / den; const num4 = 2 * sin(theta1 - theta2) * (omega1 * omega1 * l1 * (m1 + m2)); const num5 = g * (m1 + m2) * cos(theta1); const num6 = omega2 * omega2 * l2 * m2 * cos(theta1 - theta2); const alpha2 = (num4 + num5 + num6) / (l2 * (2 * m1 + m2 - m2 * cos(2 * theta1 - 2 * theta2))); omega1 += alpha1 * dt; omega2 += alpha2 * dt; theta1 += omega1 * dt; theta2 += omega2 * dt; } function draw() { ctx.clearRect(0, 0, width, height); const scale = 150; // pixels per meter const x1 = originX + l1 * scale * Math.sin(theta1); const y1 = originY + l1 * scale * Math.cos(theta1); const x2 = x1 + l2 * scale * Math.sin(theta2); const y2 = y1 + l2 * scale * Math.cos(theta2); // Draw rods ctx.beginPath(); ctx.moveTo(originX, originY); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke(); // Draw masses ctx.beginPath(); ctx.arc(x1, y1, 10 * Math.sqrt(m1), 0, 2 * Math.PI); ctx.fillStyle = '#FF0000'; ctx.fill(); ctx.beginPath(); ctx.arc(x2, y2, 10 * Math.sqrt(m2), 0, 2 * Math.PI); ctx.fillStyle = '#0000FF'; ctx.fill(); // Draw trail trail.push({x: x2, y: y2}); if (trail.length > trailLength) trail.shift(); ctx.beginPath(); ctx.moveTo(trail[0].x, trail[0].y); for (let i = 1; i < trail.length; i++) { ctx.lineTo(trail[i].x, trail[i].y); } ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; ctx.lineWidth = 1; ctx.stroke(); } function loop() { simulate(); draw(); requestAnimationFrame(loop); } updateValues(); loop(); </script> </body> </html> ```
Drag to resize
```html <!DOCTYPE html> <html> <head> <title>Double Pendulum Simulation</title> <style> /* Add some basic styling to make the simulation look decent */ canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="600" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Define the double pendulum parameters const g = 0.1; // gravitational constant const L1 = 100; // length of pendulum 1 const L2 = 100; // length of pendulum 2 const M1 = 10; // mass of pendulum 1 const M2 = 10; // mass of pendulum 2 // Initial conditions let theta1 = Math.PI / 3; // initial angle of pendulum 1 let theta2 = Math.PI / 2; // initial angle of pendulum 2 let omega1 = 0; // initial angular velocity of pendulum 1 let omega2 = 0; // initial angular velocity of pendulum 2 let alpha1 = 0; // initial angular acceleration of pendulum 1 let alpha2 = 0; // initial angular acceleration of pendulum 2 // Main simulation loop function simulate() { // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Calculate the new angular accelerations const num1 = -g * (2 * M1 + M2) * Math.sin(theta1); const num2 = -M2 * g * Math.sin(theta1 - 2 * theta2); const num3 = -2 * Math.sin(theta1 - theta2) * M2; const num4 = omega2 * omega2 * L2 + omega1 * omega1 * L1 * Math.cos(theta1 - theta2); const den = L1 * (2 * M1 + M2 - M2 * Math.cos(2 * theta1 - 2 * theta2)); alpha1 = (num1 + num2 + num3 * num4) / den; const num5 = 2 * Math.sin(theta1 - theta2); const num6 = omega1 * omega1 * L1 * (M1 + M2); const num7 = g * (M1 + M2) * Math.cos(theta1); const num8 = omega2 * omega2 * L2 * M2 * Math.cos(theta1 - theta2); const den2 = L2 * (2 * M1 + M2 - M2 * Math.cos(2 * theta1 - 2 * theta2)); alpha2 = (num5 * (num6 + num7 + num8)) / den2; // Update the angular velocities and angles omega1 += alpha1; omega2 += alpha2; theta1 += omega1; theta2 += omega2; // Draw the pendulums const x1 = canvas.width / 2 + L1 * Math.sin(theta1); const y1 = canvas.height / 2 - L1 * Math.cos(theta1); const x2 = x1 + L2 * Math.sin(theta2); const y2 = y1 - L2 * Math.cos(theta2); ctx.beginPath(); ctx.moveTo(canvas.width / 2, canvas.height / 2); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.beginPath(); ctx.arc(x1, y1, M1, 0, 2 * Math.PI); ctx.fillStyle = 'black'; ctx.fill(); ctx.beginPath(); ctx.arc(x2, y2, M2, 0, 2 * Math.PI); ctx.fillStyle = 'black'; ctx.fill(); // Request the next frame requestAnimationFrame(simulate); } // Start the simulation simulate(); </script> </body> </html> ``` This code creates a double pendulum simulation using the HTML5 canvas element and JavaScript. The simulation is based on the equations of motion for a double pendulum, which are derived from the Lagrangian mechanics. The simulation is animated using the `requestAnimationFrame` function, which is a efficient way to create smooth animations in the browser. To run the simulation, simply open the HTML file in a web browser. The simulation will start automatically, and you can observe the motion of the double pendulum. You can adjust the parameters of the simulation, such as the lengths and masses of the pendulums, to see how they affect the motion.
Drag to resize