oneshotlm
Compare all models on this prompt →

Double pendulum · step-3.7-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.

⛨ sandboxed Open in new tab ↗
transcript · 12 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 a single-page web app that simulates a double pendulum on a canvas. It needs to be self-contained in one HTML file with no external resources. I need to:

1. Implement the double pendulum physics (Lagrangian mechanics)
2. Draw on a canvas: arms, bob masses, and a colorful fading trail of the tip
3. All CSS in a `<style>` tag, JS in a `<script>` tag
4. No external resources, CDNs, or network requests
5. End with index.html present

I'll write the file directly using `write`.

The equations of motion for a double pendulum are well-established. I'll use the standard Lagrangian derivation.

Parameters:
- θ1, θ2: angles from vertical
- ω1, ω2: angular velocities
- m1, m2: masses
- L1, L2: lengths
- g: gravity

Equations:
Δ = 2*m1 + m2 - m2*cos(2*θ1 - 2*θ2)

α1 = (-g*(2*m1+m2)*sin(θ1) - m2*g*sin(θ1-2*θ2) - 2*sin(θ1-θ2)*m2*(ω2^2*L2 + ω1^2*L1*cos(θ1-θ2))) / (L1*Δ)

α2 = (2*sin(θ1-θ2)*(ω1^2*L1*(m1+m2) + g*(m1+m2)*cos(θ1) + ω2^2*L2*m2*cos(θ1-θ2))) / (L2*Δ)

I'll use a Runge-Kutta 4th order integrator or at least symplectic Euler (semi-implicit Euler) which is simpler and decent for visualization. But for a chaotic double pendulum, RK4 gives much better accuracy and energy conservation. I'll implement RK4.

Canvas: 800x600 or responsive. Fill whole screen.

Arm and bobs: strokes and circles.
Trail: store positions of tip (end of second pendulum), draw with decreasing opacity and maybe hue variation.

Interactive: maybe allow dragging or changing parameters? The user didn't ask for interactivity beyond "simulate", but I'll add a clean UI overlay with some controls that don't require external resources: maybe just a click-to-reset or some sliders for parameters using HTML inputs styled with CSS.

But to keep it robust and self-contained, I'll make the canvas fill the window, draw on it each frame, and include a minimal controls panel using simple DOM elements styled with CSS.

I'll add:
- Trail length control
- Line width / mass radius
- Maybe pause/reset buttons
- Maybe gravity control

All within the single HTML file.

Let me draft the code.

HTML structure:
- `<canvas id="canvas">`
- A small overlay div with controls.

CSS:
- body { margin:0; overflow:hidden; background:#111; font-family:sans-serif; color:#ccc; }
- canvas { display:block; }
- .controls { position:absolute; top:10px; left:10px; background:rgba(0,0,0,0.5); padding:10px; border-radius:8px; }

JS:
- Init canvas size to window inner sizes.
- Pendulum class/structure.
- State: theta1, theta2, omega1, omega2.
- Derivative function for RK4.
- RK4 step function.
- Drawing loop with requestAnimationFrame.
- Trail as an array of {x, y, hue} objects. Max length. Draw lines between them with globalAlpha fading.
- Colors: use HSL for rainbow effect along trail.

I need to make sure there are no external fonts or images. Use system fonts.

Let's write the complete code.

Double checking equations from reliable source (standard Wikipedia):
Let Δ = 2m1 + m2 - m2 cos(2θ1 - 2θ2)
θ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*Δ)
θ2'' = (2 sin(θ1-θ2)*(ω1^2*L1*(m1+m2) + g*(m1+m2)*cosθ1 + ω2^2*L2*m2*cos(θ1-θ2))) / (L2*Δ)

Yes.

For RK4:
k1 = dt * derivs(t, y)
k2 = dt * derivs(t+dt/2, y + k1/2)
k3 = dt * derivs(t+dt/2, y + k2/2)
k4 = dt * derivs(t+dt, y + k3)
y_new = y + (k1 + 2*k2 + 2*k3 + k4)/6

Where y = [θ1, ω1, θ2, ω2]

derivs returns [ω1, α1, ω2, α2]

This will work well.

I'll define lengths in pixels or scaled lengths. Let's say L1=120, L2=120 (pixels). Initial angles: θ1=Math.PI/2, θ2=Math.PI/2 for chaotic behavior. Or maybe slightly different. Actually for immediate chaos, I'll set them to something like 120° and not aligned.

Masses: m1 and m2 just for display radius and for the math. Actually math uses m1/m2 for inertia, not directly for radius. I'll set radius proportional to sqrt(m) maybe, or just linearly for visual clarity. m1=10, m2=10, radii 20 and 15? Or 20 and 10? Let's do red and blue balls.

Colors: tip trail can cycle through hue based on time or position.

Arms: draw lines from origin through angles to bobs.

Background: clear canvas each frame with slight opacity to create natural fade? No, user wants drawing the arms and a colorful fading trail of the tip. So I'll manage the trail explicitly with an array, rather than not clearing the canvas. I'll clear canvas fully each frame, then redraw trail, then redraw pendulum.

Trail rendering:
- Draw connected line segments or just dots. For a smooth trail, lines with width decreasing or just simple lines with globalAlpha based on age.
- I'll store points. Each point has x,y. When drawing, for i from 0 to len-1, segment from i to i+1. Opacity proportional to i/len. Line width maybe constant or decreasing.

To make it color the hue of the first point cycling over time, or diverge based on angle speed. I'll increment a hue variable every frame and assign to new points.

I'll add controls for:
- L1 (slider)
- L2 (slider)
- m1, m2 (maybe not showing mass effect visibly other than radii, but it affects physics)
- gravity
- dt (time step)
- Trail length
- Reset button

Keep controls minimal.

Let's write the file content.

I must not create separate files. Use write tool.

File path: /workspace/index.html

Code check:
- `window.requestAnimationFrame` loop
- `canvas.width = window.innerWidth` etc.
- handle resize
- use `ctx.lineCap = 'round'`, `ctx.lineJoin = 'round'`

OK.

I'll write a complete HTML string.

One concern: if the time step is too large, energy explodes. I'll use a small dt like 0.05-0.1 per frame but with RK4 it's more stable. Actually per physics update I can do multiple sub-steps if needed. For smoothness and stability, I'll keep dt relatively small (0.05) and maybe do one step per frame, or step multiple times per frame. I'll just do one RK4 step per animation frame. At 60fps that's fine. Maybe dt=0.05 to 0.1.

I'll set dt as adjustable. Default to 0.05. Actually in RK4, the step size controls accuracy. 0.05 might be too small resulting in slow motion. Usually we scale dt such that visual speed looks good. I'll set dt=0.2 or so with RK4? No, 0.2 might be large. Let's set dt=0.05 but the simulation runs fast because dt is in seconds? Wait: if we use seconds and dt=0.05s, that's fine. At 60fps, one frame = 0.05s = real time. That's exact. But if we want to slow it down we can use dt=0.016 or so. Let's just use fixed dt inside animation loop.

Actually better to measure real time delta and scale dt, to keep consistent speed regardless of frame rate. I'll compute `Δt = now - lastTime`, cap it, and pass that as dt to RK4, multiplied by a speed factor maybe. Or just use fixed dt. Simpler: fixed dt=0.05. That will make simulation slightly slower than real time? Actually real time passes ~0.016s per frame, so simulation will evolve faster than real time if dt=0.05. That's fine for animation.

I'll use a speed slider from 0.1x to 3x. dt = baseDt * speedMultiplier. Base dt maybe 0.05.

Let's assemble the code.

```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</title>
<style>
  /* CSS here */
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="controls">
  <!-- sliders -->
</div>
<script>
// JS here
</script>
</body>
</html>
```

Let's flesh it out.

I want the controls to be unobtrusive.

CSS:
body { background: #0a0a0f; color: #ddd; font-family: system-ui, -apple-system, sans-serif; margin: 0; overflow: hidden; }
canvas { display: block; }
.controls { position: absolute; top: 12px; left: 12px; background: rgba(10,10,20,0.7); padding: 10px 14px; border-radius: 10px; border: 1px solid #333; font-size: 13px; backdrop-filter: blur(4px); }
.controls label { display: block; margin: 4px 0; }
.controls input[type="range"] { width: 100px; margin-left: 6px; vertical-align: middle; }
.controls button { margin-top: 8px; padding: 4px 10px; cursor: pointer; background: #2a2a3a; color: #fff; border: 1px solid #555; border-radius: 4px; }
.controls button:hover { background: #3a3a4a; }

In JS, add elements dynamically or just hardcode HTML for controls. I'll hardcode them for clarity and keep it simple.

Controls:
- Gravity: range 0-20, default 9.81
- Speed: range 0.1-2, default 1
- Trail: range 100-2000, default 800
- Length 1: range 50-300, default 140
- Length 2: range 50-300, default 140
- Reset button

I'll also add a simple info text: "Double Pendulum Simulation"

Now physics code.

State object:
{
  t1: Math.PI/2,
  w1: 0,
  t2: Math.PI/2 + 0.5, // slightly offset for chaos
  w2: 0
}

Wait, if both are PI/2 they hang horizontally. To get chaos I'll set them to like Math.PI*0.6 and Math.PI*0.3.

Actually let's randomize slightly: t2 = 1.5*Math.PI, w1=0, etc. Or use fixed values.

Let's use:
t1 = Math.PI/2 (horizontal to the right)
t2 = Math.PI/2 + 0.4 (a bit offset)

Compute positions:
x1 = cx + L1 * sin(t1)
y1 = cy + L1 * cos(t1)
x2 = x1 + L2 * sin(t2)
y2 = y1 + L2 * cos(t2)

Derivatives function:

function derivs(state) {
  const {t1, w1, t2, w2, L1, L2, m1, m2, g} = state;
  const d = Math.sin(t1 - t2);
  const D = 2*m1 + m2 - m2*Math.cos(2*t1 - 2*t2);
  const a1 = (-g*(2*m1+m2)*Math.sin(t1) - m2*g*Math.sin(t1-2*t2) - 2*d*m2*(w2*w2*L2 + w1*w1*L1*Math.cos(t1-t2))) / (L1*D);
  const a2 = (2*d*(w1*w1*L1*(m1+m2) + g*(m1+m2)*Math.cos(t1) + w2*w2*L2*m2*Math.cos(t1-t2))) / (L2*D);
  return {dt1: w1, dw1: a1, dt2: w2, dw2: a2};
}

RK4 step:
function step(state, dt) {
  // clone state with params part for derivatives? I'll separate params.
}

Better: keep params in a separate `params` object. State only has state variables.

```javascript
const params = {
  L1: 140,
  L2: 140,
  m1: 10,
  m2: 10,
  g: 9.81,
  dt: 0.05,
  speed: 1,
  trailLen: 800
};

let state = { t1: Math.PI/2, w1: 0, t2: Math.PI*0.6, w2: 0 };
```

Derivs uses params and state.

RK4:
```javascript
function rk4(state, params, dt) {
  const s0 = state;
  const k1 = derivs(params, s0);
  const s1 = {
    t1: s0.t1 + k1.dt1 * dt*0.5,
    w1: s0.w1 + k1.dw1 * dt*0.5,
    t2: s0.t2 + k1.dt2 * dt*0.5,
    w2: s0.w2 + k1.dw2 * dt*0.5,
  };
  const k2 = derivs(params, s1);
  const s2 = {
    t1: s0.t1 + k2.dt1 * dt*0.5,
    w1: s0.w1 + k2.dw1 * dt*0.5,
    t2: s0.t2 + k2.dt2 * dt*0.5,
    w2: s0.w2 + k2.dw2 * dt*0.5,
  };
  const k3 = derivs(params, s2);
  const s3 = {
    t1: s0.t1 + k3.dt1 * dt,
    w1: s0.w1 + k3.dw1 * dt,
    t2: s0.t2 + k3.dt2 * dt,
    w2: s0.w2 + k3.dw2 * dt,
  };
  const k4 = derivs(params, s3);
  return {
    t1: s0.t1 + (k1.dt1 + 2*k2.dt1 + 2*k3.dt1 + k4.dt1) * dt / 6,
    w1: s0.w1 + (k1.dw1 + 2*k2.dw1 + 2*k3.dw1 + k4.dw1) * dt / 6,
    t2: s0.t2 + (k1.dt2 + 2*k2.dt2 + 2*k3.dt2 + k4.dt2) * dt / 6,
    w2: s0.w2 + (k1.dw2 + 2*k2.dw2 + 2*k3.dw2 + k4.dw2) * dt / 6,
  };
}
```

That's correct.

Trail:
```javascript
let trail = [];
function updateTrail(x2, y2) {
  trail.push({x: x2, y: y2, hue: hue});
  if (trail.length > params.trailLen) trail.shift();
}
```
Drawing trail:
```javascript
function drawTrail() {
  if (trail.length < 2) return;
  ctx.lineWidth = 2;
  ctx.lineCap = 'round';
  for (let i = 1; i < trail.length; i++) {
    const p = trail[i];
    const alpha = i / trail.length;
    ctx.beginPath();
    ctx.strokeStyle = `hsla(${p.hue}, 100%, 65%, ${alpha})`;
    ctx.lineWidth = 1 + alpha * 2;
    ctx.moveTo(trail[i-1].x, trail[i-1].y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
  }
}
```

Drawing arms:
```javascript
function drawPendulum(x1,y1,x2,y2) {
  // arm 1
  ctx.beginPath();
  ctx.strokeStyle = '#e0e0e0';
  ctx.lineWidth = 3;
  ctx.moveTo(cx, cy);
  ctx.lineTo(x1, y1);
  ctx.stroke();
  // arm 2
  ctx.beginPath();
  ctx.strokeStyle = '#e0e0e0';
  ctx.lineWidth = 2;
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
  // bob 1
  ctx.beginPath();
  ctx.fillStyle = '#ff5555';
  ctx.arc(x1, y1, 12, 0, Math.PI*2);
  ctx.fill();
  // bob 2
  ctx.beginPath();
  ctx.fillStyle = '#5599ff';
  ctx.arc(x2, y2, 10, 0, Math.PI*2);
  ctx.fill();
  // pivot
  ctx.beginPath();
  ctx.fillStyle = '#fff';
  ctx.arc(cx, cy, 4, 0, Math.PI*2);
  ctx.fill();
}
```

Loop:
```javascript
let lastTime = performance.now();
let hue = 0;
function loop(now) {
  const rawDt = (now - lastTime) / 1000;
  lastTime = now;
  const dt = rawDt * params.speed * 10; // scale because we want visual speed, maybe base dt factor.
  // Actually better to keep dt small and just multiply: baseStep = 0.05; stepsPerFrame = something.
}
```

Hmm. If we just do one RK4 step per frame with dt fixed to say 0.05, then on a 60hz monitor we get 60 steps/sec = 3 real-seconds per simulated second? No: real time per frame is ~0.016s, but dt is 0.05s fixed. So simulation advances 0.05s per frame, meaning at 60fps simulation runs at 3x real speed. That's fine. And if frame rate drops, animation runs at lower visual speed but physics timestep stays constant? Actually if frame rate drops, we still call loop less often, but each call steps by 0.05s, so simulated time = frames * 0.05. This decouples physics accuracy from frame rate but means simulation speed is tied to frame rate. Better to step multiple times per frame to advance exactly a desired amount of simulated time, or use real dt.

To handle variable frame rate, I'll accumulate a `simTime` variable and step with fixed dt multiple times to catch up with real time. Or simpler: compute `dt = rawDt * params.speed` and pass that to RK4, but cap dt to something like 0.1 to avoid explosion. Since RK4 can handle dt up to maybe 0.05-0.1, I'll pass rawDt * speed. If rawDt is ~0.016s and speed is 1x, dt=0.016 which is fine for RK4. If speed is 2x, dt=0.032, still fine. If frame rate drops and dt becomes large, I can clamp.

Actually `rawDt` fluctuating a bit is fine. I'll just pass `rawDt * params.speed * 20`? Wait, if rawDt = 0.016s per frame, we want the movement to look natural. In standard time units (seconds), gravity is 9.81 m/s^2. If we use dt = 0.016, that's just real time speed. That might be somewhat slow visually? No, it's real time speed, which is perfect. But if we want to simulate at a certain "speed", we can multiply. Let's just set `dt = rawDt * params.speed * 30`? No, that's messy.

Let's define `fixedDt = 0.05` and use a `steps` variable. At each frame, we do `steps = Math.round(rawDt * params.speed / fixedDt)` or something. Actually simpler: do one RK4 step per frame with `dt = fixedDt * params.speed`, where fixedDt is something like 0.05. This means on 60fps, we advance by 0.05*speed per frame. At 1x, 0.05s per frame => 3x real time. Maybe too fast? Actually a pendulum period is roughly `2π√(L/g) ≈ 2π*√(140/980) ≈ 2π*0.377 ≈ 2.37s`. With dt=0.05 per frame and 60fps, 0.05s per step with 60 steps per second = 3 simulated seconds per real second. So pendulum completes ~1.2 cycles per second visually, which is fine. Let's step with dt=0.03 maybe? Or let's just use real dt and speed multiplier. I'll do:

```javascript
const frameDt = 1/60; // assume ~16ms
const effectiveDt = frameDt * params.speed;
```

Actually let's just compute actual elapsed time and multiply by speed. That's most robust.

```javascript
let lastTime = performance.now();
let hue = 0;
function loop(now) {
  requestAnimationFrame(loop);
  let rawDt = (now - lastTime) / 1000;
  lastTime = now;
  if (rawDt > 0.1) rawDt = 0.1; // cap
  const dt = rawDt * params.speed * 5; // speed multiplier
}
```

If speed slider default is 1, and dt = rawDt * 5. rawDt is ~0.016, so dt ~0.08. That might be okay for RK4. But to be robust, I'll just do multiple small steps per frame if needed. Or I can implement semi-implicit Euler which is stable for any dt (though energy drifts). RK4 with small dt is best.

Alternative: use a fixed number of RK4 steps per frame with a fixed dt. Say `const k = 4; const subDt = 0.02; for (let i=0; i<k; i++) state = rk4(state, params, subDt*params.speed);`
This gives deterministic behavior regardless of frame rate. 4*0.02 = 0.08 total dt per frame. At 60fps = 4.8 simulated seconds per real second. Good enough.

I'll do exactly that: fixed sub-steps.

```javascript
function update() {
  const subDts = 4;
  const subDt = 0.02 * params.speed;
  for (let i=0; i<subDts; i++) {
    state = rk4(state, params, subDt);
  }
}
```

Simple.

Coordinate system: center of canvas as pivot.
cx = canvas.width / 2, cy = canvas.height / 2.

Position calc:
x1 = cx + L1 * Math.sin(state.t1)
y1 = cy + L1 * Math.cos(state.t1)
x2 = x1 + L2 * Math.sin(state.t2)
y2 = y1 + L2 * Math.cos(state.t2)

Trail push: updateTrail(x2, y2).

Hue increment: `hue = (hue + 0.5) % 360;`

Drawing the background: clear rectangle.
Trail drawn as described.

Mass radii: I'll make them look proportional to mass, but not too large. `r1 = 8 + params.m1`? That could be too big if m1=20. Let's say `r1 = 4 + params.m1 * 0.5` = 4+5=9. `r2 = 4 + params.m2 * 0.5`. Or just fixed: 14 and 10. I'll compute dynamic arcs based on m.

```javascript
ctx.beginPath();
ctx.arc(x1, y1, params.m1, 0, Math.PI*2);
ctx.fillStyle = '#ff5555';
ctx.fill();
```

m1 default 10, radius 10. That's fine.

Resizing:
```javascript
function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
```

Now for the controls. I'll set up event listeners to update params.

HTML forms:
```html
<div class="controls">
  <label>L1: <input type="range" id="L1" min="50" max="250" value="140"> <span id="L1V">140</span></label>
  <label>L2: <input type="range" id="L2" min="50" max="250" value="140"> <span id="L2V">140</span></label>
  <label>m1: <input type="range" id="m1" min="1" max="50" value="10"> <span id="m1V">10</span></label>
  <label>m2: <input type="range" id="m2" min="1" max="50" value="10"> <span id="m2V">10</span></label>
  <label>g: <input type="range" id="g" min="0" max="30" step="0.1" value="9.8"> <span id="gV">9.8</span></label>
  <label>Speed: <input type="range" id="speed" min="0.1" max="3" step="0.1" value="1"> <span id="speedV">1</span></label>
  <label>Trail: <input type="range" id="trail" min="100" max="3000" step="100" value="800"> <span id="trailV">800</span></label>
  <button id="resetBtn">Reset</button>
</div>
```

JS wiring:
For each slider, on input, `params.X = parseFloat(this.value);` and update span text. Also on reset, reinitialize state.

Reset:
```javascript
document.getElementById('resetBtn').addEventListener('click', () => {
  state = { t1: Math.PI/2, w1: 0, t2: Math.PI*0.6, w2: 0 };
  trail = [];
  hue = 0;
});
```

Let's add some nice UI touches: the canvas cursor, maybe hide/show controls? User didn't ask for it but a neat UI is good. I'll keep it simple.

One potential issue: if user drags length slider, the positions update immediately (because physics uses current L1/L2), but the current tip position might jump discontinuously. That's expected.

Also trail hue. Could use the speed or angular velocity for color? The user asked for "colorful fading trail". I'll just shift hue over time.

I need to make sure I don't reference any external resources. System fonts are fine.

Let's also add a subtle glow to the pendulum arms/shadow for nicer visuals. Use `ctx.shadowBlur` maybe. But keep self-contained.

I'll choose a nice dark background `#08090d`.

Let's draft the full content.

Actually I should write a very polished file. The user asked for "wrong equations" no, "correct equations". So I must ensure math is correct.

Double-checking the equations again:
For a standard double pendulum with both lengths L1, L2 from the pivot:
The angles measured from the vertical (downward).
θ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)))

Yes.

Let's code it.

Also, in derivs, I should use the state parameters.

```javascript
function derivs(s, p) {
  const d = Math.sin(s.t1 - s.t2);
  const D = 2*p.m1 + p.m2 - p.m2*Math.cos(2*s.t1 - 2*s.t2);
  const num1 = -p.g*(2*p.m1+p.m2)*Math.sin(s.t1) - p.m2*p.g*Math.sin(s.t1-2*s.t2) - 2*d*p.m2*(s.w2*s.w2*p.L2 + s.w1*s.w1*p.L1*Math.cos(s.t1-s.t2));
  const a1 = num1 / (p.L1 * D);
  const num2 = 2*d*(s.w1*s.w1*p.L1*(p.m1+p.m2) + p.g*(p.m1+p.m2)*Math.cos(s.t1) + s.w2*s.w2*p.L2*p.m2*Math.cos(s.t1-s.t2));
  const a2 = num2 / (p.L2 * D);
  return { dt1: s.w1, dw1: a1, dt2: s.w2, dw2: a2 };
}
```

RK4 helper needs to use these.

```javascript
function rk4(s, p, h) {
  const k1 = derivs(s, p);
  const s1 = {
    t1: s.t1 + k1.dt1*h*0.5, w1: s.w1 + k1.dw1*h*0.5,
    t2: s.t2 + k1.dt2*h*0.5, w2: s.w2 + k1.dw2*h*0.5,
  };
  const k2 = derivs(s1, p);
  const s2 = {
    t1: s.t1 + k2.dt1*h*0.5, w1: s.w1 + k2.dw1*h*0.5,
    t2: s.t2 + k2.dt2*h*0.5, w2: s.w2 + k2.dw2*h*0.5,
  };
  const k3 = derivs(s2, p);
  const s3 = {
    t1: s.t1 + k3.dt1*h, w1: s.w1 + k3.dw1*h,
    t2: s.t2 + k3.dt2*h, w2: s.w2 + k3.dw2*h,
  };
  const k4 = derivs(s3, p);
  return {
    t1: s.t1 + (k1.dt1 + 2*k2.dt1 + 2*k3.dt1 + k4.dt1)*h/6,
    w1: s.w1 + (k1.dw1 + 2*k2.dw1 + 2*k3.dw1 + k4.dw1)*h/6,
    t2: s.t2 + (k1.dt2 + 2*k2.dt2 + 2*k3.dt2 + k4.dt2)*h/6,
    w2: s.w2 + (k1.dw2 + 2*k2.dw2 + 2*k3.dw2 + k4.dw2)*h/6,
  };
}
```

Looks good.

Now drawing trail efficiently: if trail is long, 2000 segments per frame is okay. But can optimize by skipping some? 2000 is negligible.

```javascript
function drawTrail(ctx) {
  if (trail.length < 2) return;
  for (let i = 1; i < trail.length; i++) {
    const prev = trail[i-1];
    const curr = trail[i];
    const t = i / trail.length;
    ctx.beginPath();
    ctx.strokeStyle = `hsla(${curr.hue}, 90%, 60%, ${t})`;
    ctx.lineWidth = t * 2.5;
    ctx.moveTo(prev.x, prev.y);
    ctx.lineTo(curr.x, curr.y);
    ctx.stroke();
  }
}
```

One more polish: I'll stroke the arms with a slight glow using `shadowBlur`.

```javascript
ctx.shadowColor = 'rgba(255,255,255,0.3)';
ctx.shadowBlur = 6;
// draw arms
ctx.shadowBlur = 0;
```

I'll also draw the bobs with a slight gloss: a radial gradient or a small white highlight? I can just use a radial gradient or draw a smaller white arc for highlight. For simplicity, I'll use a solid color with a dark stroke.

```javascript
ctx.beginPath();
ctx.arc(x1, y1, r1, 0, Math.PI*2);
ctx.fillStyle = '#ff4444';
ctx.fill();
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#fff';
ctx.stroke();
```

For bob2, similar with blue.

Pivot: small white circle.

Parameters default in code:
```javascript
const params = {
  L1: 140, L2: 140,
  m1: 10, m2: 10,
  g: 9.8,
  speed: 1,
  trailLen: 800
};
```

Initial state: I want chaotic behavior immediately. Setting t2 = Math.PI*0.6 (108 degrees) while t1 = Math.PI/2 (90 degrees). That's only 18 degree offset. Might not be super chaotic? Actually if one is horizontal and the other angled, it should be chaotic. I'll set `state = { t1: Math.PI/2, w1: 0, t2: Math.PI, w2: 0 };` t2 = π means hanging down while t1 = horizontal. That's clearly chaotic. Let's use t2 = 1.2*Math.PI (216 degrees, pointing somewhat left/up). Or Math.PI*0.9.

I'll use `t2: Math.PI*0.9` (162 degrees) - a clear asymmetry.

I'll also add an option to randomize. But Reset button resets to fixed.

Now HTML/CSS fully. I'll put the controls in a `div` with absolute position. Add transparent background with blur.

```css
.controls {
  position: absolute;
  top: 12px;
  right: 12px; /* or left */
  ...
}
```

Actually left is fine.

Let's make sure the ranges scroll nicely.

Let's write the final file.

I'll include a comment at the top describing it.

Checklist:
- Single file: yes.
- No external resources: yes.
- Inline CSS and JS: yes.
- Correct equations: yes.
- Arms drawn: yes.
- Colorful fading trail: yes.

I should also handle high-DPI displays (devicePixelRatio) for sharpness. That's a nice touch.

```javascript
function resize() {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = window.innerWidth * dpr;
  canvas.height = window.innerHeight * dpr;
  canvas.style.width = window.innerWidth + 'px';
  canvas.style.height = window.innerHeight + 'px';
  ctx.scale(dpr, dpr);
}
```

But scaling every frame resets? No, setting canvas width/height resets context, so after resize we need to re-apply scaling. But it's easier to just set width/height to inner sizes without DPR scaling for simplicity. The prompt doesn't demand retina sharpness. I'll skip DPR to keep code simpler and avoid transform bugs. Actually I'll just do `canvas.width = innerWidth; canvas.height = innerHeight;`.

Alright, I'll write the file now.
write /workspace/index.html (7395 bytes)
toolResult

Successfully wrote 7395 bytes to /workspace/index.html

assistant
thinking
The user asked me to simulate a chaotic double pendulum on a canvas using correct equations of motion, drawing arms and a colorful fading trail of the tip. They want it as a single-page web app in one self-contained file named index.html in the current directory, with CSS in a <style> tag and JS in a <script> tag, inline, no external resources, CDNs, or network requests.

I just wrote the file using the write tool. Now I should check if there are any syntax errors in the file I wrote, particularly in the JavaScript where a stray character could break it. I should read it back or verify it.

Specifically, looking at my write content, I see a possible issue near the end:
```
ctx.strokeStyle = '#aaccff');
```
I have an extra closing parenthesis `)` and a stray `'`? Let's examine: `ctx.strokeStyle = '#aaccff');` This is `ctx.strokeStyle = '...');` - yes, there is an extra `)` after the string, and a semicolon inside. That is definitely a syntax error!

I need to fix it to:
```
ctx.strokeStyle = '#aaccff';
```

Also, in the trail trail drawing loop, I used `const curr = trail[i];` and then inside I used `curr.hue`. That looks fine.

But that syntax error will break the JS. I must fix it.

Let me read the file to confirm and then edit just that part.

The edit must match exactly the old text. The old text likely has:
```
  ctx.strokeStyle = '#aaccff');
```

I need to replace with:
```
  ctx.strokeStyle = '#aaccff';
```

I'll use the edit tool.

I should also scan for other potential issues like missing semicolons or wrong brackets.

Let's re-read the end portion to be sure. I'll use `read` with offset.
read {"path":"/workspace/index.html","offset":190,"limit":40}
toolResult

// Draw trailing tip if (trail.length > 1) { ctx.lineJoin = 'round'; for (let i = 1; i < trail.length; i++) { const prev = trail[i-1]; const curr = trail[i]; const t = i / trail.length; ctx.beginPath(); ctx.strokeStyle = `hsla(${curr.hue}, 100%, 60%, ${t})`; ctx.lineWidth = 1 + t * 3; ctx.moveTo(prev.x, prev.y); ctx.lineTo(curr.x, curr.y); ctx.stroke(); } } // Glow for arms ctx.shadowColor = 'rgba(160,180,220,0.35)'; ctx.shadowBlur = 8; ctx.strokeStyle = '#cfd6e4'; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; // Arm 1 ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(x1, y1); ctx.stroke(); // Arm 2 ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; // Pivot [52 more lines in file. Use offset=230 to continue.]

assistant
thinking
I need to read more to find the syntax error around line 260 maybe.
read {"limit":40,"offset":230,"path":"/workspace/index.html"}
toolResult

ctx.beginPath(); ctx.fillStyle = '#ffffff'; ctx.arc(cx, cy, 5, 0, Math.PI*2); ctx.fill(); // Bob 1 const r1 = Math.max(6, m1); const bobGrad1 = ctx.createRadialGradient(x1-2, y1-2, r1*0.2, x1, y1, r1); bobGrad1.addColorStop(0, '#ff8888'); bobGrad1.addColorStop(1, '#b22222'); ctx.beginPath(); ctx.fillStyle = bobGrad1; ctx.arc(x1, y1, r1, 0, Math.PI*2); ctx.fill(); ctx.lineWidth = 1.2; ctx.strokeStyle = '#ffaaaa'; ctx.stroke(); // Bob 2 const r2 = Math.max(6, m2); const bobGrad2 = ctx.createRadialGradient(x2-2, y2-2, r2*0.2, x2, y2, r2); bobGrad2.addColorStop(0, '#88aaff'); bobGrad2.addColorStop(1, '#2255cc'); ctx.beginPath(); ctx.fillStyle = bobGrad2; ctx.arc(x2, y2, r2, 0, Math.PI*2); ctx.fill(); ctx.lineWidth = 1.2; ctx.strokeStyle = '#aaccff'); ctx.stroke(); } let lastTime = performance.now(); function loop(now) { requestAnimationFrame(loop); const rawDt = Math.min((now - lastTime)/1000, 0.1); lastTime = now; const steps = 4; const subDt = 0.02 * params.speed; [12 more lines in file. Use offset=270 to continue.]

assistant
thinking
I see the syntax error: `ctx.strokeStyle = '#aaccff');` should be `ctx.strokeStyle = '#aaccff';`

I'll use edit to fix just that line.
edit {"path":"/workspace/index.html","edits":[{"newText":" ctx.strokeStyle = '#aaccff';\n ctx.stroke();","oldText":" ctx.strokeStyle = '#aaccf
toolResult

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

assistant
thinking
Now let me quickly scan for any other syntax errors by reading through the script or doing a grep for common issues. I'll grep for lines with multiple closing parens.

Actually I can also run a quick node syntax check on the JS part by extracting it, but it's embedded in HTML. I can just grep visually.

Let me read the whole file quickly to check for any stray characters or mismatched braces.
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</title> <style> * { box-sizing: border-box; } body { margin: 0; overflow: hidden; background: #08090d; color: #ddd; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } canvas { display: block; } .controls { position: absolute; top: 12px; left: 12px; background: rgba(10,10,20,0.75); padding: 12px 14px; border-radius: 10px; border: 1px solid #2a2a3a; backdrop-filter: blur(6px); font-size: 13px; min-width: 180px; } .controls h3 { margin: 0 0 8px 0; font-size: 14px; font-weight: 600; letter-spacing: 0.3px; color: #fff; } .controls label { display: flex; justify-content: space-between; align-items: center; margin: 6px 0; } .controls input[type="range"] { width: 80px; margin-left: 8px; accent-color: #5599ff; } .controls .val { display: inline-block; width: 36px; text-align: right; font-variant-numeric: tabular-nums; color: #aaa; } .controls button { margin-top: 10px; width: 100%; padding: 6px 10px; cursor: pointer; background: #1a1a2e; color: #e0e0ff; border: 1px solid #3a3a55; border-radius: 6px; font-size: 13px; } .controls button:hover { background: #2a2a45; } </style> </head> <body> <canvas id="canvas"></canvas> <div class="controls"> <h3>Double Pendulum</h3> <label>L1 <input type="range" id="L1" min="60" max="280" value="140"> <span class="val" id="L1V">140</span></label> <label>L2 <input type="range" id="L2" min="60" max="280" value="140"> <span class="val" id="L2V">140</span></label> <label>m1 <input type="range" id="m1" min="1" max="60" value="12"> <span class="val" id="m1V">12</span></label> <label>m2 <input type="range" id="m2" min="1" max="60" value="12"> <span class="val" id="m2V">12</span></label> <label>g <input type="range" id="g" min="0" max="30" step="0.1" value="9.8"> <span class="val" id="gV">9.8</span></label> <label>Speed <input type="range" id="speed" min="0.1" max="3" step="0.1" value="1"> <span class="val" id="speedV">1</span></label> <label>Trail <input type="range" id="trail" min="100" max="4000" step="100" value="1200"> <span class="val" id="trailV">1200</span></label> <button id="resetBtn">Reset Simulation</button> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const params = { L1: 140, L2: 140, m1: 12, m2: 12, g: 9.8, speed: 1, trailLen: 1200 }; let state = { t1: Math.PI/2, w1: 0, t2: Math.PI*0.55, w2: 0 }; let trail = []; let hue = 0; const sliders = ['L1','L2','m1','m2','g','speed','trail']; sliders.forEach(id => { const el = document.getElementById(id); const disp = document.getElementById(id+'V'); el.addEventListener('input', () => { const v = parseFloat(el.value); params[id] = v; disp.textContent = v; }); }); document.getElementById('resetBtn').addEventListener('click', () => { // Asymmetric start to guarantee chaos state = { t1: Math.PI/2, w1: 0, t2: Math.PI*0.55, w2: 0 }; trail = []; hue = 0; }); function derivs(s, p) { const d = Math.sin(s.t1 - s.t2); const D = 2*p.m1 + p.m2 - p.m2*Math.cos(2*s.t1 - 2*s.t2); const num1 = -p.g*(2*p.m1 + p.m2)*Math.sin(s.t1) - p.m2*p.g*Math.sin(s.t1 - 2*s.t2) - 2*d*p.m2*(s.w2*s.w2*p.L2 + s.w1*s.w1*p.L1*Math.cos(s.t1 - s.t2)); const a1 = num1 / (p.L1 * D); const num2 = 2*d*(s.w1*s.w1*p.L1*(p.m1 + p.m2) + p.g*(p.m1 + p.m2)*Math.cos(s.t1) + s.w2*s.w2*p.L2*p.m2*Math.cos(s.t1 - s.t2)); const a2 = num2 / (p.L2 * D); return { dt1: s.w1, dw1: a1, dt2: s.w2, dw2: a2 }; } function rk4(s, p, h) { const k1 = derivs(s, p); const s1 = { t1: s.t1 + k1.dt1*h*0.5, w1: s.w1 + k1.dw1*h*0.5, t2: s.t2 + k1.dt2*h*0.5, w2: s.w2 + k1.dw2*h*0.5, }; const k2 = derivs(s1, p); const s2 = { t1: s.t1 + k2.dt1*h*0.5, w1: s.w1 + k2.dw1*h*0.5, t2: s.t2 + k2.dt2*h*0.5, w2: s.w2 + k2.dw2*h*0.5, }; const k3 = derivs(s2, p); const s3 = { t1: s.t1 + k3.dt1*h, w1: s.w1 + k3.dw1*h, t2: s.t2 + k3.dt2*h, w2: s.w2 + k3.dw2*h, }; const k4 = derivs(s3, p); return { t1: s.t1 + (k1.dt1 + 2*k2.dt1 + 2*k3.dt1 + k4.dt1)*h/6, w1: s.w1 + (k1.dw1 + 2*k2.dw1 + 2*k3.dw1 + k4.dw1)*h/6, t2: s.t2 + (k1.dt2 + 2*k2.dt2 + 2*k3.dt2 + k4.dt2)*h/6, w2: s.w2 + (k1.dw2 + 2*k2.dw2 + 2*k3.dw2 + k4.dw2)*h/6, }; } function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resize); resize(); function draw() { const w = canvas.width; const h = canvas.height; const cx = w/2; const cy = h/2; const { L1, L2, m1, m2 } = params; const x1 = cx + L1 * Math.sin(state.t1); const y1 = cy + L1 * Math.cos(state.t1); const x2 = x1 + L2 * Math.sin(state.t2); const y2 = y1 + L2 * Math.cos(state.t2); // Record trail trail.push({ x: x2, y: y2, hue }); if (trail.length > params.trailLen) trail.shift(); // Clear background ctx.fillStyle = '#08090d'; ctx.fillRect(0, 0, w, h); // Draw trailing tip if (trail.length > 1) { ctx.lineJoin = 'round'; for (let i = 1; i < trail.length; i++) { const prev = trail[i-1]; const curr = trail[i]; const t = i / trail.length; ctx.beginPath(); ctx.strokeStyle = `hsla(${curr.hue}, 100%, 60%, ${t})`; ctx.lineWidth = 1 + t * 3; ctx.moveTo(prev.x, prev.y); ctx.lineTo(curr.x, curr.y); ctx.stroke(); } } // Glow for arms ctx.shadowColor = 'rgba(160,180,220,0.35)'; ctx.shadowBlur = 8; ctx.strokeStyle = '#cfd6e4'; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; // Arm 1 ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(x1, y1); ctx.stroke(); // Arm 2 ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; // Pivot ctx.beginPath(); ctx.fillStyle = '#ffffff'; ctx.arc(cx, cy, 5, 0, Math.PI*2); ctx.fill(); // Bob 1 const r1 = Math.max(6, m1); const bobGrad1 = ctx.createRadialGradient(x1-2, y1-2, r1*0.2, x1, y1, r1); bobGrad1.addColorStop(0, '#ff8888'); bobGrad1.addColorStop(1, '#b22222'); ctx.beginPath(); ctx.fillStyle = bobGrad1; ctx.arc(x1, y1, r1, 0, Math.PI*2); ctx.fill(); ctx.lineWidth = 1.2; ctx.strokeStyle = '#ffaaaa'; ctx.stroke(); // Bob 2 const r2 = Math.max(6, m2); const bobGrad2 = ctx.createRadialGradient(x2-2, y2-2, r2*0.2, x2, y2, r2); bobGrad2.addColorStop(0, '#88aaff'); bobGrad2.addColorStop(1, '#2255cc'); ctx.beginPath(); ctx.fillStyle = bobGrad2; ctx.arc(x2, y2, r2, 0, Math.PI*2); ctx.fill(); ctx.lineWidth = 1.2; ctx.strokeStyle = '#aaccff'; ctx.stroke(); } let lastTime = performance.now(); function loop(now) { requestAnimationFrame(loop); const rawDt = Math.min((now - lastTime)/1000, 0.1); lastTime = now; const steps = 4; const subDt = 0.02 * params.speed; for (let i = 0; i < steps; i++) { state = rk4(state, params, subDt); } hue = (hue + 0.6) % 360; draw(); } requestAnimationFrame(loop); </script> </body> </html>

assistant
thinking
The file looks syntactically correct now. I fixed the stray parenthesis.

The user asked to simulate a chaotic double pendulum with correct equations, drawing arms and a colorful fading trail. The file is written to /workspace/index.html.

I should provide a concise summary of what was built and confirm the file is present. No further tools are needed unless I want to run it (but there's no browser here). I can just state that the file is ready.

Summary:
- Single self-contained HTML file.
- Uses correct Lagrangian equations for a double pendulum, integrated with RK4.
- Canvas animation: arms, masses with gradients, colorful fading trail.
- Interactive sliders for L1, L2, masses, gravity, speed, trail length.
- Reset button.
- Fully inline CSS and JS, no external dependencies.

I should mention that it's saved and can be opened in any browser.

Done! The self‑contained file is saved at **`/workspace/index.html`**. **What’s inside** - **Physics:** Standard Lagrangian equations of motion for a double pendulum, solved with 4th‑order Runge‑Kutta integration for stability and accuracy. - **Rendering:** Canvas draws both arms, glowing pivot, and two mass bobs with radial gradients. - **Trail:** The elbow/grab tip leaves a long, rainbow‑shifting trail that fades smoothly from transparent to opaque toward the newest positions. - **Controls (in‑page, no libraries):** - Adjust arm lengths $L_1$, $L_2$ - Adjust masses $m_1$, $m_2$ - Adjust gravity $g$ - Simulation speed multiplier - Trail length (100–4000 points) - **Reset Simulation** button Just open `index.html` in any modern browser—no network, CDN, or external files needed.

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

modelstep-3.7-flash
vendorStepFun
harnesspi-agent
generated2026-07-22

EVALUATION

4/5 working

Double pendulum simulation with colorful fading trails animates continuously; controls present but inputs don't visibly change dynamics.

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 motion2.3%
on click3.5%
on drag3.6%
on wheel3.3%
on enter + space1.6%
on arrow keys4.7%
on w/a/s/d4.9%
frame spread12.5 / 255
console errors0
js errors none

METRICS

runtime106.8s
tokens in30k
tokens out12k
cached43k
cost / run$0.0221