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.
These are estimates, not measurements from a run:
gate=1.2&column=0 will decrease evacuation time by about 60 seconds.urgency=0.9&gate=1.2 will increase evacuation time by about 20 seconds.<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>One Gate — Crowd Study</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10191b;
color: #eef3e9;
}
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1250px; margin: auto; padding: 24px; }
header { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 20px; }
h1 { font-size: clamp(1.8rem, 4vw, 3rem); letter-spacing: -.06em; margin: 0; }
.eyebrow { color: #a5c1ae; text-transform: uppercase; letter-spacing: .16em; font-size: .68rem; font-weight: 700; }
header p { color: #a8b9b0; margin: 5px 0 0; font-size: .9rem; }
.badge { border: 1px solid #486057; border-radius: 999px; color: #bbd9c5; padding: 7px 12px; white-space: nowrap; font-size: .75rem; }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 310px; gap: 16px; }
.stage { background: #1b2c29; border: 1px solid #3b5149; border-radius: 14px; overflow: hidden; }
canvas { display: block; width: 100%; height: auto; aspect-ratio: 30 / 28; }
.stage-note { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 12px 16px; color: #a7bcb0; font-size: .72rem; border-top: 1px solid #3b5149; }
aside { display: flex; flex-direction: column; gap: 12px; }
.panel { background: #1a2927; border: 1px solid #3b5149; border-radius: 14px; padding: 17px; }
.panel h2 { margin: 0 0 15px; font-size: .74rem; text-transform: uppercase; letter-spacing: .14em; color: #a5c1ae; }
.control { margin-bottom: 19px; }
.control:last-child { margin-bottom: 0; }
.control label, .toggle { display: flex; justify-content: space-between; align-items: center; gap: 10px; font-size: .9rem; }
output { color: #e8d38c; font-variant-numeric: tabular-nums; font-weight: 700; }
input[type=range] { width: 100%; accent-color: #dfc97f; margin: 12px 0 0; }
input[type=checkbox] { width: 19px; height: 19px; accent-color: #dfc97f; }
.hint { color: #91a99d; line-height: 1.45; font-size: .73rem; margin: 7px 0 0; }
.metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 15px 10px; }
.metric span { display: block; color: #a5b9ae; font-size: .7rem; margin-bottom: 4px; }
.metric strong { font-size: 1.55rem; letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.metric small { font-size: .7rem; color: #a5b9ae; font-weight: 400; }
.status { color: #d8eacb; font-size: .8rem; margin: 15px 0 0; min-height: 1.2em; }
.buttons { display: flex; gap: 8px; }
button { flex: 1; border: 1px solid #61786a; background: #2b4037; color: #eef3e9; border-radius: 8px; padding: 10px; font: inherit; cursor: pointer; }
button.primary { background: #e1cf91; border-color: #e1cf91; color: #17221c; font-weight: 700; }
button:hover { filter: brightness(1.12); }
button:focus-visible, input:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
@media (max-width: 800px) {
main { padding: 14px; }
.layout { grid-template-columns: 1fr; }
header { align-items: start; }
.badge { display: none; }
aside { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
}
</style>
</head>
<body>
<main>
<header>
<div>
<div class="eyebrow">A small crowd dynamics study</div>
<h1>One gate</h1>
<p>A stadium section moves toward a single opening.</p>
</div>
<div class="badge">TOP-DOWN · 220 PEOPLE</div>
</header>
<div class="layout">
<div class="stage">
<canvas id="view" width="900" height="840" aria-label="Top-down simulation of people leaving a stadium section through one gate"></canvas>
<div class="stage-note">
<span>Stands ↑ · Exit ↓</span>
<span>One dot = one person · Distances in metres</span>
</div>
</div>
<aside>
<section class="panel">
<h2>Conditions</h2>
<div class="control">
<label for="urgency">Urgency <output id="urgencyValue"></output></label>
<input id="urgency" type="range" min="0" max="1" step="0.01">
<p class="hint">Changes desired speed and contact response while running.</p>
</div>
<div class="control">
<label for="gate">Gate width <output id="gateValue"></output></label>
<input id="gate" type="range" min="0" max="12" step="0.1">
<p class="hint">Yards. Changing the opening refills the section.</p>
</div>
<div class="control">
<label class="toggle" for="column"><span>Column in front of gate</span><input id="column" type="checkbox"></label>
<p class="hint">Changing the column also refills the section.</p>
</div>
</section>
<section class="panel" aria-live="polite">
<h2>Live measurements</h2>
<div class="metrics">
<div class="metric"><span>Elapsed / emptying time</span><strong id="time">0.0</strong> <small>sec</small></div>
<div class="metric"><span>Outflow · last 20 sec</span><strong id="flow">0</strong> <small>people/min</small></div>
<div class="metric"><span>Still inside</span><strong id="inside">220</strong></div>
<div class="metric"><span>Through the gate</span><strong id="exited">0</strong></div>
</div>
<p class="status" id="status">Running</p>
</section>
<section class="panel">
<div class="buttons">
<button id="pause" type="button">Pause</button>
<button id="reset" class="primary" type="button">Reset crowd</button>
</div>
<p class="hint">The clock is simulation time. The outflow counts actual exits in a rolling 20-second window.</p>
</section>
</aside>
</div>
</main>
<script>
(() => {
"use strict";
const W = 30, H = 28, WALL = 25, R = 0.23, DIAM = R * 2;
const COUNT = 220, STEP = 1 / 40, COLUMN_R = 0.55;
const columnX = W / 2, columnY = 22.3;
const canvas = document.getElementById("view");
const ctx = canvas.getContext("2d");
const ui = {
urgency: document.getElementById("urgency"),
urgencyValue: document.getElementById("urgencyValue"),
gate: document.getElementById("gate"),
gateValue: document.getElementById("gateValue"),
column: document.getElementById("column"),
time: document.getElementById("time"),
flow: document.getElementById("flow"),
inside: document.getElementById("inside"),
exited: document.getElementById("exited"),
status: document.getElementById("status"),
pause: document.getElementById("pause"),
reset: document.getElementById("reset")
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const params = new URLSearchParams(location.hash.slice(1));
function numberParam(name, fallback, lo, hi) {
const raw = params.get(name);
if (raw === null || raw.trim() === "") return fallback;
const n = Number(raw);
return Number.isFinite(n) ? clamp(n, lo, hi) : fallback;
}
ui.urgency.value = numberParam("urgency", 0.5, 0, 1);
ui.gate.value = numberParam("gate", 1.2, 0, 12);
ui.column.checked = numberParam("column", 0, 0, 1) >= 0.5;
let people = [], exits = [], elapsed = 0, totalExited = 0;
let running = true, accumulator = 0, previous = 0;
let urgency = 0.5, gateYards = 1.2, hasColumn = false;
function readControls() {
urgency = Number(ui.urgency.value);
gateYards = Number(ui.gate.value);
hasColumn = ui.column.checked;
ui.urgencyValue.textContent = urgency.toFixed(2);
ui.gateValue.textContent = gateYards.toFixed(1) + " yd";
}
function reset() {
readControls();
people = [];
exits = [];
elapsed = 0;
totalExited = 0;
accumulator = 0;
running = true;
ui.pause.textContent = "Pause";
// A staggered grid in the stands; all starting separations exceed a diameter.
for (let row = 0; row < 11; row++) {
for (let col = 0; col < 20; col++) {
const id = row * 20 + col;
people.push({
x: 3.05 + col * 1.25 + (row % 2) * 0.24,
y: 2.0 + row * 1.22,
vx: 0, vy: 0, fx: 0, fy: 0,
phase: id * 2.399963,
color: id % 5
});
}
}
updateMetrics();
draw();
}
function updateMetrics() {
while (exits.length && exits[0] < elapsed - 20) exits.shift();
ui.time.textContent = elapsed.toFixed(1);
ui.flow.textContent = (exits.length * 3).toFixed(0);
ui.inside.textContent = people.length;
ui.exited.textContent = totalExited;
ui.status.textContent = people.length === 0
? "Section empty — clock stopped."
: running ? "Running" : "Paused";
}
// First intersection of a movement segment with a circle. The returned
// normal points from the obstacle towards the moving person.
function circleHit(x, y, dx, dy, cx, cy, radius) {
const rx = x - cx, ry = y - cy;
const a = dx * dx + dy * dy;
if (a < 1e-18) return null;
const b = rx * dx + ry * dy;
if (b >= 0) return null;
const c = rx * rx + ry * ry - radius * radius;
const disc = b * b - a * c;
if (disc < 0) return null;
let t = (-b - Math.sqrt(disc)) / a;
if (t < 0 && c > -1e-8) t = 0;
if (t < 0 || t > 1) return null;
let nx = rx + dx * t, ny = ry + dy * t;
const length = Math.hypot(nx, ny);
if (length < 1e-12) return null;
return { t, nx: nx / length, ny: ny / length };
}
// Sweep one disc against the fixed world and all other discs. Movement
// stops at the first contact, then the remaining movement can slide along it.
function movePerson(p, index, dx, dy, left, right) {
for (let pass = 0; pass < 4; pass++) {
if (dx * dx + dy * dy < 1e-14) break;
let best = 1.000001, nx = 0, ny = 0, other = null;
function take(t, x, y, o) {
if (t >= 0 && t <= 1 && t < best) {
best = t; nx = x; ny = y; other = o || null;
}
}
if (dx < 0) take((R - p.x) / dx, 1, 0);
if (dx > 0) take((W - R - p.x) / dx, -1, 0);
if (dy < 0) take((R - p.y) / dy, 0, 1);
// The gate wall is a zero-thickness line with circular end caps.
if (dy > 0) {
const t = (WALL - R - p.y) / dy;
const x = p.x + dx * t;
if (x <= left || x >= right) take(t, 0, -1);
} else if (dy < 0) {
const t = (WALL + R - p.y) / dy;
const x = p.x + dx * t;
if (x <= left || x >= right) take(t, 0, 1);
}
for (const endX of [left, right]) {
const h = circleHit(p.x, p.y, dx, dy, endX, WALL, R);
if (h) take(h.t, h.nx, h.ny);
}
if (hasColumn) {
const h = circleHit(p.x, p.y, dx, dy, columnX, columnY, R + COLUMN_R);
if (h) take(h.t, h.nx, h.ny);
}
for (let j = 0; j < people.length; j++) {
if (j === index) continue;
const q = people[j];
const h = circleHit(p.x, p.y, dx, dy, q.x, q.y, DIAM);
if (h) take(h.t, h.nx, h.ny, q);
}
if (best > 1) {
p.x += dx; p.y += dy;
break;
}
// A tiny numerical clearance keeps rounding from producing penetration.
const distance = Math.hypot(dx, dy);
const safeT = Math.max(0, best - 1e-7 / Math.max(distance, 1e-9));
p.x += dx * safeT;
p.y += dy * safeT;
// Transfer some normal momentum to another person on contact.
if (other) {
const closing = -((p.vx - other.vx) * nx + (p.vy - other.vy) * ny);
if (closing > 0) {
const impulse = closing * (0.15 + 0.3 * urgency);
p.vx += nx * impulse; p.vy += ny * impulse;
other.vx -= nx * impulse; other.vy -= ny * impulse;
}
} else {
const vn = p.vx * nx + p.vy * ny;
if (vn < 0) { p.vx -= vn * nx; p.vy -= vn * ny; }
}
dx *= 1 - best;
dy *= 1 - best;
const into = dx * nx + dy * ny;
if (into < 0) { dx -= into * nx; dy -= into * ny; }
}
}
function step() {
elapsed += STEP;
const gateWidth = gateYards * 0.9144;
const left = W / 2 - gateWidth / 2;
const right = W / 2 + gateWidth / 2;
const desiredSpeed = 0.65 + urgency * 1.55;
// Every person follows the same steering and personal-space rules.
for (const p of people) {
let tx = W / 2, ty = WALL + 2;
// A simple shared obstacle-avoidance waypoint, not a jam detector.
if (hasColumn && p.y < columnY + 0.65 &&
p.y > columnY - 5 && Math.abs(p.x - columnX) < 4) {
const side = p.x < columnX ? -1 : 1;
tx = columnX + side * (COLUMN_R + R + 0.55);
ty = columnY + 1.4;
}
let ax = tx - p.x, ay = ty - p.y;
const length = Math.hypot(ax, ay) || 1;
ax /= length; ay /= length;
// Small, deterministic variation in gait; identical rule for everyone.
const sway = 0.035 * Math.sin(elapsed * 1.7 + p.phase);
const vx = (ax - ay * sway) * desiredSpeed;
const vy = (ay + ax * sway) * desiredSpeed;
p.fx = (vx - p.vx) * 2.4;
p.fy = (vy - p.vy) * 2.4;
}
// Equal and opposite personal-space and near-contact forces.
for (let i = 0; i < people.length; i++) {
const a = people[i];
for (let j = i + 1; j < people.length; j++) {
const b = people[j];
const dx = a.x - b.x, dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 >= 0.9 * 0.9 || d2 < 1e-16) continue;
const d = Math.sqrt(d2), nx = dx / d, ny = dy / d;
const space = Math.max(0, 0.9 - d) * 1.5;
const contact = Math.max(0, DIAM + 0.07 - d) * (8 + 25 * urgency);
const force = space + contact;
a.fx += nx * force; a.fy += ny * force;
b.fx -= nx * force; b.fy -= ny * force;
}
}
for (const p of people) {
p.vx += p.fx * STEP;
p.vy += p.fy * STEP;
// Global numerical stability cap, not a gate-specific limit.
const speed = Math.hypot(p.vx, p.vy);
if (speed > 3.5) {
p.vx *= 3.5 / speed;
p.vy *= 3.5 / speed;
}
}
// Rotate update order so the same person does not always get priority.
const start = Math.floor(elapsed / STEP) % Math.max(1, people.length);
for (let k = 0; k < people.length; k++) {
const i = (start + k) % people.length;
const p = people[i];
movePerson(p, i, p.vx * STEP, p.vy * STEP, left, right);
}
// Count an exit only after the entire disc has passed the wall.
for (let i = people.length - 1; i >= 0; i--) {
if (people[i].y > WALL + R) {
people.splice(i, 1);
exits.push(elapsed);
totalExited++;
}
}
if (!people.length) {
running = false;
ui.pause.textContent = "Finished";
}
}
function draw() {
const sx = canvas.width / W, sy = canvas.height / H;
ctx.setTransform(sx, 0, 0, sy, 0, 0);
ctx.fillStyle = "#243b34";
ctx.fillRect(0, 0, W, H);
// Stand terraces.
ctx.fillStyle = "#304940";
ctx.fillRect(1, 0.8, W - 2, 14.6);
for (let y = 1.7; y < 15; y += 1.22) {
ctx.fillStyle = "rgba(185, 211, 181, .08)";
ctx.fillRect(1, y, W - 2, 0.055);
}
ctx.fillStyle = "#9ab1a0";
ctx.font = "0.42px system-ui";
ctx.letterSpacing = "0.08px";
ctx.fillText("STANDS", 1.4, 1.35);
// Exit apron and subtle direction markings.
ctx.fillStyle = "#172a25";
ctx.fillRect(0, WALL, W, H - WALL);
ctx.fillStyle = "rgba(209, 223, 190, .18)";
for (let y = 17; y < 23; y += 1.5) {
ctx.fillRect(W / 2 - 0.025, y, 0.05, 0.65);
}
const gw = gateYards * 0.9144;
const left = W / 2 - gw / 2, right = W / 2 + gw / 2;
ctx.strokeStyle = "#d2ddc2";
ctx.lineWidth = 0.16;
ctx.beginPath();
ctx.moveTo(0, WALL); ctx.lineTo(left, WALL);
ctx.moveTo(right, WALL); ctx.lineTo(W, WALL);
ctx.stroke();
ctx.strokeStyle = "#e8d38c";
ctx.lineWidth = 0.09;
ctx.beginPath();
ctx.moveTo(left, WALL - 0.32); ctx.lineTo(left, WALL + 0.32);
ctx.moveTo(right, WALL - 0.32); ctx.lineTo(right, WALL + 0.32);
ctx.stroke();
if (hasColumn) {
ctx.beginPath();
ctx.arc(columnX, columnY, COLUMN_R, 0, Math.PI * 2);
ctx.fillStyle = "#d1cbb0";
ctx.fill();
ctx.strokeStyle = "#687a6c";
ctx.lineWidth = 0.08;
ctx.stroke();
}
const colors = ["#e9c68a", "#e2a982", "#d8d7b5", "#c4dfc3", "#d5b5a0"];
for (const p of people) {
ctx.beginPath();
ctx.arc(p.x, p.y, R, 0, Math.PI * 2);
ctx.fillStyle = colors[p.color];
ctx.fill();
ctx.strokeStyle = "rgba(18, 32, 26, .65)";
ctx.lineWidth = 0.045;
ctx.stroke();
}
ctx.fillStyle = "#d6e2ca";
ctx.font = "0.46px system-ui";
ctx.textAlign = "center";
ctx.fillText("EXIT", W / 2, 27.05);
ctx.textAlign = "start";
}
ui.urgency.addEventListener("input", readControls);
ui.gate.addEventListener("input", () => {
ui.gateValue.textContent = Number(ui.gate.value).toFixed(1) + " yd";
});
ui.gate.addEventListener("change", reset);
ui.column.addEventListener("change", reset);
ui.reset.addEventListener("click", reset);
ui.pause.addEventListener("click", () => {
if (!people.length) return;
running = !running;
accumulator = 0;
ui.pause.textContent = running ? "Pause" : "Resume";
updateMetrics();
});
function frame(now) {
if (!previous) previous = now;
const delta = Math.min((now - previous) / 1000, 0.12);
previous = now;
if (running && people.length) {
accumulator += delta;
let steps = 0;
while (accumulator >= STEP && steps < 5 && people.length) {
step();
accumulator -= STEP;
steps++;
}
// Drop excess wall-clock time rather than destabilize the simulation.
if (steps === 5) accumulator = Math.min(accumulator, STEP);
updateMetrics();
}
draw();
requestAnimationFrame(frame);
}
reset();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
People have desired motion, personal-space forces and equal-and-opposite near-contact forces. Swept-disc collision checks prevent intentional overlap and let people slide along obstacles; contact also transfers momentum. Exits are counted only after a person’s whole disc crosses the wall.
This is not a calibrated crowd model. People are identical discs, the wall has zero simulated thickness, and the column gets a simple avoidance waypoint. There is deterministic gait variation, a rotating movement-update order and a global speed cap for stability. There is no special jam-breaking nudge or gate-specific speed cap. Jams can occur, particularly with a very narrow or closed gate; the timer will keep running if people remain inside. Gate or column changes reset the crowd.