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.
Two calls, each with the mechanism I expect my particular model to produce (gate = 1.2 yd ≈ 24 px ≈ 1.7 body-widths, so the gate — not the walking — is the binding constraint):
Urgency 0.3 → 0.9, gate = 1.2, column = 0 → evacuation time DECREASES, by ≈ 15 s (roughly 95 s → 80 s). Higher urgency raises both approach speed and contact stiffness; because my contacts are nearly frictionless (see below) they don't arch into a hard clog, so the gate is simply fed faster and people reach it sooner. This is the opposite of the textbook "faster-is-slower," and I expect my clean model to land on the "faster" side.
Column on, urgency = 0.9, gate = 1.2 → evacuation time INCREASES, by ≈ 6 s (roughly 80 s → 86 s). With little clogging to relieve, a centered pillar in front of the gate acts mainly as an obstacle: people detour around it and re-merge, adding path length and a little central congestion. The flow-improving "pillar-breaks-the-arch" effect needs a strongly clogging model, which mine isn't.
Both numbers are predictions; the honest caveats are in the last section.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Section Emptying — Crowd Exit Sim</title>
<style>
:root{
--bg:#080b12; --card:#0e1420; --line:#1c2637; --ink:#e8edf4;
--muted:#8b97a8; --accent:#37e0b0; --warm:#ffb454; --cool:#39d98a;
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--ink);
font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
.wrap{max-width:1240px;margin:0 auto;padding:18px}
header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:14px}
header h1{font-size:19px;margin:0;letter-spacing:.2px}
header .sub{color:var(--muted);font-size:13px}
.main{display:flex;gap:16px;align-items:flex-start}
.stage{flex:1 1 auto;min-width:0}
canvas{width:100%;height:auto;display:block;border-radius:14px;
background:#0a0f18;border:1px solid var(--line);
box-shadow:0 10px 40px rgba(0,0,0,.5)}
.panel{flex:0 0 316px;display:flex;flex-direction:column;gap:14px}
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px}
.card h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;
color:var(--muted);margin:0 0 12px}
.metrics{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.metric{background:#0b111c;border:1px solid var(--line);border-radius:10px;padding:10px}
.metric .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
.metric .v{font-size:24px;font-weight:650;font-variant-numeric:tabular-nums;margin-top:2px}
.metric .u{font-size:12px;color:var(--muted);font-weight:400}
.metric.wide{grid-column:1 / -1}
.ctl{margin:12px 0}
.ctl:first-of-type{margin-top:0}
.ctl .row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px}
.ctl label{font-size:13px}
.ctl .val{font-size:13px;color:var(--accent);font-variant-numeric:tabular-nums}
input[type=range]{width:100%;accent-color:var(--accent)}
.toggle{display:flex;align-items:center;justify-content:space-between;
background:#0b111c;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-top:12px}
.toggle label{font-size:13px}
.switch{position:relative;width:44px;height:24px;flex:0 0 auto}
.switch input{opacity:0;width:0;height:0}
.track{position:absolute;inset:0;background:#243044;border-radius:999px;transition:.15s;cursor:pointer}
.track:before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;
background:#e8edf4;border-radius:50%;transition:.15s}
.switch input:checked + .track{background:var(--accent)}
.switch input:checked + .track:before{transform:translateX(20px);background:#04140f}
.btns{display:flex;gap:10px;margin-top:14px}
button{flex:1;background:#182233;color:var(--ink);border:1px solid var(--line);
border-radius:10px;padding:10px;font-size:13px;cursor:pointer;transition:.12s}
button:hover{background:#1f2c42}
button:active{transform:translateY(1px)}
.status{font-size:13px;color:var(--muted)}
.status b{color:var(--ink)}
.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--muted);margin-top:10px}
.legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:5px;vertical-align:-1px}
@media(max-width:880px){.main{flex-direction:column}.panel{flex-basis:auto;width:100%}}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Section Emptying</h1>
<span class="sub">Emergent crowd exit · self-written social-force + position-based solver · no libraries</span>
</header>
<div class="main">
<div class="stage">
<canvas id="sim" width="1000" height="700" aria-label="Crowd evacuation simulation"></canvas>
<div class="legend">
<span><i style="background:#ffb454"></i>in the section</span>
<span><i style="background:#39d98a"></i>through the gate</span>
<span><i style="background:#37e0b0"></i>gate</span>
<span><i style="background:#c9962f"></i>column</span>
</div>
</div>
<div class="panel">
<div class="card">
<h2>Live metrics</h2>
<div class="metrics">
<div class="metric wide">
<div class="k">Outflow rate</div>
<div class="v"><span id="rate">0.0</span> <span class="u">people / min</span></div>
</div>
<div class="metric">
<div class="k">Timer</div>
<div class="v"><span id="timer">0.0</span><span class="u"> s</span></div>
</div>
<div class="metric">
<div class="k">Still inside</div>
<div class="v" id="remaining">0</div>
</div>
<div class="metric wide">
<div class="k">Evacuated</div>
<div class="v"><span id="evacuated">0</span> <span class="u" id="totalLbl">/ 0</span></div>
</div>
</div>
<div class="status" style="margin-top:12px">Status: <b id="status">Evacuating</b></div>
</div>
<div class="card">
<h2>Controls</h2>
<div class="ctl">
<div class="row"><label for="urgency">Urgency</label><span class="val" id="urgencyVal">0.80</span></div>
<input id="urgency" type="range" min="0" max="1" step="0.01" value="0.8">
</div>
<div class="ctl">
<div class="row"><label for="gate">Gate width</label><span class="val" id="gateVal">1.2 yd</span></div>
<input id="gate" type="range" min="0.8" max="4" step="0.1" value="1.2">
</div>
<div class="toggle">
<label for="column">Column before gate</label>
<span class="switch"><input id="column" type="checkbox"><span class="track"></span></span>
</div>
<div class="btns">
<button id="pause">Pause</button>
<button id="reset">Reset / Refill</button>
</div>
</div>
</div>
</div>
</div>
<script>
"use strict";
(function(){
// ---------- world constants (logical pixels) ----------
const VW = 1000, VH = 700;
const WALL_Y = 500, WALL_T = 16; // wall top & thickness
const GATE_CX = 500; // gate centered
const EXIT_Y = 690; // evacuees removed here
const R = 7; // person body radius (px)
const PX_PER_YARD = 20; // scale
const N_PEOPLE = 280;
const PILLAR_R = 10, PILLAR_DIST = 44;// the "column" obstacle
// ---------- tuning ----------
const TAU = 0.5; // velocity relaxation (s)
const RINT = 26; // personal-space range
const B_SOCIAL = 5.5; // repulsion decay length
const CELL = 32; // spatial-hash cell (>= RINT)
const PILLAR_SOFT = 320; // steering push around column
const H = 1/60; // physics timestep
const PBD_ITERS = 5, PBD_RELAX = 0.85;
// ---------- state ----------
const params = { urgency:0.8, gate:1.2, column:true };
let people = [];
let total = 0;
let elapsed = 0, evacCount = 0, evacTimes = [];
let running = true, paused = false, empty = false, finalTime = 0;
let acc = 0, lastT = 0;
let grid = new Map();
// ---------- dom ----------
const cv = document.getElementById('sim');
const ctx = cv.getContext('2d');
const el = {
rate:document.getElementById('rate'), timer:document.getElementById('timer'),
remaining:document.getElementById('remaining'), evacuated:document.getElementById('evacuated'),
totalLbl:document.getElementById('totalLbl'), status:document.getElementById('status'),
urgency:document.getElementById('urgency'), gate:document.getElementById('gate'),
column:document.getElementById('column'), urgencyVal:document.getElementById('urgencyVal'),
gateVal:document.getElementById('gateVal'), pause:document.getElementById('pause'),
reset:document.getElementById('reset')
};
// ---------- helpers ----------
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const gatePx=()=>params.gate*PX_PER_YARD;
const desiredSpeed=()=>24+params.urgency*90; // px/s
const socialA=()=>150+params.urgency*400; // contact/stiffness scale
function setupCanvas(){
const dpr=Math.max(1,Math.min(2,window.devicePixelRatio||1));
cv.width=Math.round(VW*dpr); cv.height=Math.round(VH*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
}
function makePerson(x,y){
return { x,y, vx:0, vy:0, fx:0, fy:0, r:R,
hue: 18 + Math.random()*26, // warm spectator colors
wob: Math.random()*Math.PI*2, // symmetry-breaking phase
evacuated:false, done:false, evacTime:0 };
}
function spawn(){
people.length=0;
const x0=44, x1=VW-44, y0=64, y1=WALL_Y-24;
let minD=2.2*R, guard=0;
while(people.length<N_PEOPLE && guard<80000){
guard++;
if(guard%20000===0) minD*=0.85;
const x=x0+Math.random()*(x1-x0);
const y=y0+Math.random()*(y1-y0);
let ok=true;
for(let i=0;i<people.length;i++){
const q=people[i], dx=q.x-x, dy=q.y-y;
if(dx*dx+dy*dy<minD*minD){ ok=false; break; }
}
if(ok) people.push(makePerson(x,y));
}
total=people.length;
}
function reset(){
spawn();
elapsed=0; evacCount=0; evacTimes=[]; acc=0;
running=true; paused=false; empty=false; finalTime=0;
el.pause.textContent='Pause';
}
// ---------- spatial hash ----------
function buildGrid(){
grid.clear();
for(let i=0;i<people.length;i++){
const p=people[i]; if(p.done) continue;
const key=((p.x/CELL)|0)*1000 + ((p.y/CELL)|0);
let a=grid.get(key);
if(!a){ a=[]; grid.set(key,a); }
a.push(i);
}
}
// ---------- forces ----------
function computeForces(){
const vDes=desiredSpeed(), A=socialA();
const pillarOn=params.column;
const pillarY=WALL_Y-PILLAR_DIST;
const pillarRng=PILLAR_R+R+20;
for(let n=0;n<people.length;n++){
const p=people[n]; if(p.done) continue;
// 1) drive toward the gate (or straight out once through)
let tx,ty;
if(p.evacuated){ tx=p.x; ty=VH+60; }
else if(p.y<WALL_Y){ tx=GATE_CX; ty=WALL_Y+30; }
else { tx=p.x; ty=VH+40; } // in the passage: drive down
let dx=tx-p.x, dy=ty-p.y;
let dl=Math.hypot(dx,dy)||1;
dx/=dl; dy/=dl;
// small per-person tangential bias -> breaks perfect symmetry / deadlocks
const wob=Math.sin(elapsed*1.3+p.wob)*0.14;
let dvx=(dx - dy*wob)*vDes;
let dvy=(dy + dx*wob)*vDes;
let fx=(dvx-p.vx)/TAU, fy=(dvy-p.vy)/TAU;
// 2) personal-space repulsion from neighbours
const cx=(p.x/CELL)|0, cy=(p.y/CELL)|0;
for(let gx=cx-1;gx<=cx+1;gx++){
for(let gy=cy-1;gy<=cy+1;gy++){
const arr=grid.get(gx*1000+gy); if(!arr) continue;
for(let k=0;k<arr.length;k++){
const q=people[arr[k]]; if(q===p||q.done) continue;
const nx=p.x-q.x, ny=p.y-q.y, d2=nx*nx+ny*ny;
if(d2<RINT*RINT && d2>1e-6){
const d=Math.sqrt(d2);
const mag=A*Math.exp((2*R-d)/B_SOCIAL);
fx+=mag*nx/d; fy+=mag*ny/d;
}
}
}
}
// 3) soft steering around the column (so the crowd splits & re-merges)
if(pillarOn && !p.evacuated){
const ddx=p.x-GATE_CX, ddy=p.y-pillarY, dd=Math.hypot(ddx,ddy);
if(dd<pillarRng && dd>1e-6){
const mag=PILLAR_SOFT*(pillarRng-dd)/pillarRng;
fx+=mag*ddx/dd; fy+=mag*ddy/dd;
}
}
p.fx=fx; p.fy=fy;
}
}
function integrate(dt){
const vmax=desiredSpeed()*1.3+30;
for(let n=0;n<people.length;n++){
const p=people[n]; if(p.done) continue;
p.vx+=p.fx*dt; p.vy+=p.fy*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;
}
}
// ---------- collision resolution (position based) ----------
function resolvePairs(){
const minD=2*R, minD2=minD*minD;
for(let i=0;i<people.length;i++){
const p=people[i]; if(p.done) continue;
const cx=(p.x/CELL)|0, cy=(p.y/CELL)|0;
for(let gx=cx-1;gx<=cx+1;gx++){
for(let gy=cy-1;gy<=cy+1;gy++){
const arr=grid.get(gx*1000+gy); if(!arr) continue;
for(let k=0;k<arr.length;k++){
const qi=arr[k]; if(qi<=i) continue; // each pair once
const q=people[qi]; if(q.done) continue;
let nx=p.x-q.x, ny=p.y-q.y, d2=nx*nx+ny*ny;
if(d2<minD2){
if(d2>1e-9){
const d=Math.sqrt(d2), overlap=minD-d;
const ux=nx/d, uy=ny/d, corr=overlap*0.5*PBD_RELAX;
p.x+=ux*corr; p.y+=uy*corr;
q.x-=ux*corr; q.y-=uy*corr;
}else{
p.x+=(Math.random()-0.5)*0.6; p.y+=(Math.random()-0.5)*0.6;
}
}
}
}
}
}
}
function collideRect(p, rx0, ry0, rx1, ry1){
const qx=clamp(p.x,rx0,rx1), qy=clamp(p.y,ry0,ry1);
let dx=p.x-qx, dy=p.y-qy, d2=dx*dx+dy*dy;
if(d2<p.r*p.r){
if(d2>1e-9){
const d=Math.sqrt(d2), push=p.r-d, ux=dx/d, uy=dy/d;
p.x+=ux*push; p.y+=uy*push;
const vn=p.vx*ux+p.vy*uy; if(vn<0){ p.vx-=vn*ux; p.vy-=vn*uy; }
}else{ // center inside rect: eject along nearest face
const dl=p.x-rx0, dr=rx1-p.x, dt=p.y-ry0, db=ry1-p.y;
const m=Math.min(dl,dr,dt,db);
if(m===dt){ p.y=ry0-p.r; if(p.vy>0)p.vy=0; }
else if(m===db){ p.y=ry1+p.r; if(p.vy<0)p.vy=0; }
else if(m===dl){ p.x=rx0-p.r; if(p.vx>0)p.vx=0; }
else { p.x=rx1+p.r; if(p.vx<0)p.vx=0; }
}
}
}
function collidePillar(p){
const ox=GATE_CX, oy=WALL_Y-PILLAR_DIST, md=p.r+PILLAR_R;
let dx=p.x-ox, dy=p.y-oy, d2=dx*dx+dy*dy;
if(d2<md*md && d2>1e-9){
const d=Math.sqrt(d2), push=md-d, ux=dx/d, uy=dy/d;
p.x+=ux*push; p.y+=uy*push;
const vn=p.vx*ux+p.vy*uy; if(vn<0){ p.vx-=vn*ux; p.vy-=vn*uy; }
}
}
function resolveObstacles(){
const gl=GATE_CX-gatePx()/2, gr=GATE_CX+gatePx()/2;
const y0=WALL_Y, y1=WALL_Y+WALL_T;
const pillarOn=params.column;
for(let n=0;n<people.length;n++){
const p=people[n]; if(p.done) continue;
// world bounds
if(p.x<R){ p.x=R; if(p.vx<0)p.vx=0; }
if(p.x>VW-R){ p.x=VW-R; if(p.vx>0)p.vx=0; }
if(p.y<R){ p.y=R; if(p.vy<0)p.vy=0; }
// wall (two solid segments leaving the gate gap)
collideRect(p, 0, y0, gl, y1);
collideRect(p, gr, y0, VW, y1);
// column
if(pillarOn) collidePillar(p);
}
}
function checkEvac(){
const line=WALL_Y+WALL_T+R+2;
for(let n=0;n<people.length;n++){
const p=people[n]; if(p.done) continue;
if(!p.evacuated && p.y>line){
p.evacuated=true; p.evacTime=elapsed;
evacCount++; evacTimes.push(elapsed);
}
if(p.evacuated && p.y>EXIT_Y) p.done=true;
}
if(!empty && evacCount>=total && total>0){
empty=true; running=false; finalTime=elapsed;
}
}
function step(){
buildGrid();
computeForces();
integrate(H);
for(let it=0; it<PBD_ITERS; it++){ resolvePairs(); resolveObstacles(); }
elapsed+=H;
checkEvac();
}
// ---------- metrics ----------
function outflowRate(){
if(empty) return total/(Math.max(finalTime,0.001)/60);
const W=Math.min(5, Math.max(1, elapsed));
const t0=elapsed-W;
let c=0;
for(let i=evacTimes.length-1;i>=0;i--){ if(evacTimes[i]>=t0)c++; else break; }
return c/W*60;
}
// ---------- rendering ----------
function drawStands(){
const top=30, bot=WALL_Y;
ctx.fillStyle='#111a2b'; ctx.fillRect(0,top,VW,bot-top);
const rows=14, rh=(bot-top)/rows;
for(let i=0;i<rows;i++){
ctx.fillStyle = (i%2)?'rgba(255,255,255,0.02)':'rgba(0,0,0,0.13)';
ctx.fillRect(0, top+i*rh, VW, rh);
}
ctx.strokeStyle='rgba(255,255,255,0.04)'; ctx.lineWidth=2;
for(let x=120;x<VW;x+=176){ ctx.beginPath(); ctx.moveTo(x,top); ctx.lineTo(x,bot); ctx.stroke(); }
}
function drawWall(){
const gl=GATE_CX-gatePx()/2, gr=GATE_CX+gatePx()/2;
const y0=WALL_Y, y1=WALL_Y+WALL_T;
const g=ctx.createLinearGradient(0,y0,0,y1);
g.addColorStop(0,'#3c4a5e'); g.addColorStop(1,'#212a37');
ctx.fillStyle=g;
ctx.fillRect(0,y0,gl,y1-y0);
ctx.fillRect(gr,y0,VW-gr,y1-y0);
ctx.fillStyle='rgba(255,255,255,0.09)';
ctx.fillRect(0,y0,gl,2); ctx.fillRect(gr,y0,VW-gr,2);
// gate posts
ctx.fillStyle='#5be0c0';
ctx.fillRect(gl-3,y0-5,3,y1-y0+10);
ctx.fillRect(gr,y0-5,3,y1-y0+10);
// pulsing threshold
const pulse=0.55+0.45*Math.sin(performance.now()/300);
ctx.save();
ctx.shadowColor='#37e0b0'; ctx.shadowBlur=14*pulse;
ctx.strokeStyle='rgba(55,224,176,'+(0.6+0.3*pulse)+')';
ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(gl,y0+(y1-y0)/2); ctx.lineTo(gr,y0+(y1-y0)/2); ctx.stroke();
ctx.restore();
}
function drawPillar(){
const px=GATE_CX, py=WALL_Y-PILLAR_DIST;
ctx.save();
ctx.shadowColor='rgba(0,0,0,0.55)'; ctx.shadowBlur=10; ctx.shadowOffsetY=3;
const g=ctx.createRadialGradient(px-PILLAR_R*0.4,py-PILLAR_R*0.4,2,px,py,PILLAR_R);
g.addColorStop(0,'#ffe08a'); g.addColorStop(1,'#c9962f');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(px,py,PILLAR_R,0,Math.PI*2); ctx.fill();
ctx.restore();
ctx.strokeStyle='rgba(0,0,0,0.4)'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.arc(px,py,PILLAR_R,0,Math.PI*2); ctx.stroke();
}
function drawPeople(){
for(let n=0;n<people.length;n++){
const p=people[n]; if(p.done) continue;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
ctx.fillStyle = p.evacuated ? '#39d98a' : 'hsl('+p.hue+',85%,60%)';
ctx.fill();
ctx.lineWidth=1; ctx.strokeStyle='rgba(0,0,0,0.35)'; ctx.stroke();
}
}
function drawExitLine(){
ctx.save();
ctx.setLineDash([8,8]); ctx.strokeStyle='rgba(90,200,255,0.22)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(0,EXIT_Y); ctx.lineTo(VW,EXIT_Y); ctx.stroke();
ctx.restore();
ctx.fillStyle='rgba(120,200,255,0.45)'; ctx.font='12px system-ui';
ctx.fillText('EXIT', 12, EXIT_Y-6);
}
function render(){
const g=ctx.createLinearGradient(0,0,0,VH);
g.addColorStop(0,'#0c1220'); g.addColorStop(1,'#070a10');
ctx.fillStyle=g; ctx.fillRect(0,0,VW,VH);
drawStands();
ctx.fillStyle='#0a0f18'; ctx.fillRect(0,WALL_Y,VW,VH-WALL_Y); // concourse
drawWall();
if(params.column) drawPillar();
drawExitLine();
drawPeople();
}
function updateHUD(){
el.rate.textContent = outflowRate().toFixed(1);
el.timer.textContent = (empty?finalTime:elapsed).toFixed(1);
el.remaining.textContent = (total-evacCount);
el.evacuated.textContent = evacCount;
el.totalLbl.textContent = '/ '+total;
el.status.textContent = empty ? 'Section clear' : (paused?'Paused':'Evacuating');
el.urgencyVal.textContent = params.urgency.toFixed(2);
el.gateVal.textContent = params.gate.toFixed(1)+' yd';
}
// ---------- main loop ----------
function loop(t){
requestAnimationFrame(loop);
if(!lastT) lastT=t;
let dt=(t-lastT)/1000; lastT=t;
if(dt>0.1) dt=0.1;
if(running && !paused && !empty){
acc+=dt;
let steps=0;
while(acc>=H && steps<4){ step(); acc-=H; steps++; }
if(steps===4) acc=0;
}
render();
updateHUD();
}
// ---------- controls ----------
el.urgency.addEventListener('input',e=>{ params.urgency=parseFloat(e.target.value); });
el.gate.addEventListener('input',e=>{ params.gate=parseFloat(e.target.value); });
el.column.addEventListener('change',e=>{ params.column=e.target.checked; });
el.reset.addEventListener('click',reset);
el.pause.addEventListener('click',()=>{
if(empty) return;
paused=!paused; el.pause.textContent=paused?'Resume':'Pause';
});
function syncControls(){
el.urgency.value=params.urgency;
el.gate.value=params.gate;
el.column.checked=params.column;
}
// ---------- url params ----------
function parseHash(){
const h=(location.hash||'').replace(/^#/,'');
if(!h) return;
const out={};
h.split('&').forEach(kv=>{
if(!kv) return;
const i=kv.indexOf('=');
const k=decodeURIComponent(i<0?kv:kv.slice(0,i));
const v=decodeURIComponent(i<0?'':kv.slice(i+1));
out[k]=v;
});
if(out.urgency!==undefined){ const v=parseFloat(out.urgency); if(isFinite(v)) params.urgency=clamp(v,0,1); }
if(out.gate!==undefined){ const v=parseFloat(out.gate); if(isFinite(v)) params.gate=clamp(v,0.8,4); }
if(out.column!==undefined){ params.column=(out.column==='1'||out.column==='true'); }
}
// ---------- boot ----------
parseHash();
syncControls();
setupCanvas();
reset();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Where it behaves like a real crowd:
Where it does not, and what I special-cased:
desiredSpeed*1.3+30) to keep the explicit integrator from exploding at urgency 1.0.Net: it's a believable bottleneck-flow toy that gets the geometry, personal space, and non-overlap right, but its frictionless contacts mean the two famous counterintuitive crowd effects (faster-is-slower, pillar-improves-flow) are weak-to-absent — so treat my two numeric predictions as the honest expectation for this implementation, not as guaranteed constants.