Show code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Crowd Evacuation — 60 People, One Exit</title>
<style>
html,body{margin:0;height:100%;background:#0a0d12;color:#e8eef7;font:15px/1.4 system-ui,sans-serif}
body{display:flex;align-items:center;justify-content:center}
canvas{max-width:100%;height:auto;border-radius:10px;box-shadow:0 10px 40px #0009}
</style>
</head>
<body>
<canvas id="c" width="760" height="470"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=cv.width,H=cv.height,X1=70,Y1=80,X2=600,Y2=430,T=10,GAP=46;
const DY1=(Y1+Y2)/2-GAP/2,DY2=DY1+GAP,DYC=(DY1+DY2)/2,N=60,R=5.4,RNG=2*R+8;
const rnd=(a,b)=>a+Math.random()*(b-a);
let P=[],left=0,frames=0,avgS=0;
function init(){
P=[];left=0;frames=0;avgS=0;
while(P.length<N){
const x=rnd(X1+14,X2-100),y=rnd(Y1+14,Y2-14);
let ok=true;
for(const q of P){if((q.x-x)**2+(q.y-y)**2<(2.3*R)**2){ok=false;break;}}
if(ok)P.push({x,y,vx:0,vy:0,ax:0,ay:0,v0:rnd(1.0,1.75),off:rnd(-1,1),h:rnd(170,340),left:false});
}
}
function walls(p){
if(p.x<X2){
if(p.x<X1+R){p.x=X1+R;p.vx=Math.abs(p.vx)*0.2;}
if(p.y<Y1+R){p.y=Y1+R;p.vy=Math.abs(p.vy)*0.2;}
if(p.y>Y2-R){p.y=Y2-R;p.vy=-Math.abs(p.vy)*0.2;}
}
if(p.x+R>X2&&p.x-R<X2+T){
if(p.y<DY1+R){
if(p.y<DY1){if(p.x<X2){p.x=X2-R;p.vx=Math.min(p.vx,0)*0.15;}else{p.x=X2+T+R;p.vx=Math.max(p.vx,0);}}
else{p.y=DY1+R;p.vy=Math.max(p.vy,0)*0.3;}
}else if(p.y>DY2-R){
if(p.y>DY2){if(p.x<X2){p.x=X2-R;p.vx=Math.min(p.vx,0)*0.15;}else{p.x=X2+T+R;p.vx=Math.max(p.vx,0);}}
else{p.y=DY2-R;p.vy=Math.min(p.vy,0)*0.3;}
}
}
}
function step(){
frames++;
for(const p of P){
const through=p.x>X2+T*0.5;
const tx=through?W+180:X2-6,ty=through?DYC+p.off*52:DYC;
const dx=tx-p.x,dy=ty-p.y,d=Math.hypot(dx,dy)||1;
p.ax=(dx/d*p.v0-p.vx)/9;
p.ay=(dy/d*p.v0-p.vy)/9;
}
for(let i=0;i<N;i++){
const a=P[i];
for(let j=i+1;j<N;j++){
const b=P[j],dx=b.x-a.x,dy=b.y-a.y,d2=dx*dx+dy*dy;
if(d2<RNG*RNG&&d2>1e-6){
const d=Math.sqrt(d2),f=0.085*(RNG-d)+0.07*Math.exp((2*R-d)/2.5);
const nx=dx/d*f,ny=dy/d*f;
a.ax-=nx;a.ay-=ny;b.ax+=nx;b.ay+=ny;
}
}
}
let sum=0,act=0;
for(const p of P){
p.vx+=p.ax;p.vy+=p.ay;
const s=Math.hypot(p.vx,p.vy),m=p.v0*1.4;
if(s>m){p.vx*=m/s;p.vy*=m/s;}
p.x+=p.vx;p.y+=p.vy;
walls(p);
if(!p.left&&p.x>X2+T+26){p.left=true;left++;}
if(!p.left){sum+=Math.hypot(p.vx,p.vy);act++;}
}
avgS=avgS*0.92+(act?sum/act*60:0)*0.08;
if(left===N&&P.every(p=>p.x>W+150))init();
}
function draw(){
ctx.fillStyle='#0a0d12';ctx.fillRect(0,0,W,H);
ctx.fillStyle='#161c26';ctx.fillRect(X1,Y1,X2-X1,Y2-Y1);
ctx.fillStyle='rgba(80,230,170,.13)';ctx.fillRect(X2,DY1,T+70,DY2-DY1);
ctx.fillStyle='#3d4a5c';
ctx.fillRect(X1-T,Y1-T,X2-X1+2*T,T);
ctx.fillRect(X1-T,Y2,X2-X1+2*T,T);
ctx.fillRect(X1-T,Y1,T,Y2-Y1);
ctx.fillRect(X2,Y1,T,DY1-Y1);
ctx.fillRect(X2,DY2,T,Y2-DY2);
ctx.fillStyle='#50e6aa';ctx.fillRect(X2-1,DY1,2,GAP);
for(const p of P){
if(p.x>W+40)continue;
ctx.beginPath();ctx.arc(p.x,p.y,R,0,6.2832);
ctx.fillStyle='hsl('+p.h.toFixed(0)+' 68% '+(p.left?42:63)+'%)';ctx.fill();
ctx.strokeStyle='rgba(6,10,16,.75)';ctx.lineWidth=1.2;ctx.stroke();
ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(p.x+p.vx*5,p.y+p.vy*5);
ctx.strokeStyle='rgba(255,255,255,.35)';ctx.stroke();
}
ctx.fillStyle='#e8eef7';ctx.font='600 17px system-ui,sans-serif';
ctx.fillText('Evacuated '+left+' / '+N,X1-10,36);
ctx.fillText('Avg speed '+avgS.toFixed(1)+' px/s',270,36);
ctx.fillText('Time '+(frames/60).toFixed(1)+' s',530,36);
ctx.fillStyle='#8fa2b8';ctx.font='13px system-ui,sans-serif';
ctx.fillText('Continuous social-force crowd — steering to the exit plus neighbour repulsion builds the arch',X1-10,H-14);
}
function loop(){step();draw();requestAnimationFrame(loop);}
init();loop();
</script>
</body>
</html>