oneshotlm
Compare all models on this prompt →

Double pendulum · deepseek-v3.1-terminus

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 · 4 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
We are going to create a single-page web app that simulates a chaotic double pendulum.
 The app will have:
   - A canvas element for drawing the pendulum and its trail.
   - CSS for styling (if needed, but we can keep it minimal).
   - JavaScript that:
        * Sets up the canvas and context.
        * Defines the physics of the double pendulum (equations of motion).
        * Uses numerical integration (like Runge-Kutta) to update the state.
        * Draws the pendulum arms and a fading trail of the tip.

 We'll use the following variables for the pendulum:
   - Two masses (m1, m2)
   - Two lengths (l1, l2)
   - Two angles (theta1, theta2) and their derivatives (omega1, omega2)

 The equations of motion for a double pendulum are:

   dtheta1/dt = omega1
   dtheta2/dt = omega2

   domega1/dt = ( -g*(2*m1+m2)*sin(theta1) - m2*g*sin(theta1-2*theta2)
                 - 2*sin(theta1-theta2)*m2*(omega2^2*l2 + omega1^2*l1*cos(theta1-theta2)) )
                 / ( l1*(2*m1+m2 - m2*cos(2*theta1-2*theta2)) )

   domega2/dt = ( 2*sin(theta1-theta2)*( (omega1^2*l1*(m1+m2)) + g*(m1+m2)*cos(theta1) 
                 + omega2^2*l2*m2*cos(theta1-theta2) ) )
                 / ( l2*(2*m1+m2 - m2*cos(2*theta1-2*theta2)) )

 However, note: these equations are complex and there are different formulations. We'll use a common one.

 Alternatively, we can use Lagrangian mechanics to derive the equations. We'll use the following:

   Let:
     d1 = (m1+m2)*l1
     d2 = m2*l2
     d3 = m2*l1*l2

   Then the equations can be written as:

     M * [d2theta1/dt2, d2theta2/dt2]^T = F

   where M is a 2x2 matrix and F is a 2x1 vector.

   We can solve for the second derivatives by inverting M.

   We'll use the following steps:

     Let:
        theta1, theta2: angles
        omega1, omega2: angular velocities

        alpha1 = d2theta1/dt2, alpha2 = d2theta2/dt2

     Then:

        M = [ [ (m1+m2)*l1, m2*l2*cos(theta1-theta2) ],
              [ m2*l1*cos(theta1-theta2), m2*l2 ] ]

        F = [ -m2*l2*omega2^2*sin(theta1-theta2) - (m1+m2)*g*sin(theta1) ],
            [ m2*l1*omega1^2*sin(theta1-theta2) - m2*g*sin(theta2) ]

     Then:

        [alpha1, alpha2]^T = M^{-1} * F

   But note: the matrix M is invertible when the determinant is non-zero.

   Alternatively, we can use the following formulation (which is more common and avoids matrix inversion):

        Let:
            delta = theta2 - theta1

            denom1 = (m1+m2)*l1 - m2*l1*cos(delta)^2
            alpha1 = ( m2*l1*omega1^2*sin(delta)*cos(delta)
                      + m2*g*sin(theta2)*cos(delta)
                      + m2*l2*omega2^2*sin(delta)
                      - (m1+m2)*g*sin(theta1) ) / denom1

            denom2 = (l2/l1)*denom1
            alpha2 = ( -m2*l2*omega2^2*sin(delta)*cos(delta)
                      + (m1+m2)*g*sin(theta1)*cos(delta)
                      - (m1+m2)*l1*omega1^2*sin(delta)
                      - (m1+m2)*g*sin(theta2) ) / denom2

   However, I recall a more standard form:

        Let:
            c = cos(theta1-theta2)
            s = sin(theta1-theta2)

        alpha1 = ( -g*(2*m1+m2)*sin(theta1) - m2*g*sin(theta1-2*theta2) - 2*s*m2*(omega2^2*l2+omega1^2*l1*c) )
                  / ( l1*(2*m1+m2 - m2*cos(2*theta1-2*theta2)) )

        alpha2 = ( 2*s*( omega1^2*l1*(m1+m2) + g*(m1+m2)*cos(theta1) + omega2^2*l2*m2*c ) )
                  / ( l2*(2*m1+m2 - m2*cos(2*theta1-2*theta2)) )

   We'll use this form because it is well-known.

 Steps in the code:

   1. Set up canvas and context.
   2. Define pendulum parameters (m1, m2, l1, l2, g, theta1, theta2, omega1, omega2).
   3. Set up a trail array to store past positions of the tip (x2, y2).
   4. In the animation loop:
        a. Calculate the accelerations (alpha1, alpha2) using the equations above.
        b. Update omega1 and omega2 using Euler or Runge-Kutta (we'll use Euler for simplicity, but RK4 is more accurate).
        c. Update theta1 and theta2 by adding omega1 and omega2 (multiplied by dt).
        d. Calculate the positions of the two masses:
            x1 = l1 * sin(theta1)
            y1 = l1 * cos(theta1)
            x2 = x1 + l2 * sin(theta2)
            y2 = y1 + l2 * cos(theta2)

        e. Draw the pendulum arms and the trail.

   5. For the trail, we'll store the last N positions and draw them with fading color.

 Let's code accordingly.

 We'll use:
   - m1 = 10, m2 = 10, l1 = 100, l2 = 100, g = 0.1 (scaled for animation)

 But note: in pixels, l1 and l2 are in pixels, so we need to adjust g accordingly.

 Alternatively, we can use a fixed time step and scale the g to make the animation look good.

 We'll set:
   - dt = 0.1 (for Euler integration)

 However, for chaotic systems, we might need a smaller dt. We can use 0.05 or even 0.01.

 Let's use:

   m1 = 10, m2 = 10, l1 = 100, l2 = 100, g = 0.1

 But note: the equations use g in pixels per time step squared. We might need to adjust.

 Alternatively, we can use a more physical approach with real units and then scale.

 Since we are in a canvas, let's set:

   l1 = 100 (pixels), l2 = 100 (pixels)

   and then set g = 0.1 (pixels per time step squared) and dt = 1 (each frame is one time step) but that might be too fast.

 Alternatively, we can set dt = 0.1 and then adjust g accordingly.

 Let me try:

   We want the pendulum to move slowly enough to see the chaos. Let's set:

   g = 0.5 (pixels per time step squared) and dt = 0.1.

   Then in each frame, we update the state by dt.

   We'll use requestAnimationFrame, so the actual time between frames is variable. We can fix the time step by using a fixed dt and then updating the state multiple times per frame if needed.

   However, for simplicity, we'll assume that the frame rate is 60fps and set dt = 1/60.

   But then the pendulum might be too slow. Alternatively, we can set dt = 0.1 and then multiply the derivatives by dt.

   Let's do:

        theta1 += omega1 * dt;
        theta2 += omega2 * dt;
        omega1 += alpha1 * dt;
        omega2 += alpha2 * dt;

   We'll set dt = 0.1 and see.

   We can adjust the parameters to get a good visual.

   We'll also set the initial conditions to:

        theta1 = Math.PI / 2, theta2 = Math.PI / 2, omega1 = 0, omega2 = 0

   But to see chaos, we can set a small difference in theta1 and theta2.

   Alternatively, we can set:

        theta1 = Math.PI * 0.99, theta2 = Math.PI * 0.99

   Or we can set one of them to be slightly different.

   Let's set:

        theta1 = Math.PI / 2, theta2 = Math.PI / 2 + 0.01

   This small difference will lead to chaos.

   Now, for the trail:

        We'll store the last 500 positions of (x2, y2) in an array.
        Each time we update, we push the new position and shift if the array is too long.
        We'll draw the trail with a gradient from recent (bright) to old (faded).

   We can use a canvas gradient for the trail, but since we want a fading trail, we can draw each segment with a color that depends on its age.

   Alternatively, we can use a circular gradient that fades to transparent.

   Let's do:

        We'll draw the trail by drawing lines between consecutive points, and set the line color to rgba(0, 0, 0, alpha) where alpha decreases with the index.

   But we want colorful, so let's use a gradient of colors (like rainbow) and also fade the opacity.

   We can use HSL colors:

        hue = (index / trailLength) * 360
        then set strokeStyle to `hsla(${hue}, 100%, 50%, ${alpha})`

   where alpha goes from 1 to 0.

   Alternatively, we can use a fixed color and fade the opacity.

   Let's do a rainbow trail that fades.

   Steps for drawing the trail:

        for (let i = 1; i < trail.length; i++) {
            let alpha = i / trail.length;
            ctx.strokeStyle = `hsla(${i * 360 / trail.length}, 100%, 50%, ${alpha})`;
            ctx.beginPath();
            ctx.moveTo(trail[i-1].x, trail[i-1].y);
            ctx.lineTo(trail[i].x, trail[i].y);
            ctx.stroke();
        }

   But note: the trail is stored in order from oldest to newest? Actually, we are pushing new points and shifting old ones, so the array goes from oldest to newest.

   We want the recent ones to be brighter and more opaque, so we should reverse the order? Actually, we want the most recent to be drawn last and with the highest opacity.

   Alternatively, we can draw from the end of the array to the beginning, but then the gradient would be reversed.

   Let's store the trail as newest at the end. Then when drawing, we can iterate from the beginning (oldest) to the end (newest). The opacity and color will increase as we go.

   But then the trail will have the oldest part with the lowest opacity and the newest with the highest. That's what we want.

   However, the color will change from the oldest to the newest. We can set the hue based on the index (from 0 to 360 as we go from oldest to newest).

   So:

        for (let i = 0; i < trail.length - 1; i++) {
            let progress = i / (trail.length - 1);
            let hue = progress * 360;
            let alpha = progress; // This will make the oldest fade out? Actually, we want the oldest to be more transparent, so alpha should be 1 - progress? Let me think.

        We want:
            Oldest: low alpha, hue at 0 (red) -> then through the rainbow to violet at 360.
            Newest: high alpha, hue at 360.

        But if we set alpha = 1 - progress, then the oldest (progress=0) will have alpha=1 and the newest (progress=1) will have alpha=0. That's the opposite.

        We want the newest to be more opaque, so alpha should be higher for larger i? Actually, we are iterating from the beginning (oldest) to the end (newest). So:

            i=0 (oldest): progress=0, alpha=0, hue=0
            i=trail.length-1 (newest): progress=1, alpha=1, hue=360

        But then we want the oldest to be transparent and the newest to be opaque. So we set alpha = progress.

        So:

            alpha = i / (trail.length-1)   [if we use progress]

        However, note that the trail array has a fixed length. We can set:

            for (let i = 0; i < trail.length - 1; i++) {
                let progress = i / (trail.length - 1);
                let hue = progress * 360;
                let alpha = progress;
                ctx.strokeStyle = `hsla(${hue}, 100%, 50%, ${alpha})`;
                ... draw line from trail[i] to trail[i+1] ...
            }

        But then the oldest segment (i=0) will have alpha=0, so it won't be drawn. Then the next segment (i=1) will have a very low alpha. That's acceptable.

        Alternatively, we can set alpha = (i+1) / trail.length to avoid zero alpha for the first segment? But then the first segment would be very faint.

        Let's try with alpha = (i+1) / trail.length.

        Actually, we can set a minimum alpha so that even the oldest trail is visible? But then the trail would never completely fade.

        Alternatively, we can set the trail to have a maximum length and then remove the oldest when adding new ones, and then draw with alpha based on the index.

        Let's set the trail length to 500.

        We'll push new points and if the trail length exceeds 500, we shift (remove the first element).

        Then when drawing, we do:

            for (let i = 0; i < trail.length - 1; i++) {
                let progress = i / (trail.length - 1);
                let hue = progress * 360;
                let alpha = progress;
                ctx.strokeStyle = `hsla(${hue}, 100%, 50%, ${alpha})`;
                ... draw line ...
            }

        This will make the trail fade from the oldest to the newest, and the color will change from red to violet.

   Now, let's code accordingly.

   We'll also draw the pendulum arms as black lines and the masses as circles.

   Steps:

        // Clear the canvas
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw the trail
        for (let i = 0; i < trail.length - 1; i++) {
            ... as above ...
        }

        // Draw the pendulum arms
        ctx.strokeStyle = 'black';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(0, 0); // pivot
        ctx.lineTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();

        // Draw the masses
        ctx.fillStyle = 'black';
        ctx.beginPath();
        ctx.arc(x1, y1, 10, 0, 2*Math.PI);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(x2, y2, 10, 0, 2*Math.PI);
        ctx.fill();

   Note: We are drawing the pivot at (0,0). We need to translate the canvas so that the pivot is at a fixed position (e.g., center top).

   Let's set:

        ctx.translate(canvas.width/2, 50);

   Then the pendulum will swing from the center top.

   We also need to adjust the trail points to be in the same coordinate system.

   We are storing (x2, y2) in the coordinate system relative to the pivot. So when we draw the trail, we need to add the pivot position (canvas.width/2, 50) to each point.

   Alternatively, we can store the absolute coordinates of the trail.

   Let's change:

        Instead of storing (x2, y2) in the pendulum's coordinate system (relative to pivot), we store the absolute position.

        We can compute:

            absX2 = canvas.width/2 + x2
            absY2 = 50 + y2

        Then push {x: absX2, y: absY2} to the trail.

   But then when we draw the pendulum, we are drawing in a translated coordinate system. We can either:

        Option 1: Draw the pendulum in absolute coordinates (without translate) and then the trail in absolute coordinates.

        Option 2: Draw the pendulum in the translated system and then the trail must also be drawn in the same translated system.

   Let's go with Option 2 because it's simpler for the pendulum drawing.

   We'll set:

        ctx.translate(canvas.width/2, 50);

   Then when we compute the pendulum positions, we are in a coordinate system where the pivot is at (0,0). Then the trail points we store are also in this coordinate system.

   But when drawing the trail, we are drawing lines between points that are in the translated system. That's fine because the canvas is translated.

   However, note: the trail points are stored as (x2, y2) in the translated system. So when we draw the trail, we are drawing in the translated system.

   This is consistent.

   So we don't need to adjust the trail points.

   Let's code accordingly.

   We'll set up the canvas and the translation in the beginning.

   We'll also set the initial state and then start the animation.

   We'll use:

        let state = {
            theta1: Math.PI / 2,
            theta2: Math.PI / 2 + 0.01, // small difference to induce chaos
            omega1: 0,
            omega2: 0
        };

   And then update the state with the equations.

   We'll define a function to compute the derivatives (alpha1, alpha2) given the current state.

   Let's code the equations:

        function derivatives(state) {
            let { theta1, theta2, omega1, omega2 } = state;

            let m1 = 10, m2 = 10, l1 = 100, l2 = 100, g = 0.5;

            let c = Math.cos(theta1 - theta2);
            let s = Math.sin(theta1 - theta2);

            let denominator = 2*m1 + m2 - m2*Math.cos(2*theta1 - 2*theta2);

            let alpha1 = ( -g*(2*m1+m2)*Math.sin(theta1) - m2*g*Math.sin(theta1-2*theta2) - 2*s*m2*(omega2*omega2*l2 + omega1*omega1*l1*c) ) 
                         / ( l1 * denominator );

            let alpha2 = ( 2*s*( omega1*omega1*l1*(m1+m2) + g*(m1+m2)*Math.cos(theta1) + omega2*omega2*l2*m2*c ) )
                         / ( l2 * denominator );

            return { alpha1, alpha2 };
        }

   Then update:

        let dt = 0.1;
        let der = derivatives(state);
        state.omega1 += der.alpha1 * dt;
        state.omega2 += der.alpha2 * dt;
        state.theta1 += state.omega1 * dt;
        state.theta2 += state.omega2 * dt;

   Then compute positions:

        let x1 = l1 * Math.sin(state.theta1);
        let y1 = l1 * Math.cos(state.theta1);
        let x2 = x1 + l2 * Math.sin(state.theta2);
        let y2 = y1 + l2 * Math.cos(state.theta2);

   Then update the trail.

   We'll push {x: x2, y: y2} to the trail and shift if the trail length exceeds 500.

   Then draw.

   Let's code accordingly.

   We'll also handle resizing the canvas.

   We'll set the canvas width and height to window.innerWidth and window.innerHeight, but we can set a fixed size for simplicity.

   Let's set:

        <canvas id="canvas" width="800" height="600"></canvas>

   Then we translate to (400, 50) for the pivot.

   Now, let's write the code.

   We'll put everything in one HTML file.

   We'll use a <style> tag for minimal styling (make the canvas fullscreen if we want, but we set fixed size).

   Alternatively, we can set the canvas to fill the window.

   Let's set:

        <style>
            body { margin: 0; padding: 0; }
            canvas { display: block; }
        </style>

   and set the canvas width and height to window.innerWidth and window.innerHeight.

   We'll adjust the pivot to be at (window.innerWidth/2, 50).

   Now, code.

I'll create a chaotic double pendulum simulation with a colorful fading trail. Let me build this as a single self-contained HTML file.

write index.html (13258 bytes)
toolResult

Successfully wrote 13258 bytes to index.html

assistant
thinking
I've created a complete chaotic double pendulum simulation with the following features:

**Key Features:**
- **Accurate Physics**: Uses the proper equations of motion for a double pendulum with Runge-Kutta 4th order integration
- **Colorful Fading Trail**: The tip leaves a rainbow-colored trail that fades over time
- **Interactive Controls**: 
  - Reset to initial position
  - Randomize initial conditions
  - Pause/resume animation
  - Adjust trail length, gravity, and damping
- **Mouse/Touch Interaction**: Click and drag either pendulum joint to manipulate it
- **Responsive Design**: Automatically adjusts to window size

**Physics Implementation:**
- Correct double pendulum dynamics with coupled differential equations
- Proper handling of masses, lengths, and gravity
- Damping to simulate energy loss
- Numerical stability with capped time steps

**Visual Effects:**
- Smooth rainbow color gradient along the trail
- Fading trail effect using alpha transparency
- Clear visualization of both pendulum arms and joints
- Dark background to make the colorful trail stand out

The simulation demonstrates chaotic behavior beautifully - small changes in initial conditions lead to dramatically different trajectories over time. You can interact with it by dragging the pendulum joints or adjusting the parameters in the control panel.

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

modeldeepseek-v3.1-terminus
vendorDeepSeek
harnesspi-agent
generated2026-08-01

EVALUATION

4/5 working

Double pendulum simulates chaotic motion with a colorful fading trail; animates continuously and reacts to drag/click.

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 motion0.9%
on click1.1%
on drag1.1%
on wheel1.1%
on enter + space0.8%
on arrow keys0.9%
on w/a/s/d0.5%
frame spread15.6 / 255
console errors0
js errors none

METRICS

runtime282.0s
tokens in6.9k
tokens out9.0k
cached2.8k
cost / run$0.0105