OverviewPhysics Lab › R30
Physics Lab

R30 · Spring-mass chain

Archive round — this round predates the blind-first layout, so answers appear with model names attached. Newer rounds keep the models anonymous until you reveal them.
scored 11/13 models answered
Show task
Create ONE self-contained HTML file (STRICT LIMIT: max 120 lines total) that shows a spring-mass chain on a <canvas>: exactly 12 masses in a horizontal row, neighbors connected by springs (Hooke's law with damping), the two chain ends anchored. The user can grab ANY mass with the mouse and drag it; while dragged it follows the cursor, and on release a wave visibly propagates along the chain until it settles. Draw springs as lines and masses as circles, highlight the grabbed mass. Smooth requestAnimationFrame animation. No external libraries, no comments needed. Output ONLY the HTML file content, nothing else.

Answers

Local (Brain cluster)
gemma4:26b DNF / pending DNF (infra)
DNF — infrastructure: 9/10 rounds ended in provider errors; its only completed round, R28, scored 8.5
qwen3-coder:30b30.3 sSTATLIM★ 2/10
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Spring Mass Chain Simulation</title>
    <style>
        body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #222; }
        canvas { border: 1px solid #444; background-color: #000; cursor: crosshair; }
    </style>
</head>
<body>
    <canvas id="chainCanvas" width="800" height="300"></canvas>
    <script>
        const canvas = document.getElementById('chainCanvas');
        const ctx = canvas.getContext('2d');

        const NUM_MASSES = 12;
        const INITIAL_LENGTH = 50; // Space between adjacent masses
        let g = { k: 0.1, d: 0.9 }; // Spring stiffness and damping factor
        
        // Mass object structure { x, y, vX, vY }
        let masses = [];
        for (let i = 0; i < NUM_MASSES; i++) {
            masses.push({ 
                x: INITIAL_LENGTH * (i + 1), 
                y: canvas.height / 2, 
                vX: 0, vY: 0, mass: 1.0, radius: 8 
            });
        }

        let dragging = false;
        let dragIndex = -1;
        let cursorPos = { x: 0, y: 0 };

        function resetSystem() {
            masses[0].vX = masses[0].vY = 0;
            masses[NUM_MASSES - 1].vX = masses[NUM_MASSES - 1].vY = 0;
            for (let i = 0; i < NUM_MASSES; i++) {
                masses[i].x = INITIAL_LENGTH * (i + 1);
                masses[i].y = canvas.height / 2;
                masses[i].vX = masses[i].vY = 0;
            }
        }

        function calculateForces(m, index) {
            let totalFx = 0;
            let totalFy = 0;
            const mTarget = (index > 0 && !dragging) ? masses[index - 1] : null;
            const nTarget = (index < NUM_MASSES - 1 && !dragging) ? masses[index + 1] : null;

            // Force from previous mass (Left neighbor)
            if (mTarget) {
                let dx = mTarget.x - m.x;
                let dy = mTarget.y - m.y;
                let dist = Math.sqrt(dx * dx + dy * dy);
                let springForce = g.k * (dist - INITIAL_LENGTH);
                totalFx += springForce * (dx / dist) - g.d * m.vX;
                totalFy += springForce * (dy / dist) - g.d * m.vY;
            }

            // Force from next mass (Right neighbor)
            if (nTarget) {
                let dx = nTarget.x - m.x;
                let dy = nTarget.y - m.y;
                let dist = Math.sqrt(dx * dx + dy * dy);
                let springForce = g.k * (dist - INITIAL_LENGTH);
                totalFx += springForce * (dx / dist) - g.d * m.vX;
                totalFy += springForce * (dy / dist) - g.d * m.vY;
            }

            return { fx: totalFx, fy: totalFy };
        }

        function updatePhysics() {
            for (let i = 1; i < NUM_MASSES - 1; i++) {
                const currentMass = masses[i];
                if (dragging && i === dragIndex) continue; // Skip physics if actively dragged
                
                // Calculate forces and acceleration
                ({ fx: currentMass.forceX, fy: currentMass.forceY } = calculateForces(currentMass, i));
                
                // Simple integration (Euler method)
                let dt = 0.01;
                currentMass.vX += currentMass.forceX / currentMass.mass * dt;
                currentMass.vY += currentMass.forceY / currentMass.mass * dt;

                currentMass.x += currentMass.vX * dt;
                currentMass.y += currentMass.vY * dt;
            }
        }

        function drawChain() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // Draw Springs
            for (let i = 0; i < NUM_MASSES - 1; i++) {
                ctx.beginPath();
                ctx.strokeStyle = '#666';
                ctx.lineWidth = 2;
                ctx.moveTo(masses[i].x, masses[i].y);
                ctx.lineTo(masses[i + 1].x, masses[i + 1].y);
                ctx.stroke();
            }

            // Draw Masses
            for (let i = 0; i < NUM_MASSES; i++) {
                const m = masses[i];
                let color = '#3cdbff';
                if (i === dragIndex) color = 'rgba(255, 165, 0, 0.9)';
                
                ctx.beginPath();
                ctx.arc(m.x, m.y, m.radius, 0, Math.PI * 2);
                ctx.fillStyle = color;
                ctx.fill();
            }
        }

        function animate() {
            updatePhysics();
            drawChain();
            requestAnimationFrame(animate);
        }

        // Interaction Handlers
        canvas.addEventListener('mousedown', (e) => {
            const rect = canvas.getBoundingClientRect();
            const mouseX = e.clientX - rect.left;
            const mouseY = e.clientY - rect.top;

            for (let i = 0; i < NUM_MASSES; i++) {
                const m = masses[i];
                const dist = Math.sqrt((m.x - mouseX)**2 + (m.y - mouseY)**2);
                if (dist < m.radius * 1.5) { 
                    dragging = true;
                    dragIndex = i;
                    masses[i].vX = masses[i].vY = 0; // Stop momentum instantly upon click
                    break;
                }
            }
        });

        canvas.addEventListener('mousemove', (e) => {
            if (!dragging || dragIndex === -1) return;
            const rect = canvas.getBoundingClientRect();
            cursorPos.x = e.clientX - rect.left;
            cursorPos.y = e.clientY - rect.top;

            // Force the dragged mass to follow the cursor position directly 
            masses[dragIndex].x = cursorPos.x;
            masses[dragIndex].y = cursorPos.y;
        });

        canvas.addEventListener('mouseup', () => {
            if (dragging) {
                dragging = false;
                dragIndex = -1;
            }
        });
        window.addEventListener('mouseleave', () => { 
            if (dragging) { dragging = false; dragIndex = -1; }
        });

        // Initialization
        resetSystem();
        animate();

    </script>
</body>
</html>
```
qwen3.5:9b31.2 sOKLIM★ 7.5/10
Show code
<!DOCTYPE html>
<html>
<head>
<title>Spring Chain Simulation</title>
<style>body {margin: 0; overflow: hidden;} #canvas {display: block;}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let W = window.innerWidth;
let H = window.innerHeight * 0.7;
canvas.width = W;
canvas.height = H;

const N = 12;
const L_INITIAL = 80; // Initial spacing between masses
const K_SPRING = 0.05; // Spring constant (Hooke's law)
const DAMPING = 0.98;  // Damping factor applied to velocity
const MASS_RADIUS = 10;
const DT = 0.01;

let masses = [];
for (let i = 0; i < N; i++) {
    masses[i] = { x: 50 + i * L_INITIAL, y: H / 2, vx: 0, vx: 0 };
}

let isDragging = false;
let grabbedIndex = -1;
const ANCHOR_LIMITS = { start: 0, end: N - 1 };

function updateMass(i) {
    const m = masses[i];
    if (m === undefined) return;

    // Calculate net force on mass i from its neighbors
    let fx = 0;

    // Force from left neighbor (i-1)
    if (i > 0 && !isFrozen(i - 1)) {
        const prev = masses[i - 1];
        const dx = prev.x - m.x;
        fx += K_SPRING * dx;
        // Damping force proportional to relative velocity (simplified)
        m.vx -= DAMPING * Math.abs(prev.vx - m.vx);
    }

    // Force from right neighbor (i+1)
    if (i < N - 1 && !isFrozen(i + 1)) {
        const next = masses[i + 1];
        const dx = next.x - m.x;
        fx += K_SPRING * dx;
        m.vx -= DAMPING * Math.abs(next.vx - m.vx);
    }

    // Apply net force and update physics (using Euler integration approximation)
    let ax = fx / 1; // Assume mass 'm' = 1 for simplicity in calculation
    m.vx += ax * DT;
    m.x += m.vx * DT;


    // Keep masses within boundaries defined by anchors
    if (i === ANCHOR_LIMITS.start || i === ANCHOR_LIMITS.end) {
        m.x = Math.max(W / 2, Math.min(W - W/2, m.x));
        m.vx *= 0.1; // Dampen movement at anchor points
    } else if (i < N && i > 0) {
        // Optional boundary checks for internal masses
        if (m.x < 0 || m.x > W) m.vx = 0;
    }
}

function isFrozen(i) {
    return i <= ANCHOR_LIMITS.start * 0.1 || i >= ANCHOR_LIMITS.end * 0.9;
}

function draw() {
    ctx.clearRect(0, 0, W, H);
    const massColor = '#ff6347';
    const springColor = '#aaa';
    let highlightOff = true;

    for (let i = 0; i < N - 1; i++) {
        const m1 = masses[i];
        const m2 = masses[i + 1];
        ctx.beginPath();
        ctx.moveTo(m1.x, m1.y);
        ctx.lineTo(m2.x, m2.y);
        ctx.strokeStyle = springColor;
        ctx.lineWidth = 2;
        ctx.stroke();

        // Drawing Masses and Highlighting
        if (i === grabbedIndex || i + 1 === grabbedIndex) {
             highlightOff = false;
        }
    }

    for (let i = 0; i < N; i++) {
        const m = masses[i];
        ctx.beginPath();
        ctx.arc(m.x, m.y, MASS_RADIUS, 0, Math.PI * 2);
        if (highlightOff) {
            ctx.fillStyle = massColor;
        } else if (!isFrozen(i)) {
             ctx.fillStyle = 'red'; // Highlight grabbed/near grabbed mass
        } else {
             ctx.fillStyle = '#333'; // Anchor color
        }
        ctx.fill();
    }
}

function animate() {
    // Handle physics updates for all masses every frame
    for (let i = 1; i < N - 1; i += 2) { // Process pairs to link v and force calculation
        updateMass(i);
         if (i + 1 < N) updateMass(i+1);
    }


    draw();
    requestAnimationFrame(animate);
}

// Interaction Handlers
canvas.addEventListener('mousedown', e => {
    isDragging = true;
    grabbedIndex = getClosestMass(e.clientX, e.clientY);
});

function getClosestMass(x, y) {
    for(let i=0; i<N; i++) {
        const m = masses[i];
        const distSq = (m.x - x)**2 + (m.y - y)**2;
        if (distSq < MASS_RADIUS**2 * 3) { // Within 1.7 radii distance
            return i;
        }
    }
    return -1;
}

canvas.addEventListener('mousemove', e => {
    if (isDragging && grabbedIndex >= 0) {
        // Force the grasped mass to follow the cursor
        const m = masses[grabbedIndex];
        m.x = e.clientX();
        masses[grabbedIndex].vx = (e.clientX - m.x) / DT * 0.5; // Calculate a temporary velocity
    }
});

canvas.addEventListener('mouseup', () => {
    isDragging = false;
    grabbedIndex = -1;
    // The physics engine naturally handles the wave propagation on release
});

window.onresize = () => {
    W = window.innerWidth;
    H = window.innerHeight * 0.7;
    canvas.width = W;
    canvas.height = H;
};

animate();
</script>
</body>
</html>
qwen3:14b28.7 sERRBLANK★ 1/10
Show code
<!DOCTYPE html>
<html lang="en">
<head><title>Spring-Mass Chain</title></head>
<body><canvas id="c"></canvas><script>const canvas = document.getElementById('c');const ctx = canvas.getContext('2d');let N=12;let L=600,H=50;canvas.width=L;canvas.height=H*2;let pos={x:[],y:[]};let vel={vx:[],vy:[]};let isDragging=false;let draggedIndex=-1;$={length:N,map:()=>({x:Math.random()*L, y:H+(Math.random()-0.5)*H*2}),initializeSimulation();function initializeSimulation(){pos[0]={x:50,y:H},pos[N-1]={x:L-50,y:H];for(let i=1;i<N-1;i++){pos[i]={x:Math.random()*L, y:H+(Math.random()-0.5)*H*2}}});const k_spring=0.05;const damping=0.98;const dt=0.1;function updatePhysics(){for(let i=1;i<N-1;i++){if(!isDragging || draggedIndex !== i){pos[i].x+=vel.vx[i]*dt;pos[i].y+=vel.vy[i]*dt;}else{pos[i]={{x:lastMouseX, y:lastMouseY}}}}}let forces={vx:new Array(N).fill(0),vy:new Array(N).fill(0)};for(let i=1;i<N-1;i++){const j=((i+1)%N)===i?N-2:Math.min(i+1,N-2);const dx=pos[j].x-pos[i].x;const dy=pos[j].y-pos[i].y;let mag=Math.sqrt(dx*dx+dy*dy);let est_mag=(Math.abs(pos[i].x-pos[j].x)*0.9+Math.abs(pos[i].y-pos[j].y)*0.9)/2;$[[1,i],[1,j]].forEach(([k, index])=>{if(!isDragging||draggedIndex !== index){let force_comp = (est_mag-*mag)*k;forces.vx[index]+=force_comp*dx/mag;forces.vy[index]+=force_comp*dy/mag;} }});for(let i=1;i<N-1;i++){vel.vx[i]=damping*(pos[i].x-pos[i-1].x)/(pos[i].x-pos[i-1].x+1)+forces.vx[i];vel.vy[i]=damping*(pos[i].y-pos[i-1].y)/(pos[i].y-pos[i-1].y+1)+forces.vy[i]}const lastMouseX=L/2,lastMouseY=H;function animate(){ctx.clearRect(0,0,L,H*2);for(let i=0;i<N;i++){const px={x:pos[i].x, y:pos[i].y};if((Math.abs(px.x-lastMouseX)*isDragging+1)>0){pos[i]=px;}else{/* force update is complex/line limited */}}ctx.save();for(let i=0;i<N-1;i++){ctx.strokeStyle=`${i===draggedIndex? 'red': '#8866ff'}:`;ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(pos[i].x,pos[i].y);ctx.lineTo(pos[i+1].x,pos[i+1].y);ctx.stroke()}for(let i=0;i<N;i++){ctx.fillStyle=`${i===draggedIndex? 'red': '#ff66cc'}:`;ctx.beginPath();ctx.arc(pos[i].x,pos[i].y,7,0,2*Math.PI);ctx.fill();}ctx.restore();requestAnimationFrame(animate);}(window.addEventListener('mousedown',e=>{isDragging=true;draggedIndex=Math.floor((e.clientX/L)*N);lastMouseX=e.clientX-canvas.offsetLeft;lastMouseY=e.clientY-canvas.offsetTop;});window.addEventListener('mousemove',e=>{if(isDragging){lastMouseX=e.clientX-canvas.offsetLeft;lastMouseY=e.clientY-canvas.offsetTop;}});window.addEventListener('mouseup',()=>for(let i=1;i<N-1;i++){vel=\{vx:\n[0],vy:\n[0]\}}));</script></body></html>
qwen3:8b DNF / pending DNF (infra)
DNF — infrastructure: 10/10 requests returned status 500
llama3.116.0 sERRBLANKLIM★ 0.5/10
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Spring-Mass Chain</title>
  <style>
    canvas {
      border: 1px solid black;
    }
  </style>
</head>
<body>
<script>
const ctx = document.createElement('canvas').getContext('2d');
document.body.appendChild(ctx.canvas);
ctx.fillStyle = 'black';
ctx.font = '14pt sans-serif';

function mass(num) {
  let x = num * (200 / 12);
  return {x, spring: null};
}

function update() {
  ctx.clearRect(0, 0, 400, 300);
  ctx.beginPath();
  for (let i = 0; i < masses.length; i++) {
    const m = masses[i];
    const n = masses[(i + 1) % masses.length];
    if (!m.sprung && !n.sprung) continue;
    ctx.moveTo(m.x, 150);
    ctx.lineTo(n.x, 150);
    ctx.stroke();
    massDraw(ctx, m.x, i);
    if (i === 0 || i === masses.length - 1) {
      ctx.fillStyle = 'blue';
      ctx.font = '14pt sans-serif';
      ctx.fillText('Anchor', m.x + 10, 160);
    }
  }
  requestAnimationFrame(update);
}

function massDraw(ctx, x, num){
  if (mouseDown && mouse !== null && mass === num) {
    ctx.fillStyle = 'red';
  } else {
    ctx.fillStyle = 'black';
  }
  ctx.beginPath();
  ctx.arc(x, 150, 10, 0, Math.PI * 2);
  ctx.fill();
}

function spring(ctx, a, b) {
  const x1 = masses[a].x;
  const x2 = masses[b].x;
  if (!a.sprung && !b.sprung) return;
  let len = Math.sqrt(Math.pow(x2 - x1, 2));
  ctx.beginPath();
  ctx.moveTo(x1 + (len / 2), 150);
  ctx.lineTo(x2 - (len / 2), 150);
  ctx.stroke();
}

function simulate() {
  let damping = 0.97;
  for (let i = masses.length - 2; i >= 0; i--) {
    if (masses[i + 1].sprung) {
      const a = Math.abs(masses[(i + 1) % masses.length].x - masses[i].x);
      masses[i].spring = a;
      if (!mouseDown || mass !== i) {
        if (damping < 5) {
          masses[i].pos += ((masses[i].mass - damping * Math.sqrt(masses[i].pos * masses[i].masl)) / masses[i].mass);
        } else {
          masses[i].pos += ((100 + num * dampVal) / masses[i].mass);
        }
      }
    }
  }
}

function mouseDrag(x, y) {
  const m = Math.floor((x - ctx.canvas.offsetLeft) / (220 / masses.length));
  if (m > 0 && Math.abs(x - (220 / masses.length * m)) < 2) {
    massDraw(ctx, x, m);
  }
}

function mouseMove(x, y) {
  const pos = ctx.canvas.getBoundingClientRect();
  ctx.mouse = {x: x - pos.x + ctx.canvas.offsetLeft,
               y: y - pos.y + ctx.canvas.offsetTop
              };
  if (ctx.mouse.x > 0 && ctx.mouse.x < 190 &&
    ctx.mouse.y > 50 && ctx.mouse.y < 210) {
  }
}

let mouse = null;
let mouseDown = false;
const dampVal = 9.8 / masses[0].mass;
let num = 0;
ctx.canvas.width = 400;
ctx.canvas.height = 300;
const masses = Array(12).fill().map(mass);

document.addEventListener('mousedown', (e) => {
  const pos = ctx.canvas.getBoundingClientRect();
  if (ctx.mouse.x > e.clientX - pos.x && ctx.mouse.x < e.clientX + pos.x &&
    ctx.mouse.y > e.clientY - pos.y && ctx.mouse.y < e.clientY + pos.y &&
    i === 0 || i === masses.length - 1) {masses[i].sprung = true;} else {
      mouseDown = true;
      const massNum = Math.floor((e.clientX - ctx.canvas.offsetLeft) / (220 / masses.length));
      mouse = massNum;
      massDraw(ctx, (220 / masses.length * massNum) + 10, massNum);
    }
});

document.addEventListener('mousemove', (e) => {
  const pos = ctx.canvas.getBoundingClientRect();
  let x = e.clientX - pos.x;
  let y = e.clientY - pos.y;
  mouseMove(x, y);
});

document.addEventListener('mouseup', () => {
  mouseDown = false;
});

update();
</script>
```
deepseek-r136.1 sERRSTAT★ 2.5/10
Show code
```html
<!DOCTYPE html>
<html><body style="margin:0;display:flex;justify-content:center;align-items:center;height:100vh;background:#f0f0f0;">
<canvas id=c width=800 height=600></canvas>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
const balls=[]; const springs=[];let grabs=-1;
class Ball{constructor(x,y,k,a,vx,vy){this.x=x,this.y=y,this.k=k,this.a=a,this.vx=this.vy=0}
applySpring(j){let dx=this.x-balls[j].x, dy=this.y-balls[j].y,d=Math.sqrt(dx*dx+dy*dy),f=this.k*(1-d)/d;
balls[j].a += f*dx/(this.k);this.a -=f*dx/(this.k)}}
const k=200,a=5;const step=40;n=800/step*3.5;n/=n>step?2:1;n=12-1
c.width=step*(n+1);const w=c.width,h=c.height/2;
for(let i=0;i<12;i++)balls[i]=new Ball(step*i+5,w/2,5,i%2?0x666699:0xff33ff,.5*h??0,0);
function update(){let b=balls[grabs];if(b){b.x=canvasEvent.x+.1*step,b.y=canvasEvent.y;
updateSprings()}
else{for(let i=0;i<balls.length-1;i++)balls[i].applySpring(i+1);
updateVelocities();}
animate()}
function updateSprings(){let b=balls[grabs];if(b){
b.vx=(b.x - (b.k*ball_prev_x)).x - b.a;...}
Wait, you asked for the code but I realized that it may exceed 120 lines. Let me try to condense it.

Here's a much shorter version of a spring-mass simulation:

const c=document.getElementById('c'), ctx=c.getContext('2d');
class Ball{constructor(x,y,k,a,v=...}{...
But that still might not fit. Alternatively, can I provide the code in fewer lines?
Okay, I think this is the most condensed version possible within 120 lines. It's essentially a spring-mass chain (like a guitar) you can pull and create waves.

Here's the final code:

```html
<canvas id=c width=800 height=600></canvas>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
let balls=[],n=13,step=40;
for(let i=-1.5;i<=13-1.3;i+= (n-1)/10) balls.push(i*step);

function Wave(n){
 this.balls=[...Array(12)].map(() => 2); // positions
 this.vels=[...Array(12)].map(() => 0); // velocities
 this.lastTime=0;
}
Wave.prototype.update=function(t){
 for(let i=0;i<12;i++){
  if(i>0) this.balls[i]+=(this.balls[i-1]-this.balls[i]);
  else this.balls[0]=Math.sin(t);
 }
};
// But I need to make it fit within 120 lines.
Hmm, perhaps better to abandon the animation code.

Alternatively, perhaps a minimal version:

<canvas id=c width=800 height=600></canvas>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
let xarr=[],vx=[],step=40;
function init(){for(let i=0;i<12;i++)xarr.push( (i+1)*step );}
function animate(){
  // physics update
  for(let i=0;i<12-1;i++){
    let dx=xarr[i]-xarr[i+1];vx[i]=(vx[i] - (dx)/5);
    vx[i+1]=-vx[i];
  }
  // apply damping
     xarr.map((v,i)=>{xarr[i]+=(vx[i]|0);vx[i]*=0.9});
    requestAnimationFrame(animate);}
init();
animate();
</script>
But this is only about 25 lines.

Wait, but user wants a chain with grabbing and waves propogating. That would require more code. Hmm.

Well, to make it within 120 lines, I'll need to find a way to compress the code. But perhaps you can accept that?

Okay, final code:

```html>
<canvas id=c width=800 height=600></canvas>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
let balls=[],k=200,dampening=0.95;
class Ball{x,y,vx,vy,c;}for(let i=0;i<12;i++){balls[i]={
x:i*step+5,y:300,vx:0,vy:0,spring:(i>0?balls[i-1]:null)}
function update(){let mousedownX=-1;mousedownY=-1;let grabIndex=-1;
for(let i=0;i<balls.length;i++){if(grabIndex==-1){
let dx=mouse.x-balls[i].x,dy=mouse.y-balls[i].y,d=Math.sqrt(dx*dx+dy*dy);
if(d<20){grabIndex=i;}
else balls[i].vx *=dampening; balls[i].vy *=dampening;
balls[grabs].x=mouse.x-5;balls[grabs].y=mouse.y;}
if(grabIndex>-1){
balls[grabs].x=mouse.x;balls[grabs].y=mouse.y}
else if(mouseReleased){
// Propagation code
for(let i=0;i<balls.length-1;i++){
let dx=balls[i].x-balls[i+1].x;
let dy=balls[i].y-balls[i+1].y;
let len=Math.hypot(dx,dy);
if(len>0){
let force=k*(len-1)/len;
balls[i+1].vx -= force*dx/(k*2);
}}
requestAnimationFrame(update)
}
function draw(){ctx.clearRect(0,0,c.width,c.height);
for(let i=0;i<balls.length-1;i++){
let dx=balls[i].x-balls[i+1].x;let dy=balls[i].y-balls[i+1].y;
if(i==grabs) ctx.strokeStyle='red';else ctx.strokeStyle='black';
lines.push([[balls[i].x,balls[i].y],[balls[i].x+(dx),balls[i].y+(dy)]]);
}
for(let i of lines){
}
}

</script>
</body></html>
```
mistral-small103.4 sSTATLIM★ 2/10
Show code
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Spring-Mass Chain</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; background-color: #f0f0f0; }
</style>
</head>
<body>
<canvas id="chainCanvas" width="800" height="400"></canvas>
<script>
const canvas = document.getElementById('chainCanvas');
const ctx = canvas.getContext('2d');
const numMasses = 12;
const masses = [];
const springs = [];
const springLength = 50;
const stiffness = 0.1;
const damping = 0.98;

for (let i = 0; i < numMasses; i++) {
    masses.push({ x: i * springLength + 10, y: canvas.height / 2, vx: 0, vy: 0 });
}

for (let i = 0; i < numMasses - 1; i++) {
    springs.push({ a: i, b: i + 1 });
}

let isDragging = false;
let draggedMassIndex;

canvas.addEventListener('mousedown', e => {
    const mouseX = e.clientX;
    const mouseY = e.clientY;
    for (let i = 0; i < numMasses; i++) {
        if (Math.hypot(mouseX - masses[i].x, mouseY - masses[i].y) < 15) {
            isDragging = true;
            draggedMassIndex = i;
            break;
        }
    }
});

document.addEventListener('mouseup', () => {
    isDragging = false;
});

canvas.addEventListener('mousemove', e => {
    if (isDragging && draggedMassIndex !== undefined) {
        masses[draggedMassIndex].x = e.clientX - canvas.getBoundingClientRect().left;
        masses[draggedMassIndex].y = e.clientY - canvas.getBoundingClientRect().top;
    }
});

function animate() {
    for (let i = 0; i < numMasses - 1; i++) {
        const a = masses[i];
        const b = masses[i + 1];

        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const distance = Math.hypot(dx, dy);

        if (distance > 0) {
            const f = stiffness * (springLength - distance);
            const theta = Math.atan2(dy, dx);
            const fx = f * Math.cos(theta) * damping;
            const fy = f * Math.sin(theta) * damping;

            a.vx += fx / numMasses;
            a.vy += fy / numMasses;

            if (a !== masses[0] && a !== masses[numMasses - 1]) {
                b.vx -= fx / numMasses;
                b.vy -= fy / numMasses;
            }
        }
    }

    for (let i = 0; i < numMasses; i++) {
        if (i !== 0 && i !== numMasses - 1) {
            masses[i].x += masses[i].vx * damping;
            masses[i].y += masses[i].vy * damping;

            masses[i].vx *= damping;
            masses[i].vy *= damping;
        }
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    for (let i = 0; i < springs.length; i++) {
        const a = springs[i];
        ctx.beginPath();
        ctx.moveTo(masses[a.a].x, masses[a.a].y);
        ctx.lineTo(masses[a.b].x, masses[a.b].y);
        ctx.strokeStyle = 'black';
        ctx.stroke();
    }

    for (let i = 0; i < numMasses; i++) {
        const mass = masses[i];
        ctx.beginPath();
        ctx.arc(mass.x, mass.y, 15, 0, Math.PI * 2);
        if (!isDragging || draggedMassIndex !== i) {
            ctx.fillStyle = 'blue';
        } else {
            ctx.fillStyle = 'red';
        }
        ctx.fill();
    }

    requestAnimationFrame(animate);
}

animate();
</script>
</body>
</html>
```
Cloud
GPT-5.5 (codex)22.0 sSTAT★ 8.5/10

📝 12 masses + separate anchors, clean (8.5)

Show code
<!doctype html>
<html>
<body style="margin:0;overflow:hidden;background:#111">
<canvas id="c"></canvas>
<script>
const c=document.getElementById("c"),x=c.getContext("2d"),N=12;
let W,H,drag=-1,mouse={x:0,y:0},m=[];
function size(){
  W=c.width=innerWidth;H=c.height=innerHeight;
  let gap=W/(N+3), y=H/2;
  m=[...Array(N)].map((_,i)=>({x:gap*(i+2),y,vx:0,vy:0}));
}
size();addEventListener("resize",size);
function pos(e){
  const r=c.getBoundingClientRect();
  mouse.x=(e.clientX-r.left)*c.width/r.width;
  mouse.y=(e.clientY-r.top)*c.height/r.height;
}
c.onmousedown=e=>{
  pos(e);let best=999;
  m.forEach((p,i)=>{let d=(p.x-mouse.x)**2+(p.y-mouse.y)**2;if(d<best&&d<900)best=d,drag=i});
};
c.onmousemove=e=>{pos(e)};
onmouseup=()=>drag=-1;
function spring(a,b,k=0.018,d=0.045,L){
  let dx=b.x-a.x,dy=b.y-a.y,len=Math.hypot(dx,dy)||1;
  let nx=dx/len,ny=dy/len,rel=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
  let f=k*(len-L)+d*rel,fx=f*nx,fy=f*ny;
  if(!a.fixed){a.vx+=fx;a.vy+=fy}
  if(!b.fixed){b.vx-=fx;b.vy-=fy}
}
function loop(){
  x.clearRect(0,0,W,H);
  let gap=W/(N+3), A={x:gap,y:H/2,vx:0,vy:0,fixed:1}, B={x:gap*(N+2),y:H/2,vx:0,vy:0,fixed:1};
  if(drag>-1){m[drag].x=mouse.x;m[drag].y=mouse.y;m[drag].vx=m[drag].vy=0}
  spring(A,m[0],0.02,0.05,gap);
  for(let i=0;i<N-1;i++)spring(m[i],m[i+1],0.02,0.05,gap);
  spring(m[N-1],B,0.02,0.05,gap);
  for(let i=0;i<N;i++)if(i!==drag){
    m[i].vy+=(H/2-m[i].y)*0.001;
    m[i].vx*=0.992;m[i].vy*=0.992;
    m[i].x+=m[i].vx;m[i].y+=m[i].vy;
  }
  x.lineWidth=3;x.strokeStyle="#74d6ff";x.beginPath();
  x.moveTo(A.x,A.y);m.forEach(p=>x.lineTo(p.x,p.y));x.lineTo(B.x,B.y);x.stroke();
  x.fillStyle="#ddd";[A,B].forEach(p=>{x.beginPath();x.arc(p.x,p.y,7,0,7);x.fill()});
  m.forEach((p,i)=>{
    x.fillStyle=i===drag?"#ffcb4d":"#f3f6ff";
    x.beginPath();x.arc(p.x,p.y,16,0,7);x.fill();
    x.strokeStyle="#111";x.lineWidth=2;x.stroke();
  });
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
Claude HaikuBLANK★ 1.5/10
Show code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Spring-Mass Chain</title>
<style>
body { margin: 0; overflow: hidden; background: #1a1a1a; }
canvas { display: block; cursor: grab; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = 1000;
canvas.height = 400;

const k = 0.5, damping = 0.95;
const masses = [];
const numMasses = 12;
const spacing = canvas.width / (numMasses + 1);
let grabbed = -1;

for (let i = 0; i < numMasses; i++) {
  masses.push({
    x: spacing * (i + 1),
    y: canvas.height / 2,
    px: spacing * (i + 1),
    py: canvas.height / 2
  });
}

canvas.addEventListener('mousedown', (e) => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;
  for (let i = 0; i < masses.length; i++) {
    const dx = masses[i].x - mx, dy = masses[i].y - my;
    if (dx*dx + dy*dy < 100) {
      grabbed = i;
      break;
    }
  }
});

canvas.addEventListener('mousemove', (e) => {
  if (grabbed >= 0) {
    const rect = canvas.getBoundingClientRect();
    masses[grabbed].x = e.clientX - rect.left;
    masses[grabbed].y = e.clientY - rect.top;
    masses[grabbed].px = masses[grabbed].x;
    masses[grabbed].py = masses[grabbed].y;
  }
});

canvas.addEventListener('mouseup', () => grabbed = -1);

function update() {
  for (let i = 0; i < numMasses; i++) {
    if (i === grabbed) continue;
    const vx = (masses[i].x - masses[i].px) * damping;
    const vy = (masses[i].y - masses[i].py) * damping;
    masses[i].px = masses[i].x;
    masses[i].py = masses[i].y;
    masses[i].x += vx;
    masses[i].y += vy + 0.1;
  }
  
  for (let i = 0; i < numMasses - 1; i++) {
    const dx = masses[i+1].x - masses[i].x;
    const dy = masses[i+1].y - masses[i].y;
    const len = Math.sqrt(dx*dx + dy*dy);
    const force = (len - spacing) * k / len;
    const fx = force * dx, fy = force * dy;
    if (i !== grabbed) {
      masses[i].x += fx;
      masses[i].y += fy;
    }
    if (i+1 !== grabbed) {
      masses[i+1].x -= fx;
      masses[i+1].y -= fy;
    }
  }
}

function draw() {
  ctx.fillStyle = '#1a1a1a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  ctx.strokeStyle = '#666';
  ctx.lineWidth = 2;
  ctx.beginPath();
  masses.forEach((m, i) => {
    if (i === 0) ctx.moveTo(m.x, m.y);
    else ctx.lineTo(m.x, m.y);
  });
  ctx.stroke();
  
  masses.forEach((m, i) => {
    ctx.fillStyle = (i === grabbed) ? '#ffff00' : '#4488ff';
    ctx.beginPath();
    ctx.arc(m.x, m.y, 6, 0, Math.PI * 2);
    ctx.fill();
  });
}

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();
</script>
</body>
</html>
Claude SonnetSTAT★ 8/10

📝 chain settles correctly, anchor marked (8.0); drag not testable headless

Show code
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Spring Chain</title>
<style>body{margin:0;background:#101418;overflow:hidden}canvas{display:block;cursor:grab}</style></head>
<body><canvas id="c"></canvas>
<script>
const cvs=document.getElementById('c'),ctx=cvs.getContext('2d');
function resize(){cvs.width=innerWidth;cvs.height=innerHeight}
resize();addEventListener('resize',resize);
const N=12,k=180,damping=0.94,anchorK=400,mass=1;
let pts=[];
function layout(){
  const margin=80,w=cvs.width-margin*2,y=cvs.height/2;
  pts=[];
  for(let i=0;i<N;i++){
    const hx=margin+w*i/(N-1);
    pts.push({x:hx,y,vx:0,vy:0,hx,hy:y});
  }
}
layout();addEventListener('resize',layout);
let dragIdx=-1,mouse={x:0,y:0};
cvs.addEventListener('mousedown',e=>{
  const mx=e.clientX,my=e.clientY;
  let best=-1,bd=25;
  pts.forEach((p,i)=>{const d=Math.hypot(p.x-mx,p.y-my);if(d<bd){bd=d;best=i;}});
  dragIdx=best;
});
addEventListener('mousemove',e=>{mouse.x=e.clientX;mouse.y=e.clientY;});
addEventListener('mouseup',()=>{dragIdx=-1;});
function frame(){
  ctx.fillStyle='#101418';
  ctx.fillRect(0,0,cvs.width,cvs.height);
  for(let i=0;i<N;i++){
    const p=pts[i];
    if(i===dragIdx){
      p.vx=(mouse.x-p.x)*10;p.vy=(mouse.y-p.y)*10;
      p.x=mouse.x;p.y=mouse.y;
      continue;
    }
    let fx=0,fy=0;
    if(i>0){
      const q=pts[i-1],restLen=Math.hypot(q.hx-p.hx,q.hy-p.hy);
      const dx=q.x-p.x,dy=q.y-p.y,len=Math.hypot(dx,dy)||1;
      const f=k*(len-restLen);
      fx+=f*dx/len;fy+=f*dy/len;
    }
    if(i<N-1){
      const q=pts[i+1],restLen=Math.hypot(q.hx-p.hx,q.hy-p.hy);
      const dx=q.x-p.x,dy=q.y-p.y,len=Math.hypot(dx,dy)||1;
      const f=k*(len-restLen);
      fx+=f*dx/len;fy+=f*dy/len;
    }
    if(i===0||i===N-1){
      fx+=(p.hx-p.x)*anchorK;fy+=(p.hy-p.y)*anchorK;
    }
    p.vx+=fx/mass*0.016;p.vy+=fy/mass*0.016;
    p.vx*=damping;p.vy*=damping;
    p.x+=p.vx*0.016;p.y+=p.vy*0.016;
  }
  ctx.strokeStyle='#5a7';ctx.lineWidth=2;
  ctx.beginPath();
  for(let i=0;i<N-1;i++){
    ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[i+1].x,pts[i+1].y);
  }
  ctx.stroke();
  pts.forEach((p,i)=>{
    ctx.beginPath();ctx.arc(p.x,p.y,i===dragIdx?12:8,0,7);
    ctx.fillStyle=i===dragIdx?'#ffcc33':(i===0||i===N-1?'#ff6666':'#66ccff');
    ctx.fill();
  });
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script></body></html>
Claude OpusOK★ 8.5/10
Show code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Spring-Mass Chain</title>
<style>html,body{margin:0;background:#0a0e1a;overflow:hidden}canvas{display:block;cursor:grab}</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
let W,H;
function resize(){W=cv.width=innerWidth;H=cv.height=innerHeight;init();}
const N=12,k=0.25,damp=0.96,rest=0;let m=[],restLen,grab=-1,mx=0,my=0;
function init(){restLen=W*0.7/(N-1);const y=H/2,x0=W*0.15;m=[];for(let i=0;i<N;i++)m.push({x:x0+i*restLen,y,vx:0,vy:0,anchor:i==0||i==N-1});}
addEventListener('resize',resize);resize();
cv.onmousedown=e=>{for(let i=0;i<N;i++){if(m[i].anchor)continue;if(Math.hypot(e.clientX-m[i].x,e.clientY-m[i].y)<16){grab=i;break;}}mx=e.clientX;my=e.clientY;};
cv.onmousemove=e=>{mx=e.clientX;my=e.clientY;};
addEventListener('mouseup',()=>grab=-1);
function step(){
 for(let i=0;i<N;i++){const p=m[i];if(p.anchor)continue;
  if(i==grab){p.x=mx;p.y=my;p.vx=0;p.vy=0;continue;}
  let fx=0,fy=0;
  for(const j of[i-1,i+1]){const q=m[j];const dx=q.x-p.x,dy=q.y-p.y,d=Math.hypot(dx,dy)||1;const f=k*(d-restLen);fx+=f*dx/d;fy+=f*dy/d;}
  p.vx=(p.vx+fx)*damp;p.vy=(p.vy+fy)*damp;p.x+=p.vx;p.y+=p.vy;
 }
 ctx.fillStyle='#0a0e1a';ctx.fillRect(0,0,W,H);
 ctx.strokeStyle='#5a7bb5';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(m[0].x,m[0].y);for(let i=1;i<N;i++)ctx.lineTo(m[i].x,m[i].y);ctx.stroke();
 for(let i=0;i<N;i++){const p=m[i];ctx.fillStyle=i==grab?'#ffd23f':p.anchor?'#888':'#4dd2ff';ctx.beginPath();ctx.arc(p.x,p.y,i==grab?11:8,0,7);ctx.fill();}
 requestAnimationFrame(step);}
step();
</script>
</body>
</html>
Claude FableSTAT★ 8/10
Claude Fable – rendered output R30

📝 chain settles correctly, anchor marked (8.0); drag not testable headless

Show code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Spring-Mass Chain</title>
<style>html,body{margin:0;height:100%;background:#0a0e1a;overflow:hidden}canvas{display:block;cursor:grab}</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
let W,H,rest,drag=-1,mx=0,my=0;const N=12,masses=[];
function resize(){W=cv.width=innerWidth;H=cv.height=innerHeight;
const x0=W*0.08,x1=W*0.92;rest=(x1-x0)/(N-1);masses.length=0;
for(let i=0;i<N;i++)masses.push({x:x0+i*rest,y:H/2,vx:0,vy:0,fx:0,fy:0,fixed:i===0||i===N-1});}
resize();addEventListener('resize',resize);
cv.addEventListener('mousedown',e=>{let best=-1,bd=1600;
masses.forEach((m,i)=>{if(m.fixed)return;
const d=(m.x-e.clientX)**2+(m.y-e.clientY)**2;if(d<bd){bd=d;best=i;}});
if(best>=0){drag=best;mx=e.clientX;my=e.clientY;}});
addEventListener('mousemove',e=>{mx=e.clientX;my=e.clientY;});
addEventListener('mouseup',()=>drag=-1);
const K=1400,DAMP=6,M=1;
function physics(h){
for(const m of masses){m.fx=0;m.fy=0;}
for(let i=0;i<N-1;i++){const a=masses[i],b=masses[i+1];
const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1e-6,ux=dx/d,uy=dy/d;
const rel=(b.vx-a.vx)*ux+(b.vy-a.vy)*uy;
const f=K*(d-rest)+DAMP*rel;
a.fx+=f*ux;a.fy+=f*uy;b.fx-=f*ux;b.fy-=f*uy;}
masses.forEach((m,i)=>{if(m.fixed)return;
if(i===drag){m.x=mx;m.y=my;m.vx=0;m.vy=0;return;}
m.vx=(m.vx+m.fx/M*h)*0.999;m.vy=(m.vy+m.fy/M*h)*0.999;
m.x+=m.vx*h;m.y+=m.vy*h;});}
let last=performance.now();
function frame(t){requestAnimationFrame(frame);
const dt=Math.min((t-last)/1000,0.033);last=t;
const steps=Math.max(1,Math.round(dt/0.002));
for(let i=0;i<steps;i++)physics(dt/steps);
ctx.fillStyle='#0a0e1a';ctx.fillRect(0,0,W,H);
ctx.strokeStyle='#5c7cfa';ctx.lineWidth=2;
for(let i=0;i<N-1;i++){ctx.beginPath();ctx.moveTo(masses[i].x,masses[i].y);
ctx.lineTo(masses[i+1].x,masses[i+1].y);ctx.stroke();}
masses.forEach((m,i)=>{ctx.beginPath();ctx.arc(m.x,m.y,i===drag?13:9,0,7);
ctx.fillStyle=m.fixed?'#546e7a':i===drag?'#ffca28':'#90caf9';ctx.fill();
if(i===drag){ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke();
ctx.strokeStyle='#5c7cfa';}});}
requestAnimationFrame(frame);
</script>
</body>
</html>