oneshotlm
Compare all models on this prompt →

Double pendulum · laguna-m.1

Simulate a chaotic double pendulum on a canvas using the correct equations of motion, drawing the arms and a colorful fading trail of the tip.

⛨ sandboxed Open in new tab ↗
transcript · 28 messages
user

Simulate a chaotic double pendulum on a canvas using the correct equations of motion, drawing the arms and a colorful fading trail of the tip.

assistant
thinking
The user wants me to create a single-page web app that simulates a chaotic double pendulum. Let me break down the requirements:

1. Single HTML file with embedded CSS and JS
2. No external resources/CDNs
3. Simulate a double pendulum using correct equations of motion
4. Draw arms on canvas
5. Draw colorful fading trail of the tip
6. Self-contained index.html in current directory

Let me create this simulation. A double pendulum consists of two pendulums attached end to end, with the second pendulum attached to the end of the first. The equations of motion are complex and involve coupled nonlinear differential equations.

The Lagrangian approach gives us the equations. For a double pendulum:
- m1, m2 are masses
- l1, l2 are lengths
- θ1, θ2 are angles from vertical
- We need to solve for θ1'', θ2''

I'll use numerical integration (Runge-Kutta 4th order) to solve the equations of motion.

Let me write the complete index.html file:

I'll create a self-contained double pendulum simulation with chaotic dynamics, drawing arms and a colorful fading trail of the tip.

write /workspace/index.html (6775 bytes)
toolResult

Successfully wrote 6775 bytes to /workspace/index.html

assistant
thinking
The file has been written. Let me verify it was created correctly and maybe check the content since I want to make sure the double pendulum physics are correct. Actually, I should double-check the equations of motion - the standard double pendulum equations might need adjustment. Let me re-read the file to verify.

read {"path":"/workspace/index.html"}
toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Double Pendulum Chaos</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #111; overflow: hidden; font-family: monospace; } canvas { display: block; } #ui { position: fixed; top: 10px; left: 10px; color: #fff; z-index: 100; } #ui input[type="range"] { width: 150px; } #ui label { display: block; margin: 5px 0; font-size: 12px; } #info { position: fixed; bottom: 10px; left: 10px; color: #888; font-size: 11px; } </style> </head> <body> <div id="ui"> <label>Length 1: <input type="range" id="l1" min="50" max="200" value="150"></label> <label>Length 2: <input type="range" id="l2" min="50" max="200" value="150"></label> <label>Gravity: <input type="range" id="g" min="0" max="2" step="0.1" value="1"></label> <label>Trail Length: <input type="range" id="trail" min="10" max="500" value="200"></label> <button id="reset">Random Reset</button> </div> <div id="info">Double Pendulum - Chaotic Motion</div> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Physical parameters let l1 = 150, l2 = 150; let m1 = 10, m2 = 10; let g = 1; // gravity (scaled) // State: [theta1, omega1, theta2, omega2] let state = [Math.PI/2, 0, Math.PI/3, 0]; // Trail points let trail = []; let maxTrail = 200; // Scale and offset for drawing const origin = { x: canvas.width/2, y: canvas.height/2 }; let scale = 1; // Equations of motion from Lagrangian mechanics function derivatives(s) { const [th1, w1, th2, w2] = s; const delta = th2 - th1; const den = 2*m1 + m2 - m2*Math.cos(2*delta); const th1_dot = w1; const th2_dot = w2; const w1_dot = (-g*(2*m1 + m2)*Math.sin(th1) - m2*g*Math.sin(th1 - 2*th2) - 2*Math.sin(delta)*m2*(w2*w2*l2 + w1*w1*l1*Math.cos(delta))) / (l1 * den); const w2_dot = (2*Math.sin(delta)*(w1*w1*l1*(m1 + m2) + g*(m1 + m2)*Math.cos(th1) + w2*w2*l2*m2*Math.cos(delta))) / (l2 * den); return [th1_dot, w1_dot, th2_dot, w2_dot]; } // 4th order Runge-Kutta integration function integrate(s, dt) { const k1 = derivatives(s); const s2 = s.map((v, i) => v + 0.5*dt*k1[i]); const k2 = derivatives(s2); const s3 = s.map((v, i) => v + 0.5*dt*k2[i]); const k3 = derivatives(s3); const s4 = s.map((v, i) => v + dt*k3[i]); const k4 = derivatives(s4); return s.map((v, i) => v + dt*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])/6); } function reset() { state = [ Math.PI/2 + (Math.random()-0.5)*1.5, (Math.random()-0.5)*4, Math.PI/3 + (Math.random()-0.5)*1.5, (Math.random()-0.5)*4 ]; trail = []; } // Convert angles to screen coordinates function getPositions() { const x1 = origin.x + l1 * scale * Math.sin(state[0]); const y1 = origin.y + l1 * scale * Math.cos(state[0]); const x2 = x1 + l2 * scale * Math.sin(state[2]); const y2 = y1 + l2 * scale * Math.cos(state[2]); return { x1, y1, x2, y2 }; } function animate() { // Clear with fade effect ctx.fillStyle = 'rgba(17,17,17,0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Integrate physics state = integrate(state, 0.016); const { x1, y1, x2, y2 } = getPositions(); // Add to trail trail.push({ x: x2, y: y2 }); if (trail.length > maxTrail) trail.shift(); // Draw trail with color gradient const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); for (let i = 0; i <= 1; i += 0.1) { gradient.addColorStop(i, `hsl(${(360*i + Date.now()/20)%360}, 80%, 60%)`); } for (let i = 0; i < trail.length - 1; i++) { const p1 = trail[i]; const p2 = trail[i+1]; const alpha = i/trail.length; ctx.strokeStyle = `hsla(${(360*(1-i/trail.length) + Date.now()/30)%360}, 80%, 60%, ${alpha})`; ctx.lineWidth = 2 + alpha*3; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } // Draw arms ctx.strokeStyle = '#fff'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(x1, y1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); // Draw joints ctx.fillStyle = '#0ff'; ctx.beginPath(); ctx.arc(origin.x, origin.y, 8, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#f0f'; ctx.beginPath(); ctx.arc(x1, y1, 12, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(x2, y2, 14, 0, Math.PI*2); ctx.fill(); requestAnimationFrame(animate); } // UI controls document.getElementById('l1').oninput = e => l1 = +e.target.value; document.getElementById('l2').oninput = e => l2 = +e.target.value; document.getElementById('g').oninput = e => g = +e.target.value; document.getElementById('trail').oninput = e => maxTrail = +e.target.value; document.getElementById('reset').onclick = reset; window.onresize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; Object.assign(origin, { x: canvas.width/2, y: canvas.height/2 }); scale = Math.min(canvas.width, canvas.height) / 600; }; scale = Math.min(canvas.width, canvas.height) / 600; reset(); animate(); </script> </body> </html>

assistant
thinking
Let me verify the file exists and check its size.

bash {"command":"ls -la /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 6775 Jul 22 17:16 /workspace/index.html

assistant
thinking
The file is created. Let me verify it works by checking the structure. Actually, I realize there's a potential issue - the equations of motion I used might not be fully correct. Let me double-check and fix the double pendulum equations. The standard double pendulum equations are more complex and involve the masses properly. Let me fix this to ensure accurate chaotic behavior.

The correct equations for a double pendulum from Lagrangian mechanics are:

For normalized masses (m1=m2=1):
θ1'' = (−g(2m1+m2)sin(θ1) − m2*g*sin(θ1-2θ2) − 2sin(θ1-θ2)*m2*(θ2'²*l2 + θ1'²*l1*cos(θ1-θ2))) / (l1*(2m1+m2-m2*cos(2θ1-2θ2)))

θ2'' = (2sin(θ1-θ2)*(θ1'²*l1*(m1+m2) + g*(m1+m2)*cos(θ1) + θ2'²*l2*m2*cos(θ1-θ2))) / (l2*(2m1+m2-m2*cos(2θ1-2θ2)))

Actually, looking at my code more carefully, I see that the equations are mostly correct but I should verify the structure. The key issue is that I'm using a fixed mass ratio and the equations might not produce truly chaotic behavior. Let me improve the simulation with proper double pendulum physics.

Let me improve the double pendulum simulation with more accurate physics and better visuals:

write /workspace/index.html (6948 bytes)
toolResult

Successfully wrote 6948 bytes to /workspace/index.html

assistant
thinking
I notice there's an issue - in the equations of motion, I have `den = 2*m1 + m2 - m2 * Math.cos(2*delta)` but I'm using `delta = th1 - th2`. The standard double pendulum equations typically use `delta = th2 - th1`. Let me check and fix the equations.

Actually, looking more carefully at the standard equations, the denominator should be `2*m1 + m2 - m2*cos(2*(th1-th2))` when using the conventional derivation. Let me verify with a reference.

The standard double pendulum equations (from Wikipedia and physics references) are:

For the denominator: (m1 + m2) * (2*m1 - m2*cos(2*delta)) + ... 

Actually, let me use a cleaner, more standard formulation. The equations I have look close but let me make sure they're correct.

Standard equations:
θ1'' = [-g(2m1+m2)sin(θ1) - m2*g*sin(θ1-2θ2) - 2sin(θ1-θ2)m2(θ2'²L2 + θ1'²L1*cos(θ1-θ2))] / [L1(2m1+m2-m2*cos(2(θ1-θ2)))]

θ2'' = [2sin(θ1-θ2)(θ1'²L1(m1+m2) + g(m1+m2)cos(θ1) + θ2'²L2*m2*cos(θ1-θ2))] / [L2(2m1+m2-m2*cos(2(θ1-θ2)))]

So my equations are correct with `delta = th1 - th2`. Let me double-check - actually I had `delta = th1 - th2` which is correct for these equations.

Wait, I changed it to `delta = th1 - th2` in the second version. That should be correct. Let me just verify the simulation runs properly.

Actually let me re-read the file to make sure it's correct.

Let me verify and fix the equations to use the standard double pendulum formulation:

read {"path":"/workspace/index.html"}
toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chaotic Double Pendulum</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: monospace; } canvas { display: block; } #ui { position: fixed; top: 10px; left: 10px; color: #fff; z-index: 100; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 5px; } #ui input[type="range"] { width: 150px; vertical-align: middle; } #ui label { display: block; margin: 8px 0; font-size: 13px; } #info { position: fixed; bottom: 10px; left: 10px; color: #666; font-size: 12px; } </style> </head> <body> <div id="ui"> <label>Length 1: <input type="range" id="l1" min="50" max="250" value="150"></label> <label>Length 2: <input type="range" id="l2" min="50" max="250" value="150"></label> <label>Gravity: <input type="range" id="g" min="0" max="2" step="0.1" value="1"></label> <label>Trail Length: <input type="range" id="trail" min="50" max="500" value="300"></label> <button id="reset">Random Reset</button> </div> <div id="info">Double Pendulum Chaos - Colorful Trail</div> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Physical parameters let l1 = 150, l2 = 150; const m1 = 1, m2 = 1; // normalized masses let g = 1; // State: [theta1, omega1, theta2, omega2] let state = [Math.PI/2, 0, Math.PI/2.5, 0]; // Trail points let trail = []; let maxTrail = 300; // Scale for drawing let scale = 1; const origin = { x: 0, y: 0 }; // Double pendulum equations of motion (derived from Lagrangian) function derivatives(s) { const [th1, w1, th2, w2] = s; const delta = th1 - th2; const den = 2*m1 + m2 - m2 * Math.cos(2*delta); const th1_dot = w1; const th2_dot = w2; const num1 = -g * (2*m1 + m2) * Math.sin(th1) - m2 * g * Math.sin(th1 - 2*th2) - 2 * Math.sin(delta) * m2 * (w2*w2*l2 + w1*w1*l1*Math.cos(delta)); const w1_dot = num1 / (l1 * den); const num2 = 2 * Math.sin(delta) * (w1*w1*l1*(m1 + m2) + g*(m1 + m2)*Math.cos(th1) + w2*w2*l2*m2*Math.cos(delta)); const w2_dot = num2 / (l2 * den); return [th1_dot, w1_dot, th2_dot, w2_dot]; } // 4th order Runge-Kutta integration function integrate(s, dt) { const k1 = derivatives(s); const s2 = s.map((v, i) => v + 0.5*dt*k1[i]); const k2 = derivatives(s2); const s3 = s.map((v, i) => v + 0.5*dt*k2[i]); const k3 = derivatives(s3); const s4 = s.map((v, i) => v + dt*k3[i]); const k4 = derivatives(s4); return s.map((v, i) => v + dt*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])/6); } function reset() { state = [ Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 5, Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 5 ]; trail = []; } // Convert angles to screen coordinates function getPositions() { const x1 = origin.x + l1 * scale * Math.sin(state[0]); const y1 = origin.y + l1 * scale * Math.cos(state[0]); const x2 = x1 + l2 * scale * Math.sin(state[2]); const y2 = y1 + l2 * scale * Math.cos(state[2]); return { x1, y1, x2, y2 }; } let hue = 0; function animate() { // Dark fade for trails ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Integrate physics state = integrate(state, 0.016); const { x1, y1, x2, y2 } = getPositions(); // Add to trail trail.push({ x: x2, y: y2 }); if (trail.length > maxTrail) trail.shift(); // Draw colorful trail with fading for (let i = 0; i < trail.length - 1; i++) { const p1 = trail[i]; const p2 = trail[i+1]; const t = i / trail.length; hue = (Date.now() * 0.1 + t * 60) % 360; const alpha = 0.3 + t * 0.7; ctx.strokeStyle = `hsla(${hue}, 90%, 60%, ${alpha})`; ctx.lineWidth = 1 + t * 4; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } // Draw arms with glow ctx.strokeStyle = '#fff'; ctx.lineWidth = 4; ctx.shadowColor = '#0ff'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(x1, y1); ctx.stroke(); ctx.strokeStyle = '#ff0'; ctx.shadowColor = '#f0f'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; // Draw joints ctx.fillStyle = '#0ff'; ctx.beginPath(); ctx.arc(origin.x, origin.y, 8, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#f0f'; ctx.beginPath(); ctx.arc(x1, y1, 14, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(x2, y2, 18, 0, Math.PI*2); ctx.fill(); requestAnimationFrame(animate); } function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; origin.x = canvas.width / 2; origin.y = canvas.height / 2.3; scale = Math.min(canvas.width, canvas.height) / 600; } // UI controls document.getElementById('l1').oninput = e => { l1 = +e.target.value; trail = []; }; document.getElementById('l2').oninput = e => { l2 = +e.target.value; trail = []; }; document.getElementById('g').oninput = e => g = +e.target.value; document.getElementById('trail').oninput = e => maxTrail = +e.target.value; document.getElementById('reset').onclick = reset; window.onresize = resize; resize(); reset(); animate(); </script> </body> </html>

assistant
thinking
The equations look mostly correct but I see an issue - the second equation has `th1 - 2*th2` but it should be `th2 - 2*th1` based on the standard double pendulum equations. Let me fix this to ensure correct physics. The standard equations are:

θ1'' = [-g(2m1+m2)sinθ1 - m2*g*sin(θ1-2θ2) - 2sin(θ1-θ2)m2(θ2'²L2 + θ1'²L1cos(θ1-θ2))] / [L1(2m1+m2-m2cos(2θ1-2θ2))]

θ2'' = [2sin(θ1-θ2)(θ1'²L1(m1+m2) + g(m1+m2)cosθ1 + θ2'²L2m2cos(θ1-θ2))] / [L2(2m1+m2-m2cos(2θ1-2θ2))]

Actually, looking at it more carefully, the equations I have seem to have some issues. Let me write the corrected version. The standard form uses θ1 and θ2 where θ1-θ2 appears in the coupling term.

Let me rewrite with the correct standard equations.

Let me fix the equations to use the correct double pendulum formulation:

edit {"newText":"// Double pendulum equations of motion (derived from Lagrangian)\n // Uses standard formulation with theta1, theta2 measu
toolResult

Successfully replaced 1 block(s) in /workspace/index.html.

assistant
thinking
Now let me also improve the trail drawing to make it more visually appealing and ensure the simulation runs properly. The current trail implementation has some issues - let me rewrite it to be cleaner and more efficient.

read {"path":"/workspace/index.html"}
toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chaotic Double Pendulum</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: monospace; } canvas { display: block; } #ui { position: fixed; top: 10px; left: 10px; color: #fff; z-index: 100; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 5px; } #ui input[type="range"] { width: 150px; vertical-align: middle; } #ui label { display: block; margin: 8px 0; font-size: 13px; } #info { position: fixed; bottom: 10px; left: 10px; color: #666; font-size: 12px; } </style> </head> <body> <div id="ui"> <label>Length 1: <input type="range" id="l1" min="50" max="250" value="150"></label> <label>Length 2: <input type="range" id="l2" min="50" max="250" value="150"></label> <label>Gravity: <input type="range" id="g" min="0" max="2" step="0.1" value="1"></label> <label>Trail Length: <input type="range" id="trail" min="50" max="500" value="300"></label> <button id="reset">Random Reset</button> </div> <div id="info">Double Pendulum Chaos - Colorful Trail</div> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Physical parameters let l1 = 150, l2 = 150; const m1 = 1, m2 = 1; // normalized masses let g = 1; // State: [theta1, omega1, theta2, omega2] let state = [Math.PI/2, 0, Math.PI/2.5, 0]; // Trail points let trail = []; let maxTrail = 300; // Scale for drawing let scale = 1; const origin = { x: 0, y: 0 }; // Double pendulum equations of motion (derived from Lagrangian) // Uses standard formulation with theta1, theta2 measured from vertical function derivatives(s) { const [th1, w1, th2, w2] = s; const d = th1 - th2; const den = 2*m1 + m2 - m2 * Math.cos(2*d); // Angular accelerations from Lagrangian mechanics const w1_dot = (-g*(2*m1 + m2)*Math.sin(th1) - m2*g*Math.sin(th1 - 2*th2) - 2*Math.sin(d)*m2*(w2*w2*l2 + w1*w1*l1*Math.cos(d))) / (l1 * den); const w2_dot = (2*Math.sin(d)*(w1*w1*l1*(m1 + m2) + g*(m1 + m2)*Math.cos(th1) + w2*w2*l2*m2*Math.cos(d))) / (l2 * den); return [w1, w1_dot, w2, w2_dot]; } // 4th order Runge-Kutta integration function integrate(s, dt) { const k1 = derivatives(s); const s2 = s.map((v, i) => v + 0.5*dt*k1[i]); const k2 = derivatives(s2); const s3 = s.map((v, i) => v + 0.5*dt*k2[i]); const k3 = derivatives(s3); const s4 = s.map((v, i) => v + dt*k3[i]); const k4 = derivatives(s4); return s.map((v, i) => v + dt*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])/6); } function reset() { state = [ Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 5, Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 5 ]; trail = []; } // Convert angles to screen coordinates function getPositions() { const x1 = origin.x + l1 * scale * Math.sin(state[0]); const y1 = origin.y + l1 * scale * Math.cos(state[0]); const x2 = x1 + l2 * scale * Math.sin(state[2]); const y2 = y1 + l2 * scale * Math.cos(state[2]); return { x1, y1, x2, y2 }; } let hue = 0; function animate() { // Dark fade for trails ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Integrate physics state = integrate(state, 0.016); const { x1, y1, x2, y2 } = getPositions(); // Add to trail trail.push({ x: x2, y: y2 }); if (trail.length > maxTrail) trail.shift(); // Draw colorful trail with fading for (let i = 0; i < trail.length - 1; i++) { const p1 = trail[i]; const p2 = trail[i+1]; const t = i / trail.length; hue = (Date.now() * 0.1 + t * 60) % 360; const alpha = 0.3 + t * 0.7; ctx.strokeStyle = `hsla(${hue}, 90%, 60%, ${alpha})`; ctx.lineWidth = 1 + t * 4; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } // Draw arms with glow ctx.strokeStyle = '#fff'; ctx.lineWidth = 4; ctx.shadowColor = '#0ff'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(x1, y1); ctx.stroke(); ctx.strokeStyle = '#ff0'; ctx.shadowColor = '#f0f'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; // Draw joints ctx.fillStyle = '#0ff'; ctx.beginPath(); ctx.arc(origin.x, origin.y, 8, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#f0f'; ctx.beginPath(); ctx.arc(x1, y1, 14, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(x2, y2, 18, 0, Math.PI*2); ctx.fill(); requestAnimationFrame(animate); } function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; origin.x = canvas.width / 2; origin.y = canvas.height / 2.3; scale = Math.min(canvas.width, canvas.height) / 600; } // UI controls document.getElementById('l1').oninput = e => { l1 = +e.target.value; trail = []; }; document.getElementById('l2').oninput = e => { l2 = +e.target.value; trail = []; }; document.getElementById('g').oninput = e => g = +e.target.value; document.getElementById('trail').oninput = e => maxTrail = +e.target.value; document.getElementById('reset').onclick = reset; window.onresize = resize; resize(); reset(); animate(); </script> </body> </html>

assistant
thinking
The code looks correct now. Let me improve it to make the trail even more colorful and add a better visual effect. I'll rewrite the whole file with an enhanced version that has a proper particle trail effect with fade.

Let me enhance the simulation with a better visual trail effect:

write /workspace/index.html (8679 bytes)
toolResult

Successfully wrote 8679 bytes to /workspace/index.html

assistant
thinking
Good, the file has been written. Let me verify it exists and check its content.

bash {"command":"ls -la /workspace/index.html && head -50 /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 8681 Jul 22 17:17 /workspace/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chaotic Double Pendulum</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: monospace; } canvas { display: block; cursor: crosshair; } #ui { position: fixed; top: 10px; left: 10px; color: #fff; z-index: 100; background: rgba(0,0,0,0.7); padding: 12px; border-radius: 6px; backdrop-filter: blur(5px); } #ui input[type="range"] { width: 160px; vertical-align: middle; } #ui label { display: block; margin: 8px 0; font-size: 13px; } #ui span { color: #0ff; font-weight: bold; } #info { position: fixed; bottom: 10px; left: 10px; color: #555; font-size: 11px; } </style> </head> <body> <div id="ui"> <div style="margin-bottom:8px;"><span>●</span> Double Pendulum Chaos</div> <label>Length 1: <input type="range" id="l1" min="50" max="250" value="150"> <span id="l1v">150</span></label> <label>Length 2: <input type="range" id="l2" min="50" max="250" value="150"> <span id="l2v">150</span></label> <label>Gravity: <input type="range" id="g" min="0" max="2" step="0.1" value="1"> <span id="gv">1</span></label> <label>Trail: <input type="range" id="trail" min="50" max="500" value="300"> <span id="trailv">300</span></label> <button id="reset" style="margin-top:8px;width:100%;padding:6px;background:#222;color:#0ff;border:1px solid #0ff;border-radius:4px;cursor:pointer;">Random Reset</button> </div> <div id="info">Pure JS | No Dependencies | Chaotic Dynamics</div> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Physical parameters let l1 = 150, l2 = 150; const m1 = 1, m2 = 1; let g = 1; // State: [theta1, omega1, theta2, omega2] let state = [Math.PI/2, 0, Math.PI/2, 0]; // Trail points with velocity for particle effect let trail = []; let maxTrail = 300; // Scale and origin for drawing let scale = 1; const origin = { x: 0, y: 0 };

assistant
thinking
The file looks good. Let me verify the complete structure is correct and the physics are sound. The file is self-contained with all CSS and JS inline, no external dependencies, and uses the correct double pendulum equations derived from Lagrangian mechanics.

Let me read the rest to make sure everything is complete.

read {"path":"/workspace/index.html","offset":50}
toolResult

const origin = { x: 0, y: 0 }; // Double pendulum equations of motion from Lagrangian mechanics function derivatives(s) { const [th1, w1, th2, w2] = s; const d = th1 - th2; const cd = Math.cos(d); const den = 2*m1 + m2 - m2 * Math.cos(2*d); const w1_dot = (-g*(2*m1 + m2)*Math.sin(th1) - m2*g*Math.sin(th1 - 2*th2) - 2*Math.sin(d)*m2*(w2*w2*l2 + w1*w1*l1*cd)) / (l1 * den); const w2_dot = (2*Math.sin(d)*(w1*w1*l1*(m1 + m2) + g*(m1 + m2)*Math.cos(th1) + w2*w2*l2*m2*cd)) / (l2 * den); return [w1, w1_dot, w2, w2_dot]; } // 4th order Runge-Kutta integration function integrate(s, dt) { const k1 = derivatives(s); const s2 = s.map((v, i) => v + 0.5*dt*k1[i]); const k2 = derivatives(s2); const s3 = s.map((v, i) => v + 0.5*dt*k2[i]); const k3 = derivatives(s3); const s4 = s.map((v, i) => v + dt*k3[i]); const k4 = derivatives(s4); return s.map((v, i) => v + dt*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])/6); } function reset() { state = [ Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 6, Math.PI/2 + (Math.random() - 0.5) * Math.PI, (Math.random() - 0.5) * 6 ]; trail = []; } // Get positions of both joints and tip function getPositions() { const x1 = origin.x + l1 * scale * Math.sin(state[0]); const y1 = origin.y + l1 * scale * Math.cos(state[0]); const x2 = x1 + l2 * scale * Math.sin(state[2]); const y2 = y1 + l2 * scale * Math.cos(state[2]); // Calculate tip velocity for particle trail const v0 = state[1]; const v2 = state[3]; const vx = l1 * scale * v0 * Math.cos(state[0]) + l2 * scale * v2 * Math.cos(state[2]); const vy = l1 * scale * v0 * Math.sin(state[0]) + l2 * scale * v2 * Math.sin(state[2]); return { x1, y1, x2, y2, vx, vy }; } function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; origin.x = canvas.width / 2; origin.y = canvas.height / 3; scale = Math.min(canvas.width, canvas.height) / 600; } function animate() { // Semi-transparent fade for ghosting effect ctx.fillStyle = 'rgba(0,0,0,0.12)'; ctx.fillRect(0, 0, canvas.width, canvas.height); state = integrate(state, 0.016); const { x1, y1, x2, y2, vx, vy } = getPositions(); // Add tip position to trail trail.push({ x: x2, y: y2, vx, vy }); if (trail.length > maxTrail) trail.shift(); // Draw colorful fading trail for (let i = 0; i < trail.length; i++) { const t = i / trail.length; const p = trail[i]; const hue = (Date.now() * 0.05 + t * 80) % 360; const alpha = 0.2 + t * 0.5; const size = 2 + t * 5; ctx.fillStyle = `hsla(${hue}, 90%, 65%, ${alpha})`; ctx.beginPath(); ctx.arc(p.x, p.y, size, 0, Math.PI*2); ctx.fill(); } // Draw trail lines ctx.lineWidth = 2; for (let i = trail.length - 1; i > 0; i--) { const t = i / trail.length; const p1 = trail[i]; const p2 = trail[i-1]; const hue = (Date.now() * 0.05 + t * 80) % 360; const alpha = 0.1 + t * 0.3; ctx.strokeStyle = `hsla(${hue}, 80%, 70%, ${alpha})`; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } // Draw pivot ctx.fillStyle = '#0ff'; ctx.beginPath(); ctx.arc(origin.x, origin.y, 6, 0, Math.PI*2); ctx.fill(); // Draw arms with glow ctx.strokeStyle = '#fff'; ctx.lineWidth = 3; ctx.shadowBlur = 8; ctx.shadowColor = '#0ff'; ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(x1, y1); ctx.stroke(); ctx.strokeStyle = '#f8f'; ctx.shadowColor = '#f0f'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; // Draw joints ctx.fillStyle = '#f0f'; ctx.beginPath(); ctx.arc(x1, y1, 10, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(x2, y2, 14, 0, Math.PI*2); ctx.fill(); // Update value displays document.getElementById('l1v').textContent = l1; document.getElementById('l2v').textContent = l2; document.getElementById('gv').textContent = g; document.getElementById('trailv').textContent = maxTrail; requestAnimationFrame(animate); } // UI controls document.getElementById('l1').oninput = e => { l1 = +e.target.value; document.getElementById('l1v').textContent = l1; }; document.getElementById('l2').oninput = e => { l2 = +e.target.value; document.getElementById('l2v').textContent = l2; }; document.getElementById('g').oninput = e => { g = +e.target.value; document.getElementById('gv').textContent = g; }; document.getElementById('trail').oninput = e => { maxTrail = +e.target.value; document.getElementById('trailv').textContent = maxTrail; }; document.getElementById('reset').onclick = reset; window.onresize = resize; resize(); reset(); animate(); </script> </body> </html>

assistant
thinking
The simulation looks good. Let me verify the file is complete and valid.

bash {"command":"wc -l /workspace/index.html && file /workspace/index.html"}
toolResult

217 /workspace/index.html /bin/bash: line 1: file: command not found Command exited with code 127

assistant
thinking
The file is created successfully at /workspace/index.html. Let me verify it's complete and self-contained by checking if it ends properly.

read {"path":"/workspace/index.html","offset":200}
toolResult

document.getElementById('trailv').textContent = maxTrail; requestAnimationFrame(animate); } // UI controls document.getElementById('l1').oninput = e => { l1 = +e.target.value; document.getElementById('l1v').textContent = l1; }; document.getElementById('l2').oninput = e => { l2 = +e.target.value; document.getElementById('l2v').textContent = l2; }; document.getElementById('g').oninput = e => { g = +e.target.value; document.getElementById('gv').textContent = g; }; document.getElementById('trail').oninput = e => { maxTrail = +e.target.value; document.getElementById('trailv').textContent = maxTrail; }; document.getElementById('reset').onclick = reset; window.onresize = resize; resize(); reset(); animate(); </script> </body> </html>

assistant
thinking
Good, the file looks complete. Let me verify it has all the necessary components - double pendulum physics, chaotic equations, trail, and it's all in one file. Let me also double-check the structure ends correctly.

The file is complete. Let me verify the file ends properly:

bash {"command":"tail -10 /workspace/index.html"}
toolResult

document.getElementById('trail').oninput = e => { maxTrail = +e.target.value; document.getElementById('trailv').textContent = maxTrail; }; document.getElementById('reset').onclick = reset; window.onresize = resize; resize(); reset(); animate(); </script> </body> </html>

assistant
thinking
The file is complete and valid. Let me verify it's a valid HTML file by checking key elements are present.

The `/workspace/index.html` file is complete and ready. It contains a self-contained double pendulum simulation with: - **CSS**: Embedded in `<style>` tag with dark theme styling - **JS**: All physics in `<script>` tag with: - Correct Lagrangian-derived equations of motion for chaotic double pendulum - 4th order Runge-Kutta integration - Dynamic trail rendering with fading colorful particles - Interactive controls for length, gravity, and trail length - Responsive canvas sizing **Key physics**: The simulation uses the standard double pendulum equations where the angular accelerations are solved from the coupled differential equations of motion, producing truly chaotic behavior sensitive to initial conditions. **Visual features**: - Glowing pendulum arms in cyan and magenta - Colorful trail that fades and cycles through hues - Particle effects at the tip trajectory - UI overlay with real-time value displays - No external dependencies - runs completely sandboxed

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modellaguna-m.1
vendorPoolside
harnesspi-agent
generated2026-07-22

EVALUATION

4/5 working

Chaotic double pendulum animates smoothly with vivid colorful fading trail; sliders present but inputs mainly just show simulation progressing.

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion8.5%
on click9.2%
on drag9.0%
on wheel7.6%
on enter + space9.8%
on arrow keys7.4%
on w/a/s/d7.8%
frame spread25.8 / 255
console errors0
js errors none

METRICS

runtime121.0s
tokens in29k
tokens out10k
cached157k
cost / run$0.0256