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.
gate=1.2, column=0: evacuation time decreases. At 0.3 the crowd walks (~2 yd/s), keeps its distance, and the 1.2 yd throat runs near its low-pressure capacity (~1.3 people/s → ~140 s for 220 people including approach). At 0.9 they run (~4.7 yd/s) and body-to-body pressure drives the throat harder (~2–2.5 people/s) despite clogging pulses. Predicted difference ≈ 45 s faster (roughly ~140 s → ~95 s).column=1) at urgency=0.9, gate=1.2: evacuation time increases. The column sits ~4.4 yd in front of the gate, blocks the central approach lane, misaligns the stream, and seeds intermittent arching upstream of the throat. Predicted difference ≈ +15 s (roughly 10–20 s).Both numbers are falsifiable against the on-screen timer: load #urgency=0.3&gate=1.2&column=0, then #urgency=0.9&gate=1.2&column=0, then #urgency=0.9&gate=1.2&column=1, and compare the cleared times.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Egress Study Nº 114 — one gate, one section</title>
<style>
:root{
--bg:#0d1014; --line:#2a303c; --ink:#d8d2c4; --dim:#8d8778; --amber:#ffb03a;
--mono:ui-monospace,"SF Mono","Cascadia Mono",Consolas,"Liberation Mono",monospace;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--bg);color:var(--ink)}
body{font-family:var(--serif);padding:26px 18px 46px;-webkit-font-smoothing:antialiased}
.wrap{max-width:1020px;margin:0 auto}
header{display:flex;justify-content:space-between;align-items:flex-end;gap:26px;flex-wrap:wrap;
padding-bottom:16px;border-bottom:1px solid var(--line);margin-bottom:14px}
h1{font-size:31px;font-weight:600;letter-spacing:.01em}
h1 em{font-style:italic;color:var(--amber);font-weight:500}
.sub{color:var(--dim);font-size:14.5px;margin-top:5px;font-style:italic}
.metrics{display:flex;gap:30px}
.m label{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin-bottom:2px}
.m strong{display:block;font-family:var(--mono);font-size:30px;font-weight:600;line-height:1.12;font-variant-numeric:tabular-nums}
.m span{font-family:var(--mono);font-size:11px;color:var(--dim)}
.m.hot strong{color:var(--amber)}
canvas{display:block;width:100%;aspect-ratio:46/40;border:1px solid var(--line);background:#0f1319}
.note{font-size:13px;color:var(--dim);margin:10px 2px 18px;font-style:italic}
footer{display:flex;gap:30px;flex-wrap:wrap;align-items:flex-end;border-top:1px solid var(--line);padding-top:16px}
.ctl label{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim);margin-bottom:8px}
.ctl b{color:var(--amber);font-weight:600}
input[type=range]{width:200px;accent-color:var(--amber);background:transparent;vertical-align:middle}
button{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
background:transparent;color:var(--ink);border:1px solid #3a4150;padding:8px 14px;cursor:pointer}
button:hover{border-color:var(--amber);color:var(--amber)}
.toggle i{display:inline-block;width:26px;height:11px;border:1px solid #556;margin-right:9px;vertical-align:1px}
.toggle[aria-pressed="true"]{border-color:var(--amber);color:var(--amber)}
.toggle[aria-pressed="true"] i{background:var(--amber);border-color:var(--amber)}
.btns{display:flex;gap:10px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>Egress Study <em>Nº 114</em></h1>
<p class="sub">A football stand section emptying through one gate — 220 agents, top-down, social-force + contact</p>
</div>
<div class="metrics">
<div class="m"><label>Outflow</label><strong id="mFlow">—</strong><span>people / min</span></div>
<div class="m"><label>Elapsed</label><strong id="mTime">0.0</strong><span>seconds</span></div>
<div class="m hot"><label>Out</label><strong id="mOut">0 / 220</strong><span id="mState">running</span></div>
</div>
</header>
<canvas id="cv" role="img" aria-label="Top-down crowd evacuation simulation"></canvas>
<p class="note">Every agent runs the same rules — seek the gate, keep personal distance, push on contact — and overlaps are projected away each step. A person's colour is measured local pressure, not decoration.</p>
<footer>
<div class="ctl">
<label>Urgency <b id="uVal">0.80</b></label>
<input id="uSl" type="range" min="0" max="1" step="0.05" value="0.8">
</div>
<div class="ctl">
<label>Gate width <b id="gVal">1.2 yd</b></label>
<input id="gSl" type="range" min="0.7" max="4.5" step="0.1" value="1.2">
</div>
<div class="ctl">
<label>Column in front of gate</label>
<button id="colBtn" class="toggle" aria-pressed="false"><i></i><span id="colTxt">off</span></button>
</div>
<div class="ctl">
<label>Run</label>
<div class="btns">
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset stands</button>
</div>
</div>
</footer>
</div>
<script>
'use strict';
/* ---------- settings from the URL fragment: #urgency=0.8&gate=1.2&column=1 ---------- */
let urgency=0.8, gateW=1.2, columnOn=false;
(function(){
const raw=location.hash.replace(/^#/,'');
if(!raw) return;
const q=new URLSearchParams(raw);
const u=parseFloat(q.get('urgency'));
if(!Number.isNaN(u)) urgency=Math.min(1,Math.max(0,u));
const g=parseFloat(q.get('gate'));
if(!Number.isNaN(g)) gateW=Math.min(4.5,Math.max(0.7,g));
const c=parseInt(q.get('column'),10);
if(!Number.isNaN(c)) columnOn=c!==0;
})();
function writeHash(){
const h='urgency='+urgency.toFixed(2)+'&gate='+gateW.toFixed(1)+'&column='+(columnOn?1:0);
try{ history.replaceState(null,'','#'+h); }catch(e){}
}
/* ---------- world, in yards ---------- */
const W=46, H=40;
const L=1.6, R=44.4, T=1.6, BOT=30; /* section bounds; front wall at y=30 */
const GX=23; /* gate centre x */
const COLY=25.6, COLR=0.55; /* optional column */
const N_ROWS=10, ROW0=5.8, ROWDY=1.55;
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
let SCALE=20;
function resize(){
const cssW=cv.clientWidth||900;
const dpr=Math.min(2, window.devicePixelRatio||1);
cv.width=Math.round(cssW*dpr);
cv.height=Math.round(cssW*dpr*H/W);
SCALE=cv.width/W;
}
window.addEventListener('resize',resize);
/* walls [x1,y1,x2,y2]; rebuilt when the gate width changes */
let SEGS=[];
function buildSegs(){
const gl=GX-gateW/2, gr=GX+gateW/2;
SEGS=[
[L,T,R,T], /* back wall */
[L,T,L,BOT], /* side walls */
[R,T,R,BOT],
[L,BOT,gl,BOT], /* front wall with the gate gap */
[gr,BOT,R,BOT],
[gl,BOT,gl,33.4], /* exit throat guides */
[gr,BOT,gr,33.4]
];
}
/* ---------- agents ---------- */
const PPL=[], LEAVE=[];
let t=0, outCount=0, finished=false, finalT=0, running=true;
let exitTimes=[];
function populate(){
PPL.length=0; LEAVE.length=0;
t=0; outCount=0; finished=false; finalT=0; exitTimes=[];
const perRow=22, x0=6.5, span=33;
for(let i=0;i<N_ROWS;i++){
const y=ROW0+i*ROWDY;
for(let k=0;k<perRow;k++){
PPL.push({
x:x0+(k+0.5)*(span/perRow)+(Math.random()-0.5)*0.8,
y:y+(Math.random()-0.5)*0.5,
vx:0, vy:0, r:0.3, ax:0, ay:0,
bias:Math.random()*2-1, /* preferred lane inside the gate (same rule, per-person value) */
wob:Math.random()*6.28, /* heading-wobble phase */
dx:0, dy:-1, /* current desired direction */
cnt:0, ov:0, heat:0, out:false
});
}
}
}
/* ---------- physics ---------- */
const speedOf=u=>1.3+4.6*Math.pow(u,1.4); /* desired speed, yd/s */
const tauOf =u=>0.5-0.27*u; /* reaction time, s */
function closest(px,py,s){
const ax=s[0],ay=s[1],bx=s[2],by=s[3];
const abx=bx-ax,aby=by-ay;
const k=Math.max(0,Math.min(1,((px-ax)*abx+(py-ay)*aby)/((abx*abx+aby*aby)||1e-9)));
return [ax+abx*k, ay+aby*k];
}
function pushSeg(p,s){ /* wall / post force on person */
const q=closest(p.x,p.y,s);
const dx=p.x-q[0], dy=p.y-q[1];
const reach=p.r+0.6, d2=dx*dx+dy*dy;
if(d2>reach*reach) return;
const d=Math.sqrt(d2)||1e-4, nx=dx/d, ny=dy/d;
let f=14*Math.exp((p.r-d)/0.3); /* personal space from obstacles */
if(d<p.r){ /* hard contact with obstacle */
const vn=p.vx*nx+p.vy*ny;
f+=(40+160*urgency)*(p.r-d)-9*Math.min(0,vn);
const tvx=p.vx-vn*nx, tvy=p.vy-vn*ny;
p.ax-=5*tvx; p.ay-=5*tvy; /* friction */
}
p.ax+=nx*f; p.ay+=ny*f;
}
function pushCircle(p,cx,cy,cr){ /* column force on person */
const dx=p.x-cx, dy=p.y-cy, rr=cr+p.r, d2=dx*dx+dy*dy;
const reach=rr+0.6;
if(d2>reach*reach) return;
const d=Math.sqrt(d2)||1e-4, nx=dx/d, ny=dy/d;
let f=14*Math.exp((rr-d)/0.3);
if(d<rr){
const vn=p.vx*nx+p.vy*ny;
f+=(40+160*urgency)*(rr-d)-9*Math.min(0,vn);
const tvx=p.vx-vn*nx, tvy=p.vy-vn*ny;
p.ax-=5*tvx; p.ay-=5*tvy;
}
p.ax+=nx*f; p.ay+=ny*f;
}
function projSeg(p,s){ /* hard non-overlap vs wall */
const q=closest(p.x,p.y,s);
const dx=p.x-q[0], dy=p.y-q[1], d2=dx*dx+dy*dy;
if(d2>=p.r*p.r) return;
let nx,ny; const d=Math.sqrt(d2);
if(d<1e-5){ const sx=s[2]-s[0],sy=s[3]-s[1],l=Math.hypot(sx,sy)||1; nx=-sy/l; ny=sx/l; }
else{ nx=dx/d; ny=dy/d; }
p.x+=nx*(p.r-d); p.y+=ny*(p.r-d);
const vn=p.vx*nx+p.vy*ny;
if(vn<0){ p.vx-=vn*nx; p.vy-=vn*ny; }
}
function projCircle(p,cx,cy,cr){ /* hard non-overlap vs column */
const dx=p.x-cx, dy=p.y-cy, rr=cr+p.r, d2=dx*dx+dy*dy;
if(d2>=rr*rr) return;
const d=Math.sqrt(d2)||1e-4, nx=dx/d, ny=dy/d;
p.x+=nx*(rr-d); p.y+=ny*(rr-d);
const vn=p.vx*nx+p.vy*ny;
if(vn<0){ p.vx-=vn*nx; p.vy-=vn*ny; }
}
function step(dt){
const n=PPL.length, vDes=speedOf(urgency), tau=tauOf(urgency), kC=60+130*urgency;
/* rule 1 — desired velocity: head for the gate (with a small per-person wobble) */
for(const p of PPL){
if(p.out) continue;
p.wob+=(Math.random()-0.5)*6*dt; p.wob*=0.985;
const tx=GX+p.bias*0.42*gateW, ty=31.2;
const dx=tx-p.x, dy=ty-p.y, d=Math.hypot(dx,dy)||1;
const ux=dx/d, uy=dy/d, c=Math.cos(p.wob), s=Math.sin(p.wob);
p.dx=ux*c-uy*s; p.dy=ux*s+uy*c;
p.ax=(p.dx*vDes-p.vx)/tau;
p.ay=(p.dy*vDes-p.vy)/tau;
p.cnt=0; p.ov=0;
}
/* rule 2+3 — pair forces: personal space + hard contact push */
for(let i=0;i<n;i++){
const a=PPL[i]; if(a.out) continue;
for(let j=i+1;j<n;j++){
const b=PPL[j]; if(b.out) continue;
const dx=b.x-a.x, dy=b.y-a.y;
if(dx>1.8||dx<-1.8||dy>1.8||dy<-1.8) continue;
const rs=a.r+b.r, d2=dx*dx+dy*dy;
if(d2>3.24) continue;
const d=Math.sqrt(d2)||1e-4, nx=dx/d, ny=dy/d;
let f=16*Math.exp((rs-d)/0.32); /* personal-space repulsion */
const ov=rs-d;
if(ov>0){ /* bodies touching: push + damping + friction */
const rvn=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
f+=kC*ov-9*rvn;
const tvx=(b.vx-a.vx)-rvn*nx, tvy=(b.vy-a.vy)-rvn*ny;
a.ax-=6*tvx; a.ay-=6*tvy; b.ax+=6*tvx; b.ay+=6*tvy;
a.ov+=ov; a.cnt++; b.ov+=ov; b.cnt++;
}
a.ax-=f*nx; a.ay-=f*ny;
b.ax+=f*nx; b.ay+=f*ny;
}
}
/* obstacles + anti-deadlock sway for pressed, nearly-stationary agents */
for(const p of PPL){
if(p.out) continue;
for(const s of SEGS) pushSeg(p,s);
if(columnOn) pushCircle(p,GX,COLY,COLR);
if(p.cnt>0 && p.vx*p.vx+p.vy*p.vy<0.25){
const sw=Math.sin(t*2.3+p.wob*7)*(6+16*urgency);
p.ax+=-p.dy*sw; p.ay+=p.dx*sw;
}
}
/* integrate */
const vmax=speedOf(1)+2.5;
for(const p of PPL){
if(p.out) continue;
p.vx+=p.ax*dt; p.vy+=p.ay*dt;
const sp=Math.hypot(p.vx,p.vy);
if(sp>vmax){ p.vx*=vmax/sp; p.vy*=vmax/sp; }
p.x+=p.vx*dt; p.y+=p.vy*dt;
const h=p.cnt?Math.min(1,p.ov/0.18):0;
p.heat+=(h-p.heat)*Math.min(1,dt*6);
if(p.y>BOT+0.45){ /* past the gate line: counted as out */
p.out=true; outCount++; exitTimes.push(t);
LEAVE.push({x:p.x,y:p.y,vx:p.vx*0.3,vy:Math.min(4,Math.max(1.8,p.vy)),age:0});
}
}
/* positional projection: nobody overlaps anybody (2 relaxation passes) */
for(let it=0;it<2;it++){
for(let i=0;i<n;i++){
const a=PPL[i]; if(a.out) continue;
for(let j=i+1;j<n;j++){
const b=PPL[j]; if(b.out) continue;
const dx=b.x-a.x, dy=b.y-a.y, rs=a.r+b.r, d2=dx*dx+dy*dy;
if(d2>=rs*rs||d2===0) continue;
const d=Math.sqrt(d2), nx=dx/d, ny=dy/d, push=(rs-d)*0.425;
a.x-=nx*push; a.y-=ny*push; b.x+=nx*push; b.y+=ny*push;
}
for(const s of SEGS) projSeg(a,s);
if(columnOn) projCircle(a,GX,COLY,COLR);
if(a.x<L+a.r) a.x=L+a.r; else if(a.x>R-a.r) a.x=R-a.r;
if(a.y<T+a.r) a.y=T+a.r;
}
}
/* leavers drift onto the pitch and fade */
for(let i=LEAVE.length-1;i>=0;i--){
const l=LEAVE[i];
l.x+=l.vx*dt; l.y+=l.vy*dt; l.vx*=0.98; l.age+=dt;
if(l.age>2.6||l.y>H-0.8) LEAVE.splice(i,1);
}
t+=dt;
if(!finished&&outCount>=PPL.length){ finished=true; finalT=t; }
}
/* ---------- rendering ---------- */
const SERIF='"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif';
const MONO='ui-monospace,Consolas,"Liberation Mono",monospace';
function line(a,b,c,d){ ctx.beginPath(); ctx.moveTo(a,b); ctx.lineTo(c,d); ctx.stroke(); }
function text(s,x,y,size,family,color,align,bl,weight){
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
ctx.font=(weight||'500')+' '+(size*SCALE).toFixed(1)+'px '+family;
ctx.fillStyle=color; ctx.textAlign=align||'center'; ctx.textBaseline=bl||'middle';
ctx.fillText(s,x*SCALE,y*SCALE);
ctx.restore();
}
function chevron(x,y,s){
ctx.beginPath(); ctx.moveTo(x-s,y-s*0.85); ctx.lineTo(x,y); ctx.lineTo(x+s,y-s*0.85); ctx.stroke();
}
function arrow(x,y,dir){
const k=0.18;
ctx.beginPath(); ctx.moveTo(x-dir*k,y-k*0.7); ctx.lineTo(x,y); ctx.lineTo(x-dir*k,y+k*0.7); ctx.stroke();
}
function draw(){
ctx.setTransform(SCALE,0,0,SCALE,0,0);
ctx.fillStyle='#0b0e13'; ctx.fillRect(0,0,W,H);
/* pitch beyond the exit */
ctx.fillStyle='#15301f'; ctx.fillRect(0,33.4,W,H-33.4);
ctx.fillStyle='rgba(255,255,255,0.04)';
for(let i=0;i<6;i+=2) ctx.fillRect(i*8,33.4,8,H-33.4);
ctx.strokeStyle='rgba(238,238,224,0.5)'; ctx.lineWidth=0.12;
line(0.5,33.9,W-0.5,33.9);
text('PITCH',L+0.5,35.4,0.55,MONO,'rgba(216,210,196,0.4)','left');
/* stand floor: treads, nosings, seats */
ctx.fillStyle='#202733'; ctx.fillRect(L,T,R-L,21-T);
for(let i=0;i<N_ROWS;i++){
const y=ROW0+i*ROWDY;
ctx.fillStyle='#1b212d'; ctx.fillRect(L,y-1.05,R-L,1.05);
ctx.strokeStyle='#10141c'; ctx.lineWidth=0.16; line(L,y,R,y);
ctx.fillStyle='#28303f';
for(let x=L+0.9;x<R-0.5;x+=0.85) ctx.fillRect(x,y-0.72,0.5,0.26);
}
text('114',GX,3.4,2.4,SERIF,'rgba(255,176,58,0.15)','center','middle','700');
/* concourse */
ctx.fillStyle='#171c26'; ctx.fillRect(L,21,R-L,BOT-21);
ctx.strokeStyle='rgba(255,255,255,0.045)'; ctx.lineWidth=0.06;
for(let x=L+4.6;x<R;x+=4.6) line(x,21,x,BOT);
const gl=GX-gateW/2, gr=GX+gateW/2;
/* light spilling from the gate + floor chevrons */
const g=ctx.createRadialGradient(GX,30.4,0.2,GX,30.4,5);
g.addColorStop(0,'rgba(255,176,58,0.15)'); g.addColorStop(1,'rgba(255,176,58,0)');
ctx.fillStyle=g; ctx.fillRect(GX-5.2,25.4,10.4,9.6);
ctx.strokeStyle='rgba(255,176,58,0.45)'; ctx.lineWidth=0.14;
chevron(GX,26.6,0.55); chevron(GX,27.8,0.55);
/* exit throat floor */
ctx.fillStyle='#242a35'; ctx.fillRect(gl,BOT,gateW,3.4);
/* walls + gate posts */
ctx.strokeStyle='#454d5c'; ctx.lineWidth=0.3; ctx.lineCap='round';
line(L,T,R,T); line(L,T,L,BOT); line(R,T,R,BOT);
line(L,BOT,gl,BOT); line(gr,BOT,R,BOT);
line(gl,BOT,gl,33.4); line(gr,BOT,gr,33.4);
ctx.fillStyle='#ffb03a';
ctx.fillRect(gl-0.17,BOT-0.17,0.34,0.34);
ctx.fillRect(gr-0.17,BOT-0.17,0.34,0.34);
/* live dimension callout for the gate */
ctx.strokeStyle='rgba(255,176,58,0.55)'; ctx.lineWidth=0.05;
line(gl,BOT+0.35,gl,31.7); line(gr,BOT+0.35,gr,31.7);
line(gl,31.55,gr,31.55);
arrow(gl,31.55,-1); arrow(gr,31.55,1);
text('GATE '+gateW.toFixed(1)+' YD',GX,32.35,0.55,MONO,'rgba(255,176,58,0.8)');
/* people — colour is live local pressure */
for(const p of PPL){
if(p.out) continue;
const h=p.heat;
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath(); ctx.arc(p.x+0.07,p.y+0.1,p.r,0,7); ctx.fill();
ctx.fillStyle='rgb('+((233+22*h)|0)+','+((226-134*h)|0)+','+((208-162*h)|0)+')';
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill();
ctx.strokeStyle='rgba(8,10,14,0.55)'; ctx.lineWidth=0.06; ctx.stroke();
const sp=Math.hypot(p.vx,p.vy);
if(sp>0.5){
const nx=p.vx/sp, ny=p.vy/sp;
ctx.strokeStyle='rgba(10,12,16,0.6)'; ctx.lineWidth=0.09;
line(p.x+nx*p.r*0.2,p.y+ny*p.r*0.2,p.x+nx*p.r*1.5,p.y+ny*p.r*1.5);
}
}
/* fading leavers on the pitch */
for(const l of LEAVE){
const a=Math.max(0,1-l.age/2.6);
ctx.fillStyle='rgba(233,226,208,'+(0.85*a).toFixed(3)+')';
ctx.beginPath(); ctx.arc(l.x,l.y,0.3,0,7); ctx.fill();
}
/* the column (drawn solid, over people) */
if(columnOn){
ctx.fillStyle='rgba(0,0,0,0.4)';
ctx.beginPath(); ctx.arc(GX+0.1,COLY+0.12,COLR,0,7); ctx.fill();
ctx.fillStyle='#2e3542';
ctx.beginPath(); ctx.arc(GX,COLY,COLR,0,7); ctx.fill();
ctx.strokeStyle='#5b6579'; ctx.lineWidth=0.12; ctx.stroke();
ctx.strokeStyle='rgba(255,176,58,0.5)'; ctx.lineWidth=0.07;
ctx.setLineDash([0.18,0.14]);
ctx.beginPath(); ctx.arc(GX,COLY,COLR+0.22,0,7); ctx.stroke();
ctx.setLineDash([]);
text('COL \u00D8 '+(COLR*2).toFixed(1)+' YD',GX,COLY+1.5,0.45,MONO,'rgba(216,210,196,0.55)');
}
/* hanging EXIT sign */
ctx.fillStyle='#ffb03a'; ctx.fillRect(GX-1.25,28.85,2.5,0.66);
ctx.strokeStyle='rgba(0,0,0,0.5)'; ctx.lineWidth=0.06; ctx.strokeRect(GX-1.25,28.85,2.5,0.66);
text('EXIT',GX,29.19,0.5,MONO,'#181410','center','middle','700');
if(finished){
text('SECTION CLEARED \u2014 '+finalT.toFixed(1)+' S',GX,24.4,0.95,SERIF,'rgba(255,176,58,0.92)','center','middle','700');
}
/* scale bar */
ctx.strokeStyle='rgba(216,210,196,0.5)'; ctx.lineWidth=0.08; ctx.lineCap='butt';
line(L+0.4,38.5,L+10.4,38.5);
line(L+0.4,38.25,L+0.4,38.75); line(L+10.4,38.25,L+10.4,38.75);
text('10 YD',L+5.4,39.3,0.5,MONO,'rgba(216,210,196,0.6)');
}
/* ---------- HUD + controls ---------- */
const mFlow=document.getElementById('mFlow'), mTime=document.getElementById('mTime'),
mOut=document.getElementById('mOut'), mState=document.getElementById('mState');
const uSl=document.getElementById('uSl'), uVal=document.getElementById('uVal'),
gSl=document.getElementById('gSl'), gVal=document.getElementById('gVal'),
colBtn=document.getElementById('colBtn'), colTxt=document.getElementById('colTxt'),
resetBtn=document.getElementById('resetBtn'), pauseBtn=document.getElementById('pauseBtn');
function updateHUD(){
let flow;
if(finished) flow='\u2014';
else{
while(exitTimes.length&&exitTimes[0]<t-12) exitTimes.shift();
const win=Math.min(12,Math.max(t,2));
flow=String(Math.round(exitTimes.length/win*60));
}
mFlow.textContent=flow;
mTime.textContent=(finished?finalT:t).toFixed(1);
mOut.textContent=outCount+' / '+PPL.length;
mState.textContent=finished?('cleared \u00B7 '+finalT.toFixed(1)+' s'):(running?'running':'paused');
}
uSl.value=urgency; uVal.textContent=urgency.toFixed(2);
gSl.value=gateW; gVal.textContent=gateW.toFixed(1)+' yd';
function setColUI(){ colBtn.setAttribute('aria-pressed',columnOn?'true':'false'); colTxt.textContent=columnOn?'on':'off'; }
setColUI();
uSl.addEventListener('input',()=>{ urgency=parseFloat(uSl.value); uVal.textContent=urgency.toFixed(2); writeHash(); });
gSl.addEventListener('input',()=>{ gateW=parseFloat(gSl.value); gVal.textContent=gateW.toFixed(1)+' yd'; buildSegs(); writeHash(); });
colBtn.addEventListener('click',()=>{ columnOn=!columnOn; setColUI(); writeHash(); });
resetBtn.addEventListener('click',()=>{ populate(); running=true; pauseBtn.textContent='Pause'; });
pauseBtn.addEventListener('click',()=>{ running=!running; pauseBtn.textContent=running?'Pause':'Resume'; });
/* ---------- main loop ---------- */
let last=performance.now(), acc=0;
const DT=1/60;
function frame(now){
requestAnimationFrame(frame);
let dt=(now-last)/1000; last=now;
if(dt>0.25) dt=0.25;
if(running){
acc+=dt; let steps=0;
while(acc>=DT&&steps<4){ step(DT); acc-=DT; steps++; }
if(steps===4) acc=0;
}
draw(); updateHUD();
}
resize(); buildSegs(); populate(); writeHash();
requestAnimationFrame(frame);
</script>
</body>
</html>
Where it behaves like a real crowd. The motion is a hand-written social-force model: every agent seeks the gate with a bounded acceleration, keeps exponential personal-space distance from neighbours, and transmits real contact forces (stiffness + normal damping + tangential friction) to people, walls, gate posts and the column. From that you get emergent lane formation on the concourse, merging into a single stream, density waves from the back of the crush, and a genuinely capacity-limited gate — outflow is counted from actual exit events in a rolling 12 s window, not from a formula. At low urgency you get an orderly queue; at high urgency the throat pulses as an arch forms and collapses, which the pressure-colour map makes visible (people flush from bone-white to orange-red under compression). The column typically costs time by splitting and misaligning the stream, so you can test the prediction live.
Where it's idealised, and every special case named:
urgency=0.8, gate=1.2, column=0, and the page auto-starts either way.