Double pendulum · deepseek-v4-flash
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.
transcript · 12 messages
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.
thinking
The user wants me to build a single-page web app that simulates a chaotic double pendulum. I need to use correct equations of motion (Lagrangian mechanics / Runge-Kutta integration), draw the arms and a fading colorful trail. Everything must be self-contained in index.html. Let me write the complete implementation: 1. HTML structure with a canvas 2. CSS for styling 3. JavaScript for: - Double pendulum physics using RK4 integration - Drawing arms (two segments) - Drawing a fading colorful trail of the tip The double pendulum equations of motion (using Lagrangian mechanics): For a double pendulum with masses m1, m2 and lengths l1, l2: θ1'' = (-g(2m1 + m2)sin(θ1) - m2*g*sin(θ1 - 2θ2) - 2*sin(θ1 - θ2)*m2*(θ2'^2*l2 + θ1'^2*l1*cos(θ1 - θ2))) / (l1*(2m1 + m2 - m2*cos(2θ1 - 2θ2))) θ2'' = (2*sin(θ1 - θ2)*(θ1'^2*l1*(m1 + m2) + g*(m1 + m2)*cos(θ1) + θ2'^2*l2*m2*cos(θ1 - θ2))) / (l2*(2m1 + m2 - m2*cos(2θ1 - 2θ2))) Let me implement this with RK4 integration, drawn on canvas with a trail. Actually, let me simplify the state vector approach and use RK4 properly. State: [θ1, θ2, ω1, ω2] Let me code this up carefully. For the trail, I'll store points and fade their opacity/color based on age. Let me write it all out.
/workspace/index.html (11946 bytes)Successfully wrote 11946 bytes to /workspace/index.html
thinking
Let me verify the file was written correctly and do a quick check of the physics equations. Let me review the file to make sure it's correct.
The double pendulum equations I used:
For θ1'':
(-g(2m1 + m2)sin(θ1) - m2*g*sin(θ1 - 2θ2) - 2*sin(θ1 - θ2)*m2*(θ2'²*l2 + θ1'²*l1*cos(θ1 - θ2))) / (l1*(2m1 + m2 - m2*cos(2θ1 - 2θ2)))
For θ2'':
(2*sin(θ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)))
Let me verify my implementation matches.
In `accelerations`:
```js
const dt = t1 - t2;
const c = Math.cos(dt); // cos(θ1 - θ2)
const s = Math.sin(dt); // sin(θ1 - θ2)
const c1 = Math.cos(t1);
const c2 = Math.cos(t2);
const s2 = Math.sin(t2);
```
Wait, I have `c2` and `s2` defined but never used. Let me check if they're needed... Actually in the equation for θ1'' I don't use cos(θ2) and sin(θ2) directly (they're used inside sin(θ1 - 2θ2) = sin(θ1)cos(2θ2) - cos(θ1)sin(2θ2), but I use the compound form directly). Let me check my Math.sin(t1 - 2*t2) - that's fine.
Actually, looking at my code more carefully, `c2` and `s2` are unused. That's fine, just unused variables. Not a bug.
Let me verify the equations once more:
θ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)))
My code:
```js
const a1 = (
-G * (2 * m1 + m2) * Math.sin(t1)
- m2 * G * Math.sin(t1 - 2 * t2)
- 2 * s * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * c)
) / (l1 * den);
```
✓
θ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)))
My code:
```js
const a2 = (
2 * s * (
w1 * w1 * l1 * (m1 + m2)
+ G * (m1 + m2) * c1
+ w2 * w2 * l2 * m2 * c
)
) / (l2 * den);
```
✓
Good, the equations look correct. The RK4 integration is also standard.
Let me also check if there's an issue with the canvas coordinate system. I'm using `CX` and `CY` in CSS pixel space, but `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` at the start of draw. Wait, `CX = W / 2 = size * dpr / 2`. But then in draw I set `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)`, so the drawing coordinates are in CSS pixels. But `CX` is in device pixels (canvas.width / 2). That's a bug!
Similarly, `CY = H * 0.32` is in device pixels.
Let me fix this. I should work in CSS pixel space throughout.
Actually, let me rethink. The canvas.width is set to size * dpr. The canvas.style.width is set to size. So CSS pixel space is [0, size] x [0, size].
In draw(), I set `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` which means I'm drawing in CSS pixel space.
But `CX = W / 2 = size * dpr / 2` - that's in device pixels, not CSS pixels.
So I need to use CSS pixel coordinates for everything. Let me fix this.
I have `cssW()` and `cssH()` functions that return the CSS pixel dimensions. But I'm not using them consistently.
Let me fix the code to use proper CSS pixel coordinates throughout.
Also, I realize the `getPositions()` function uses `CX` and `CY` which are in device pixels. And `l1` and `l2` are computed from `cssW()` which is in CSS pixels. So there's a dimension mismatch.
Let me fix all of this.
I'll rewrite the file with the fixes.Let me verify the physics and fix a coordinate system issue I spotted:
{"path":"/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: #0a0a12; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Segoe UI', system-ui, sans-serif; overflow: hidden; } .container { display: flex; flex-direction: column; align-items: center; gap: 10px; } canvas { display: block; background: #0e0e18; border-radius: 12px; box-shadow: 0 0 40px rgba(100, 60, 255, 0.08); cursor: pointer; } .controls { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; justify-content: center; } .controls label { color: #8888aa; font-size: 13px; display: flex; align-items: center; gap: 6px; } .controls input[type="range"] { width: 90px; accent-color: #7c5cfc; } .controls input[type="color"] { width: 32px; height: 32px; border: 2px solid #333; border-radius: 6px; background: transparent; padding: 2px; cursor: pointer; } button { background: #2a2a3e; color: #ccccee; border: 1px solid #3f3f5a; padding: 6px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.15s; } button:hover { background: #3c3c56; border-color: #7c5cfc; color: #fff; } .info { color: #666688; font-size: 12px; letter-spacing: 0.3px; } </style> </head> <body> <div class="container"> <canvas id="canvas"></canvas> <div class="controls"> <label>Mass 1 <input type="range" id="m1" min="1" max="20" value="10" step="0.5"></label> <label>Mass 2 <input type="range" id="m2" min="1" max="20" value="10" step="0.5"></label> <label>Speed <input type="range" id="speed" min="0.1" max="5" value="1" step="0.1"></label> <label>Trail <input type="range" id="trailLen" min="50" max="800" value="300" step="10"></label> <label>Color <input type="color" id="colorPicker" value="#7c5cfc"></label> <button id="resetBtn">↺ Reset</button> <button id="pauseBtn">⏸ Pause</button> </div> <div class="info">Click canvas to randomize · Drag to nudge pendulum 1</div> </div> <script> "use strict"; // ─── Physics: Double Pendulum ─────────────────────────────────────────────── const G = 9.81; function accelerations(state, m1, m2, l1, l2) { const [t1, t2, w1, w2] = state; const dt = t1 - t2; const c = Math.cos(dt); const s = Math.sin(dt); const c1 = Math.cos(t1); const c2 = Math.cos(t2); const s2 = Math.sin(t2); const den = 2 * m1 + m2 - m2 * Math.cos(2 * dt); const a1 = ( -G * (2 * m1 + m2) * Math.sin(t1) - m2 * G * Math.sin(t1 - 2 * t2) - 2 * s * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * c) ) / (l1 * den); const a2 = ( 2 * s * ( w1 * w1 * l1 * (m1 + m2) + G * (m1 + m2) * c1 + w2 * w2 * l2 * m2 * c ) ) / (l2 * den); return [a1, a2]; } function derivs(state, m1, m2, l1, l2) { const [t1, t2, w1, w2] = state; const [a1, a2] = accelerations(state, m1, m2, l1, l2); return [w1, w2, a1, a2]; } function rk4Step(state, dt, m1, m2, l1, l2) { const k1 = derivs(state, m1, m2, l1, l2).map(v => v * dt); const s2 = state.map((s, i) => s + k1[i] * 0.5); const k2 = derivs(s2, m1, m2, l1, l2).map(v => v * dt); const s3 = state.map((s, i) => s + k2[i] * 0.5); const k3 = derivs(s3, m1, m2, l1, l2).map(v => v * dt); const s4 = state.map((s, i) => s + k3[i]); const k4 = derivs(s4, m1, m2, l1, l2).map(v => v * dt); return state.map((s, i) => s + (k1[i] + 2*k2[i] + 2*k3[i] + k4[i]) / 6); } // ─── Rendering ────────────────────────────────────────────────────────────── const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let W, H, CX, CY; function resize() { const dpr = window.devicePixelRatio || 1; const maxW = Math.min(window.innerWidth - 20, 900); const maxH = Math.min(window.innerHeight - 140, 800); const size = Math.min(maxW, maxH); W = canvas.width = size * dpr; H = canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; CX = W / 2; CY = H * 0.32; ctx.scale(dpr, dpr); // convert back to CSS pixel coordinates for internal use // We'll work in CSS pixels and scale } resize(); // Work in CSS-pixel space function cssW() { return W / (window.devicePixelRatio || 1); } function cssH() { return H / (window.devicePixelRatio || 1); } const L1_RATIO = 0.22; const L2_RATIO = 0.22; const MASS_RADIUS_RATIO = 0.045; // ─── State ────────────────────────────────────────────────────────────────── let m1 = 10, m2 = 10; let l1, l2; let state = [Math.PI * 1.2, Math.PI * 0.6, 0, 0]; let trail = []; let maxTrail = 300; let paused = false; let speed = 1; let hueBase = 270; // matches #7c5cfc roughly function randomizeState() { state = [ Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, ]; } function computeLengths() { const cssWidth = cssW(); l1 = cssWidth * L1_RATIO; l2 = cssWidth * L2_RATIO; } function getPositions() { 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); return { x1, y1, x2, y2 }; } // ─── Drawing ──────────────────────────────────────────────────────────────── function hslStr(h, s, l, a = 1) { return `hsla(${h}, ${s}%, ${l}%, ${a})`; } function draw() { const cssW_ = cssW(); const cssH_ = cssH(); const dpr = window.devicePixelRatio || 1; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // Clear with slight trail fade ctx.fillStyle = 'rgba(14, 14, 24, 0.12)'; ctx.fillRect(0, 0, cssW_, cssH_); // Full clear every ~30 frames to avoid ghosting // We do a partial fade above, and periodically a full clear if (trail.length > 0 && trail.length % 30 === 0) { ctx.fillStyle = '#0e0e18'; ctx.globalAlpha = 0.4; ctx.fillRect(0, 0, cssW_, cssH_); ctx.globalAlpha = 1; } const pos = getPositions(); const { x1, y1, x2, y2 } = pos; // ── Draw trail ── const len = trail.length; if (len > 1) { // Compute trail bounding box for gradient direction for (let i = 1; i < len; i++) { const p = trail[i]; const q = trail[i - 1]; const t = i / len; // 0 = oldest, 1 = newest const alpha = t * t * 0.85 + 0.03; const hue = (hueBase + i * 1.8) % 360; const lightness = 45 + t * 35; ctx.strokeStyle = hslStr(hue, 90, lightness, alpha); ctx.lineWidth = 1.5 + t * 3; ctx.beginPath(); ctx.moveTo(q.x, q.y); ctx.lineTo(p.x, p.y); ctx.stroke(); } } // ── Arms ── const mr = cssW_ * MASS_RADIUS_RATIO; // Arm 1 ctx.beginPath(); ctx.moveTo(CX, CY); ctx.lineTo(x1, y1); ctx.strokeStyle = 'rgba(200, 200, 230, 0.6)'; ctx.lineWidth = 3; ctx.stroke(); // Arm 2 ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = 'rgba(200, 200, 230, 0.5)'; ctx.lineWidth = 2.5; ctx.stroke(); // Pivot ctx.beginPath(); ctx.arc(CX, CY, 4, 0, 2 * Math.PI); ctx.fillStyle = '#444466'; ctx.fill(); // Mass 1 const grad1 = ctx.createRadialGradient(x1 - mr * 0.3, y1 - mr * 0.3, mr * 0.1, x1, y1, mr); const h1 = (hueBase + 30) % 360; grad1.addColorStop(0, hslStr(h1, 85, 75)); grad1.addColorStop(1, hslStr(h1, 80, 45)); ctx.beginPath(); ctx.arc(x1, y1, mr, 0, 2 * Math.PI); ctx.fillStyle = grad1; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1; ctx.stroke(); // Mass 2 const grad2 = ctx.createRadialGradient(x2 - mr * 0.3, y2 - mr * 0.3, mr * 0.1, x2, y2, mr); const h2 = (hueBase + 180) % 360; grad2.addColorStop(0, hslStr(h2, 90, 80)); grad2.addColorStop(1, hslStr(h2, 85, 50)); ctx.beginPath(); ctx.arc(x2, y2, mr, 0, 2 * Math.PI); ctx.fillStyle = grad2; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1; ctx.stroke(); // Glow on tip const gradG = ctx.createRadialGradient(x2, y2, 0, x2, y2, mr * 2.5); const hG = (hueBase + (len * 1.8) % 360); gradG.addColorStop(0, hslStr(hG, 100, 85, 0.5)); gradG.addColorStop(1, hslStr(hG, 100, 85, 0)); ctx.beginPath(); ctx.arc(x2, y2, mr * 2.5, 0, 2 * Math.PI); ctx.fillStyle = gradG; ctx.fill(); } // ─── Simulation Loop ──────────────────────────────────────────────────────── let lastTime = 0; const SUBSTEPS = 8; function tick(time) { if (!lastTime) lastTime = time; let dt = (time - lastTime) / 1000; lastTime = time; if (!paused) { dt = Math.min(dt, 0.05) * speed; const subDt = dt / SUBSTEPS; for (let i = 0; i < SUBSTEPS; i++) { state = rk4Step(state, subDt, m1, m2, l1, l2); } const pos = getPositions(); // Add trail point (in CSS pixel coords) trail.push({ x: pos.x2, y: pos.y2 }); if (trail.length > maxTrail) { trail.splice(0, trail.length - maxTrail); } } draw(); requestAnimationFrame(tick); } // ─── Controls ─────────────────────────────────────────────────────────────── document.getElementById('m1').addEventListener('input', e => { m1 = parseFloat(e.target.value); }); document.getElementById('m2').addEventListener('input', e => { m2 = parseFloat(e.target.value); }); document.getElementById('speed').addEventListener('input', e => { speed = parseFloat(e.target.value); }); document.getElementById('trailLen').addEventListener('input', e => { maxTrail = parseInt(e.target.value); if (trail.length > maxTrail) { trail.splice(0, trail.length - maxTrail); } }); document.getElementById('colorPicker').addEventListener('input', e => { // Convert hex to hue const hex = e.target.value; const r = parseInt(hex.slice(1,3), 16) / 255; const g = parseInt(hex.slice(3,5), 16) / 255; const b = parseInt(hex.slice(5,7), 16) / 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h = 0; if (max !== min) { const d = max - min; if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60; else if (max === g) h = ((b - r) / d + 2) * 60; else h = ((r - g) / d + 4) * 60; } hueBase = h; }); document.getElementById('resetBtn').addEventListener('click', () => { trail = []; const m1v = document.getElementById('m1').value; const m2v = document.getElementById('m2').value; m1 = parseFloat(m1v); m2 = parseFloat(m2v); state = [Math.PI * 1.2, Math.PI * 0.6, 0, 0]; lastTime = 0; }); document.getElementById('pauseBtn').addEventListener('click', () => { paused = !paused; document.getElementById('pauseBtn').textContent = paused ? '▶ Resume' : '⏸ Pause'; if (!paused) lastTime = 0; }); // Click to randomize canvas.addEventListener('click', (e) => { trail = []; randomizeState(); lastTime = 0; }); // Drag to nudge pendulum 1 let dragging = false; canvas.addEventListener('mousedown', (e) => { dragging = true; }); canvas.addEventListener('mousemove', (e) => { if (!dragging) return; const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); const mx = (e.clientX - rect.left); const my = (e.clientY - rect.top); const dx = mx - CX / dpr; const dy = my - CY / dpr; const angle = Math.atan2(dx, dy); state[0] = angle; state[1] = angle + 0.3 + (Math.random() - 0.5) * 0.5; state[2] = 0; state[3] = 0; trail = []; }); document.addEventListener('mouseup', () => { dragging = false; }); // Resize window.addEventListener('resize', () => { resize(); computeLengths(); // Recompute positions stay, trail may need adapt — keep }); // ─── Init ─────────────────────────────────────────────────────────────────── computeLengths(); requestAnimationFrame(tick); </script> </body> </html>
thinking
I see the coordinate system issue. `CX = W / 2` where W is in device pixels. But in `draw()`, I set `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` and then draw using CSS pixel coordinates. So `CX` and `CY` should be in CSS pixel coordinates, not device pixels. Let me fix: 1. `CX` and `CY` should be in CSS pixel space 2. `getPositions()` uses `CX`, `CY` which should be CSS pixels 3. The drag handler computes `CX / dpr` which is wrong - should be just `CX` (in CSS pixels) Let me fix all coordinate-related issues. Also, I should remove the unused `c2` and `s2` variables. Let me rewrite the file with all fixes.
I see a coordinate system bug — `CX`/`CY` are in device pixels but drawing happens in CSS pixel space. Let me fix that and clean up unused variables.
/workspace/index.html (11396 bytes)Successfully wrote 11396 bytes to /workspace/index.html
thinking
Let me verify the file looks correct by reading it back and checking a few critical sections.
Let me verify the file is complete and correct
{"path":"/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: #0a0a12; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Segoe UI', system-ui, sans-serif; overflow: hidden; } .container { display: flex; flex-direction: column; align-items: center; gap: 10px; } canvas { display: block; background: #0e0e18; border-radius: 12px; box-shadow: 0 0 40px rgba(100, 60, 255, 0.08); cursor: pointer; } .controls { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; justify-content: center; } .controls label { color: #8888aa; font-size: 13px; display: flex; align-items: center; gap: 6px; } .controls input[type="range"] { width: 90px; accent-color: #7c5cfc; } .controls input[type="color"] { width: 32px; height: 32px; border: 2px solid #333; border-radius: 6px; background: transparent; padding: 2px; cursor: pointer; } button { background: #2a2a3e; color: #ccccee; border: 1px solid #3f3f5a; padding: 6px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.15s; } button:hover { background: #3c3c56; border-color: #7c5cfc; color: #fff; } .info { color: #666688; font-size: 12px; letter-spacing: 0.3px; } </style> </head> <body> <div class="container"> <canvas id="canvas"></canvas> <div class="controls"> <label>Mass 1 <input type="range" id="m1" min="1" max="20" value="10" step="0.5"></label> <label>Mass 2 <input type="range" id="m2" min="1" max="20" value="10" step="0.5"></label> <label>Speed <input type="range" id="speed" min="0.1" max="5" value="1" step="0.1"></label> <label>Trail <input type="range" id="trailLen" min="50" max="800" value="300" step="10"></label> <label>Color <input type="color" id="colorPicker" value="#7c5cfc"></label> <button id="resetBtn">↺ Reset</button> <button id="pauseBtn">⏸ Pause</button> </div> <div class="info">Click canvas to randomize · Drag to nudge pendulum 1</div> </div> <script> "use strict"; // ─── Physics: Double Pendulum ─────────────────────────────────────────────── const G = 9.81; function accelerations(state, m1, m2, l1, l2) { const [t1, t2, w1, w2] = state; const dt = t1 - t2; const c = Math.cos(dt); const s = Math.sin(dt); const c1 = Math.cos(t1); const den = 2 * m1 + m2 - m2 * Math.cos(2 * dt); const a1 = ( -G * (2 * m1 + m2) * Math.sin(t1) - m2 * G * Math.sin(t1 - 2 * t2) - 2 * s * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * c) ) / (l1 * den); const a2 = ( 2 * s * ( w1 * w1 * l1 * (m1 + m2) + G * (m1 + m2) * c1 + w2 * w2 * l2 * m2 * c ) ) / (l2 * den); return [a1, a2]; } function derivs(state, m1, m2, l1, l2) { const [t1, t2, w1, w2] = state; const [a1, a2] = accelerations(state, m1, m2, l1, l2); return [w1, w2, a1, a2]; } function rk4Step(state, dt, m1, m2, l1, l2) { const k1 = derivs(state, m1, m2, l1, l2).map(v => v * dt); const s2 = state.map((s, i) => s + k1[i] * 0.5); const k2 = derivs(s2, m1, m2, l1, l2).map(v => v * dt); const s3 = state.map((s, i) => s + k2[i] * 0.5); const k3 = derivs(s3, m1, m2, l1, l2).map(v => v * dt); const s4 = state.map((s, i) => s + k3[i]); const k4 = derivs(s4, m1, m2, l1, l2).map(v => v * dt); return state.map((s, i) => s + (k1[i] + 2*k2[i] + 2*k3[i] + k4[i]) / 6); } // ─── Rendering ────────────────────────────────────────────────────────────── const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let cssSize; // CSS pixel size of canvas (square) let CX, CY; // pivot center in CSS pixels let dpr = 1; function resize() { dpr = window.devicePixelRatio || 1; const maxW = Math.min(window.innerWidth - 20, 900); const maxH = Math.min(window.innerHeight - 140, 800); cssSize = Math.min(maxW, maxH); canvas.width = cssSize * dpr; canvas.height = cssSize * dpr; canvas.style.width = cssSize + 'px'; canvas.style.height = cssSize + 'px'; CX = cssSize / 2; CY = cssSize * 0.32; } resize(); const L1_RATIO = 0.22; const L2_RATIO = 0.22; const MASS_RADIUS_RATIO = 0.045; // ─── State ────────────────────────────────────────────────────────────────── let m1 = 10, m2 = 10; let l1, l2; let state = [Math.PI * 1.2, Math.PI * 0.6, 0, 0]; let trail = []; let maxTrail = 300; let paused = false; let speed = 1; let hueBase = 270; // matches #7c5cfc roughly function randomizeState() { state = [ Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, ]; } function computeLengths() { l1 = cssSize * L1_RATIO; l2 = cssSize * L2_RATIO; } function getPositions() { 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); return { x1, y1, x2, y2 }; } // ─── Drawing ──────────────────────────────────────────────────────────────── function hslStr(h, s, l, a = 1) { return `hsla(${h}, ${s}%, ${l}%, ${a})`; } function draw() { // Set transform so coordinates are in CSS pixels ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // Partial fade clear for trail persistence ctx.fillStyle = 'rgba(14, 14, 24, 0.12)'; ctx.fillRect(0, 0, cssSize, cssSize); // Full clear every ~30 frames to prevent ghost buildup if (trail.length > 0 && trail.length % 30 === 0) { ctx.fillStyle = '#0e0e18'; ctx.globalAlpha = 0.4; ctx.fillRect(0, 0, cssSize, cssSize); ctx.globalAlpha = 1; } const pos = getPositions(); const { x1, y1, x2, y2 } = pos; // ── Draw trail ── const len = trail.length; if (len > 1) { for (let i = 1; i < len; i++) { const p = trail[i]; const q = trail[i - 1]; const t = i / len; // 0 = oldest, 1 = newest const alpha = t * t * 0.85 + 0.03; const hue = (hueBase + i * 1.8) % 360; const lightness = 45 + t * 35; ctx.strokeStyle = hslStr(hue, 90, lightness, alpha); ctx.lineWidth = 1.5 + t * 3; ctx.beginPath(); ctx.moveTo(q.x, q.y); ctx.lineTo(p.x, p.y); ctx.stroke(); } } // ── Arms ── const mr = cssSize * MASS_RADIUS_RATIO; // Arm 1 ctx.beginPath(); ctx.moveTo(CX, CY); ctx.lineTo(x1, y1); ctx.strokeStyle = 'rgba(200, 200, 230, 0.6)'; ctx.lineWidth = 3; ctx.stroke(); // Arm 2 ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = 'rgba(200, 200, 230, 0.5)'; ctx.lineWidth = 2.5; ctx.stroke(); // Pivot ctx.beginPath(); ctx.arc(CX, CY, 4, 0, 2 * Math.PI); ctx.fillStyle = '#444466'; ctx.fill(); // Mass 1 const grad1 = ctx.createRadialGradient(x1 - mr * 0.3, y1 - mr * 0.3, mr * 0.1, x1, y1, mr); const h1 = (hueBase + 30) % 360; grad1.addColorStop(0, hslStr(h1, 85, 75)); grad1.addColorStop(1, hslStr(h1, 80, 45)); ctx.beginPath(); ctx.arc(x1, y1, mr, 0, 2 * Math.PI); ctx.fillStyle = grad1; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1; ctx.stroke(); // Mass 2 const grad2 = ctx.createRadialGradient(x2 - mr * 0.3, y2 - mr * 0.3, mr * 0.1, x2, y2, mr); const h2 = (hueBase + 180) % 360; grad2.addColorStop(0, hslStr(h2, 90, 80)); grad2.addColorStop(1, hslStr(h2, 85, 50)); ctx.beginPath(); ctx.arc(x2, y2, mr, 0, 2 * Math.PI); ctx.fillStyle = grad2; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1; ctx.stroke(); // Glow on tip const gradG = ctx.createRadialGradient(x2, y2, 0, x2, y2, mr * 2.5); const hG = (hueBase + (len * 1.8) % 360); gradG.addColorStop(0, hslStr(hG, 100, 85, 0.5)); gradG.addColorStop(1, hslStr(hG, 100, 85, 0)); ctx.beginPath(); ctx.arc(x2, y2, mr * 2.5, 0, 2 * Math.PI); ctx.fillStyle = gradG; ctx.fill(); } // ─── Simulation Loop ──────────────────────────────────────────────────────── let lastTime = 0; const SUBSTEPS = 8; function tick(time) { if (!lastTime) lastTime = time; let dt = (time - lastTime) / 1000; lastTime = time; if (!paused) { dt = Math.min(dt, 0.05) * speed; const subDt = dt / SUBSTEPS; for (let i = 0; i < SUBSTEPS; i++) { state = rk4Step(state, subDt, m1, m2, l1, l2); } const pos = getPositions(); trail.push({ x: pos.x2, y: pos.y2 }); if (trail.length > maxTrail) { trail.splice(0, trail.length - maxTrail); } } draw(); requestAnimationFrame(tick); } // ─── Controls ─────────────────────────────────────────────────────────────── document.getElementById('m1').addEventListener('input', e => { m1 = parseFloat(e.target.value); }); document.getElementById('m2').addEventListener('input', e => { m2 = parseFloat(e.target.value); }); document.getElementById('speed').addEventListener('input', e => { speed = parseFloat(e.target.value); }); document.getElementById('trailLen').addEventListener('input', e => { maxTrail = parseInt(e.target.value); if (trail.length > maxTrail) { trail.splice(0, trail.length - maxTrail); } }); document.getElementById('colorPicker').addEventListener('input', e => { // Convert hex to hue const hex = e.target.value; const r = parseInt(hex.slice(1,3), 16) / 255; const g = parseInt(hex.slice(3,5), 16) / 255; const b = parseInt(hex.slice(5,7), 16) / 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h = 0; if (max !== min) { const d = max - min; if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60; else if (max === g) h = ((b - r) / d + 2) * 60; else h = ((r - g) / d + 4) * 60; } hueBase = h; }); document.getElementById('resetBtn').addEventListener('click', () => { trail = []; const m1v = document.getElementById('m1').value; const m2v = document.getElementById('m2').value; m1 = parseFloat(m1v); m2 = parseFloat(m2v); state = [Math.PI * 1.2, Math.PI * 0.6, 0, 0]; lastTime = 0; }); document.getElementById('pauseBtn').addEventListener('click', () => { paused = !paused; document.getElementById('pauseBtn').textContent = paused ? '▶ Resume' : '⏸ Pause'; if (!paused) lastTime = 0; }); // Click to randomize canvas.addEventListener('click', (e) => { trail = []; randomizeState(); lastTime = 0; }); // Drag to nudge pendulum 1 let dragging = false; canvas.addEventListener('mousedown', (e) => { dragging = true; }); canvas.addEventListener('mousemove', (e) => { if (!dragging) return; const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const dx = mx - CX; const dy = my - CY; const angle = Math.atan2(dx, dy); state[0] = angle; state[1] = angle + 0.3 + (Math.random() - 0.5) * 0.5; state[2] = 0; state[3] = 0; trail = []; }); document.addEventListener('mouseup', () => { dragging = false; }); // Resize window.addEventListener('resize', () => { resize(); computeLengths(); }); // ─── Init ─────────────────────────────────────────────────────────────────── computeLengths(); requestAnimationFrame(tick); </script> </body> </html>
thinking
The file looks good. Let me verify it's syntactically valid and check the file size.
{"command":"wc -c /workspace/index.html && node -e \"\nconst fs = require('fs');\nconst html = fs.readFileSync('/workspace/index.html', 'utf12313 /workspace/index.html JS syntax: OK No external resources: OK All checks passed
thinking
Everything checks out. The file is syntactically valid, has no external references, and contains all the physics and rendering logic. Let me provide a summary to the user.
`index.html` is written (12.3 KB, single self-contained file). Here's what's inside: **Physics — correct double pendulum equations** - Lagrangian-derived equations of motion via `accelerations()` - 4th-order Runge-Kutta integration (`rk4Step`) with 8 substeps per frame for numerical stability - State vector `[θ₁, θ₂, ω₁, ω₂]` **Rendering** - Canvas with `devicePixelRatio` scaling for crisp rendering on HiDPI displays - **Two arms** drawn as lines from pivot → mass 1 → mass 2 - **Colorful fading trail** — each segment gets a cycling hue (`hueBase + i×1.8`), opacity ramps up quadratically with age (`t²`), line width thickens toward the tip - Radial gradient glows on both masses and a tip glow **Controls** | Control | Action | |---------|--------| | Mass 1 / Mass 2 sliders | Adjust pendulum masses (1–20) | | Speed slider | Simulation speed (0.1×–5×) | | Trail slider | Trail length (50–800 points) | | Color picker | Base hue for the trail & masses | | Reset button | Return to initial angles | | Pause/Resume | Freeze/unfreeze simulation | | Click canvas | Randomize angles & angular velocities | | Drag on canvas | Nudge pendulum 1 to follow cursor |
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
EVALUATION
Double pendulum simulates with colorful fading trail; motion continues idly and reacts to clicks/drags/keys.
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.