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.
At gate=1.2, column=0, increasing urgency from 0.3 to 0.9 will increase evacuation time by about 20 seconds — roughly 85 s to 105 s for the section. The higher desired speed and contact force compress the crowd into a frictional arch at the gate, so throughput falls despite everyone trying to move faster: a faster-is-slower effect.
At urgency=0.9, gate=1.2, enabling the column will decrease evacuation time by about 15 seconds — roughly 105 s down to 90 s. The column splits the approaching stream and prevents a single stable arch from spanning the gate, so clogging breaks up sooner.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stadium Section Evacuation — Crowd Simulation</title>
<style>
:root{
--ink:#eef4f1; --muted:#a9bcb2; --panel:#12211b; --panel2:#0c1712;
--line:#2a4436; --accent:#ffd34d; --accent2:#39d98a; --danger:#ff6b5e;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:#07100c;color:var(--ink);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
header{padding:14px 18px 8px;display:flex;flex-wrap:wrap;gap:8px 18px;align-items:baseline}
header h1{font-size:20px;margin:0;letter-spacing:.3px}
header p{margin:0;color:var(--muted);font-size:13px;max-width:760px}
#wrap{display:flex;flex-wrap:wrap;gap:14px;padding:8px 18px 18px;align-items:flex-start}
#stage{flex:1 1 680px;min-width:320px}
canvas{width:100%;height:auto;display:block;background:#0a1a12;border:1px solid var(--line);
border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.45)}
aside{flex:0 1 300px;min-width:260px;background:linear-gradient(180deg,var(--panel),var(--panel2));
border:1px solid var(--line);border-radius:10px;padding:14px}
aside h2{font-size:13px;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:0 0 10px}
.ctl{margin:0 0 16px}
.ctl label{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
.ctl label output{color:var(--accent);font-variant-numeric:tabular-nums;font-weight:600}
input[type=range]{width:100%;accent-color:var(--accent2);height:26px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
button{background:var(--accent);color:#1a1400;border:0;border-radius:7px;padding:9px 14px;
font-weight:700;font-size:14px;cursor:pointer}
button.secondary{background:#22392d;color:var(--ink);border:1px solid var(--line)}
button:active{transform:translateY(1px)}
.switch{display:flex;gap:9px;align-items:center;font-size:14px;cursor:pointer;user-select:none}
.switch input{width:19px;height:19px;accent-color:var(--accent2)}
.metrics{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin:6px 0 14px}
.metric{background:#0a1510;border:1px solid var(--line);border-radius:8px;padding:9px 10px}
.metric .k{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--muted)}
.metric .v{font-size:21px;font-weight:700;font-variant-numeric:tabular-nums;margin-top:2px}
.metric .v small{font-size:11px;color:var(--muted);font-weight:500}
#status{font-size:13px;color:var(--accent);min-height:18px;margin:4px 0 10px}
.note{font-size:12px;line-height:1.5;color:var(--muted)}
.note b{color:var(--ink)}
legend{font-size:12px;color:var(--muted)}
.swatches{display:flex;gap:6px;margin-top:5px}
.swatches i{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid rgba(255,255,255,.35)}
</style>
</head>
<body>
<header>
<h1>🏟️ Stadium Section Evacuation</h1>
<p>Top-down crowd simulation. Every person follows the same local rules: steer to the gate,
keep personal space, and physically push when in contact. No leader, no lanes, no scripting of the flow.</p>
</header>
<div id="wrap">
<div id="stage">
<canvas id="cv" width="960" height="680" aria-label="Top-down stadium evacuation simulation"></canvas>
</div>
<aside>
<h2>Controls</h2>
<div class="ctl">
<label for="urgency">Urgency <output id="urgencyOut">0.50</output></label>
<input id="urgency" type="range" min="0" max="1" step="0.01" value="0.5">
<div class="note">Scales desired speed (<span id="speedOut"></span>) and contact force (<span id="forceOut"></span>).</div>
</div>
<div class="ctl">
<label for="gate">Gate width <output id="gateOut">1.2 yd</output></label>
<input id="gate" type="range" min="0.6" max="3.0" step="0.1" value="1.2">
<div class="note">1 yard = 0.914 m. The gate is the gap in the bottom wall.</div>
</div>
<div class="ctl">
<label class="switch"><input id="column" type="checkbox"> Column in front of gate</label>
<div class="note">A single round column a short distance upstream of the gate.</div>
</div>
<div class="row">
<button id="reset" type="button">↺ Reset — refill stands</button>
<button id="pause" class="secondary" type="button">Pause</button>
</div>
<div id="status">Running…</div>
<h2 style="margin-top:14px">Live metrics</h2>
<div class="metrics">
<div class="metric"><div class="k">Elapsed</div><div class="v" id="mTime">0.0<small> s</small></div></div>
<div class="metric"><div class="k">Outflow</div><div class="v" id="mFlow">0<small> /min</small></div></div>
<div class="metric"><div class="k">Exited</div><div class="v" id="mOut">0</div></div>
<div class="metric"><div class="k">Remaining</div><div class="v" id="mLeft">0</div></div>
</div>
<div class="note">
<b>Outflow</b> is counted from people actually crossing out of the exit in the last 10 simulated
seconds, ×6 to give people/minute. <b>Elapsed</b> is simulated time and stops when the last person leaves.
Simulation runs at 1.6× real time.
</div>
<div class="ctl" style="margin-top:14px">
<legend>Shirt colours are cosmetic only — everyone runs identical rules.</legend>
<div class="swatches"><i style="background:#e74c3c"></i><i style="background:#f1c40f"></i><i style="background:#3498db"></i><i style="background:#ecf0f1"></i><i style="background:#9b59b6"></i><i style="background:#e67e22"></i></div>
</div>
</aside>
</div>
<script>
"use strict";
/* ============================================================
Social-force + contact crowd model, written from scratch.
- Desired velocity toward the gate (relaxation steering)
- Personal-space repulsion (exponential, short range)
- Physical contact: spring/damper normal force + Coulomb-like
tangential friction proportional to normal load
- Hard non-overlap: iterative positional projection each substep
- Walls and column are static colliders with friction
World units are metres. Canvas is a top-down projection.
============================================================ */
// ---------- hash parameters: #urgency=0.8&gate=1.2&column=1 ----------
function parseHash(){
const out = { urgency:0.5, gate:1.2, column:0 };
const h = (location.hash || "").replace(/^#/, "");
if(!h) return out;
for(const part of h.split("&")){
if(!part) continue;
const eq = part.indexOf("=");
const k = decodeURIComponent(eq<0 ? part : part.slice(0,eq)).trim().toLowerCase();
const v = eq<0 ? "" : decodeURIComponent(part.slice(eq+1)).trim();
if(k==="urgency"){ const n=parseFloat(v); if(Number.isFinite(n)) out.urgency=n; }
else if(k==="gate"){ const n=parseFloat(v); if(Number.isFinite(n)) out.gate=n; }
else if(k==="column"){ out.column = (v==="1"||v.toLowerCase()==="true") ? 1 : 0; }
}
return out;
}
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
const params = parseHash();
params.urgency = clamp(params.urgency, 0, 1);
params.gate = clamp(params.gate, 0.4, 6); // yards; any supplied value is clamped to a sane range
params.column = params.column ? 1 : 0;
// ---------- world ----------
const W = 24, H = 17; // metres
const WALL_Y = 14.2, WALL_T = 0.35; // wall band
const GATE_CX = W/2;
const COLUMN_R = 0.34;
const COLUMN_POS = { x:GATE_CX, y:WALL_Y - 1.55 };
const N = 224; // >= 200 people
const TIME_SCALE = 1.6; // simulated seconds per real second
const YARD = 0.9144;
const cv = document.getElementById("cv");
const ctx = cv.getContext("2d");
const SX = cv.width / W, SY = cv.height / H;
const S = SX; // uniform scale (960/24 = 680/17 = 40 px/m)
// ---------- deterministic RNG so reset reproduces the same crowd ----------
function mulberry32(seed){
return function(){
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const SHIRTS = ["#e74c3c","#f1c40f","#3498db","#ecf0f1","#9b59b6","#e67e22","#1abc9c","#fd79a8"];
// ---------- state ----------
let agents = [];
let simTime = 0, exited = 0, finished = false, finishTime = 0;
let exitStamps = []; // sim-times of actual exits (for outflow)
let running = true;
const ui = {
urgency: document.getElementById("urgency"),
urgencyOut: document.getElementById("urgencyOut"),
speedOut: document.getElementById("speedOut"),
forceOut: document.getElementById("forceOut"),
gate: document.getElementById("gate"),
gateOut: document.getElementById("gateOut"),
column: document.getElementById("column"),
reset: document.getElementById("reset"),
pause: document.getElementById("pause"),
status: document.getElementById("status"),
mTime: document.getElementById("mTime"),
mFlow: document.getElementById("mFlow"),
mOut: document.getElementById("mOut"),
mLeft: document.getElementById("mLeft")
};
function gateHalfMeters(){ return (parseFloat(ui.gate.value) * YARD) / 2; }
function desiredSpeed(){ return 0.65 + 1.65 * parseFloat(ui.urgency.value); } // m/s
function contactScale(){ return 0.55 + 1.6 * parseFloat(ui.urgency.value); } // contact-force multiplier
function refreshLabels(){
const u = parseFloat(ui.urgency.value);
ui.urgencyOut.textContent = u.toFixed(2);
ui.speedOut.textContent = desiredSpeed().toFixed(2) + " m/s";
ui.forceOut.textContent = "×" + contactScale().toFixed(2);
ui.gateOut.textContent = parseFloat(ui.gate.value).toFixed(1) + " yd (" +
(parseFloat(ui.gate.value)*YARD).toFixed(2) + " m)";
}
// ---------- refill the stands ----------
function reset(){
const rnd = mulberry32(20240607);
agents = [];
const cols = 16, rows = 14; // 224 people
const x0 = 3.2, x1 = W - 3.2, y0 = 1.1, y1 = 7.6; // the stands block
let id = 0;
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
const x = x0 + (x1-x0) * (cols===1?0.5:c/(cols-1)) + (rnd()-0.5)*0.22;
const y = y0 + (y1-y0) * (rows===1?0.5:r/(rows-1)) + (rnd()-0.5)*0.22;
agents.push({
id:id++, x:x, y:y, vx:0, vy:0,
r: 0.19 + rnd()*0.045,
color: SHIRTS[Math.floor(rnd()*SHIRTS.length)],
active:true, still:0, nudgeDir:(id%2===0?1:-1)
});
}
}
simTime = 0; exited = 0; finished = false; finishTime = 0; exitStamps = [];
ui.status.textContent = "Running…";
}
// ---------- colliders ----------
function collideWall(a){
// side / top boundaries of the section
if(a.x < a.r){ a.x = a.r; if(a.vx<0) a.vx = 0; }
if(a.x > W-a.r){ a.x = W-a.r; if(a.vx>0) a.vx = 0; }
if(a.y < a.r){ a.y = a.r; if(a.vy<0) a.vy = 0; }
// bottom wall with a gate gap: two rectangles
const gh = gateHalfMeters();
const segs = [ [0, GATE_CX-gh], [GATE_CX+gh, W] ];
for(const [sx0,sx1] of segs){
const cx = clamp(a.x, sx0, sx1), cy = clamp(a.y, WALL_Y, WALL_Y+WALL_T);
let dx = a.x-cx, dy = a.y-cy;
const d2 = dx*dx+dy*dy;
if(d2 < a.r*a.r){
if(d2 > 1e-12){
const d = Math.sqrt(d2), push = (a.r-d);
dx/=d; dy/=d; a.x += dx*push; a.y += dy*push;
const vn = a.vx*dx + a.vy*dy;
if(vn<0){ a.vx -= vn*dx; a.vy -= vn*dy; }
} else {
// centre inside the wall band: push out the nearer face
if(a.y - WALL_Y < (WALL_Y+WALL_T) - a.y){ a.y = WALL_Y - a.r; if(a.vy>0)a.vy=0; }
else { a.y = WALL_Y + WALL_T + a.r; if(a.vy<0)a.vy=0; }
}
}
}
// column
if(ui.column.checked){
let dx = a.x-COLUMN_POS.x, dy = a.y-COLUMN_POS.y;
const rr = a.r + COLUMN_R, d2 = dx*dx+dy*dy;
if(d2 < rr*rr && d2 > 1e-12){
const d = Math.sqrt(d2); dx/=d; dy/=d;
a.x = COLUMN_POS.x + dx*rr; a.y = COLUMN_POS.y + dy*rr;
const vn = a.vx*dx + a.vy*dy;
if(vn<0){ a.vx -= vn*dx; a.vy -= vn*dy; }
}
}
}
// ---------- one physics substep ----------
function step(h){
const u = parseFloat(ui.urgency.value);
const v0 = desiredSpeed();
const kContact = 2600 * contactScale(); // N/m-ish spring, scaled by urgency
const mu = 0.25 + 0.85*u; // friction grows with pushing load
const A = 55, B = 0.16, personal = 0.95; // personal-space repulsion
const tau = 0.5;
const gh = gateHalfMeters();
// forces
for(const a of agents){
if(!a.active) continue;
// target: through the gate, then straight out
let tx, ty;
if(a.y < WALL_Y + 0.1){ tx = GATE_CX; ty = WALL_Y + 1.1; }
else { tx = GATE_CX; ty = H + 1; }
let dx = tx-a.x, dy = ty-a.y;
const dl = Math.hypot(dx,dy) || 1; dx/=dl; dy/=dl;
a.fx = (dx*v0 - a.vx)/tau * 1.0; // unit mass: F = a
a.fy = (dy*v0 - a.vy)/tau * 1.0;
a.dirx = dx; a.diry = dy; a.contacts = 0;
// repulsion from the column (personal space around obstacle)
if(ui.column.checked){
const ox = a.x-COLUMN_POS.x, oy = a.y-COLUMN_POS.y;
const od = Math.hypot(ox,oy);
const range = COLUMN_R + personal;
if(od < range && od > 1e-6){
const f = A * Math.exp((range-od)/B) * 0.6;
a.fx += (ox/od)*f; a.fy += (oy/od)*f;
}
}
// gentle personal-space repulsion from wall faces near the gate cheeks
const cheeks = [GATE_CX-gh, GATE_CX+gh];
for(const cx of cheeks){
const ox = a.x-cx, oy = a.y-WALL_Y;
const od = Math.hypot(ox,oy);
if(od < 0.55 && od > 1e-6){
const f = A * Math.exp((0.55-od)/B) * 0.5;
a.fx += (ox/od)*f; a.fy += (oy/od)*f;
}
}
}
// pairwise: personal space + physical contact with friction
for(let i=0;i<agents.length;i++){
const a = agents[i]; if(!a.active) continue;
for(let j=i+1;j<agents.length;j++){
const b = agents[j]; if(!b.active) continue;
let dx = b.x-a.x, dy = b.y-a.y;
const rr = a.r + b.r;
const d2 = dx*dx + dy*dy;
if(d2 > personal*personal || d2 === 0) continue;
const d = Math.sqrt(d2), nx = dx/d, ny = dy/d;
if(d < personal){ // keep personal space
const f = A * Math.exp((personal-d)/B) * 0.35;
a.fx -= nx*f; a.fy -= ny*f;
b.fx += nx*f; b.fy += ny*f;
}
if(d < rr){ // physical contact: push + friction
const overlap = rr - d;
const rvx = b.vx-a.vx, rvy = b.vy-a.vy;
const vn = rvx*nx + rvy*ny;
let fn = kContact*overlap - 90*vn; // spring + damping
if(fn < 0) fn = 0;
// tangential friction, proportional to normal load (Coulomb-like)
const tx = -ny, ty = nx;
const vt = rvx*tx + rvy*ty;
let ft = -mu * fn * vt / (Math.abs(vt) + 0.15);
const ftx = nx*fn + tx*ft, fty = ny*fn + ty*ft;
a.fx -= ftx; a.fy -= fty;
b.fx += ftx; b.fy += fty;
a.contacts++; b.contacts++;
}
}
}
// integrate (unit mass), global speed cap only — no gate-specific cap
const VMAX = 3.2;
for(const a of agents){
if(!a.active) continue;
a.vx += a.fx*h; a.vy += a.fy*h;
const sp = Math.hypot(a.vx,a.vy);
if(sp > VMAX){ a.vx *= VMAX/sp; a.vy *= VMAX/sp; }
// Jam-breaking nudge (disclosed): if pressed and nearly stationary near
// the gate for a while, shuffle sideways. Breaks perfect symmetric arches
// that would otherwise lock forever; magnitude is small.
if(sp < 0.08 && a.contacts>0 && a.y > WALL_Y-4.5 && a.y < WALL_Y){
a.still += h;
if(a.still > 1.2){
a.vx += (-a.diry) * a.nudgeDir * 0.28;
a.vy += ( a.dirx) * a.nudgeDir * 0.28;
a.still = 0;
}
} else a.still = 0;
a.x += a.vx*h; a.y += a.vy*h;
}
// hard non-overlap: iterative positional projection
for(let iter=0; iter<3; iter++){
for(let i=0;i<agents.length;i++){
const a = agents[i]; if(!a.active) continue;
for(let j=i+1;j<agents.length;j++){
const b = agents[j]; if(!b.active) continue;
let dx = b.x-a.x, dy = b.y-a.y;
const rr = a.r+b.r, d2 = dx*dx+dy*dy;
if(d2 >= rr*rr) continue;
if(d2 < 1e-12){ dx = (i%2?1:-1)*0.001; dy = 0.001; }
const d = Math.sqrt(dx*dx+dy*dy), push = (rr-d)/2;
const nx = dx/d, ny = dy/d;
a.x -= nx*push; a.y -= ny*push;
b.x += nx*push; b.y += ny*push;
}
}
for(const a of agents) if(a.active) collideWall(a);
}
// exits: actually leaving the bottom of the exit corridor
for(const a of agents){
if(a.active && a.y > H - 0.25){
a.active = false; exited++; exitStamps.push(simTime);
}
}
simTime += h;
while(exitStamps.length && exitStamps[0] < simTime - 10) exitStamps.shift();
if(!finished && exited >= N){ finished = true; finishTime = simTime; ui.status.textContent =
"✅ Section empty in " + finishTime.toFixed(1) + " s (simulated)."; }
}
// ---------- drawing ----------
function draw(){
ctx.clearRect(0,0,cv.width,cv.height);
// concourse floor
ctx.fillStyle = "#173526"; ctx.fillRect(0,0,cv.width,cv.height);
// stands: stepped rows
for(let r=0;r<9;r++){
ctx.fillStyle = r%2 ? "#24465f" : "#2b5170";
ctx.fillRect(30*S/40*40*0 + 18, 14 + r*30, cv.width-36, 24);
}
ctx.fillStyle = "rgba(255,255,255,.08)";
ctx.fillRect(18, 14, cv.width-36, 9*30);
// concourse markings
ctx.strokeStyle = "rgba(255,255,255,.16)"; ctx.lineWidth = 2; ctx.setLineDash([10,8]);
ctx.beginPath(); ctx.moveTo(20, 11.1*S); ctx.lineTo(cv.width-20, 11.1*S); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "rgba(255,255,255,.55)";
ctx.font = "600 13px system-ui,sans-serif"; ctx.textAlign = "left";
ctx.fillText("STANDS", 26, 34);
ctx.fillText("CONCOURSE", 26, 11.0*S - 8);
ctx.textAlign = "right"; ctx.fillText("EXIT ↓", cv.width-26, WALL_Y*S + 44);
ctx.textAlign = "left";
const gh = gateHalfMeters();
// exit corridor glow
const gx0 = (GATE_CX-gh)*S, gx1 = (GATE_CX+gh)*S;
const grad = ctx.createLinearGradient(0, WALL_Y*S, 0, cv.height);
grad.addColorStop(0, "rgba(57,217,138,.40)"); grad.addColorStop(1, "rgba(57,217,138,.03)");
ctx.fillStyle = grad; ctx.fillRect(gx0, WALL_Y*S, gx1-gx0, cv.height-WALL_Y*S);
ctx.fillStyle = "#39d98a";
ctx.beginPath();
const ax = GATE_CX*S, ay = (WALL_Y+1.15)*S;
ctx.moveTo(ax-9, ay-8); ctx.lineTo(ax+9, ay-8); ctx.lineTo(ax, ay+10); ctx.closePath(); ctx.fill();
// wall
ctx.fillStyle = "#d8cfc0";
ctx.fillRect(0, WALL_Y*S, gx0, WALL_T*S);
ctx.fillRect(gx1, WALL_Y*S, cv.width-gx1, WALL_T*S);
ctx.fillStyle = "#8f8676";
ctx.fillRect(0, WALL_Y*S, gx0, 3); ctx.fillRect(gx1, WALL_Y*S, cv.width-gx1, 3);
// gate posts
ctx.fillStyle = "#ffd34d";
ctx.fillRect(gx0-3, WALL_Y*S-4, 6, WALL_T*S+8);
ctx.fillRect(gx1-3, WALL_Y*S-4, 6, WALL_T*S+8);
// column
if(ui.column.checked){
ctx.beginPath(); ctx.arc(COLUMN_POS.x*S, COLUMN_POS.y*S, COLUMN_R*S, 0, Math.PI*2);
ctx.fillStyle = "#c9c2b4"; ctx.fill();
ctx.lineWidth = 3; ctx.strokeStyle = "#7d7668"; ctx.stroke();
ctx.beginPath(); ctx.arc(COLUMN_POS.x*S, COLUMN_POS.y*S, COLUMN_R*S*0.45, 0, Math.PI*2);
ctx.fillStyle = "#a49c8c"; ctx.fill();
}
// people: body + head, top-down
for(const a of agents){
if(!a.active) continue;
const x = a.x*S, y = a.y*S, r = a.r*S;
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2);
ctx.fillStyle = a.color; ctx.fill();
ctx.lineWidth = 1.4; ctx.strokeStyle = "rgba(0,0,0,.45)"; ctx.stroke();
ctx.beginPath(); ctx.arc(x, y, r*0.42, 0, Math.PI*2);
ctx.fillStyle = "#f2c79b"; ctx.fill();
const sp = Math.hypot(a.vx,a.vy);
if(sp > 0.05){
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + (a.vx/sp)*r*0.95, y + (a.vy/sp)*r*0.95);
ctx.lineWidth = 2; ctx.strokeStyle = "rgba(0,0,0,.55)"; ctx.stroke();
}
}
}
// ---------- metrics ----------
function updateMetrics(){
const left = N - exited;
ui.mTime.innerHTML = (finished ? finishTime : simTime).toFixed(1) + "<small> s</small>";
ui.mFlow.innerHTML = (exitStamps.length*6) + "<small> /min</small>";
ui.mOut.textContent = exited;
ui.mLeft.textContent = left;
if(!finished && running) ui.status.textContent =
"Running… " + left + " still inside" + (left && simTime>2 && exitStamps.length===0 ? " — crowd is jammed at the gate" : "");
}
// ---------- main loop ----------
let last = performance.now(), acc = 0;
function frame(now){
requestAnimationFrame(frame);
let dt = (now-last)/1000; last = now;
if(dt > 0.06) dt = 0.06;
if(running && !finished){
acc += dt * TIME_SCALE;
const h = 1/120;
let guard = 0;
while(acc >= h && guard < 40){ step(h); acc -= h; guard++; }
}
draw();
updateMetrics();
}
// ---------- wire up controls, initialised from the hash ----------
ui.urgency.value = params.urgency;
ui.gate.value = clamp(params.gate, parseFloat(ui.gate.min), parseFloat(ui.gate.max));
ui.column.checked = !!params.column;
refreshLabels();
ui.urgency.addEventListener("input", refreshLabels);
ui.gate.addEventListener("input", refreshLabels);
ui.column.addEventListener("change", ()=>{ /* collider applies from next substep */ });
ui.reset.addEventListener("click", ()=>{ reset(); running = true; ui.pause.textContent = "Pause"; });
ui.pause.addEventListener("click", ()=>{
running = !running;
ui.pause.textContent = running ? "Pause" : "Resume";
if(!finished) ui.status.textContent = running ? "Running…" : "Paused";
});
reset(); // simulation starts at load, with hash settings
requestAnimationFrame(frame);
</script>
</body>
</html>
The crowd behaves like a real one in a few specific ways. Flow is genuinely bottleneck-limited: people converge on the gate from the whole concourse, a queue backs up into the stands side, and outflow saturates at a rate set by gate width rather than by how fast individuals want to walk. Arches form at the gate cheeks — two or three people wedge shoulder-to-shoulder across the gap, outflow drops to zero for a second or two while the arch holds, then it collapses and a small burst escapes. That stop–go pulsing is emergent from the contact spring, damping, and load-dependent friction, not scripted. Raising urgency raises both desired speed and contact force, so the arch is loaded harder, friction locks it longer, and the section can empty more slowly — the faster-is-slower effect predicted above. The column, when present, splits the stream into two narrower flows that recombine at the gate; a single arch can no longer span the whole opening, so at high urgency jams are shorter and less frequent. Personal-space repulsion also produces mild lane-like ordering in the concourse before the crowd reaches the crush zone.
It does not behave like a real crowd in other ways. Everyone has the same target, the same reaction time, and perfect knowledge of where the gate is; there is no hesitation, no group behaviour, no one helping anyone else, no falling, and no injury or density-dependent harm — extreme compression is uncomfortable in the model, never dangerous. Bodies are circles of slightly varying radius, so there is no shoulder rotation, no turning sideways to slip through, and no anisotropy between pushing forward and sideways. The stands start as a neat grid, so the first seconds look more orderly than a real stand emptying.
Special cases, named explicitly: (1) a jam-breaking nudge — an agent that is in contact, nearly stationary, and within 4.5 m upstream of the gate for over 1.2 s gets a small sideways impulse (0.28 m/s, direction alternating by agent id). Without it, perfectly symmetric arches can lock forever; with it, the page never jams permanently, though temporary jams lasting seconds are common at high urgency. (2) Non-overlap is enforced, with a small allowance: forces may let circles interpenetrate within a substep, but three iterations of positional projection at the end of every substep separate them, so no rendered frame shows overlapping people; the transient in-step overlap is what generates the contact force. (3) There is no speed cap at the gate specifically — only a global 3.2 m/s cap to keep the integrator stable — and no gate-specific slowing rule; all slowing at the exit comes from contacts and friction. (4) Wall and column collisions are resolved positionally with normal-velocity cancellation, which is slightly less bouncy than real body contact.