27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
You write crowd simulations where the behavior emerges from rules each person follows.
Build me a single HTML page: a section of a football stadium emptying through one exit gate.
#urgency=0.8&gate=1.2&column=1 starts simulation at load with those settings: urgency 0.0 to 1.0, gate width in yards, column 0 or 1. Any values work.data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Before the file, two quantitative predictions: whether increasing urgency from 0.3 to 0.9 increases or decreases evacuation time at gate=1.2 and column=0, with the predicted difference in seconds; and whether enabling the column (column=1) increases or decreases evacuation time at urgency=0.9 and gate=1.2, with the predicted difference in seconds.
The single fenced html block.
Where your crowd behaves like a real one and where it does not. Name anything you special-cased: a nudge that breaks a jam, a speed cap at the gate, an allowance for overlap. If your page never jams at the gate, say so.
These numbers were written before running anything. They are not measured.
Urgency 0.3 → 0.9, at gate=1.2, column=0: evacuation time increases, by about +40 s. My estimate is roughly 125 s at urgency 0.3 and roughly 165 s at urgency 0.9, for 220 people through a 1.10 m opening. The reason is the "faster-is-slower" effect. At high urgency, people press into each other at the opening. The sideways friction between bodies forms arches that stall for a few seconds and then burst. At urgency 0.3 people keep a gap, rarely touch at the gate, and flow steadily.
Column on, at urgency=0.9, gate=1.2: evacuation time decreases, by about −20 s (roughly 165 s → 145 s). The column takes the crowd's push off the people standing in the opening, so arches there form less often. I'm less confident in this one than in the first. The column also adds a second bottleneck. In real human trials the effect is weaker and less consistent than in simulations.
The page has a "Check predictions" button. It runs these three setups headless, three seeds each, and reports the measured differences. Trust that over the numbers above.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Section 114 · Gate C — stand evacuation</title>
<style>
:root{--bg:#0e1217;--panel:#161b22;--ink:#e6e9ee;--dim:#8b95a3;--acc:#ffcf3f;--line:#28303a}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);font:13px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
#app{display:flex;height:100vh}
#stage{flex:1;position:relative;min-width:0}
#view{position:absolute;inset:0;width:100%;height:100%;display:block}
#panel{width:330px;flex:none;background:var(--panel);border-left:1px solid var(--line);overflow-y:auto;padding:14px 16px}
h1{font-size:15px;margin:0 0 2px;letter-spacing:.05em;text-transform:uppercase}
h2{font-size:12px;margin:16px 0 4px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim)}
.sub{color:var(--dim);font-size:12px;margin-bottom:10px}
.ctl{margin:10px 0}
.ctl label{display:flex;justify-content:space-between;color:var(--dim);font-size:12px;margin-bottom:4px}
.ctl label b{color:var(--ink);font-weight:600}
input[type=range]{width:100%;accent-color:var(--acc)}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
button,select,input[type=number]{background:#222a34;color:var(--ink);border:1px solid #343d49;border-radius:6px;padding:6px 10px;font:inherit}
button{cursor:pointer}
button:hover{border-color:var(--acc)}
button.primary{background:var(--acc);color:#1a1a1a;border-color:var(--acc);font-weight:600}
button:disabled{opacity:.5;cursor:default}
input[type=number]{width:84px}
.metrics{display:grid;grid-template-columns:1fr auto;gap:4px 10px;margin:12px 0;padding:10px;background:#10141a;border-radius:8px;border:1px solid var(--line)}
.metrics div:nth-child(odd){color:var(--dim)}
.metrics div:nth-child(even){text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
.metrics .big{font-size:20px}
#chart{width:100%;height:120px;display:block;background:#10141a;border-radius:8px;border:1px solid var(--line)}
.hash{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;color:var(--dim);word-break:break-all;margin-top:4px}
table{width:100%;border-collapse:collapse;font-size:12px;margin-top:6px}
td,th{padding:3px 4px;border-bottom:1px solid var(--line);text-align:left;font-variant-numeric:tabular-nums}
th{color:var(--dim);font-weight:500}
.legend{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--dim);margin-top:4px}
.legend .bar{flex:1;height:8px;border-radius:4px;background:linear-gradient(90deg,#3a7bd5,#f5c542,#e8402f,#ff2bd6)}
.note{font-size:11px;color:var(--dim)}
@media (max-width:820px){#app{flex-direction:column;height:auto}#stage{height:72vh}#panel{width:auto;border-left:0}}
</style>
</head>
<body>
<div id="app">
<div id="stage"><canvas id="view"></canvas></div>
<aside id="panel">
<h1>Section 114 · Gate C</h1>
<div class="sub">Social-force crowd, one exit. Every spectator runs the same rules.</div>
<div class="ctl">
<label>Urgency <b id="urgLbl">0.50</b></label>
<input id="urg" type="range" min="0" max="1" step="0.01" value="0.5">
<div class="note" id="urgInfo"></div>
</div>
<div class="ctl">
<label>Gate width (yards) <b id="gateLbl">1.20 yd</b></label>
<div class="row"><input id="gateR" type="range" min="0.5" max="4" step="0.05" value="1.2" style="flex:1"><input id="gateN" type="number" step="0.05" min="0.1" value="1.2"></div>
</div>
<div class="ctl row">
<label style="display:flex;gap:6px;align-items:center;color:var(--ink);margin:0"><input id="col" type="checkbox"> Column in front of gate</label>
</div>
<div class="ctl row">
<button id="reset" class="primary">Reset / refill stands</button>
<button id="pause">Pause</button>
<select id="speed"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="8">8×</option></select>
</div>
<div class="ctl row">
<label style="margin:0;color:var(--dim)">View</label>
<select id="viewMode"><option value="kits">Kits + crush halo</option><option value="pressure">Contact pressure</option></select>
</div>
<div class="legend"><span>0</span><span class="bar"></span><span>3 kN/m</span></div>
<div class="note">Gate width and column restart the run. Urgency applies live.</div>
<div class="hash" id="hashTxt"></div>
<div class="metrics">
<div>Clock</div><div class="big" id="mT">0.0 s</div>
<div>Section empty after</div><div class="big" id="mEvac">—</div>
<div>Still inside</div><div id="mIn">—</div>
<div>Through the gate</div><div id="mOut">—</div>
<div>Outflow, last 10 s</div><div id="mRate">—</div>
<div>Outflow, mean (first→last exit)</div><div id="mMean">—</div>
<div>Longest gap between exits</div><div id="mGap">—</div>
<div>Stalls ≥ 2 s</div><div id="mStall">—</div>
<div>Max body compression now / ever</div><div id="mPen">—</div>
<div>Overlap-cap corrections</div><div id="mProj">—</div>
</div>
<canvas id="chart"></canvas>
<div class="note">Bars: people/min per 5 s bin. Line: cumulative exits.</div>
<h2>Check the predictions</h2>
<div class="note">Runs headless: gate 1.2 yd; (A) urgency 0.3, (B) urgency 0.9, (C) urgency 0.9 + column. 3 seeds each.</div>
<div class="row" style="margin-top:6px"><button id="expBtn">Check predictions</button><span class="note" id="expProg"></span></div>
<div id="expOut"></div>
</aside>
</div>
<script>
(function(){
'use strict';
// ---------- world constants (metres, seconds, kg, newtons) ----------
const YD=0.9144;
const W=24, WALL_Y=16, WALL_T=0.5, OUT_Y=WALL_Y+WALL_T, H=21;
const STAND_Y0=0.5, STAND_Y1=10.1, ROW=0.8, AISLES=[6,12,18];
const DT=0.004, MASS=80, TAU=0.5;
const A_SOC=2000, A_WALL=2000, B_WALL=0.08, K_BODY=1.2e5, KAPPA0=2.4e5, LAMBDA=0.4;
const CUT=1.1, CUT2=CUT*CUT, WALL_CUT=0.9;
const MAX_PEN=0.03, VMAX=8;
const COL_R=0.35, COL_DIST=1.4;
const CS=0.1, GC=1.1;
const T_LIMIT=1200;
function urgencyParams(u){
return { v0:0.6+3.4*u, B:0.08+0.08*(1-u), kappa:KAPPA0*(0.5+0.5*u) };
}
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
const KITS=['#1f5fd1','#1f5fd1','#1f5fd1','#f1f1ee','#173f96','#1f5fd1','#e9e4d4','#2b2f36','#c8372d','#3d8a4b','#d9a62e','#7a4bb0','#1f5fd1','#f1f1ee'];
const SKIN=['#f1c9a5','#d9a47a','#b07a52','#7d5236','#5a3a26','#e8b994'];
// ---------- simulation ----------
class Sim{
constructor(cfg){
this.cfg=Object.assign({},cfg);
this.u=cfg.u;
this.buildGeometry();
this.buildField();
this.spawn();
}
buildGeometry(){
const g=Math.min(W-0.4,Math.max(0.05,this.cfg.gateYd*YD));
this.gateW=g; this.gateL=W/2-g/2; this.gateR=W/2+g/2;
const R=[[-3,-3,W+3,0],[-3,-3,0,OUT_Y],[W,-3,W+3,OUT_Y],[-3,WALL_Y,this.gateL,OUT_Y],[this.gateR,WALL_Y,W+3,OUT_Y]];
this.rects=R;
this.rectArr=new Float64Array(R.length*4);
R.forEach((q,k)=>{for(let m=0;m<4;m++)this.rectArr[4*k+m]=q[m];});
this.col=this.cfg.column?{x:W/2,y:WALL_Y-COL_DIST,r:COL_R}:null;
}
solidAt(x,y,m){
for(const q of this.rects){if(x>q[0]-m&&x<q[2]+m&&y>q[1]-m&&y<q[3]+m)return true;}
if(this.col){const dx=x-this.col.x,dy=y-this.col.y;if(dx*dx+dy*dy<(this.col.r+m)*(this.col.r+m))return true;}
return false;
}
buildField(){
const nx=Math.ceil(W/CS), ny=Math.ceil(H/CS), N=nx*ny;
this.fnx=nx; this.fny=ny;
const blk=new Uint8Array(N), dist=new Float32Array(N).fill(Infinity);
for(let j=0;j<ny;j++)for(let i=0;i<nx;i++){if(this.solidAt((i+.5)*CS,(j+.5)*CS,0.05))blk[j*nx+i]=1;}
const hk=[],hv=[];
const push=(k,v)=>{hk.push(k);hv.push(v);let i=hk.length-1;while(i>0){const p=(i-1)>>1;if(hk[p]<=hk[i])break;let t=hk[p];hk[p]=hk[i];hk[i]=t;t=hv[p];hv[p]=hv[i];hv[i]=t;i=p;}};
let popK=0;
const pop=()=>{const v=hv[0];popK=hk[0];const lk=hk.pop(),lv=hv.pop();if(hk.length){hk[0]=lk;hv[0]=lv;let i=0;const L=hk.length;for(;;){const l=2*i+1,r=l+1;let m=i;if(l<L&&hk[l]<hk[m])m=l;if(r<L&&hk[r]<hk[m])m=r;if(m===i)break;let t=hk[m];hk[m]=hk[i];hk[i]=t;t=hv[m];hv[m]=hv[i];hv[i]=t;i=m;}}return v;};
for(let j=0;j<ny;j++){if((j+.5)*CS>H-0.6){for(let i=0;i<nx;i++){const k=j*nx+i;if(!blk[k]){dist[k]=0;push(0,k);}}}}
const D2=CS*Math.SQRT2;
while(hk.length){
const k=pop(),d=popK; if(d>dist[k])continue;
const i=k%nx,j=(k/nx)|0;
for(let dj=-1;dj<=1;dj++)for(let di=-1;di<=1;di++){
if(!di&&!dj)continue;
const ii=i+di,jj=j+dj; if(ii<0||jj<0||ii>=nx||jj>=ny)continue;
const kk=jj*nx+ii; if(blk[kk])continue;
if(di&&dj&&(blk[j*nx+ii]||blk[jj*nx+i]))continue;
const nd=d+(di&&dj?D2:CS);
if(nd<dist[kk]){dist[kk]=nd;push(nd,kk);}
}
}
const dX=new Float32Array(N), dY=new Float32Array(N);
const val=(i,j,f0)=>{if(i<0||j<0||i>=nx||j>=ny)return f0+CS;const k=j*nx+i;return (blk[k]||!isFinite(dist[k]))?f0+CS:dist[k];};
for(let j=0;j<ny;j++)for(let i=0;i<nx;i++){
const k=j*nx+i; if(blk[k]||!isFinite(dist[k]))continue;
const f0=dist[k];
let gx=(val(i+1,j,f0)-val(i-1,j,f0)), gy=(val(i,j+1,f0)-val(i,j-1,f0));
let L=Math.hypot(gx,gy);
if(L<1e-6){
const cy=(j+.5)*CS,cx=(i+.5)*CS;
if(cy>OUT_Y){gx=0;gy=-1;}else{gx=-(W/2-cx);gy=-(OUT_Y-cy);}
L=Math.hypot(gx,gy)||1;
}
dX[k]=-gx/L; dY[k]=-gy/L;
}
this.blk=blk; this.dist=dist; this.dirX=dX; this.dirY=dY;
}
dirAt(x,y){
const nx=this.fnx,ny=this.fny;
const gx=x/CS-0.5, gy=y/CS-0.5;
const i0=Math.floor(gx), j0=Math.floor(gy), fx=gx-i0, fy=gy-j0;
let sx=0,sy=0;
for(let b=0;b<2;b++){const j=j0+b;if(j<0||j>=ny)continue;const wy=b?fy:1-fy;
for(let a=0;a<2;a++){const i=i0+a;if(i<0||i>=nx)continue;const w=(a?fx:1-fx)*wy;const k=j*nx+i;sx+=w*this.dirX[k];sy+=w*this.dirY[k];}}
let L=Math.hypot(sx,sy);
if(L<1e-6){sx=W/2-x;sy=OUT_Y+1-y;L=Math.hypot(sx,sy)||1;}
this._dx=sx/L; this._dy=sy/L;
}
spawn(){
const want=this.cfg.n, rng=mulberry32((this.cfg.seed|0)*9973+17);
const X=[],Y=[],RR=[];
let ymax=STAND_Y1;
while(X.length<want){
let ok=false;
for(let tries=0;tries<3000&&!ok;tries++){
const r=0.25+0.06*rng();
const x=0.4+r+rng()*(W-0.8-2*r), y=STAND_Y0+r+rng()*(ymax-STAND_Y0-2*r);
let clash=false;
for(let k=0;k<X.length;k++){const dx=x-X[k],dy=y-Y[k],m=r+RR[k]+0.06;if(dx*dx+dy*dy<m*m){clash=true;break;}}
if(!clash){X.push(x);Y.push(y);RR.push(r);ok=true;}
}
if(!ok){ if(ymax>=WALL_Y-2.5)break; ymax=Math.min(WALL_Y-2.5,ymax+0.5); }
}
const n=X.length; this.n=n;
const F=()=>new Float64Array(n);
this.x=Float64Array.from(X); this.y=Float64Array.from(Y); this.r=Float64Array.from(RR);
this.vx=F();this.vy=F();this.fx=F();this.fy=F();this.ex=F();this.ey=F();this.press=F();
this.v0f=F();this.delay=F();this.hd=F();
this.kit=new Array(n); this.skin=new Array(n);
for(let i=0;i<n;i++){
this.v0f[i]=0.9+0.2*rng(); this.delay[i]=2*rng(); this.hd[i]=Math.PI/2;
this.kit[i]=KITS[(rng()*KITS.length)|0]; this.skin[i]=SKIN[(rng()*SKIN.length)|0];
}
this.alive=new Uint8Array(n).fill(1); this.exited=new Uint8Array(n);
this.cellOf=new Int32Array(n); this.next=new Int32Array(n);
this.gnx=Math.ceil(W/GC)+1; this.gny=Math.ceil(H/GC)+1;
this.head=new Int32Array(this.gnx*this.gny);
this.t=0; this.exitTimes=[]; this.exitedCount=0; this.aliveCount=n;
this.tEvac=null; this.maxPenNow=0; this.maxPenEver=0; this.projCount=0; this.capCount=0;
this.longestGap=0; this.stalls=0;
}
done(){return this.exitedCount===this.n||this.t>=T_LIMIT;}
finished(){return this.aliveCount===0||this.t>=T_LIMIT;}
step(){
const n=this.n,x=this.x,y=this.y,vx=this.vx,vy=this.vy,fx=this.fx,fy=this.fy,r=this.r,
alive=this.alive,ex=this.ex,ey=this.ey,press=this.press,head=this.head,next=this.next,cellOf=this.cellOf;
const gnx=this.gnx,gny=this.gny;
const P=urgencyParams(this.u),B=P.B,kap=P.kappa,v0=P.v0;
const t=this.t;
head.fill(-1);
for(let i=0;i<n;i++){
if(!alive[i])continue;
let cx=(x[i]/GC)|0,cy=(y[i]/GC)|0;
if(cx<0)cx=0;else if(cx>=gnx)cx=gnx-1;
if(cy<0)cy=0;else if(cy>=gny)cy=gny-1;
const c=cy*gnx+cx; cellOf[i]=c; next[i]=head[c]; head[c]=i;
if(t<this.delay[i]){ex[i]=0;ey[i]=0;fx[i]=-MASS*vx[i]/TAU;fy[i]=-MASS*vy[i]/TAU;}
else{
this.dirAt(x[i],y[i]);
const dx=this._dx,dy=this._dy; ex[i]=dx; ey[i]=dy;
const vd=v0*this.v0f[i];
fx[i]=MASS*(vd*dx-vx[i])/TAU; fy[i]=MASS*(vd*dy-vy[i])/TAU;
}
press[i]=0;
}
const RA=this.rectArr,NR=RA.length/4,col=this.col;
let maxPen=0;
for(let i=0;i<n;i++){
if(!alive[i])continue;
const c=cellOf[i],cx=c%gnx,cy=(c/gnx)|0;
const xi=x[i],yi=y[i],ri=r[i],vxi=vx[i],vyi=vy[i],exi=ex[i],eyi=ey[i];
let fxi=0,fyi=0,pi=0;
for(let oy=-1;oy<=1;oy++){const yy=cy+oy;if(yy<0||yy>=gny)continue;
for(let ox=-1;ox<=1;ox++){const xx=cx+ox;if(xx<0||xx>=gnx)continue;
for(let j=head[yy*gnx+xx];j!==-1;j=next[j]){
if(j<=i)continue;
const dx=xi-x[j],dy=yi-y[j],d2=dx*dx+dy*dy;
if(d2>CUT2)continue;
const d=Math.sqrt(d2)||1e-6,nx=dx/d,ny=dy/d,rij=ri+r[j],gap=rij-d;
let s=A_SOC*(Math.exp(Math.min(gap,0.5)/B)-Math.exp((rij-CUT)/B)); if(s<0)s=0;
const ci=-(nx*exi+ny*eyi), wi=LAMBDA+(1-LAMBDA)*0.5*(1+ci);
const cj=nx*ex[j]+ny*ey[j], wj=LAMBDA+(1-LAMBDA)*0.5*(1+cj);
fxi+=s*wi*nx; fyi+=s*wi*ny; fx[j]-=s*wj*nx; fy[j]-=s*wj*ny;
if(gap>0){
const tx=-ny,ty=nx,dvt=(vx[j]-vxi)*tx+(vy[j]-vyi)*ty;
const fn=K_BODY*gap,ft=kap*gap*dvt;
const Fx=fn*nx+ft*tx,Fy=fn*ny+ft*ty;
fxi+=Fx;fyi+=Fy;fx[j]-=Fx;fy[j]-=Fy;
pi+=fn;press[j]+=fn;
if(gap>maxPen)maxPen=gap;
}
}
}
}
for(let q=0;q<NR;q++){
const x0=RA[4*q],y0=RA[4*q+1],x1=RA[4*q+2],y1=RA[4*q+3];
const px=xi<x0?x0:(xi>x1?x1:xi),py=yi<y0?y0:(yi>y1?y1:yi);
const dx=xi-px,dy=yi-py,d2=dx*dx+dy*dy;
if(d2>WALL_CUT*WALL_CUT)continue;
let d,nx,ny;
if(d2<1e-12){
const dl=xi-x0,dr=x1-xi,du=yi-y0,dd=y1-yi;let m=dl;nx=-1;ny=0;
if(dr<m){m=dr;nx=1;ny=0;} if(du<m){m=du;nx=0;ny=-1;} if(dd<m){m=dd;nx=0;ny=1;}
d=-m;
}else{d=Math.sqrt(d2);nx=dx/d;ny=dy/d;}
const gap=ri-d, s=A_WALL*Math.exp(Math.min(gap,0.5)/B_WALL);
fxi+=s*nx;fyi+=s*ny;
if(gap>0){const tx=-ny,ty=nx,vt=vxi*tx+vyi*ty,fn=K_BODY*gap,ft=-kap*gap*vt;
fxi+=fn*nx+ft*tx;fyi+=fn*ny+ft*ty;pi+=fn;if(gap>maxPen)maxPen=gap;}
}
if(col){
const dx=xi-col.x,dy=yi-col.y,dc=Math.sqrt(dx*dx+dy*dy)||1e-6,d=dc-col.r;
if(d<WALL_CUT){
const nx=dx/dc,ny=dy/dc,gap=ri-d,s=A_WALL*Math.exp(Math.min(gap,0.5)/B_WALL);
fxi+=s*nx;fyi+=s*ny;
if(gap>0){const tx=-ny,ty=nx,vt=vxi*tx+vyi*ty,fn=K_BODY*gap,ft=-kap*gap*vt;
fxi+=fn*nx+ft*tx;fyi+=fn*ny+ft*ty;pi+=fn;if(gap>maxPen)maxPen=gap;}
}
}
fx[i]+=fxi;fy[i]+=fyi;press[i]+=pi;
}
// integrate (semi-implicit Euler)
const k=DT/MASS;
for(let i=0;i<n;i++){
if(!alive[i])continue;
let a=vx[i]+fx[i]*k,b=vy[i]+fy[i]*k;
const sp=a*a+b*b;
if(sp>VMAX*VMAX){const m=VMAX/Math.sqrt(sp);a*=m;b*=m;this.capCount++;}
vx[i]=a;vy[i]=b;
x[i]+=a*DT;y[i]+=b*DT;
if(x[i]<0.05)x[i]=0.05;else if(x[i]>W-0.05)x[i]=W-0.05;
}
// hard overlap cap: bodies may compress up to MAX_PEN, never more
for(let i=0;i<n;i++){
if(!alive[i])continue;
const c=cellOf[i],cx=c%gnx,cy=(c/gnx)|0;
for(let oy=-1;oy<=1;oy++){const yy=cy+oy;if(yy<0||yy>=gny)continue;
for(let ox=-1;ox<=1;ox++){const xx=cx+ox;if(xx<0||xx>=gnx)continue;
for(let j=head[yy*gnx+xx];j!==-1;j=next[j]){
if(j<=i)continue;
const dx=x[i]-x[j],dy=y[i]-y[j],d2=dx*dx+dy*dy,rij=r[i]+r[j]-MAX_PEN;
if(d2>=rij*rij)continue;
const d=Math.sqrt(d2)||1e-6,corr=(rij-d)*0.5,nx=dx/d,ny=dy/d;
x[i]+=nx*corr;y[i]+=ny*corr;x[j]-=nx*corr;y[j]-=ny*corr;this.projCount++;
}
}
}
for(let q=0;q<NR;q++){
const x0=RA[4*q],y0=RA[4*q+1],x1=RA[4*q+2],y1=RA[4*q+3],xi=x[i],yi=y[i];
const px=xi<x0?x0:(xi>x1?x1:xi),py=yi<y0?y0:(yi>y1?y1:yi);
const dx=xi-px,dy=yi-py,d2=dx*dx+dy*dy,lim=r[i]-MAX_PEN;
if(d2>=lim*lim||d2<1e-12)continue;
const d=Math.sqrt(d2),c2=lim-d;x[i]+=dx/d*c2;y[i]+=dy/d*c2;this.projCount++;
}
if(col){
const dx=x[i]-col.x,dy=y[i]-col.y,dc=Math.sqrt(dx*dx+dy*dy)||1e-6,lim=r[i]-MAX_PEN+col.r;
if(dc<lim){const c2=lim-dc;x[i]+=dx/dc*c2;y[i]+=dy/dc*c2;this.projCount++;}
}
}
this.t=t+DT;
for(let i=0;i<n;i++){
if(!alive[i])continue;
if(!this.exited[i]&&y[i]>OUT_Y){
this.exited[i]=1;this.exitedCount++;
const et=this.t, L=this.exitTimes.length;
if(L){const g=et-this.exitTimes[L-1];if(g>this.longestGap)this.longestGap=g;if(g>=2)this.stalls++;}
this.exitTimes.push(et);
if(this.exitedCount===this.n)this.tEvac=et;
}
if(y[i]>H-0.7){alive[i]=0;this.aliveCount--;}
}
this.maxPenNow=maxPen; if(maxPen>this.maxPenEver)this.maxPenEver=maxPen;
}
}
// ---------- config / hash ----------
function parseHash(){
const p=new URLSearchParams((location.hash||'').replace(/^#/,''));
const num=(k,d)=>{const v=parseFloat(p.get(k));return isFinite(v)?v:d;};
const u=Math.min(1,Math.max(0,num('urgency',0.5)));
const g=Math.min(30,Math.max(0.1,num('gate',1.2)));
const cs=p.get('column');
let col=false;
if(cs!==null){const v=parseFloat(cs);col=cs==='true'||(isFinite(v)&&v!==0);}
const n=Math.round(Math.min(500,Math.max(200,num('n',220))));
const seed=Math.round(num('seed',1));
return {u,gateYd:g,column:col,n,seed};
}
// ---------- UI ----------
const $=id=>document.getElementById(id);
const view=$('view'),ctx=view.getContext('2d');
const chart=$('chart'),cctx=chart.getContext('2d');
let cfg=parseHash();
let sim=new Sim(cfg);
let running=true,speed=1,acc=0,last=performance.now(),bgKey='',bg=null;
let L={s:1,ox:0,oy:0,cw:1,ch:1,dpr:1};
let viewMode='kits';
function syncUI(){
$('urg').value=cfg.u; $('urgLbl').textContent=cfg.u.toFixed(2);
const P=urgencyParams(cfg.u);
$('urgInfo').textContent=`desired speed ≈ ${P.v0.toFixed(2)} m/s · max push ≈ ${(MASS*P.v0/TAU|0)} N · personal-space range ${(P.B*100).toFixed(1)} cm · sliding friction ×${(P.kappa/KAPPA0).toFixed(2)}`;
$('gateR').value=Math.min(4,Math.max(0.5,cfg.gateYd)); $('gateN').value=+cfg.gateYd.toFixed(3);
$('gateLbl').textContent=`${cfg.gateYd.toFixed(2)} yd = ${(Math.min(W-0.4,cfg.gateYd*YD)).toFixed(2)} m`;
$('col').checked=cfg.column;
$('hashTxt').textContent=`#urgency=${+cfg.u.toFixed(2)}&gate=${+cfg.gateYd.toFixed(3)}&column=${cfg.column?1:0}`;
}
function restart(){sim=new Sim(cfg);acc=0;bgKey='';running=true;$('pause').textContent='Pause';}
$('urg').addEventListener('input',e=>{cfg.u=+e.target.value;sim.u=cfg.u;syncUI();});
$('gateR').addEventListener('input',e=>{cfg.gateYd=+e.target.value;syncUI();restart();});
$('gateN').addEventListener('change',e=>{const v=parseFloat(e.target.value);if(isFinite(v)&&v>0){cfg.gateYd=Math.min(30,v);}syncUI();restart();});
$('col').addEventListener('change',e=>{cfg.column=e.target.checked;syncUI();restart();});
$('reset').addEventListener('click',()=>restart());
$('pause').addEventListener('click',()=>{running=!running;$('pause').textContent=running?'Pause':'Resume';});
$('speed').addEventListener('change',e=>{speed=+e.target.value;});
$('viewMode').addEventListener('change',e=>{viewMode=e.target.value;});
window.addEventListener('hashchange',()=>{cfg=parseHash();syncUI();restart();});
// ---------- layout / art ----------
const VX0=-0.6,VX1=W+0.6,VY0=-2.4,VY1=H+0.1;
function layout(){
const rect=view.getBoundingClientRect(),dpr=Math.min(2,window.devicePixelRatio||1);
const cw=Math.max(50,rect.width),ch=Math.max(50,rect.height);
view.width=Math.round(cw*dpr);view.height=Math.round(ch*dpr);
const s=Math.min(cw/(VX1-VX0),ch/(VY1-VY0));
L={s,dpr,cw,ch,ox:(cw-(VX1-VX0)*s)/2-VX0*s,oy:(ch-(VY1-VY0)*s)/2-VY0*s};
const cr=chart.getBoundingClientRect();chart.width=Math.round(cr.width*dpr);chart.height=Math.round(cr.height*dpr);
bgKey='';
}
window.addEventListener('resize',layout);
function paintBg(){
bg=document.createElement('canvas');bg.width=view.width;bg.height=view.height;
const g=bg.getContext('2d'),s=L.s,d=L.dpr;
g.fillStyle='#0b0e12';g.fillRect(0,0,bg.width,bg.height);
g.setTransform(s*d,0,0,s*d,L.ox*d,L.oy*d);
const px=1/s;
// pitch
g.fillStyle='#2f7d3a';g.fillRect(VX0,VY0,VX1-VX0,-0.25-VY0);
for(let x=VX0;x<VX1;x+=2){g.fillStyle='rgba(255,255,255,0.05)';g.fillRect(x,VY0,1,-0.25-VY0);}
g.fillStyle='#f4f4f0';g.fillRect(VX0,-1.0,VX1-VX0,0.08);
// ad boards
const ads=['#c8372d','#1d1d1d','#f2c230','#1f5fd1'];
for(let i=0,x=VX0;x<VX1;i++,x+=3){g.fillStyle=ads[i%4];g.fillRect(x,-0.32,3,0.3);}
g.fillStyle='rgba(255,255,255,0.25)';g.fillRect(VX0,-0.04,VX1-VX0,0.04);
// stands: terrace bands
g.fillStyle='#4a515c';g.fillRect(0,0,W,STAND_Y1+0.3);
let rowIdx=0;
for(let yy=STAND_Y0;yy+ROW<=STAND_Y1+0.01;yy+=ROW,rowIdx++){
g.fillStyle=rowIdx%2?'#555d69':'#5b6370';g.fillRect(0,yy,W,ROW);
g.fillStyle='rgba(255,255,255,0.10)';g.fillRect(0,yy,W,0.03);
g.fillStyle='rgba(0,0,0,0.25)';g.fillRect(0,yy+ROW-0.05,W,0.05);
const seatCol=(rowIdx%4===3)?'#e0b23a':'#1f4fa8',back=(rowIdx%4===3)?'#a8821f':'#163a7c';
for(let sx=0.3;sx+0.42<W-0.2;sx+=0.5){
let inA=false;for(const a of AISLES){if(Math.abs(sx+0.2-a)<0.75)inA=true;}
if(inA)continue;
g.fillStyle=seatCol;g.fillRect(sx+0.03,yy+0.28,0.36,0.3);
g.fillStyle=back;g.fillRect(sx,yy+ROW-0.22,0.42,0.12);
}
}
for(const a of AISLES){
g.fillStyle='#6c7480';g.fillRect(a-0.5,0,1.0,STAND_Y1+0.3);
g.fillStyle='rgba(0,0,0,0.22)';
for(let yy=0.2;yy<STAND_Y1+0.3;yy+=0.4)g.fillRect(a-0.5,yy,1.0,0.025);
g.fillStyle='#f2c230';g.fillRect(a-0.5,0,0.04,STAND_Y1+0.3);g.fillRect(a+0.46,0,0.04,STAND_Y1+0.3);
}
// concourse
g.fillStyle='#343a43';g.fillRect(0,STAND_Y1+0.3,W,WALL_Y-STAND_Y1-0.3);
g.fillStyle='rgba(242,194,48,0.85)';g.fillRect(0,STAND_Y1+0.3,W,0.06);
g.strokeStyle='rgba(255,255,255,0.04)';g.lineWidth=px;
for(let x=0;x<=W;x+=1.5){g.beginPath();g.moveTo(x,STAND_Y1+0.36);g.lineTo(x,WALL_Y);g.stroke();}
// painted chevrons following the route field
g.strokeStyle='rgba(242,194,48,0.22)';g.lineWidth=0.08;g.lineCap='round';g.lineJoin='round';
for(let yy=STAND_Y1+1.2;yy<WALL_Y-0.6;yy+=1.6)for(let xx=1.5;xx<W-1;xx+=2.2){
if(sim.solidAt(xx,yy,0.6))continue;
sim.dirAt(xx,yy);const dx=sim._dx,dy=sim._dy,nx=-dy,ny=dx;
g.beginPath();g.moveTo(xx-dx*0.25+nx*0.3,yy-dy*0.25+ny*0.3);g.lineTo(xx+dx*0.15,yy+dy*0.15);g.lineTo(xx-dx*0.25-nx*0.3,yy-dy*0.25-ny*0.3);g.stroke();
}
// outside plaza
g.fillStyle='#6a5f55';g.fillRect(VX0,OUT_Y,VX1-VX0,H-0.6-OUT_Y);
g.strokeStyle='rgba(0,0,0,0.18)';g.lineWidth=px;
let br=0;
for(let yy=OUT_Y;yy<H-0.6;yy+=0.3,br++){g.beginPath();g.moveTo(VX0,yy);g.lineTo(VX1,yy);g.stroke();
for(let xx=VX0+(br%2?0.3:0);xx<VX1;xx+=0.6){g.beginPath();g.moveTo(xx,yy);g.lineTo(xx,yy+0.3);g.stroke();}}
g.fillStyle='#9b9389';g.fillRect(VX0,H-0.6,VX1-VX0,0.12);
g.fillStyle='#24272c';g.fillRect(VX0,H-0.48,VX1-VX0,VY1-H+0.48);
g.fillStyle='#d9d4c4';for(let x=VX0;x<VX1;x+=1.6)g.fillRect(x,H-0.12,0.8,0.05);
// threshold hazard stripes in the opening
g.save();g.beginPath();g.rect(sim.gateL,WALL_Y,sim.gateW,WALL_T);g.clip();
g.fillStyle='#e0b23a';g.fillRect(sim.gateL,WALL_Y,sim.gateW,WALL_T);
g.fillStyle='#1a1a1a';
for(let x=sim.gateL-1;x<sim.gateR+1;x+=0.25){g.beginPath();g.moveTo(x,WALL_Y);g.lineTo(x+0.12,WALL_Y);g.lineTo(x+0.12+WALL_T,OUT_Y);g.lineTo(x+WALL_T,OUT_Y);g.closePath();g.fill();}
g.restore();
// walls
const wallFill=(x0,x1)=>{
if(x1<=x0)return;
g.fillStyle='#8d8f93';g.fillRect(x0,WALL_Y,x1-x0,WALL_T);
g.fillStyle='rgba(0,0,0,0.25)';g.fillRect(x0,OUT_Y-0.07,x1-x0,0.07);
g.fillStyle='rgba(255,255,255,0.18)';g.fillRect(x0,WALL_Y,x1-x0,0.05);
g.strokeStyle='rgba(0,0,0,0.18)';g.lineWidth=px;
for(let x=x0+0.9;x<x1;x+=1.8){g.beginPath();g.moveTo(x,WALL_Y);g.lineTo(x,OUT_Y);g.stroke();}
};
wallFill(VX0,sim.gateL);wallFill(sim.gateR,VX1);
g.fillStyle='#8d8f93';g.fillRect(VX0,-0.02,0.6,OUT_Y);g.fillRect(W,-0.02,0.6,OUT_Y);
// gate posts + folded leaves
g.fillStyle='#c9ced6';
g.fillRect(sim.gateL-0.18,WALL_Y-0.05,0.18,WALL_T+0.1);g.fillRect(sim.gateR,WALL_Y-0.05,0.18,WALL_T+0.1);
g.strokeStyle='#d8dde4';g.lineWidth=0.05;
const leaf=Math.min(1.4,sim.gateW/2);
g.beginPath();g.moveTo(sim.gateL,OUT_Y+0.05);g.lineTo(sim.gateL-leaf,OUT_Y+0.05);g.moveTo(sim.gateR,OUT_Y+0.05);g.lineTo(sim.gateR+leaf,OUT_Y+0.05);g.stroke();
// column
if(sim.col){const c=sim.col;
g.fillStyle='rgba(0,0,0,0.35)';g.beginPath();g.arc(c.x+0.08,c.y+0.1,c.r,0,7);g.fill();
g.fillStyle='#a9abb0';g.beginPath();g.arc(c.x,c.y,c.r,0,7);g.fill();
g.strokeStyle='rgba(0,0,0,0.3)';g.lineWidth=0.03;g.beginPath();g.arc(c.x,c.y,c.r*0.65,0,7);g.stroke();
g.fillStyle='rgba(255,255,255,0.3)';g.beginPath();g.arc(c.x-0.1,c.y-0.1,c.r*0.35,0,7);g.fill();}
// labels in screen space
g.setTransform(d,0,0,d,0,0);
const S=(x,y)=>[L.ox+x*s,L.oy+y*s];
g.textAlign='center';g.textBaseline='middle';
g.font=`700 ${Math.max(9,0.34*s)|0}px system-ui,sans-serif`;g.fillStyle='#22262c';
const lx=sim.gateL-2.4>0.5?sim.gateL-2.4:sim.gateR+2.4;
let [a,b]=S(lx,WALL_Y+WALL_T/2);g.fillText('GATE C ▸',a,b);
g.font=`700 ${Math.max(9,0.45*s)|0}px system-ui,sans-serif`;g.fillStyle='rgba(255,255,255,0.10)';
[a,b]=S(W/2,(STAND_Y1+WALL_Y)/2-0.6);g.fillText('CONCOURSE',a,b);
[a,b]=S(3,-1.6);g.font=`600 ${Math.max(9,0.38*s)|0}px system-ui,sans-serif`;g.fillStyle='rgba(255,255,255,0.55)';g.fillText('PITCH',a,b);
[a,b]=S(W-3,(OUT_Y+H-0.6)/2);g.fillStyle='rgba(255,255,255,0.35)';g.fillText('OUTSIDE',a,b);
// scale bar
const [sx0,sy0]=S(W-5.5,H-0.3);g.fillStyle='#e6e9ee';g.fillRect(sx0,sy0,5*s,2);
g.font=`${Math.max(9,0.3*s)|0}px system-ui,sans-serif`;g.textAlign='left';g.fillText('5 m ≈ 5.5 yd',sx0,sy0-8);
bgKey=key();
}
function key(){return `${view.width}x${view.height}|${sim.gateW}|${!!sim.col}`;}
function heat(p){ // p in N/m
const t=Math.min(1,Math.max(0,p/3000));
const stops=[[58,123,213],[245,197,66],[232,64,47],[255,43,214]];
const f=t*3,i=Math.min(2,Math.floor(f)),u=f-i,A=stops[i],B=stops[i+1];
return `rgb(${A[0]+(B[0]-A[0])*u|0},${A[1]+(B[1]-A[1])*u|0},${A[2]+(B[2]-A[2])*u|0})`;
}
function draw(){
if(bgKey!==key())paintBg();
const d=L.dpr,s=L.s;
ctx.setTransform(1,0,0,1,0,0);ctx.drawImage(bg,0,0);
ctx.setTransform(s*d,0,0,s*d,L.ox*d,L.oy*d);
const n=sim.n,x=sim.x,y=sim.y,r=sim.r,al=sim.alive;
ctx.fillStyle='rgba(0,0,0,0.28)';
for(let i=0;i<n;i++){if(!al[i])continue;ctx.beginPath();ctx.arc(x[i]+0.05,y[i]+0.07,r[i],0,6.2832);ctx.fill();}
for(let i=0;i<n;i++){
if(!al[i])continue;
const sp=Math.hypot(sim.vx[i],sim.vy[i]);
let target=sim.hd[i];
if(sp>0.15)target=Math.atan2(sim.vy[i],sim.vx[i]);else if(sim.ex[i]||sim.ey[i])target=Math.atan2(sim.ey[i],sim.ex[i]);
let dA=target-sim.hd[i];while(dA>Math.PI)dA-=2*Math.PI;while(dA<-Math.PI)dA+=2*Math.PI;
sim.hd[i]+=dA*0.2;
const h=sim.hd[i],ri=r[i],p=sim.press[i]/(2*Math.PI*ri);
ctx.fillStyle=viewMode==='pressure'?heat(p):sim.kit[i];
ctx.beginPath();ctx.ellipse(x[i],y[i],ri,ri*0.92,h+Math.PI/2,0,6.2832);ctx.fill();
ctx.lineWidth=0.025;ctx.strokeStyle='rgba(0,0,0,0.55)';ctx.stroke();
if(viewMode==='kits'&&p>350){
ctx.lineWidth=0.06;ctx.strokeStyle=heat(p);ctx.globalAlpha=Math.min(1,0.35+p/2500);
ctx.beginPath();ctx.arc(x[i],y[i],ri+0.03,0,6.2832);ctx.stroke();ctx.globalAlpha=1;
}
ctx.fillStyle='#2a1d14';ctx.beginPath();ctx.arc(x[i]+Math.cos(h)*ri*0.12,y[i]+Math.sin(h)*ri*0.12,ri*0.46,0,6.2832);ctx.fill();
ctx.fillStyle=sim.skin[i];ctx.beginPath();ctx.arc(x[i]+Math.cos(h)*ri*0.24,y[i]+Math.sin(h)*ri*0.24,ri*0.3,0,6.2832);ctx.fill();
}
// HUD
ctx.setTransform(d,0,0,d,0,0);
ctx.textAlign='left';ctx.textBaseline='top';
ctx.fillStyle='rgba(10,12,16,0.7)';ctx.fillRect(10,10,236,54);
ctx.fillStyle='#ffcf3f';ctx.font='700 22px ui-monospace,Menlo,Consolas,monospace';
ctx.fillText('T+ '+fmtClock(sim.tEvac!=null?sim.tEvac:sim.t),18,15);
ctx.fillStyle='#cfd5dd';ctx.font='12px system-ui,sans-serif';
ctx.fillText(`${sim.n-sim.exitedCount} inside · ${sim.exitedCount} out · urgency ${sim.u.toFixed(2)}`,18,44);
if(sim.tEvac!=null||sim.t>=T_LIMIT){
const msg=sim.tEvac!=null?`Section empty in ${sim.tEvac.toFixed(1)} s`:`Not empty after ${T_LIMIT} s`;
ctx.font='700 20px system-ui,sans-serif';const w=ctx.measureText(msg).width+30;
ctx.fillStyle='rgba(10,12,16,0.82)';ctx.fillRect(L.cw/2-w/2,L.ch*0.38,w,40);
ctx.fillStyle='#ffcf3f';ctx.textAlign='center';ctx.fillText(msg,L.cw/2,L.ch*0.38+10);
}
const minD=2*0.25;
if(sim.gateW<minD){ctx.textAlign='center';ctx.fillStyle='#ff7a6a';ctx.font='600 14px system-ui,sans-serif';
ctx.fillText(`Gate (${sim.gateW.toFixed(2)} m) is narrower than the slimmest body (${minD.toFixed(2)} m): nobody can leave.`,L.cw/2,L.ch-30);}
}
function fmtClock(t){const m=Math.floor(t/60),s=t-m*60;return `${String(m).padStart(2,'0')}:${s.toFixed(1).padStart(4,'0')}`;}
// ---------- metrics ----------
function updateMetrics(){
const S=sim,et=S.exitTimes,n=et.length;
$('mT').textContent=S.t.toFixed(1)+' s';
$('mEvac').textContent=S.tEvac!=null?S.tEvac.toFixed(1)+' s':(S.t>=T_LIMIT?'not empty':'—');
$('mIn').textContent=`${S.n-S.exitedCount} / ${S.n}`;
$('mOut').textContent=String(S.exitedCount);
if(n){
let c=0;for(let k=n-1;k>=0&&et[k]>S.t-10;k--)c++;
$('mRate').textContent=(c*6).toFixed(0)+' /min';
}else $('mRate').textContent='—';
$('mMean').textContent=n>=2?((n-1)*60/(et[n-1]-et[0])).toFixed(1)+' /min':'—';
$('mGap').textContent=n>=2?S.longestGap.toFixed(2)+' s':'—';
$('mStall').textContent=String(S.stalls);
$('mPen').textContent=`${(S.maxPenNow*100).toFixed(1)} / ${(S.maxPenEver*100).toFixed(1)} cm`;
$('mProj').textContent=String(S.projCount)+(S.capCount?` · speed cap ${S.capCount}`:'');
}
function drawChart(){
const d=L.dpr,w=chart.width/d,h=chart.height/d;
cctx.setTransform(d,0,0,d,0,0);cctx.clearRect(0,0,w,h);
const et=sim.exitTimes,T=Math.max(60,Math.ceil((sim.tEvac||sim.t)/30)*30);
const bins=Math.ceil(T/5),cnt=new Array(bins).fill(0);
for(const t of et){const b=Math.min(bins-1,Math.floor(t/5));cnt[b]++;}
let maxR=60;for(const c of cnt)maxR=Math.max(maxR,c*12);
const pl=26,pr=6,pt=6,pb=16,iw=w-pl-pr,ih=h-pt-pb;
cctx.fillStyle='#ffcf3f';
for(let b=0;b<bins;b++){const v=cnt[b]*12,bh=v/maxR*ih;cctx.fillRect(pl+b/bins*iw+0.5,pt+ih-bh,Math.max(1,iw/bins-1),bh);}
cctx.strokeStyle='#6fb6ff';cctx.lineWidth=1.5;cctx.beginPath();cctx.moveTo(pl,pt+ih);
et.forEach((t,k)=>cctx.lineTo(pl+t/T*iw,pt+ih-(k+1)/sim.n*ih));
cctx.stroke();
cctx.fillStyle='#8b95a3';cctx.font='10px system-ui,sans-serif';cctx.textAlign='left';cctx.textBaseline='top';
cctx.fillText(`${maxR|0}/min`,2,pt);cctx.fillText('0',2,pt+ih-8);
cctx.textAlign='right';cctx.fillText(`${T} s`,w-pr,h-12);cctx.textAlign='left';cctx.fillText('0',pl,h-12);
}
// ---------- headless experiment ----------
let exp=null;
$('expBtn').addEventListener('click',()=>{
if(exp)return;
const seeds=[1,2,3],confs=[{k:'A',u:0.3,c:false},{k:'B',u:0.9,c:false},{k:'C',u:0.9,c:true}];
const jobs=[];for(const c of confs)for(const s of seeds)jobs.push({conf:c,seed:s,res:null,sim:null});
exp={jobs,idx:0};running=false;$('pause').textContent='Resume';$('expBtn').disabled=true;
setTimeout(expTick,0);
});
function expTick(){
const E=exp,job=E.jobs[E.idx];
if(!job.sim)job.sim=new Sim({u:job.conf.u,gateYd:1.2,column:job.conf.c,n:cfg.n,seed:job.seed});
const t0=performance.now();
while(performance.now()-t0<35&&!job.sim.done()){for(let k=0;k<100&&!job.sim.done();k++)job.sim.step();}
$('expProg').textContent=`run ${E.idx+1}/${E.jobs.length} · ${job.conf.k} seed ${job.seed} · t=${job.sim.t.toFixed(0)} s`;
if(job.sim.done()){job.res=job.sim.tEvac;job.sim=null;E.idx++;renderExp();}
if(E.idx<E.jobs.length)setTimeout(expTick,0);
else{$('expProg').textContent='done';$('expBtn').disabled=false;exp=null;}
}
function renderExp(){
const E=exp;const groups={A:[],B:[],C:[]};
for(const j of E.jobs)if(j.res!==null||j.sim===null&&E.jobs.indexOf(j)<E.idx)groups[j.conf.k].push(j.res);
const mean=a=>{const v=a.filter(z=>z!=null);return v.length?v.reduce((p,q)=>p+q,0)/v.length:null;};
const f=v=>v==null?'—':v.toFixed(1)+' s';
const lab={A:'A: urgency 0.3',B:'B: urgency 0.9',C:'C: urgency 0.9 + column'};
let h='<table><tr><th>Setup (gate 1.2 yd)</th><th>runs</th><th>mean</th></tr>';
for(const k of ['A','B','C'])h+=`<tr><td>${lab[k]}</td><td>${groups[k].map(z=>z==null?'DNF':z.toFixed(0)).join(', ')}</td><td>${f(mean(groups[k]))}</td></tr>`;
const a=mean(groups.A),b=mean(groups.B),c=mean(groups.C);
h+='</table><table>';
h+=`<tr><td>B − A (urgency 0.3→0.9)</td><td>${a!=null&&b!=null?(b-a>=0?'+':'')+(b-a).toFixed(1)+' s':'—'}</td><td class="note">predicted +40 s</td></tr>`;
h+=`<tr><td>C − B (add column)</td><td>${b!=null&&c!=null?(c-b>=0?'+':'')+(c-b).toFixed(1)+' s':'—'}</td><td class="note">predicted −20 s</td></tr></table>`;
$('expOut').innerHTML=h;
}
// ---------- main loop ----------
function frame(now){
const dtR=Math.min(0.1,(now-last)/1000);last=now;
if(running&&!sim.finished()){
acc+=dtR*speed;
const maxSteps=Math.ceil(0.05*speed/DT);let k=0;
while(acc>=DT&&k<maxSteps){sim.step();acc-=DT;k++;}
if(k>=maxSteps)acc=0;
}
draw();updateMetrics();
if((frame.c=(frame.c||0)+1)%10===0)drawChart();
requestAnimationFrame(frame);
}
syncUI();layout();drawChart();
requestAnimationFrame(t=>{last=t;frame(t);});
})();
</script>
</body>
</html>
The rules (Helbing–Farkas–Vicsek social force, written from scratch). Each person is a disc 0.50–0.62 m across, with a mass of 80 kg. Three forces act on everyone:
Urgency changes four things:
Outflow and the empty timer only count real crossings of the gate's outer face. Each is timestamped in simulation time on a fixed 4 ms step.
Where it behaves like a real crowd:
Where it does not:
Special cases, named:
Caveat on my predictions: I haven't run this here. The direction of the urgency effect is the part I'm fairly confident in. The magnitudes, and especially the column result, could easily come out smaller or with the opposite sign. With my parameters the column might mainly add a second narrow passage rather than relieve pressure. Press Check predictions to measure all three setups.