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, should increase evacuation time by approximately 26 seconds—about 228 s → 254 s—as denser, higher-force arches outweigh the higher desired speed.urgency=0.9&gate=1.2 should decrease evacuation time by approximately 18 seconds—about 254 s → 236 s—by dividing the approaching crowd and disrupting some gate-spanning arches.<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<link rel="icon" href="data:,">
<title>Section 114 — One Gate</title>
<style>
:root {
--bg: #0b1114;
--panel: #131d21;
--panel-2: #172328;
--line: rgba(224, 234, 226, .12);
--text: #edf0e8;
--muted: #91a19f;
--amber: #edb56d;
--green: #86c6a5;
--red: #df8b75;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--text);
background:
radial-gradient(circle at 12% -10%, rgba(80, 119, 116, .16), transparent 35rem),
radial-gradient(circle at 100% 15%, rgba(237, 181, 109, .07), transparent 30rem),
var(--bg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button, input { font: inherit; }
button { cursor: pointer; }
.shell {
width: min(1400px, 100%);
margin: 0 auto;
padding: 26px clamp(14px, 3vw, 34px) 34px;
}
.masthead {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 20px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--amber);
font-size: 11px;
font-weight: 750;
letter-spacing: .18em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(28px, 4vw, 48px);
font-weight: 630;
letter-spacing: -.045em;
line-height: 1.03;
}
.intro {
max-width: 450px;
margin: 0 0 3px;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.workbench {
display: grid;
grid-template-columns: minmax(0, 1fr) 326px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 20px;
background: rgba(16, 25, 29, .92);
box-shadow: 0 26px 80px rgba(0, 0, 0, .28);
}
.stage {
min-width: 0;
padding: 15px;
}
.stagebar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 35px;
padding: 0 2px 11px;
}
.view-name {
color: #b9c5c0;
font-size: 10px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--green);
font-size: 10px;
font-weight: 750;
letter-spacing: .12em;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 12%, transparent);
}
.status[data-state="live"] .status-dot { animation: pulse 1.8s ease-in-out infinite; }
.status[data-state="paused"] { color: var(--amber); }
.status[data-state="empty"] { color: #b8c8c2; }
@keyframes pulse {
50% { opacity: .42; transform: scale(.8); }
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 32 / 23;
border: 1px solid rgba(255, 255, 255, .08);
border-radius: 12px;
background: #152025;
}
.stage-footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px 18px;
padding: 12px 3px 0;
color: #7f918f;
font-size: 10px;
line-height: 1.5;
}
.legend {
display: inline-flex;
align-items: center;
gap: 7px;
}
.legend-swatch {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--amber);
}
.panel {
padding: 22px;
border-left: 1px solid var(--line);
background: linear-gradient(180deg, rgba(25, 37, 42, .98), rgba(17, 27, 31, .98));
}
.clock-block {
padding-bottom: 18px;
border-bottom: 1px solid var(--line);
}
.metric-label {
display: block;
margin-bottom: 5px;
color: #879996;
font-size: 9px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
}
.clock {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 48px;
font-weight: 520;
letter-spacing: -.06em;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.metric-note {
margin-top: 5px;
color: var(--muted);
font-size: 11px;
line-height: 1.45;
}
.flow-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
padding: 17px 0;
border-bottom: 1px solid var(--line);
}
.flow-value {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 28px;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.flow-unit {
color: #a5b3af;
font-size: 10px;
}
.count-line {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 12px 0 18px;
color: #92a29f;
font-size: 11px;
}
.count-line strong {
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-weight: 550;
font-variant-numeric: tabular-nums;
}
.controls-title {
margin: 0 0 15px;
color: #b8c4c0;
font-size: 10px;
font-weight: 750;
letter-spacing: .15em;
text-transform: uppercase;
}
.control { margin-bottom: 19px; }
.control-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 9px;
}
.control-head label {
color: #dbe2dc;
font-size: 12px;
font-weight: 600;
}
.control-head output {
color: var(--amber);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 12px;
font-variant-numeric: tabular-nums;
}
input[type="range"] {
width: 100%;
height: 18px;
margin: 0;
accent-color: var(--amber);
cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 3px;
border-radius: 4px;
background: #354348;
}
input[type="range"]::-webkit-slider-thumb {
width: 15px;
height: 15px;
margin-top: -6px;
border: 2px solid #1a2529;
border-radius: 50%;
background: var(--amber);
-webkit-appearance: none;
}
input[type="range"] { -webkit-appearance: none; background: transparent; }
.range-ends {
display: flex;
justify-content: space-between;
margin-top: -1px;
color: #617572;
font-size: 9px;
}
.control-detail {
min-height: 15px;
margin-top: 5px;
color: #829491;
font-size: 10px;
line-height: 1.45;
}
.toggle {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(255, 255, 255, .018);
cursor: pointer;
}
.toggle-copy strong {
display: block;
color: #dbe2dc;
font-size: 12px;
font-weight: 600;
}
.toggle-copy small {
display: block;
margin-top: 3px;
color: #7f918e;
font-size: 9px;
line-height: 1.4;
}
.toggle input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.switch {
position: relative;
flex: 0 0 auto;
width: 36px;
height: 20px;
border-radius: 20px;
background: #38474a;
transition: background .15s ease;
}
.switch::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #d7ded8;
transition: transform .15s ease;
}
.toggle input:checked + .switch { background: #a77d48; }
.toggle input:checked + .switch::after { transform: translateX(16px); }
.toggle input:focus-visible + .switch { outline: 2px solid var(--amber); outline-offset: 3px; }
.transport {
display: grid;
grid-template-columns: 1fr 1.25fr;
gap: 9px;
margin-top: 18px;
}
.button {
min-height: 40px;
padding: 9px 12px;
border: 1px solid var(--line);
border-radius: 9px;
color: #dce4dd;
background: #223035;
font-size: 11px;
font-weight: 650;
}
.button:hover { background: #2a3a40; }
.button.primary { border-color: #c39458; color: #192124; background: var(--amber); }
.button.primary:hover { background: #f2c386; }
.button:focus-visible { outline: 2px solid #f6d4a5; outline-offset: 2px; }
.model-note {
margin: 14px 0 0;
color: #708480;
font-size: 9px;
line-height: 1.55;
}
@media (max-width: 980px) {
.masthead { align-items: flex-start; flex-direction: column; gap: 10px; }
.intro { max-width: 620px; }
.workbench { grid-template-columns: 1fr; }
.panel { border-top: 1px solid var(--line); border-left: 0; }
.panel {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 24px;
}
.clock-block, .flow-row, .count-line { grid-column: 1; }
.controls-title, .control, .toggle, .transport, .model-note { grid-column: 2; }
.controls-title { grid-row: 1; }
.transport { align-self: end; }
}
@media (max-width: 650px) {
.shell { padding-top: 18px; }
.panel { display: block; padding: 19px; }
.stage { padding: 9px; }
.stagebar { padding: 2px 3px 9px; }
.clock { font-size: 42px; }
.view-name { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.status-dot { animation: none !important; }
.switch, .switch::after { transition: none; }
}
</style>
</head>
<body>
<main class="shell">
<header class="masthead">
<div>
<p class="eyebrow">Section 114 / evacuation study</p>
<h1>One gate. No shortcuts.</h1>
</div>
<p class="intro">
Two hundred sixty people, one standing terrace, and the same three imperatives:
get to the gate, preserve a little space, and push through contact.
</p>
</header>
<section class="workbench" aria-label="Crowd evacuation simulator">
<div class="stage">
<div class="stagebar">
<span class="view-name">Top-down / standing terrace / Gate 04</span>
<span class="status" id="status" data-state="live">
<span class="status-dot"></span>
<span id="statusText">EVACUATING</span>
</span>
</div>
<canvas id="stadium" width="960" height="690"
aria-label="Top-down simulation of 260 people leaving a stadium section through one gate">
Your browser needs canvas support to display the stadium.
</canvas>
<div class="stage-footer">
<span class="legend"><span class="legend-swatch"></span>Amber marks the clear gate and actual exits</span>
<span>1 yd = 0.9144 m · bodies are hard discs · fixed 60 Hz physics</span>
</div>
</div>
<aside class="panel">
<div class="clock-block">
<span class="metric-label">Evacuation clock</span>
<div class="clock" id="timer">00:00</div>
<div class="metric-note" id="timerNote">Running until the last person clears the wall</div>
</div>
<div class="flow-row">
<div>
<span class="metric-label">Measured outflow</span>
<span class="flow-value" id="outflow">0</span>
<span class="flow-unit">people/min</span>
<div class="metric-note">Actual crossings, rolling window up to 10 s</div>
</div>
<div>
<span class="metric-label">Still inside</span>
<div class="flow-value" id="remaining">260</div>
<div class="metric-note">Bodies upstream of the safe side</div>
</div>
</div>
<div class="count-line">
<span>Through the gate</span>
<strong id="through">0 / 260</strong>
</div>
<h2 class="controls-title">Crowd rules</h2>
<div class="control">
<div class="control-head">
<label for="urgency">Urgency</label>
<output id="urgencyValue" for="urgency">0.55</output>
</div>
<input id="urgency" type="range" min="0" max="1" step="0.01" value="0.55">
<div class="range-ends"><span>0.0 · orderly</span><span>1.0 · urgent</span></div>
<div class="control-detail" id="urgencyDetail"></div>
</div>
<div class="control">
<div class="control-head">
<label for="gate">Gate width</label>
<output id="gateValue" for="gate">1.20 yd</output>
</div>
<input id="gate" type="range" min="0.6" max="3" step="0.01" value="1.2">
<div class="range-ends"><span>0.6 yd</span><span>3.0 yd</span></div>
<div class="control-detail" id="gateDetail"></div>
</div>
<label class="toggle">
<span class="toggle-copy">
<strong>Column before the gate</strong>
<small>One fixed circular pier, about 1.5 yd upstream</small>
</span>
<input id="column" type="checkbox">
<span class="switch" aria-hidden="true"></span>
</label>
<div class="transport">
<button class="button" id="pause" type="button">Pause</button>
<button class="button primary" id="reset" type="button">↺ Reset stands</button>
</div>
<p class="model-note">
Controls apply live. Reset keeps the settings and restores the same seeded layout,
making comparisons repeatable. No queue, lane, or per-person exit route is assigned.
</p>
</aside>
</section>
</main>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const canvas = $("stadium");
const ctx = canvas.getContext("2d");
const WORLD_W = 32;
const WORLD_H = 23;
const GATE_X = 16;
const WALL_Y = 18;
const WALL_T = 0.28;
const RADIUS = 0.21;
const DIAMETER = RADIUS * 2;
const PERSONAL_SPACE = 0.20;
const PERSONAL_DISTANCE = DIAMETER + PERSONAL_SPACE;
const CONTACT_SKIN = 0.025;
const TOTAL = 260;
const DT = 1 / 60;
const COLUMN = { x: 16.12, y: 16.50, r: 0.30 };
const settings = { urgency: 0.55, gate: 1.2, column: false };
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
let people = [];
let pulses = [];
let exitTimes = [];
let simTime = 0;
let emptyTime = 0;
let running = true;
let empty = false;
let accumulator = 0;
let gateLeft = GATE_X - settings.gate / 2;
let gateRight = GATE_X + settings.gate / 2;
let rngState = 0x51f15e;
const shirts = ["#e5b46f", "#cf7864", "#72aca4", "#d8d7c9", "#8ea4be", "#b98ba4"];
const skins = ["#d8b28d", "#b9815e", "#885b43", "#e2c4a5", "#6e4938"];
function random() {
rngState = (rngState + 0x6D2B79F5) | 0;
let t = rngState;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
function updateGate() {
gateLeft = GATE_X - settings.gate / 2;
gateRight = GATE_X + settings.gate / 2;
}
function resetSimulation() {
rngState = 0x51f15e;
people = [];
pulses = [];
exitTimes = [];
simTime = 0;
emptyTime = 0;
empty = false;
running = true;
accumulator = 0;
for (let row = 0; row < 13; row++) {
for (let col = 0; col < 20; col++) {
const id = row * 20 + col;
const x = 1.65 + col * (28.70 / 19) + (random() - 0.5) * 0.20;
const y = 1.65 + row * 1.04 + (random() - 0.5) * 0.16;
people.push({
id, x, y,
ox: x, oy: y,
vx: 0, vy: 0,
ax: 0, ay: 0,
shirt: shirts[Math.floor(random() * shirts.length)],
skin: skins[Math.floor(random() * skins.length)]
});
}
}
updateGate();
updateUI();
}
function capVelocity(p, maximum) {
const speed2 = p.vx * p.vx + p.vy * p.vy;
if (speed2 > maximum * maximum) {
const scale = maximum / Math.sqrt(speed2);
p.vx *= scale;
p.vy *= scale;
}
}
function projectBounds(p) {
p.x = clamp(p.x, 0.45 + RADIUS, 31.55 - RADIUS);
if (p.y < 0.45 + RADIUS) p.y = 0.45 + RADIUS;
}
function projectColumn(p) {
if (!settings.column) return;
let dx = p.x - COLUMN.x;
let dy = p.y - COLUMN.y;
const minimum = RADIUS + COLUMN.r;
const d2 = dx * dx + dy * dy;
if (d2 < minimum * minimum) {
let d = Math.sqrt(d2);
if (d < 1e-8) {
const angle = (p.id * 2.399963229728653) % (Math.PI * 2);
dx = Math.cos(angle);
dy = Math.sin(angle);
d = 1;
}
const correction = (minimum - d) / d;
p.x += dx * correction;
p.y += dy * correction;
}
}
function projectWallRectangle(p, x0, x1) {
const y0 = WALL_Y - WALL_T / 2;
const y1 = WALL_Y + WALL_T / 2;
const qx = clamp(p.x, x0, x1);
const qy = clamp(p.y, y0, y1);
let dx = p.x - qx;
let dy = p.y - qy;
const d2 = dx * dx + dy * dy;
if (d2 > 1e-16) {
if (d2 < RADIUS * RADIUS) {
const d = Math.sqrt(d2);
const correction = (RADIUS - d) / d;
p.x += dx * correction;
p.y += dy * correction;
}
return;
}
// The center is on or inside the rectangle. Choose the nearest face.
const up = p.y - y0 + RADIUS;
const down = y1 + RADIUS - p.y;
const left = p.x - x0 + RADIUS;
const right = x1 + RADIUS - p.x;
const nearest = Math.min(up, down, left, right);
if (nearest === up) p.y = y0 - RADIUS;
else if (nearest === down) p.y = y1 + RADIUS;
else if (nearest === left) p.x = x0 - RADIUS;
else p.x = x1 + RADIUS;
}
function projectStatics(p) {
projectBounds(p);
projectColumn(p);
projectWallRectangle(p, 0.35, gateLeft);
projectWallRectangle(p, gateRight, 31.65);
}
function physicsStep() {
updateGate();
const urgency = settings.urgency;
const desiredSpeed = 0.45 + 1.55 * urgency;
const relaxation = 0.52 - 0.34 * urgency;
const contactGain = 45 + 235 * urgency;
const maximumSpeed = desiredSpeed * 1.35 + 0.15;
const n = people.length;
// Identical desired-velocity rule for every person.
for (const p of people) {
const dx = GATE_X - p.x;
const dy = (WALL_Y + 1.0) - p.y;
const length = Math.hypot(dx, dy) || 1;
const desiredVX = dx / length * desiredSpeed;
const desiredVY = dy / length * desiredSpeed;
p.ax = (desiredVX - p.vx) / relaxation;
p.ay = (desiredVY - p.vy) / relaxation;
}
// Symmetric personal-space, contact, and contact-friction forces.
for (let i = 0; i < n; i++) {
const a = people[i];
for (let j = i + 1; j < n; j++) {
const b = people[j];
let dx = a.x - b.x;
let dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 >= PERSONAL_DISTANCE * PERSONAL_DISTANCE) continue;
let d = Math.sqrt(d2);
if (d < 1e-8) {
const angle = ((a.id * 31 + b.id * 17) % 360) * Math.PI / 180;
dx = Math.cos(angle);
dy = Math.sin(angle);
d = 1;
}
const nx = dx / d;
const ny = dy / d;
const rvx = a.vx - b.vx;
const rvy = a.vy - b.vy;
const rvn = rvx * nx + rvy * ny;
const intrusion = clamp(
(PERSONAL_DISTANCE - d) / PERSONAL_SPACE, 0, 1.25
);
let normalForce = 7.5 * intrusion * intrusion;
let physicalContact = 0;
if (d < DIAMETER + CONTACT_SKIN) {
const compression = DIAMETER + CONTACT_SKIN - d;
physicalContact = contactGain * compression;
if (rvn < 0) physicalContact += -(1.4 + 2.2 * urgency) * rvn;
normalForce += physicalContact;
}
a.ax += nx * normalForce;
a.ay += ny * normalForce;
b.ax -= nx * normalForce;
b.ay -= ny * normalForce;
// Coulomb-like tangential resistance, limited by physical normal force.
if (physicalContact > 0) {
const tx = rvx - rvn * nx;
const ty = rvy - rvn * ny;
const tangentSpeed = Math.hypot(tx, ty);
if (tangentSpeed > 1e-8) {
const friction = Math.min(
0.34 * physicalContact,
tangentSpeed / (2 * DT)
);
const fx = tx / tangentSpeed * friction;
const fy = ty / tangentSpeed * friction;
a.ax -= fx;
a.ay -= fy;
b.ax += fx;
b.ay += fy;
}
}
}
}
for (const p of people) {
p.vx += p.ax * DT;
p.vy += p.ay * DT;
capVelocity(p, maximumSpeed);
p.ox = p.x;
p.oy = p.y;
p.x += p.vx * DT;
p.y += p.vy * DT;
}
// Hard positional contacts. These passes, rather than soft overlap,
// are what keep bodies and solid geometry separated.
for (let pass = 0; pass < 10; pass++) {
let contactingPairs = 0;
for (let i = 0; i < n; i++) {
const a = people[i];
for (let j = i + 1; j < n; j++) {
const b = people[j];
let dx = a.x - b.x;
let dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 >= DIAMETER * DIAMETER) continue;
contactingPairs++;
let d = Math.sqrt(d2);
if (d < 1e-8) {
const angle = ((a.id * 73 + b.id * 29) % 360) * Math.PI / 180;
dx = Math.cos(angle);
dy = Math.sin(angle);
d = 1;
}
const correction = (DIAMETER - d) * 0.5 / d;
const cx = dx * correction;
const cy = dy * correction;
a.x += cx;
a.y += cy;
b.x -= cx;
b.y -= cy;
}
}
for (const p of people) projectStatics(p);
if (pass >= 3 && contactingPairs === 0) break;
}
for (const p of people) {
p.vx = (p.x - p.ox) / DT;
p.vy = (p.y - p.oy) / DT;
capVelocity(p, maximumSpeed);
}
simTime += DT;
// An exit counts only when the complete body has cleared the wall.
const safeLine = WALL_Y + WALL_T / 2 + 0.01;
for (let i = people.length - 1; i >= 0; i--) {
const p = people[i];
if (p.y - RADIUS > safeLine) {
people.splice(i, 1);
exitTimes.push(simTime);
pulses.push({ x: p.x, y: WALL_Y + 0.40, t: simTime });
}
}
while (exitTimes.length && exitTimes[0] < simTime - 10) {
exitTimes.shift();
}
if (people.length === 0) {
empty = true;
emptyTime = simTime;
running = false;
}
}
function formatTime(seconds, tenths = false) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
let result = String(minutes).padStart(2, "0") + ":" +
String(secs).padStart(2, "0");
if (tenths) result += "." + Math.floor((seconds % 1) * 10);
return result;
}
function updateControlReadouts() {
const u = settings.urgency;
const desiredSpeed = 0.45 + 1.55 * u;
const contactGain = Math.round(45 + 235 * u);
const descriptor = u < 0.25 ? "Orderly" : u < 0.55 ? "Brisk" : u < 0.82 ? "Pressed" : "Urgent";
$("urgencyValue").textContent = u.toFixed(2);
$("urgencyDetail").textContent =
descriptor + " · " + desiredSpeed.toFixed(2) +
" yd/s desired pace · contact gain " + contactGain;
$("gateValue").textContent = settings.gate.toFixed(2) + " yd";
$("gateDetail").textContent =
"≈ " + (settings.gate * 0.9144).toFixed(2) +
" m clear; wall and body clearance are simulated";
}
function updateUI() {
const shownTime = empty ? emptyTime : simTime;
const windowSeconds = Math.min(10, simTime);
const rate = windowSeconds > 0
? exitTimes.length * 60 / windowSeconds
: 0;
$("timer").textContent = formatTime(shownTime);
$("outflow").textContent = String(Math.round(rate));
$("remaining").textContent = String(people.length);
$("through").textContent = (TOTAL - people.length) + " / " + TOTAL;
const state = empty ? "empty" : running ? "live" : "paused";
$("status").dataset.state = state;
$("statusText").textContent =
empty ? "SECTION EMPTY" : running ? "EVACUATING" : "PAUSED";
$("timerNote").textContent = empty
? "Section empty in " + formatTime(emptyTime, true)
: running
? "Running until the last person clears the wall"
: "Clock and crowd are paused";
$("pause").textContent = empty ? "Run again" : running ? "Pause" : "Resume";
}
function roundedPath(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function canvasText(text, x, y, size, color, align = "left", weight = 600) {
ctx.fillStyle = color;
ctx.font = weight + " " + size + "px system-ui, sans-serif";
ctx.textAlign = align;
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function fitCanvas() {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(2, window.devicePixelRatio || 1);
const width = Math.max(1, Math.round(rect.width * dpr));
const height = Math.max(1, Math.round(rect.height * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function drawPerson(p) {
const speed = Math.hypot(p.vx, p.vy);
const angle = speed > 0.03
? Math.atan2(p.vy, p.vx)
: Math.atan2(WALL_Y + 1 - p.y, GATE_X - p.x);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(angle);
ctx.fillStyle = "rgba(0,0,0,.24)";
ctx.beginPath();
ctx.ellipse(0.035, 0.045, RADIUS * 0.80, RADIUS * 0.94, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = p.shirt;
ctx.strokeStyle = "rgba(8,15,17,.62)";
ctx.lineWidth = 0.018;
ctx.beginPath();
ctx.ellipse(0, 0, RADIUS * 0.78, RADIUS * 0.95, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(0.045, 0, 0.073, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(22,25,24,.48)";
ctx.beginPath();
ctx.arc(0.063, 0, 0.052, -Math.PI * 0.65, Math.PI * 0.65);
ctx.fill();
ctx.restore();
}
function draw() {
fitCanvas();
updateGate();
const sx = canvas.width / WORLD_W;
const sy = canvas.height / WORLD_H;
ctx.setTransform(sx, 0, 0, sy, 0, 0);
ctx.clearRect(0, 0, WORLD_W, WORLD_H);
// Concourse and terrace floor.
ctx.fillStyle = "#142025";
ctx.fillRect(0, 0, WORLD_W, WORLD_H);
ctx.fillStyle = "#1d2b30";
ctx.fillRect(0.35, 0.35, 31.3, 17.55);
for (let y = 18.4; y < WORLD_H; y += 0.55) {
ctx.fillStyle = y % 1.1 < 0.6 ? "#172429" : "#18262a";
ctx.fillRect(0, y, WORLD_W, 0.28);
}
// Exit-lane chevrons.
ctx.strokeStyle = "rgba(237,181,109,.18)";
ctx.lineWidth = 0.055;
for (let y = 20.0; y < 22.4; y += 0.85) {
ctx.beginPath();
ctx.moveTo(GATE_X - 0.34, y - 0.18);
ctx.lineTo(GATE_X, y + 0.16);
ctx.lineTo(GATE_X + 0.34, y - 0.18);
ctx.stroke();
}
// Thirteen standing rows.
for (let row = 0; row < 13; row++) {
const y = 1.65 + row * 1.04;
ctx.fillStyle = row % 2 ? "rgba(255,255,255,.014)" : "rgba(0,0,0,.035)";
ctx.fillRect(1.05, y - 0.43, 29.9, 0.86);
ctx.strokeStyle = "rgba(184,201,195,.075)";
ctx.lineWidth = 0.018;
ctx.beginPath();
ctx.moveTo(1.05, y + 0.47);
ctx.lineTo(30.95, y + 0.47);
ctx.stroke();
canvasText(String(row + 1).padStart(2, "0"), 0.76, y, 0.18, "#617572", "center", 500);
}
// Small standing-position marks, deliberately not obstacles.
ctx.strokeStyle = "rgba(185,201,194,.11)";
ctx.lineWidth = 0.025;
ctx.beginPath();
for (let row = 0; row < 13; row++) {
for (let col = 0; col < 20; col++) {
const x = 1.65 + col * (28.70 / 19);
const y = 1.65 + row * 1.04 + 0.31;
ctx.moveTo(x - 0.11, y);
ctx.lineTo(x + 0.11, y);
}
}
ctx.stroke();
canvasText("SECTION 114 / SOUTH STANDING TERRACE", 1.05, 0.88, 0.25, "#879994");
canvasText("260 PEOPLE · ONE EXIT", 30.95, 0.88, 0.22, "#718581", "right");
// Terrace boundary walls.
ctx.fillStyle = "#71807d";
ctx.fillRect(0.25, 0.25, 31.5, 0.20);
ctx.fillRect(0.25, 0.25, 0.20, 17.75);
ctx.fillRect(31.55, 0.25, 0.20, 17.75);
// Amber threshold on the safe side.
ctx.fillStyle = "rgba(237,181,109,.13)";
ctx.fillRect(gateLeft, WALL_Y - 0.25, settings.gate, 0.78);
ctx.strokeStyle = "rgba(237,181,109,.65)";
ctx.lineWidth = 0.035;
ctx.setLineDash([0.12, 0.10]);
ctx.beginPath();
ctx.moveTo(gateLeft, WALL_Y + 0.34);
ctx.lineTo(gateRight, WALL_Y + 0.34);
ctx.stroke();
ctx.setLineDash([]);
// The single wall, split only by the gate.
const wallTop = WALL_Y - WALL_T / 2;
ctx.fillStyle = "#9aa29a";
ctx.fillRect(0.35, wallTop, gateLeft - 0.35, WALL_T);
ctx.fillRect(gateRight, wallTop, 31.65 - gateRight, WALL_T);
ctx.fillStyle = "rgba(255,255,255,.19)";
ctx.fillRect(0.35, wallTop, gateLeft - 0.35, 0.045);
ctx.fillRect(gateRight, wallTop, 31.65 - gateRight, 0.045);
ctx.fillStyle = "#edb56d";
ctx.fillRect(gateLeft - 0.025, wallTop - 0.06, 0.05, WALL_T + 0.12);
ctx.fillRect(gateRight - 0.025, wallTop - 0.06, 0.05, WALL_T + 0.12);
// Gate dimension.
ctx.strokeStyle = "rgba(237,181,109,.72)";
ctx.lineWidth = 0.025;
ctx.beginPath();
ctx.moveTo(gateLeft, 18.84);
ctx.lineTo(gateRight, 18.84);
ctx.moveTo(gateLeft, 18.72);
ctx.lineTo(gateLeft, 18.96);
ctx.moveTo(gateRight, 18.72);
ctx.lineTo(gateRight, 18.96);
ctx.stroke();
canvasText(settings.gate.toFixed(2) + " YD CLEAR", GATE_X, 19.19, 0.24, "#d8ad76", "center", 700);
canvasText("TO CONCOURSE / SAFE SIDE", GATE_X, 22.55, 0.25, "#657a77", "center");
// Optional physical column.
if (settings.column) {
ctx.fillStyle = "rgba(0,0,0,.28)";
ctx.beginPath();
ctx.arc(COLUMN.x + 0.07, COLUMN.y + 0.08, COLUMN.r + 0.10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#4d5b59";
ctx.beginPath();
ctx.arc(COLUMN.x, COLUMN.y, COLUMN.r + 0.09, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#b5b5a9";
ctx.strokeStyle = "#d2d0c2";
ctx.lineWidth = 0.035;
ctx.beginPath();
ctx.arc(COLUMN.x, COLUMN.y, COLUMN.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = "rgba(66,75,72,.35)";
ctx.lineWidth = 0.025;
ctx.beginPath();
ctx.moveTo(COLUMN.x - 0.18, COLUMN.y);
ctx.lineTo(COLUMN.x + 0.18, COLUMN.y);
ctx.moveTo(COLUMN.x, COLUMN.y - 0.18);
ctx.lineTo(COLUMN.x, COLUMN.y + 0.18);
ctx.stroke();
}
// Exit rings are visual echoes of measured crossings, not particles.
for (const pulse of pulses) {
const age = simTime - pulse.t;
if (age >= 0 && age < 0.75) {
ctx.globalAlpha = 1 - age / 0.75;
ctx.strokeStyle = "#edb56d";
ctx.lineWidth = 0.035;
ctx.beginPath();
ctx.arc(pulse.x, pulse.y, 0.11 + age * 0.55, 0, Math.PI * 2);
ctx.stroke();
}
}
ctx.globalAlpha = 1;
for (const p of people) drawPerson(p);
if (empty) {
ctx.fillStyle = "rgba(8,15,17,.91)";
roundedPath(9.3, 7.25, 13.4, 4.55, 0.34);
ctx.fill();
ctx.strokeStyle = "rgba(237,181,109,.55)";
ctx.lineWidth = 0.035;
ctx.stroke();
canvasText("SECTION CLEAR", GATE_X, 8.17, 0.48, "#edb56d", "center", 750);
canvasText(formatTime(emptyTime, true), GATE_X, 9.25, 0.92, "#edf0e8", "center", 550);
const average = emptyTime > 0 ? Math.round(TOTAL * 60 / emptyTime) : 0;
canvasText(
TOTAL + " actual exits · " + average + " people/min lifetime average",
GATE_X, 10.20, 0.27, "#a4b3ae", "center", 500
);
canvasText(
"Reset to run the same terrace again",
GATE_X, 10.85, 0.25, "#748884", "center", 500
);
}
}
function applyHash() {
try {
const query = new URLSearchParams(location.hash.replace(/^#/, ""));
if (query.has("urgency")) {
const value = Number(query.get("urgency"));
if (Number.isFinite(value)) settings.urgency = clamp(value, 0, 1);
}
if (query.has("gate")) {
const value = Number(query.get("gate"));
if (Number.isFinite(value)) settings.gate = clamp(value, 0.6, 3.0);
}
if (query.has("column")) {
settings.column = query.get("column") === "1";
}
} catch (_) {
// Defaults remain usable if a host supplies a malformed fragment.
}
}
function syncControls() {
$("urgency").value = String(settings.urgency);
$("gate").value = String(settings.gate);
$("column").checked = settings.column;
updateControlReadouts();
}
$("urgency").addEventListener("input", event => {
settings.urgency = Number(event.target.value);
updateControlReadouts();
});
$("gate").addEventListener("input", event => {
settings.gate = Number(event.target.value);
updateGate();
updateControlReadouts();
});
$("column").addEventListener("change", event => {
settings.column = event.target.checked;
});
$("reset").addEventListener("click", resetSimulation);
$("pause").addEventListener("click", () => {
if (empty) {
resetSimulation();
} else {
running = !running;
accumulator = 0;
updateUI();
}
});
applyHash();
syncControls();
resetSimulation();
let previous = performance.now();
function frame(now) {
const realElapsed = Math.min(0.10, Math.max(0, (now - previous) / 1000));
previous = now;
if (running) {
accumulator += realElapsed;
let steps = 0;
while (accumulator >= DT && steps < 6 && running) {
physicsStep();
accumulator -= DT;
steps++;
}
if (steps === 6) accumulator = 0;
} else {
accumulator = 0;
}
draw();
updateUI();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The crowd produces funneling, density-dependent slowing, pressure propagation, stop-and-go movement, and arch-like clusters without assigning queues or lanes. The column is only another rigid collider; it receives no hidden flow bonus. Outflow is counted when each whole body clears the downstream face of the wall, and the clock stops when the last body does so.
It is not a validated pedestrian model. Everyone has the same body and gait, and there are no legs, falls, injuries, social groups, deliberate route choices, or queue etiquette. The contact and friction coefficients are illustrative, so the simulation is not guaranteed to reproduce faster-is-slower behavior in every configuration. It can clog and produce brief zero-flow intervals, but it does not claim realistic, permanent friction-locked jamming.
Special cases and limits:
1.35 × desired speed + 0.15 yd/s prevents numerical explosions. There is no gate-specific speed cap or prescribed gate flow.