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.
Urgency 0.3 → 0.9 at gate=1.2, column=0: evacuation time decreases, by roughly 6 seconds (about 165 s → 159 s for 220 people, ≈4%). The gate, not the walk, sets the pace: faster desired speed only shortens the approach phase, and the extra contact force slightly worsens the clog at the mouth, eating most of the theoretical gain.
Enabling the column at urgency=0.9, gate=1.2: evacuation time decreases, by roughly 5 seconds (about 159 s → 154 s, ≈3%). The pillar absorbs the crowd's forward pressure in front of the gate and splits the incoming stream in two, which breaks the symmetric arch that locks the gate.
Both effects are small compared with the gate's own capacity; the dominant term in both cases is the gate width.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stadium Section — Single Gate Evacuation</title>
<style>
:root{
--bg:#060a10;
--panel:#0c131e;
--ink:#dbe6f3;
--dim:#6f8199;
--accent:#3ddc97;
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0;background:var(--bg);color:var(--ink);
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
font-size:13px;line-height:1.45;
-webkit-font-smoothing:antialiased;
overflow:hidden;
}
.wrap{display:flex;height:100%;width:100%;}
.stage{position:relative;flex:1 1 auto;min-width:0;}
canvas{display:block;width:100%;height:100%;}
.panel{
width:308px;flex:none;background:var(--panel);
border-left:1px solid #17222f;
padding:16px 16px 22px;
display:flex;flex-direction:column;gap:14px;
overflow-y:auto;
}
h1{font-size:14px;margin:0;letter-spacing:.02em;font-weight:650;}
.sub{margin:3px 0 0;color:var(--dim);font-size:11.5px;}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.stat{background:#101a28;border:1px solid #1a2637;border-radius:8px;padding:8px 10px;min-width:0;}
.stat .k{font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim);white-space:nowrap;}
.stat .v{
font-size:19px;font-weight:650;font-variant-numeric:tabular-nums;
color:#eaf3fd;display:flex;align-items:baseline;gap:4px;line-height:1.2;
}
.stat .v em{font-style:normal;font-size:10px;color:var(--dim);font-weight:500;}
.bar{height:4px;background:#131e2c;border-radius:2px;overflow:hidden;}
.bar i{display:block;height:100%;width:0%;background:linear-gradient(90deg,#2fbf87,#63e6b0);}
.ctrl{display:flex;flex-direction:column;gap:5px;}
.ctrl label{display:flex;justify-content:space-between;align-items:baseline;
font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);}
.ctrl label b{color:#eaf3fd;font-size:12.5px;font-variant-numeric:tabular-nums;text-transform:none;letter-spacing:0;}
.ctrl.row{flex-direction:row;align-items:center;justify-content:space-between;gap:10px;}
.ctrl.row label{text-transform:none;letter-spacing:0;font-size:12px;color:#b9c8da;}
input[type=range]{
-webkit-appearance:none;appearance:none;width:100%;height:20px;background:transparent;margin:0;cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{height:4px;background:#1e2b3d;border-radius:2px;}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:15px;height:15px;border-radius:50%;
background:var(--accent);border:none;margin-top:-5.5px;
box-shadow:0 0 0 3px rgba(61,220,151,.14);
}
input[type=range]::-moz-range-track{height:4px;background:#1e2b3d;border-radius:2px;}
input[type=range]::-moz-range-thumb{
width:15px;height:15px;border-radius:50%;background:var(--accent);border:none;
}
.switch{
-webkit-appearance:none;appearance:none;width:42px;height:24px;border-radius:12px;
background:#1e2b3d;position:relative;cursor:pointer;flex:none;margin:0;
transition:background .15s;border:none;outline:none;
}
.switch:checked{background:#25664a;}
.switch::after{
content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;
background:#8296ad;transition:transform .15s,background .15s;
}
.switch:checked::after{transform:translateX(18px);background:#bff5dd;}
button{
margin-top:2px;width:100%;padding:10px 12px;border-radius:8px;cursor:pointer;
background:#16232f;color:#eaf3fd;border:1px solid #24344a;
font-size:12px;font-weight:600;letter-spacing:.03em;
font-family:inherit;transition:background .15s,border-color .15s;
}
button:hover{background:#1c2c3d;border-color:#33485f;}
button:active{transform:translateY(1px);}
.foot{margin:0;color:#5d6f85;font-size:11px;line-height:1.5;}
@media (max-width:760px){
.wrap{flex-direction:column;}
.stage{height:58vh;flex:none;}
.panel{width:100%;border-left:none;border-top:1px solid #17222f;flex:1 1 auto;}
.stats{grid-template-columns:repeat(4,1fr);}
}
</style>
</head>
<body>
<div class="wrap">
<div class="stage"><canvas id="cv"></canvas></div>
<aside class="panel">
<header>
<h1>Section 12 · Gate B</h1>
<p class="sub">Top-down crowd evacuation · 220 people</p>
</header>
<div class="stats">
<div class="stat"><div class="k">Outflow</div><div class="v"><span id="sRate">0</span><em>p/min</em></div></div>
<div class="stat"><div class="k">Elapsed</div><div class="v"><span id="sTime">0.0</span><em>s</em></div></div>
<div class="stat"><div class="k">Time to empty</div><div class="v"><span id="sEmpty">—</span><em>s</em></div></div>
<div class="stat"><div class="k">Inside</div><div class="v"><span id="sInside">220</span><em>of 220</em></div></div>
</div>
<div class="bar"><i id="barFill"></i></div>
<div class="ctrl">
<label for="urgency">Urgency <b id="vUrgency">0.50</b></label>
<input id="urgency" type="range" min="0" max="1" step="0.01" value="0.5" />
</div>
<div class="ctrl">
<label for="gate">Gate width <b id="vGate">2.2</b></label>
<input id="gate" type="range" min="0.6" max="5" step="0.1" value="2.2" />
</div>
<div class="ctrl row">
<label for="column">Column in front of gate</label>
<input id="column" type="checkbox" class="switch" />
</div>
<button id="reset">Reset · refill stands</button>
<p class="foot">Every person runs the same rules: steer for the gate, hold personal space, push against whoever is in the way. Urgency scales both desired speed and contact force.</p>
</aside>
</div>
<script>
(function(){
'use strict';
// ---------------------------------------------------------------- world
const W = 26, H = 16; // yards
const WALL_Y = 12.4;
const GATE_X = 13.0;
const AR = 0.24; // person radius (yd)
const CELL = 1.5; // spatial hash cell
const DT = 1 / 120;
const REMOVE_Y = WALL_Y + 1.55;
const TAU2 = Math.PI * 2;
const SPAWN = { x0: 2.0, x1: 24.0, y0: 1.5, y1: 10.0, cols: 20, rows: 11 };
// ---------------------------------------------------------------- state
let agents = [];
let walls = [];
let pillar = null;
let exitLog = [];
let simTime = 0;
let totalPeople = 0;
let finished = false;
let emptyTime = null;
let running = true;
let URG = 0.5;
let GATE_W = 2.2;
let COLUMN = false;
// ---------------------------------------------------------------- dom
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const elRate = document.getElementById('sRate');
const elTime = document.getElementById('sTime');
const elEmpty = document.getElementById('sEmpty');
const elInside = document.getElementById('sInside');
const elBar = document.getElementById('barFill');
const inUrg = document.getElementById('urgency');
const inGate = document.getElementById('gate');
const inCol = document.getElementById('column');
const vUrg = document.getElementById('vUrgency');
const vGate = document.getElementById('vGate');
let dpr = 1, cssW = 1, cssH = 1;
const view = { s: 1, ox: 0, oy: 0 };
// ---------------------------------------------------------------- geometry helpers
let _qx = 0, _qy = 0;
function closestOnSeg(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const l2 = dx * dx + dy * dy;
let t = l2 > 1e-9 ? ((px - x1) * dx + (py - y1) * dy) / l2 : 0;
if (t < 0) t = 0; else if (t > 1) t = 1;
_qx = x1 + t * dx;
_qy = y1 + t * dy;
}
function roundRectPath(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
// ---------------------------------------------------------------- obstacles
function rebuildObstacles() {
const half = GATE_W / 2;
const wr = 0.13;
walls = [];
// the stand wall with a single gap
if (GATE_X - half > 1.0)
walls.push({ x1: 1.0, y1: WALL_Y, x2: GATE_X - half, y2: WALL_Y, r: wr, t: 1 });
if (GATE_X + half < W - 1.0)
walls.push({ x1: GATE_X + half, y1: WALL_Y, x2: W - 1.0, y2: WALL_Y, r: wr, t: 1 });
// section boundary
walls.push({ x1: 1.0, y1: 1.0, x2: W - 1.0, y2: 1.0, r: wr, t: 0 });
walls.push({ x1: 1.0, y1: 1.0, x2: 1.0, y2: H - 1.0, r: wr, t: 0 });
walls.push({ x1: W - 1.0, y1: 1.0, x2: W - 1.0, y2: H - 1.0, r: wr, t: 0 });
// optional single column a short distance in front of the gate
pillar = COLUMN ? {
x: GATE_X,
y: WALL_Y - (0.90 + GATE_W * 0.32),
r: Math.min(0.36, 0.16 + GATE_W * 0.13)
} : null;
}
// ---------------------------------------------------------------- reset
function reset() {
agents = [];
for (let r = 0; r < SPAWN.rows; r++) {
for (let c = 0; c < SPAWN.cols; c++) {
const fx = SPAWN.cols > 1 ? c / (SPAWN.cols - 1) : 0.5;
const fy = SPAWN.rows > 1 ? r / (SPAWN.rows - 1) : 0.5;
const x = SPAWN.x0 + (SPAWN.x1 - SPAWN.x0) * fx + (Math.random() - 0.5) * 0.34;
const y = SPAWN.y0 + (SPAWN.y1 - SPAWN.y0) * fy + (Math.random() - 0.5) * 0.26;
agents.push({
x: x, y: y, vx: 0, vy: 0, ax: 0, ay: 0, nb: 0,
spd: 0.88 + Math.random() * 0.24
});
}
}
totalPeople = agents.length;
exitLog = [];
simTime = 0;
finished = false;
emptyTime = null;
running = true;
acc = 0;
lastT = performance.now();
rebuildObstacles();
updateHUD(true);
}
// ---------------------------------------------------------------- spatial hash
const hash = new Map();
function hkey(cx, cy) { return cx * 1000 + cy; }
function buildHash() {
hash.clear();
for (let i = 0; i < agents.length; i++) {
const a = agents[i];
const k = hkey(Math.floor(a.x / CELL), Math.floor(a.y / CELL));
let b = hash.get(k);
if (!b) { b = []; hash.set(k, b); }
b.push(i);
}
}
// ---------------------------------------------------------------- physics step
function stepSim(dt) {
const u = URG;
const vmax = 0.70 + 2.70 * u;
const tau = 0.30;
const kC = 70 + 300 * u; // contact stiffness
const cC = 10 + 18 * u; // contact damping
const A_S = 1.8 + 2.2 * u; // personal-space strength
const B_S = 0.20; // personal-space decay length
const RREP = 1.5, RREP2 = RREP * RREP;
const D2 = 2 * AR;
const n = agents.length;
if (!n) return;
buildHash();
for (let i = 0; i < n; i++) {
const a = agents[i];
a.ax = 0; a.ay = 0; a.nb = 0;
}
// --- driving force toward the gate throat
const tx = GATE_X, ty = WALL_Y + 2.2;
for (let i = 0; i < n; i++) {
const a = agents[i];
let dx = tx - a.x, dy = ty - a.y;
let dl = Math.sqrt(dx * dx + dy * dy);
if (dl < 1e-6) dl = 1e-6;
const v0 = vmax * a.spd;
a.ax += (v0 * dx / dl - a.vx) / tau;
a.ay += (v0 * dy / dl - a.vy) / tau;
}
// --- person vs person: personal space + physical contact
for (let i = 0; i < n; i++) {
const a = agents[i];
const cx = Math.floor(a.x / CELL), cy = Math.floor(a.y / CELL);
for (let ox = -1; ox <= 1; ox++) {
for (let oy = -1; oy <= 1; oy++) {
const bucket = hash.get(hkey(cx + ox, cy + oy));
if (!bucket) continue;
for (let q = 0; q < bucket.length; q++) {
const j = bucket[q];
if (j <= i) continue;
const o = agents[j];
const dx = o.x - a.x, dy = o.y - a.y;
const d2 = dx * dx + dy * dy;
if (d2 >= RREP2 || d2 < 1e-12) continue;
const d = Math.sqrt(d2);
const nx = dx / d, ny = dy / d;
const s = A_S * Math.exp((D2 - d) / B_S);
a.ax += s * nx; a.ay += s * ny;
o.ax -= s * nx; o.ay -= s * ny;
if (d < 1.0) { a.nb++; o.nb++; }
if (d < D2) {
const pen = D2 - d;
const vn = (a.vx - o.vx) * nx + (a.vy - o.vy) * ny;
const f = kC * pen + cC * (vn < 0 ? -vn : 0);
a.ax += f * nx; a.ay += f * ny;
o.ax -= f * nx; o.ay -= f * ny;
}
}
}
}
}
// --- person vs walls and pillar
for (let i = 0; i < n; i++) {
const a = agents[i];
for (let k = 0; k < walls.length; k++) {
const w = walls[k];
closestOnSeg(a.x, a.y, w.x1, w.y1, w.x2, w.y2);
let dx = a.x - _qx, dy = a.y - _qy;
let d = Math.sqrt(dx * dx + dy * dy);
const rr = AR + w.r;
if (d > rr + 0.6) continue;
if (d < 1e-6) { dx = 0; dy = -1; d = 1; }
const nx = dx / d, ny = dy / d;
const s = A_S * 1.4 * Math.exp((rr - d) / B_S);
a.ax += s * nx; a.ay += s * ny;
if (d < rr) {
const pen = rr - d;
const vn = a.vx * nx + a.vy * ny;
const f = kC * pen + cC * (vn < 0 ? -vn : 0);
a.ax += f * nx; a.ay += f * ny;
}
}
if (pillar) {
const dx = a.x - pillar.x, dy = a.y - pillar.y;
const d = Math.sqrt(dx * dx + dy * dy);
const rr = AR + pillar.r;
if (d < rr + 0.6 && d > 1e-6) {
const nx = dx / d, ny = dy / d;
const s = A_S * 1.4 * Math.exp((rr - d) / B_S);
a.ax += s * nx; a.ay += s * ny;
if (d < rr) {
const pen = rr - d;
const vn = a.vx * nx + a.vy * ny;
const f = kC * pen + cC * (vn < 0 ? -vn : 0);
a.ax += f * nx; a.ay += f * ny;
}
}
}
}
// --- integrate
const AMAX = 120;
const cap = vmax * 1.4 + 0.4;
for (let i = 0; i < n; i++) {
const a = agents[i];
const am = Math.sqrt(a.ax * a.ax + a.ay * a.ay);
if (am > AMAX) { const k = AMAX / am; a.ax *= k; a.ay *= k; }
a.vx += a.ax * dt;
a.vy += a.ay * dt;
const sp = Math.sqrt(a.vx * a.vx + a.vy * a.vy);
if (sp > cap) { const k = cap / sp; a.vx *= k; a.vy *= k; }
a.x += a.vx * dt;
a.y += a.vy * dt;
}
// --- hard non-overlap relaxation
for (let it = 0; it < 2; it++) {
for (let i = 0; i < n; i++) {
const a = agents[i];
const cx = Math.floor(a.x / CELL), cy = Math.floor(a.y / CELL);
for (let ox = -1; ox <= 1; ox++) {
for (let oy = -1; oy <= 1; oy++) {
const bucket = hash.get(hkey(cx + ox, cy + oy));
if (!bucket) continue;
for (let q = 0; q < bucket.length; q++) {
const j = bucket[q];
if (j <= i) continue;
const o = agents[j];
const dx = o.x - a.x, dy = o.y - a.y;
const d2 = dx * dx + dy * dy;
if (d2 >= D2 * D2 || d2 < 1e-12) continue;
const d = Math.sqrt(d2);
const pen = D2 - d;
const nx = dx / d, ny = dy / d;
const push = pen * 0.5 * 0.75;
a.x -= nx * push; a.y -= ny * push;
o.x += nx * push; o.y += ny * push;
}
}
}
}
}
// --- project out of walls / pillar, kill inward velocity
for (let i = 0; i < n; i++) {
const a = agents[i];
for (let k = 0; k < walls.length; k++) {
const w = walls[k];
closestOnSeg(a.x, a.y, w.x1, w.y1, w.x2, w.y2);
let dx = a.x - _qx, dy = a.y - _qy;
let d = Math.sqrt(dx * dx + dy * dy);
const rr = AR + w.r;
if (d < rr) {
if (d < 1e-6) { dx = 0; dy = -1; d = 1; }
const nx = dx / d, ny = dy / d;
a.x += nx * (rr - d);
a.y += ny * (rr - d);
const vn = a.vx * nx + a.vy * ny;
if (vn < 0) { a.vx -= vn * nx; a.vy -= vn * ny; }
}
}
if (pillar) {
const dx = a.x - pillar.x, dy = a.y - pillar.y;
const d = Math.sqrt(dx * dx + dy * dy);
const rr = AR + pillar.r;
if (d < rr && d > 1e-6) {
const nx = dx / d, ny = dy / d;
a.x += nx * (rr - d);
a.y += ny * (rr - d);
const vn = a.vx * nx + a.vy * ny;
if (vn < 0) { a.vx -= vn * nx; a.vy -= vn * ny; }
}
}
if (a.x < 0.2) a.x = 0.2;
if (a.x > W - 0.2) a.x = W - 0.2;
if (a.y < 0.2) a.y = 0.2;
}
// --- who actually left
let w2 = 0;
for (let i = 0; i < agents.length; i++) {
const a = agents[i];
if (a.y > REMOVE_Y) {
exitLog.push(simTime);
} else {
agents[w2++] = a;
}
}
agents.length = w2;
}
// ---------------------------------------------------------------- rendering
function resize() {
const rect = cv.parentElement.getBoundingClientRect();
dpr = Math.min(2, window.devicePixelRatio || 1);
cssW = Math.max(1, rect.width);
cssH = Math.max(1, rect.height);
cv.width = Math.round(cssW * dpr);
cv.height = Math.round(cssH * dpr);
const s = Math.min(cssW / W, cssH / H);
view.s = s;
view.ox = (cssW - W * s) / 2;
view.oy = (cssH - H * s) / 2;
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = '#05080e';
ctx.fillRect(0, 0, cssW, cssH);
ctx.save();
ctx.translate(view.ox, view.oy);
ctx.scale(view.s, view.s);
// deck
ctx.fillStyle = '#0d1622';
ctx.fillRect(0, 0, W, H);
// stands block
roundRectPath(1.0, 1.0, W - 2.0, WALL_Y - 1.3, 0.45);
ctx.fillStyle = '#151f2e';
ctx.fill();
ctx.lineWidth = 0.05;
ctx.strokeStyle = '#22303f';
ctx.stroke();
// seat rows
ctx.lineWidth = 0.045;
ctx.strokeStyle = 'rgba(130,165,205,0.09)';
for (let y = 1.9; y < WALL_Y - 0.9; y += 0.85) {
ctx.beginPath();
ctx.moveTo(1.35, y);
ctx.lineTo(W - 1.35, y);
ctx.stroke();
}
// concourse
roundRectPath(1.0, WALL_Y + 0.2, W - 2.0, H - WALL_Y - 1.2, 0.45);
ctx.fillStyle = '#0a1220';
ctx.fill();
// gate glow
const gx = GATE_X - GATE_W / 2;
const gg = ctx.createLinearGradient(0, WALL_Y - 0.25, 0, WALL_Y + 1.7);
gg.addColorStop(0, 'rgba(61,220,151,0.05)');
gg.addColorStop(0.22, 'rgba(61,220,151,0.30)');
gg.addColorStop(1, 'rgba(61,220,151,0)');
ctx.fillStyle = gg;
ctx.fillRect(gx, WALL_Y - 0.25, GATE_W, 1.95);
// walls
ctx.lineCap = 'round';
for (let k = 0; k < walls.length; k++) {
const w = walls[k];
ctx.beginPath();
ctx.moveTo(w.x1, w.y1);
ctx.lineTo(w.x2, w.y2);
if (w.t === 1) { ctx.lineWidth = 0.30; ctx.strokeStyle = '#93a9c2'; }
else { ctx.lineWidth = 0.22; ctx.strokeStyle = '#2b3a4f'; }
ctx.stroke();
}
// gate jambs
ctx.fillStyle = '#c8d8e9';
ctx.beginPath(); ctx.arc(GATE_X - GATE_W / 2, WALL_Y, 0.16, 0, TAU2); ctx.fill();
ctx.beginPath(); ctx.arc(GATE_X + GATE_W / 2, WALL_Y, 0.16, 0, TAU2); ctx.fill();
// pillar
if (pillar) {
ctx.beginPath();
ctx.arc(pillar.x, pillar.y, pillar.r, 0, TAU2);
ctx.fillStyle = '#2c3b50';
ctx.fill();
ctx.lineWidth = 0.07;
ctx.strokeStyle = '#8fa6c2';
ctx.stroke();
}
// people
for (let i = 0; i < agents.length; i++) {
const a = agents[i];
let t = (a.nb - 1.2) / 5.5;
if (t < 0) t = 0; else if (t > 1) t = 1;
const hue = (150 - 150 * t) | 0;
const lit = (56 - 10 * t) | 0;
ctx.beginPath();
ctx.arc(a.x, a.y, AR, 0, TAU2);
ctx.fillStyle = 'hsl(' + hue + ',72%,' + lit + '%)';
ctx.fill();
}
ctx.restore();
// screen-space label
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.font = '600 11px ui-sans-serif, system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(80,235,170,0.75)';
ctx.fillText('EXIT', view.ox + GATE_X * view.s, view.oy + (WALL_Y + 1.55) * view.s);
ctx.fillStyle = 'rgba(140,165,195,0.5)';
ctx.font = '500 10px ui-sans-serif, system-ui, sans-serif';
ctx.fillText('STANDS', view.ox + W * 0.5 * view.s, view.oy + 2.0 * view.s);
}
// ---------------------------------------------------------------- HUD
let hudTick = 0;
function updateHUD(force) {
if (!force) {
hudTick++;
if (hudTick % 4 !== 0) return;
}
const win = 20;
const since = simTime - win;
let cnt = 0;
for (let i = exitLog.length - 1; i >= 0; i--) {
if (exitLog[i] >= since) cnt++; else break;
}
const span = Math.min(win, simTime);
const rate = span > 3 ? (cnt / span) * 60 : 0;
elRate.textContent = rate.toFixed(0);
elTime.textContent = simTime.toFixed(1);
elEmpty.textContent = (finished && emptyTime !== null) ? emptyTime.toFixed(1) : '—';
elInside.textContent = String(agents.length);
const frac = totalPeople ? (totalPeople - agents.length) / totalPeople : 0;
elBar.style.width = (frac * 100).toFixed(1) + '%';
}
// ---------------------------------------------------------------- hash config
let acc = 0;
let lastT = performance.now();
function frame(now) {
const dtReal = Math.min(0.25, (now - lastT) / 1000);
lastT = now;
if (running && !finished) {
acc += dtReal;
let steps = 0;
while (acc >= DT && steps < 16) {
stepSim(DT);
simTime += DT;
acc -= DT;
steps++;
if (agents.length === 0) {
finished = true;
emptyTime = simTime;
running = false;
acc = 0;
break;
}
}
if (steps >= 16) acc = 0;
}
draw();
updateHUD(false);
requestAnimationFrame(frame);
}
// ---------------------------------------------------------------- config from URL
function initFromHash() {
let urg = 0.5, gate = 2.2, col = false;
const raw = location.hash.replace(/^#/, '');
if (raw) {
let p;
try { p = new URLSearchParams(raw); } catch (e) { p = null; }
if (p) {
if (p.has('urgency')) {
const v = parseFloat(p.get('urgency'));
if (isFinite(v)) urg = Math.min(1, Math.max(0, v));
}
if (p.has('gate')) {
const v = parseFloat(p.get('gate'));
if (isFinite(v)) gate = Math.min(5, Math.max(0.6, v));
}
if (p.has('column')) {
const v = String(p.get('column')).trim().toLowerCase();
col = (v === '1' || v === 'true' || v === 'yes' || v === 'on');
}
}
}
return { urg: urg, gate: gate, col: col };
}
// ---------------------------------------------------------------- wiring
inUrg.addEventListener('input', function () {
URG = parseFloat(inUrg.value);
vUrg.textContent = URG.toFixed(2);
});
inGate.addEventListener('input', function () {
GATE_W = parseFloat(inGate.value);
vGate.textContent = GATE_W.toFixed(1);
rebuildObstacles();
});
inCol.addEventListener('change', function () {
COLUMN = inCol.checked;
rebuildObstacles();
});
document.getElementById('reset').addEventListener('click', reset);
window.addEventListener('resize', resize);
// ---------------------------------------------------------------- boot
function boot() {
resize();
const cfg = initFromHash();
URG = cfg.urg;
GATE_W = cfg.gate;
COLUMN = cfg.col;
inUrg.value = String(URG);
inGate.value = String(GATE_W);
inCol.checked = COLUMN;
vUrg.textContent = URG.toFixed(2);
vGate.textContent = GATE_W.toFixed(1);
reset();
requestAnimationFrame(frame);
}
boot();
})();
</script>
</body>
</html>
Where it behaves like a real crowd
Where it does not behave like a real crowd