Show code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Verlet Rope</title>
<style>
html,body{margin:0;height:100%;background:#0b0e14;overflow:hidden}
canvas{display:block;width:100vw;height:100vh}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const N=40,ITER=12,G=1400,DAMP=0.995;
let W=0,H=0,dpr=1,pts=[],segLen=0,ax=0,ay=0,bx=0,by=0,t=0,nextKick=1.2;
function setup(){
dpr=Math.min(window.devicePixelRatio||1,2);
W=c.clientWidth;H=c.clientHeight;
c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);
const gap=Math.min(W*0.55,520);
ax=W/2-gap/2;ay=H*0.18;bx=W/2+gap/2;by=H*0.18;
const slack=Math.min(gap*1.55,H*0.72);
segLen=slack/(N-1);
pts=[];
for(let i=0;i<N;i++){
const u=i/(N-1);
const sag=Math.sin(u*Math.PI)*slack*0.42;
const px=ax+(bx-ax)*u,py=ay+(by-ay)*u+sag;
pts.push({x:px,y:py,px:px,py:py});
}
t=0;nextKick=1.0;
}
function constrain(){
pts[0].x=ax;pts[0].y=ay;
pts[N-1].x=bx;pts[N-1].y=by;
for(let k=0;k<ITER;k++){
for(let i=0;i<N-1;i++){
const a=pts[i],b=pts[i+1];
let dx=b.x-a.x,dy=b.y-a.y;
let d=Math.hypot(dx,dy)||1e-6;
const diff=(d-segLen)/d*0.5;
const ox=dx*diff,oy=dy*diff;
if(i!==0){a.x+=ox;a.y+=oy;}else{a.x=ax;a.y=ay;}
if(i+1!==N-1){b.x-=ox;b.y-=oy;}else{b.x=bx;b.y=by;}
}
pts[0].x=ax;pts[0].y=ay;
pts[N-1].x=bx;pts[N-1].y=by;
}
}
function step(dt){
for(let i=1;i<N-1;i++){
const p=pts[i];
const vx=(p.x-p.px)*DAMP,vy=(p.y-p.py)*DAMP;
p.px=p.x;p.py=p.y;
p.x+=vx;p.y+=vy+G*dt*dt;
}
constrain();
}
function kick(){
const dir=Math.random()<0.5?-1:1;
const power=(9+Math.random()*9)*dir;
for(let i=1;i<N-1;i++){
const u=i/(N-1);
const w=Math.sin(u*Math.PI);
pts[i].px-=power*w;
pts[i].py-=power*w*0.35*Math.random();
}
}
function draw(){
const g=x.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0b0e14');g.addColorStop(1,'#141a26');
x.fillStyle=g;x.fillRect(0,0,W,H);
x.lineCap='round';x.lineJoin='round';
x.strokeStyle='rgba(0,0,0,0.45)';x.lineWidth=11;
x.beginPath();x.moveTo(pts[0].x,pts[0].y+5);
for(let i=1;i<N;i++)x.lineTo(pts[i].x,pts[i].y+5);
x.stroke();
const rg=x.createLinearGradient(ax,ay,bx,by+200);
rg.addColorStop(0,'#f0b96b');rg.addColorStop(0.5,'#d98d3f');rg.addColorStop(1,'#f0b96b');
x.strokeStyle=rg;x.lineWidth=7;
x.beginPath();x.moveTo(pts[0].x,pts[0].y);
for(let i=1;i<N;i++)x.lineTo(pts[i].x,pts[i].y);
x.stroke();
x.strokeStyle='rgba(255,240,210,0.35)';x.lineWidth=2;
x.beginPath();x.moveTo(pts[0].x,pts[0].y-1.5);
for(let i=1;i<N;i++)x.lineTo(pts[i].x,pts[i].y-1.5);
x.stroke();
x.fillStyle='#e8e2d6';
for(let i=0;i<N;i++){x.beginPath();x.arc(pts[i].x,pts[i].y,1.8,0,6.2832);x.fill();}
x.fillStyle='#8fa3bf';
[[ax,ay],[bx,by]].forEach(p=>{x.beginPath();x.arc(p[0],p[1],7,0,6.2832);x.fill();});
}
let last=performance.now();
function loop(now){
let dt=(now-last)/1000;last=now;
if(dt>0.05)dt=0.05;
t+=dt;
if(t>=nextKick){kick();nextKick=t+3+Math.random()*2;}
const sub=2,sdt=dt/sub;
for(let s=0;s<sub;s++)step(sdt);
draw();
requestAnimationFrame(loop);
}
window.addEventListener('resize',setup);
setup();
requestAnimationFrame(loop);
</script>
</body>
</html>