oneshotlm
Compare all models on this prompt →

Double pendulum · minimax-m3

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 · 6 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 simulating a chaotic double pendulum. I need to:

1. Create a single `index.html` file with everything inline
2. Use correct equations of motion for a double pendulum (Lagrangian mechanics)
3. Draw the two arms
4. Draw a colorful fading trail of the tip (the second bob)
5. No external resources, no CDNs, fully sandboxed

Let me think about the physics:

For a double pendulum with two rods of length L1, L2 and masses m1, m2:
- Let θ1, θ2 be the angles from vertical
- Let ω1, ω2 be the angular velocities

The equations of motion (derived from Lagrangian):

θ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)))

I'll use RK4 for integration to keep it stable and chaotic-looking. Or I could use a simple symplectic Euler or RK4. Let me use RK4 for accuracy.

For the trail, I'll keep a buffer of past positions and draw them with fading colors. To make it "colorful", I can color the trail based on the speed or position, using HSL color cycling.

Let me design:
- Canvas filling the viewport
- Black background
- White/colored arms with circles at joints
- Trail: array of recent tip positions, drawn as fading line segments with HSL color
- Controls panel for parameters (gravity, lengths, masses, damping)
- Reset button

Let me code this:

```javascript
// Physics
const g = 9.81;
let L1 = 1.0, L2 = 1.0;
let m1 = 1.0, m2 = 1.0;
let theta1 = Math.PI / 2, theta2 = Math.PI / 2;
let omega1 = 0, omega2 = 0;

function derivatives(s) {
  const [t1, t2, w1, w2] = s;
  const dt = t1 - t2;
  const sin_dt = Math.sin(dt);
  const cos_dt = Math.cos(dt);
  const denom = 2*m1 + m2 - m2*Math.cos(2*t1 - 2*t2);
  
  const dw1 = (-g*(2*m1+m2)*Math.sin(t1) - m2*g*Math.sin(t1-2*t2) 
               - 2*sin_dt*m2*(w2*w2*L2 + w1*w1*L1*cos_dt)) / (L1*denom);
  
  const dw2 = (2*sin_dt*(w1*w1*L1*(m1+m2) + g*(m1+m2)*Math.cos(t1) 
               + w2*w2*L2*m2*cos_dt)) / (L2*denom);
  
  return [w1, w2, dw1, dw2];
}

function rk4Step(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);
}
```

For the trail, I'll use a circular buffer and draw segments with decreasing alpha and color cycling.

Let me think about scale. L1, L2 in meters, but the canvas is in pixels. I'll set a scale factor (pixels per meter) and convert. The pivot will be at top-center.

For the colorful fading trail, I can cycle through HSL based on time or position, with alpha fading for old points.

Let me write the full file now.

Style considerations:
- Dark theme background
- White pendulum arms with bob circles
- Colorful trail (rainbow hue cycling based on time, alpha fading)
- Some control panel with sliders for parameters
- Reset button, pause/play button

Let me code it all up.

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chaotic Double Pendulum</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: system-ui, sans-serif; color: #fff; }
  canvas { display: block; }
  #ui {
    position: fixed; top: 12px; left: 12px;
    background: rgba(0,0,0,0.6);
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 10px;
    padding: 12px 14px;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    min-width: 240px;
    user-select: none;
  }
  #ui h1 { font-size: 14px; font-weight: 600; margin-bottom: 8px; letter-spacing: 0.5px; opacity: 0.9; }
  #ui .row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 12px; }
  #ui label { flex: 0 0 60px; opacity: 0.8; }
  #ui input[type=range] { flex: 1; }
  #ui .val { flex: 0 0 42px; text-align: right; opacity: 0.7; font-variant-numeric: tabular-nums; }
  #ui .btns { display: flex; gap: 6px; margin-top: 10px; }
  #ui button {
    flex: 1;
    background: rgba(255,255,255,0.08);
    color: #fff; border: 1px solid rgba(255,255,255,0.15);
    padding: 6px 8px; border-radius: 6px; font-size: 12px;
    cursor: pointer; transition: background 0.15s;
  }
  #ui button:hover { background: rgba(255,255,255,0.18); }
  #info { position: fixed; bottom: 10px; right: 12px; font-size: 11px; opacity: 0.5; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
  <h1>Double Pendulum</h1>
  <div class="row"><label>Length 1</label><input type="range" id="l1" min="0.3" max="1.5" step="0.01" value="1.0"><span class="val" id="l1v">1.00</span></div>
  <div class="row"><label>Length 2</label><input type="range" id="l2" min="0.3" max="1.5" step="0.01" value="1.0"><span class="val" id="l2v">1.00</span></div>
  <div class="row"><label>Mass 1</label><input type="range" id="m1" min="0.2" max="3.0" step="0.05" value="1.0"><span class="val" id="m1v">1.00</span></div>
  <div class="row"><label>Mass 2</label><input type="range" id="m2" min="0.2" max="3.0" step="0.05" value="1.0"><span class="val" id="m2v">1.00</span></div>
  <div class="row"><label>Gravity</label><input type="range" id="grav" min="1" max="25" step="0.1" value="9.81"><span class="val" id="gravv">9.8</span></div>
  <div class="row"><label>Trail</label><input type="range" id="trail" min="50" max="2000" step="10" value="600"><span class="val" id="trailv">600</span></div>
  <div class="btns">
    <button id="reset">Reset</button>
    <button id="pause">Pause</button>
    <button id="randomize">Random</button>
  </div>
</div>
<div id="info">RK4 integration · click & drag to reposition bobs</div>

<script>
(() => {
  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');

  let W = 0, H = 0, DPR = 1;
  function resize() {
    DPR = window.devicePixelRatio || 1;
    W = window.innerWidth;
    H = window.innerHeight;
    canvas.width = W * DPR;
    canvas.height = H * DPR;
    canvas.style.width = W + 'px';
    canvas.style.height = H + 'px';
    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  // Scale: total pendulum length should fit nicely
  let scale = 0;

  // State
  let params = {
    L1: 1.0, L2: 1.0,
    m1: 1.0, m2: 1.0,
    g: 9.81,
    trailLen: 600
  };
  let state = [Math.PI*0.6, Math.PI*0.6, 0, 0]; // [theta1, theta2, omega1, omega2]
  let paused = false;
  let timeAcc = 0;

  // Trail buffer
  const trail = []; // array of {x, y, t}

  function reset() {
    state = [Math.PI*0.6, Math.PI*0.6, 0, 0];
    trail.length = 0;
  }

  function randomize() {
    state = [
      (Math.random() - 0.5) * Math.PI * 1.6,
      (Math.random() - 0.5) * Math.PI * 1.6,
      (Math.random() - 0.5) * 4,
      (Math.random() - 0.5) * 4
    ];
    trail.length = 0;
  }

  function deriv(s) {
    const t1 = s[0], t2 = s[1], w1 = s[2], w2 = s[3];
    const dt = t1 - t2;
    const sin_dt = Math.sin(dt);
    const cos_dt = Math.cos(dt);
    const denom = 2*params.m1 + params.m2 - params.m2*Math.cos(2*t1 - 2*t2);
    
    const a1 = (
      -params.g*(2*params.m1 + params.m2)*Math.sin(t1)
      - params.m2*params.g*Math.sin(t1 - 2*t2)
      - 2*sin_dt*params.m2*(w2*w2*params.L2 + w1*w1*params.L1*cos_dt)
    ) / (params.L1 * denom);
    
    const a2 = (
      2*sin_dt*(
        w1*w1*params.L1*(params.m1 + params.m2)
        + params.g*(params.m1 + params.m2)*Math.cos(t1)
        + w2*w2*params.L2*params.m2*cos_dt
      )
    ) / (params.L2 * denom);
    
    return [w1, w2, a1, a2];
  }

  function rk4(s, dt) {
    const k1 = deriv(s);
    const s2 = s.map((v, i) => v + 0.5*dt*k1[i]);
    const k2 = deriv(s2);
    const s3 = s.map((v, i) => v + 0.5*dt*k2[i]);
    const k3 = deriv(s3);
    const s4 = s.map((v, i) => v + dt*k3[i]);
    const k4 = deriv(s4);
    return s.map((v, i) => v + dt*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i])/6);
  }

  // Pixel positions of bobs
  function bobPositions(s) {
    const x1 = params.L1 * Math.sin(s[0]);
    const y1 = params.L1 * Math.cos(s[0]);
    const x2 = x1 + params.L2 * Math.sin(s[1]);
    const y2 = y1 + params.L2 * Math.cos(s[1]);
    return { x1, y1, x2, y2 };
  }

  function getPivot() {
    scale = Math.min(W, H) * 0.36 / Math.max(params.L1 + params.L2, 0.1);
    return { px: W/2, py: H*0.30 };
  }

  let dragging = null;
  canvas.addEventListener('mousedown', e => {
    const rect = canvas.getBoundingClientRect();
    const mx = e.clientX - rect.left, my = e.clientY - rect.top;
    const { px, py } = getPivot();
    const { x1, y1, x2, y2 } = bobPositions(state);
    const p1x = px + x1*scale, p1y = py + y1*scale;
    const p2x = px + x2*scale, p2y = py + y2*scale;
    const d2 = Math.hypot(mx-p2x, my-p2y);
    const d1 = Math.hypot(mx-p1x, my-p1y);
    if (d2 < 30) dragging = 2;
    else if (d1 < 30) dragging = 1;
    else dragging = 'pendulum';
    // If dragging the whole pendulum, set dragging to null and just compute angles from mouse relative to pivot?
    // Actually let me keep it simple — drag individual bobs.
    if (dragging === 'pendulum') dragging = null;
  });
  window.addEventListener('mousemove', e => {
    if (!dragging) return;
    const rect = canvas.getBoundingClientRect();
    const mx = e.clientX - rect.left, my = e.clientY - rect.top;
    const { px, py } = getPivot();
    if (dragging === 1) {
      // Compute theta1 from position relative to pivot
      const dx = mx - px, dy = my - py;
      state[0] = Math.atan2(dx, dy);
      state[2] = 0;
    } else if (dragging === 2) {
      // For bob 2, need to handle constraint: it must be at distance L2 from bob1
      // Simplest: project mouse onto circle around bob1, OR just compute both angles
      // We'll set theta2 based on mouse position assuming bob1 stays put. But bob1 will move.
      // Better: compute both angles. theta1 from pivot to bob1, then theta2 from bob1's angle to bob2.
      // If we don't move bob1, just set theta2:
      const { x1, y1 } = bobPositions(state);
      const b1x = px + x1*scale, b1y = py + y1*scale;
      const dx = mx - b1x, dy = my - b1y;
      state[1] = Math.atan2(dx, dy);
      state[3] = 0;
    }
    trail.length = 0;
  });
  window.addEventListener('mouseup', () => dragging = null);

  // UI bindings
  function bindSlider(id, key, parser, fmt) {
    const el = document.getElementById(id);
    const v = document.getElementById(id+'v');
    el.addEventListener('input', () => {
      params[key] = parser(el.value);
      v.textContent = fmt(params[key]);
    });
    v.textContent = fmt(params[key]);
  }
  bindSlider('l1', 'L1', parseFloat, v => v.toFixed(2));
  bindSlider('l2', 'L2', parseFloat, v => v.toFixed(2));
  bindSlider('m1', 'm1', parseFloat, v => v.toFixed(2));
  bindSlider('m2', 'm2', parseFloat, v => v.toFixed(2));
  bindSlider('grav', 'g', parseFloat, v => v.toFixed(1));
  bindSlider('trail', 'trailLen', parseInt, v => v);

  document.getElementById('reset').onclick = reset;
  document.getElementById('pause').onclick = e => {
    paused = !paused;
    e.target.textContent = paused ? 'Play' : 'Pause';
  };
  document.getElementById('randomize').onclick = randomize;

  // Animation
  let lastT = performance.now();
  function frame(now) {
    const dt = Math.min(0.033, (now - lastT) / 1000);
    lastT = now;
    
    if (!paused && !dragging) {
      // Physics step — use substeps for stability
      const substeps = 4;
      const subDt = dt / substeps;
      for (let i = 0; i < substeps; i++) {
        state = rk4(state, subDt);
      }
      timeAcc += dt;
      const { x2, y2 } = bobPositions(state);
      const { px, py } = getPivot();
      trail.push({ x: px + x2*scale, y: py + y2*scale, t: timeAcc });
      if (trail.length > params.trailLen) trail.shift();
    } else if (dragging) {
      const { x2, y2 } = bobPositions(state);
      const { px, py } = getPivot();
      trail.push({ x: px + x2*scale, y: py + y2*scale, t: timeAcc });
    }

    // Draw
    // Trail: subtle dim of background not needed; trail itself is the colorful part
    // Fade background slightly to leave faint afterimages
    ctx.fillStyle = 'rgba(0,0,0,0.18)';
    ctx.fillRect(0, 0, W, H);

    // Draw trail
    if (trail.length > 1) {
      const n = trail.length;
      for (let i = 1; i < n; i++) {
        const p0 = trail[i-1], p1 = trail[i];
        const age = 1 - i/n; // 1 = newest, 0 = oldest
        // Color cycling based on time
        const hue = (p1.t * 40) % 360;
        const alpha = age * 0.9;
        ctx.strokeStyle = `hsla(${hue}, 95%, 60%, ${alpha})`;
        ctx.lineWidth = 1.2 + age * 1.8;
        ctx.beginPath();
        ctx.moveTo(p0.x, p0.y);
        ctx.lineTo(p1.x, p1.y);
        ctx.stroke();
      }
    }

    // Draw pendulum arms
    const { px, py } = getPivot();
    const { x1, y1, x2, y2 } = bobPositions(state);
    const p1x = px + x1*scale, p1y = py + y1*scale;
    const p2x = px + x2*scale, p2y = py + y2*scale;

    // Glow on arms
    ctx.shadowColor = 'rgba(255,255,255,0.6)';
    ctx.shadowBlur = 12;
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(px, py);
    ctx.lineTo(p1x, p1y);
    ctx.lineTo(p2x, p2y);
    ctx.stroke();
    ctx.shadowBlur = 0;

    // Pivots
    ctx.fillStyle = '#ffd166';
    ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#06d6a0';
    ctx.beginPath(); ctx.arc(p1x, p1y, 7 + params.m1*2, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#ef476f';
    ctx.beginPath(); ctx.arc(p2x, p2y, 7 + params.m2*2, 0, Math.PI*2); ctx.fill();

    // Inner highlights
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.beginPath(); ctx.arc(p1x-2, p1y-2, 2, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(p2x-2, p2y-2, 2, 0, Math.PI*2); ctx.fill();

    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
```

Let me review:
- Equations: The Lagrangian equations of motion for a double pendulum. Let me double-check:

Standard form (from Wikipedia):
θ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))]

θ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))]

Yes, that matches. Good.

- RK4 integration: Correct.
- Trail: Stored positions, drawn as segments with HSL color based on time and alpha fading.
- Initial conditions: theta1 = 0.6π, theta2 = 0.6π (both at ~108°), zero velocity. This is chaotic enough.

Wait, I should think about the colorful trail more carefully. The user asked for "colorful fading trail". I'll use a hue cycling based on time so it shifts colors. With `hue = (p1.t * 40) % 360`, at 40 deg/sec it takes 9 seconds for a full cycle. That seems good.

Actually, let me make it more vibrant — maybe use a different rate or base it on velocity magnitude for more dramatic color shifts.

Let me also add velocity to the trail so I can use it for color. Or just use time-based hue cycling.

I think time-based cycling looks nice. Let me keep it.

One issue: when the user pauses, the trail still accumulates if I push during dragging. Let me remove that.

Actually I'll keep the trail push during dragging so it doesn't pop when dragging ends. Or maybe better: only push when running. Let me only push during the running case (no pause, no dragging).

Wait, I did this: `if (!paused && !dragging) { ... trail.push }` and the else if is for dragging — it does push. That's fine.

Let me also add touch support for mobile. Actually that's optional, let me skip for now.

Let me also think about NaN protection. If something explodes, NaN would propagate. Let me add a safety check.

```js
if (!isFinite(state[0]) || !isFinite(state[1])) reset();
```

Let me add that.

Also let me think about the trail color cycling rate. 40 deg/sec means it cycles every 9 seconds. That should be visible but not distracting. Good.

One thing to consider: When I dim the background with `fillStyle = 'rgba(0,0,0,0.18)'`, this creates a nice motion blur effect on the trail. Good.

Hmm, but with trail buffer already providing the fade, the background dim is mostly redundant but adds extra blur. Let me keep it.

Actually wait — the background dim causes the entire screen to fade. So the pendulum arms also fade. Hmm, but they get redrawn each frame so that's fine. The trail gets a nice fade.

Let me finalize the file.

Actually, I realize there's an issue with the trail length slider. It uses `params.trailLen` but it's a string from `parseInt`. Let me check... `bindSlider('trail', 'trailLen', parseInt, v => v)` — yes this is fine.

Let me also make the initial state more dramatic for visual effect. Maybe use a near-vertical drop: theta1 = 0.7π, theta2 = 0.5π (or similar). Actually the classic chaotic initial conditions are something like θ1 = θ2 = π/2 or π.

Let me use θ1 = 2.0 rad (~115°), θ2 = 2.0 rad. That should produce nice chaotic motion.

Let me write the file.

One more thought: the trail line width. `1.2 + age * 1.8` means width ranges from 1.2 (oldest) to 3.0 (newest). That's good.

Hue cycling at 40 deg/sec gives a smooth color shift. Let me try 60 to make it more vibrant.

Actually I'll just commit to a clean implementation. Let me write it now.

Let me also think about the body overflow. With `overflow: hidden` on body and `display: block` on canvas, the canvas should fill the viewport. Good.

Let me also handle the touch interaction for completeness... actually skip, the prompt doesn't ask for mobile.

Final check on the equations once more:

θ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))]

θ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))]

In my code:
```
const a1 = (
  -params.g*(2*params.m1 + params.m2)*Math.sin(t1)
  - params.m2*params.g*Math.sin(t1 - 2*t2)
  - 2*sin_dt*params.m2*(w2*w2*params.L2 + w1*w1*params.L1*cos_dt)
) / (params.L1 * denom);

const a2 = (
  2*sin_dt*(
    w1*w1*params.L1*(params.m1 + params.m2)
    + params.g*(params.m1 + params.m2)*Math.cos(t1)
    + w2*w2*params.L2*params.m2*cos_dt
  )
) / (params.L2 * denom);
```

That matches. Good.

Let me write the file.

I'll build a self-contained HTML file with a chaotic double pendulum using the correct Lagrangian equations of motion (RK4 integration) and a color-cycling fading trail.

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

Successfully wrote 15522 bytes to /workspace/index.html

assistant
thinking
Let me verify the file was created and check its structure quickly
bash {"command":"ls -la /workspace/index.html && echo \"---\" && wc -l /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 15547 Jul 22 17:17 /workspace/index.html --- 493 /workspace/index.html

assistant
thinking
The file is created. Let me also do a quick sanity check on the JS by looking for any obvious syntax issues.

Created `/workspace/index.html` — a fully self-contained single-page app. **What's inside:** - **Correct physics** — exact Lagrangian equations of motion for a planar double pendulum: - θ₁″ = [−g(2m₁+m₂)sinθ₁ − m₂g sin(θ₁−2θ₂) − 2 sin(θ₁−θ₂)·m₂·(ω₂²L₂ + ω₁²L₁cos(θ₁−θ₂))] / [L₁(2m₁+m₂−m₂cos(2θ₁−2θ₂))] - θ₂″ = [2 sin(θ₁−θ₂)·(ω₁²L₁(m₁+m₂) + g(m₁+m₂)cosθ₁ + ω₂²L₂m₂cos(θ₁−θ₂))] / [L₂(2m₁+m₂−m₂cos(2θ₁−2θ₂))] - **RK4 integration** with 4 substeps per frame for stability during chaotic motion. - **NaN guard** that resets state if it ever blows up. - **Colorful fading trail** of the tip's path: HSL hue cycles over time, alpha fades with age (1 − i/n), drawn in two passes — a soft glow underlay and a crisp line on top. - **Pendulum arms** rendered with subtle white shadow-blur glow, plus gradient-filled bobs sized by mass. - **Controls panel** for L1, L2, m1, m2, gravity, trail length, and hue cycling rate, plus Reset / Pause / Randomize buttons. - **Drag interaction** — grab bob 1 or bob 2 with mouse/touch to reposition and re-release. - **DPR-aware canvas** that fills the viewport, resizes with the window, and clears with a slight per-frame dim to give the trail a motion-blurred afterglow. Open `index.html` in any modern browser — no network required.

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

modelminimax-m3
vendorMiniMax
harnesspi-agent
generated2026-07-22

EVALUATION

4/5 working

Colorful double pendulum simulation with fading rainbow trails, animates smoothly and responds to dragging bobs.

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.2%
on click13.7%
on drag16.4%
on enter + space6.0%
frame spread34.1 / 255
console errors0
js errors none

METRICS

runtime172.2s
tokens in13k
tokens out13k
cached16k
cost / run$0.0200