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.
1. Urgency 0.3 → 0.9 at gate=1.2, column=0: evacuation time DECREASES, by ≈ 30 s. My estimate: ≈ 2:10 at urgency 0.3 versus ≈ 1:40 at urgency 0.9 (± 15 s). The reasoning: even at 0.3, two hundred people funnel into a 3.6-ft opening within ~10 s, so both runs are gate-limited, not travel-limited. The gain comes from faster approach travel plus modestly higher pressed flow through the pinch — a sublinear payoff (the model's local version of "faster is slower"), but not a reversal, because contact pressure still adds pushing force at the posts.
2. Column ON at urgency=0.9, gate=1.2: evacuation time DECREASES, by ≈ 7 s (band ~3–12 s). Mechanism: a column ~5 ft (≈3 body-widths) in front of the opening breaks the symmetric arch that wedges against the two gate posts and splits the central push into two offset streams; the small extra path length around it costs less than the jamming it prevents.
Both are falsifiable on the page itself: set the sliders, hit RESET, read ELAPSED.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Section 114 — Gate 7</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:#0d0f0e}
body{color:#cfd6d1;font:14px/1.5 ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;display:flex;justify-content:center;padding:34px 18px 60px;-webkit-font-smoothing:antialiased}
.wrap{width:100%;max-width:1024px}
.kicker{font-size:10px;letter-spacing:.3em;text-transform:uppercase;color:#6d7a74;margin-bottom:8px}
h1{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:27px;color:#e9eeea;margin-bottom:8px}
.sub{font-size:12px;color:#8a958f;max-width:70ch;margin-bottom:18px}
.frame{border:1px solid #262b29;border-radius:6px;overflow:hidden;background:#0a0c0b}
canvas{display:block;width:100%;height:auto}
.console{border-top:1px solid #262b29;background:#111413;padding:16px 20px 14px;display:flex;flex-wrap:wrap;gap:14px 30px;align-items:flex-end}
.ctl{flex:1 1 180px;min-width:170px}
.lab{display:flex;justify-content:space-between;align-items:baseline;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:#6d7a74;margin-bottom:7px}
.lab b{font-weight:400;color:#e8a24b;font-size:12px;letter-spacing:.04em}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:20px;background:transparent;cursor:pointer}
input[type=range]:focus{outline:none}
input[type=range]::-webkit-slider-runnable-track{height:2px;background:#2e3532}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;margin-top:-5px;background:#e8a24b;border-radius:2px}
input[type=range]::-moz-range-track{height:2px;background:#2e3532}
input[type=range]::-moz-range-thumb{width:12px;height:12px;background:#e8a24b;border-radius:2px;border:none}
.switch{display:inline-flex;align-items:center;gap:10px;cursor:pointer;user-select:none;padding:1px 0}
.switch input{position:absolute;opacity:0;pointer-events:none}
.pill{flex:none;width:34px;height:18px;border:1px solid #3a4140;border-radius:10px;position:relative;transition:border-color .15s}
.pill::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#6b7570;transition:left .15s,background .15s}
.switch input:checked~.pill{border-color:#e8a24b}
.switch input:checked~.pill::after{left:18px;background:#e8a24b}
.slab{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:#6d7a74}
.btns{display:flex;gap:10px;flex:0 0 auto;padding-bottom:1px}
button{font:inherit;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:#cfd6d1;background:transparent;border:1px solid #3a4140;border-radius:3px;padding:9px 16px;cursor:pointer;transition:border-color .15s,color .15s}
button:hover{border-color:#e8a24b;color:#ffcf8a}
.note{margin-top:14px;font-size:11px;color:#5d6863;max-width:100ch}
</style>
</head>
<body>
<div class="wrap">
<div class="kicker">Crowd simulation · social force · top down</div>
<h1>Section 114 empties through Gate 7</h1>
<p class="sub">216 spectators, one opening in the perimeter wall. Every person runs the same three rules — head for the gate, keep your distance, push against whoever and whatever is in the way. Urgency sets how fast everyone wants to move and how hard they shove.</p>
<div class="frame">
<canvas id="cv" width="1024" height="736"></canvas>
<div class="console">
<div class="ctl">
<div class="lab"><span>Urgency</span><b id="uVal">0.70</b></div>
<input id="uRange" type="range" min="0" max="1" step="0.01" value="0.7">
</div>
<div class="ctl">
<div class="lab"><span>Gate width</span><b id="gVal">1.2 yd</b></div>
<input id="gRange" type="range" min="0.5" max="3" step="0.1" value="1.2">
</div>
<div class="ctl">
<div class="lab"><span>Column in front of gate</span><b id="cVal">OFF</b></div>
<label class="switch"><input id="cChk" type="checkbox"><span class="pill"></span><span class="slab">place / remove</span></label>
</div>
<div class="btns">
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset</button>
</div>
</div>
</div>
<p class="note">Set a control, hit RESET, and read ELAPSED on the scoreboard — the outflow figure counts only people actually crossing the line. The address hash tracks your settings, so any run is bookmarkable. No libraries, no physics engine: the forces, the jamming and the queueing are hand-rolled, and the crowd's behavior is emergent.</p>
</div>
<script>
(function(){
'use strict';
const TAU=Math.PI*2;
// world: feet & seconds; 16 px per foot. Scene sits under a 64 px scoreboard strip.
const FT=16, W=64, H=42, SB=64;
const CW=W*FT, CH=SB+H*FT;
const WALL_Y=33.5, GATE_X=32, R=0.75, EXIT_Y=35.6; // wall line, gate center, body radius, "fully out" line
const COL_R=0.75, COL_D=5.0; // column radius & stand-off from the wall
const N_ROWS=12, ROW_N=18, NP=N_ROWS*ROW_N; // 216 people
const STEP=1/60;
const cv=document.getElementById('cv');
const ctx=cv.getContext('2d');
const bg=document.createElement('canvas'); bg.width=CW; bg.height=H*FT;
const bgx=bg.getContext('2d');
let bgDirty=true;
// floodlight vignette over the scene only
const vg=ctx.createRadialGradient(CW/2,SB+H*FT*0.45,CH*0.22, CW/2,SB+H*FT*0.5,CH*0.72);
vg.addColorStop(0,'rgba(0,0,0,0)');
vg.addColorStop(1,'rgba(0,0,0,0.30)');
let urgency=0.7, gateYd=1.2, column=false;
let running=true, finished=false, finalT=0, simT=0;
let people=[], ghosts=[], exitT=[], peakFlow=0, curFlow=0, dispFlow=0;
let gx0=0, gx1=0, segs=[];
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
function buildWalls(){
const half=gateYd*1.5; // yards -> feet
gx0=GATE_X-half; gx1=GATE_X+half;
segs=[
{ax:1, ay:1, bx:W-1, by:1, nx:0, ny:1}, // top
{ax:1, ay:1, bx:1, by:WALL_Y, nx:1, ny:0}, // left
{ax:W-1, ay:1, bx:W-1, by:WALL_Y, nx:-1,ny:0}, // right
{ax:1, ay:WALL_Y, bx:gx0, by:WALL_Y, nx:0, ny:-1}, // perimeter wall, left of gate
{ax:gx1, ay:WALL_Y, bx:W-1, by:WALL_Y, nx:0, ny:-1} // perimeter wall, right of gate
];
// live gate-width change: nobody gets stranded inside the wall band
for(const p of people){
if(p.y>WALL_Y-R && p.y<EXIT_Y && (p.x<gx0+R||p.x>gx1-R)){ p.y=WALL_Y-R-0.02; if(p.vy>0)p.vy=0; }
}
bgDirty=true;
}
function makeCrowd(){
people=[];ghosts=[];exitT=[];simT=0;finished=false;finalT=0;peakFlow=0;curFlow=0;dispFlow=0;
for(let r=0;r<N_ROWS;r++)for(let c=0;c<ROW_N;c++){
people.push({
x:8.2+c*2.72+(Math.random()-0.5)*0.5,
y:3.6+r*1.72+(Math.random()-0.5)*0.4,
vx:0,vy:0,fx:0,fy:0,
side:Math.random()<0.5?-1:1, // persistent shuffle side, used only by the jam-breaker
wA:(Math.random()-0.5)*0.6, // wander phase
stuck:0
});
}
}
// ----- spatial hash -----
const CS=3, grid=new Map();
const gkey=(cx,cy)=>(cx+2)+(cy+2)*40;
function buildGrid(){
grid.clear();
for(let i=0;i<people.length;i++){
const p=people[i], k=gkey((p.x/CS)|0,(p.y/CS)|0);
const a=grid.get(k); if(a)a.push(i); else grid.set(k,[i]);
}
}
// ----- forces -----
function pairForce(a,b,kc){
const rs=1.5, cut=rs+0.9;
const dx=b.x-a.x, dy=b.y-a.y, d2=dx*dx+dy*dy;
if(d2>cut*cut) return;
let d=Math.sqrt(d2), nx,ny;
if(d<1e-4){ const t=Math.random()*TAU; nx=Math.cos(t); ny=Math.sin(t); d=1e-4; }
else { nx=dx/d; ny=dy/d; }
let f=10*Math.exp((rs-d)/0.32); // personal space, fades with distance
const ov=rs-d;
if(ov>0){ // hard contact: spring + inelastic damping
const rvn=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
f=Math.max(0, f + kc*ov - rvn*10);
}
a.fx-=nx*f; a.fy-=ny*f;
b.fx+=nx*f; b.fy+=ny*f;
}
function segClosest(p,s){
const abx=s.bx-s.ax, aby=s.by-s.ay, apx=p.x-s.ax, apy=p.y-s.ay;
const L2=abx*abx+aby*aby;
let t=L2>0?(apx*abx+apy*aby)/L2:0; t=t<0?0:(t>1?1:t);
return [s.ax+abx*t, s.ay+aby*t];
}
function wallForce(p,s,kc){
const q=segClosest(p,s);
const dx=p.x-q[0], dy=p.y-q[1], d2=dx*dx+dy*dy, cut=R+0.9;
if(d2>cut*cut) return;
let d=Math.sqrt(d2), nx,ny;
if(d<1e-4){ nx=s.nx; ny=s.ny; d=1e-4; } else { nx=dx/d; ny=dy/d; }
let f=8*Math.exp((R-d)/0.32);
const ov=R-d;
if(ov>0){
const rvn=p.vx*nx+p.vy*ny;
f=Math.max(0, f + kc*ov*1.5 - rvn*10);
}
p.fx+=nx*f; p.fy+=ny*f;
}
function colForce(p,kc){
const dx=p.x-GATE_X, dy=p.y-(WALL_Y-COL_D), rs=R+COL_R, d2=dx*dx+dy*dy, cut=rs+0.9;
if(d2>cut*cut) return;
let d=Math.sqrt(d2), nx,ny;
if(d<1e-4){ nx=0; ny=-1; d=1e-4; } else { nx=dx/d; ny=dy/d; }
let f=12*Math.exp((rs-d)/0.3);
const ov=rs-d;
if(ov>0){
const rvn=p.vx*nx+p.vy*ny;
f=Math.max(0, f + kc*ov*1.5 - rvn*10);
}
p.fx+=nx*f; p.fy+=ny*f;
}
// ----- kinematic separation: nobody overlaps anybody -----
function separate(a,b){
const rs=1.5;
const dx=b.x-a.x, dy=b.y-a.y, d2=dx*dx+dy*dy;
if(d2>=rs*rs) return;
let d=Math.sqrt(d2), nx,ny;
if(d<1e-4){ const t=Math.random()*TAU; nx=Math.cos(t); ny=Math.sin(t); d=1e-4; }
else { nx=dx/d; ny=dy/d; }
const push=(rs-d)*0.225;
a.x-=nx*push; a.y-=ny*push;
b.x+=nx*push; b.y+=ny*push;
}
function wallClamp(p,s){
const q=segClosest(p,s);
const dx=p.x-q[0], dy=p.y-q[1], d2=dx*dx+dy*dy;
if(d2<R*R){
let d=Math.sqrt(d2), nx,ny;
if(d<1e-6){ nx=s.nx; ny=s.ny; d=1e-6; } else { nx=dx/d; ny=dy/d; }
p.x+=nx*(R-d); p.y+=ny*(R-d);
const vn=p.vx*nx+p.vy*ny;
if(vn<0){ p.vx-=nx*vn; p.vy-=ny*vn; }
}
}
function colClamp(p){
const cx=GATE_X, cy=WALL_Y-COL_D, rs=R+COL_R;
const dx=p.x-cx, dy=p.y-cy, d2=dx*dx+dy*dy;
if(d2<rs*rs){
let d=Math.sqrt(d2), nx,ny;
if(d<1e-6){ nx=0; ny=-1; d=1e-6; } else { nx=dx/d; ny=dy/d; }
p.x+=nx*(rs-d); p.y+=ny*(rs-d);
const vn=p.vx*nx+p.vy*ny;
if(vn<0){ p.vx-=nx*vn; p.vy-=ny*vn; }
}
}
function subStep(h){
const n=people.length; if(!n) return;
const vd=1.3+5.2*urgency; // desired speed, ft/s
const kc=40+260*urgency; // contact force scale
buildGrid();
for(let i=0;i<n;i++){ const p=people[i]; p.fx=0; p.fy=0; }
for(let i=0;i<n;i++){
const p=people[i];
// wander: tiny slowly-varying heading noise
p.wA+=(Math.random()-0.5)*4*h; p.wA*=(1-1.4*h);
// rule 1 — head for the nearest slice of the opening
const tx=clamp(p.x, gx0+0.55, gx1-0.55);
let dx=tx-p.x, dy=(WALL_Y+2.5)-p.y;
const d=Math.hypot(dx,dy)||1e-6; dx/=d; dy/=d;
if(p.wA){
const a=p.wA*0.8, ca=Math.cos(a), sa=Math.sin(a);
const rx=dx*ca-dy*sa, ry=dx*sa+dy*ca; dx=rx; dy=ry;
}
p.fx+=(dx*vd-p.vx)*2.5; // relaxation time 0.4 s
p.fy+=(dy*vd-p.vy)*2.5;
// jam-breaker: nearly stationary near the gate -> scripted sideways shuffle
const sp=Math.hypot(p.vx,p.vy);
const dgy=WALL_Y-p.y;
if(sp<vd*0.25 && Math.abs(GATE_X-p.x)<12 && dgy>-0.5 && dgy<10) p.stuck+=h;
else p.stuck=Math.max(0,p.stuck-2*h);
if(p.stuck>1.2){
if(sp<vd*0.6) p.fx+=p.side*(10+18*urgency);
if(p.stuck>1.5) p.stuck=0.45;
}
// rules 2 & 3 — personal space and pushing against walls / column
for(let s=0;s<segs.length;s++) wallForce(p,segs[s],kc);
if(column) colForce(p,kc);
}
// neighbor pairs via 3x3 cells
for(let i=0;i<n;i++){
const p=people[i], cx=(p.x/CS)|0, cy=(p.y/CS)|0;
for(let oy=-1;oy<=1;oy++){
const yy=cy+oy;
for(let ox=-1;ox<=1;ox++){
const a=grid.get(gkey(cx+ox,yy)); if(!a) continue;
for(let m=0;m<a.length;m++){ const j=a[m]; if(j>i) pairForce(p,people[j],kc); }
}
}
}
// integrate with a hard speed cap
for(let i=0;i<n;i++){
const p=people[i];
p.vx+=p.fx*h; p.vy+=p.fy*h;
const s2=p.vx*p.vx+p.vy*p.vy;
if(s2>72.25){ const s=Math.sqrt(s2); p.vx*=8.5/s; p.vy*=8.5/s; }
p.x+=p.vx*h; p.y+=p.vy*h;
}
// two rounds of positional separation against each other, walls and column
for(let it=0;it<2;it++){
buildGrid();
for(let i=0;i<n;i++){
const p=people[i], cx=(p.x/CS)|0, cy=(p.y/CS)|0;
for(let oy=-1;oy<=1;oy++){
const yy=cy+oy;
for(let ox=-1;ox<=1;ox++){
const a=grid.get(gkey(cx+ox,yy)); if(!a) continue;
for(let m=0;m<a.length;m++){ const j=a[m]; if(j>i) separate(p,people[j]); }
}
}
}
for(let i=0;i<n;i++){
const p=people[i];
for(let s=0;s<segs.length;s++) wallClamp(p,segs[s]);
if(column) colClamp(p);
}
}
}
function simStep(dt){
simT+=dt;
const h=dt/2;
subStep(h); subStep(h);
// crossings: a person is "out" once fully past the wall band
for(let i=people.length-1;i>=0;i--){
const p=people[i];
if(p.y>EXIT_Y){
exitT.push(simT);
ghosts.push({x:p.x,y:p.y,vx:p.vx*0.6,vy:p.vy*0.6,a:0.9});
const lastP=people.pop();
if(i<people.length) people[i]=lastP;
if(people.length===0 && !finished){ finished=true; finalT=simT; }
}
}
for(let i=ghosts.length-1;i>=0;i--){
const g=ghosts[i];
g.x+=g.vx*dt; g.y+=g.vy*dt; g.vx*=0.92; g.vy*=0.92; g.a-=dt*1.8;
if(g.a<=0){ ghosts[i]=ghosts[ghosts.length-1]; ghosts.pop(); }
}
// outflow: sliding 12 s window over actual crossing timestamps
while(exitT.length && exitT[0]<=simT-12) exitT.shift();
const win=Math.min(simT,12);
curFlow = win>0.5 ? exitT.length*60/win : 0;
if(curFlow>peakFlow && !finished) peakFlow=curFlow;
}
// ----- art -----
const C0=[92,106,120], C1=[209,146,80], C2=[255,226,178];
function speedColor(s){
let t=s/6.5; if(t<0)t=0; else if(t>1)t=1;
let r,g,b;
if(t<0.55){ const k=t/0.55;
r=C0[0]+(C1[0]-C0[0])*k; g=C0[1]+(C1[1]-C0[1])*k; b=C0[2]+(C1[2]-C0[2])*k;
} else { const k=(t-0.55)/0.45;
r=C1[0]+(C2[0]-C1[0])*k; g=C1[1]+(C2[1]-C1[1])*k; b=C1[2]+(C2[2]-C1[2])*k;
}
return 'rgb('+(r|0)+','+(g|0)+','+(b|0)+')';
}
function drawBG(){
const g=bgx;
g.clearRect(0,0,CW,H*FT);
// terrace
g.fillStyle='#1e2427'; g.fillRect(0,0,CW,23*FT);
for(let r=0;r<N_ROWS;r++){
const y=3.6+r*1.72;
g.fillStyle=(r%2)?'#232a2e':'#20262a';
g.fillRect(0,(y-0.86)*FT,CW,1.72*FT);
g.fillStyle='rgba(160,175,185,0.10)';
g.fillRect(0,(y+0.74)*FT,CW,2);
}
for(let r=0;r<N_ROWS;r++){
const y=3.6+r*1.72;
for(let c=0;c<ROW_N;c++){
const x=8.2+c*2.72;
g.fillStyle='rgba(0,0,0,0.30)';
g.fillRect((x-0.55)*FT,(y-0.42)*FT,1.10*FT,0.62*FT);
g.fillStyle='rgba(190,200,205,0.05)';
g.fillRect((x-0.55)*FT,(y-0.42)*FT,1.10*FT,2);
}
}
// outer walls of the section
g.fillStyle='#33393d';
g.fillRect(0,0,CW,FT); g.fillRect(0,0,FT,WALL_Y*FT); g.fillRect((W-1)*FT,0,FT,WALL_Y*FT);
// promenade
g.fillStyle='#2a2e30'; g.fillRect(0,23*FT,CW,10.5*FT);
g.strokeStyle='rgba(0,0,0,0.22)'; g.lineWidth=1;
for(let x=8;x<W;x+=8){ g.beginPath(); g.moveTo(x*FT+0.5,23*FT); g.lineTo(x*FT+0.5,WALL_Y*FT); g.stroke(); }
// worn approach path in front of the gate
g.fillStyle='rgba(0,0,0,0.09)';
g.beginPath();
g.moveTo((GATE_X-2)*FT,WALL_Y*FT); g.lineTo((GATE_X+2)*FT,WALL_Y*FT);
g.lineTo((GATE_X+14)*FT,23*FT); g.lineTo((GATE_X-14)*FT,23*FT);
g.closePath(); g.fill();
// perimeter wall with the opening
g.fillStyle='#41484c'; g.fillRect(0,WALL_Y*FT,CW,FT);
g.fillStyle='rgba(255,255,255,0.08)'; g.fillRect(0,WALL_Y*FT,CW,2);
g.fillStyle='rgba(0,0,0,0.30)'; g.fillRect(0,(WALL_Y+1)*FT-2,CW,2);
g.fillStyle='#171b1d'; g.fillRect(gx0*FT,WALL_Y*FT,(gx1-gx0)*FT,FT);
g.strokeStyle='rgba(255,177,77,0.35)'; g.lineWidth=2; g.setLineDash([6,6]);
g.beginPath(); g.moveTo(gx0*FT,(WALL_Y+0.5)*FT); g.lineTo(gx1*FT,(WALL_Y+0.5)*FT); g.stroke();
g.setLineDash([]);
g.fillStyle='#79838a';
g.fillRect((gx0-0.5)*FT,(WALL_Y-0.15)*FT,8,1.3*FT);
g.fillRect(gx1*FT,(WALL_Y-0.15)*FT,8,1.3*FT);
g.font='10px Menlo, Consolas, monospace';
g.fillStyle='rgba(205,214,216,0.5)';
g.textAlign='right'; g.fillText('SECTION 114',(gx0-1.0)*FT,(WALL_Y+0.68)*FT);
g.textAlign='left'; g.fillText('GATE 7',(gx1+1.0)*FT,(WALL_Y+0.68)*FT);
// pitch beyond the gate
g.fillStyle='#1d4c31'; g.fillRect(0,34.5*FT,CW,(H-34.5)*FT);
for(let i=0;i<4;i++) if(i%2===0){ g.fillStyle='rgba(255,255,255,0.05)'; g.fillRect(0,(34.5+i*1.9)*FT,CW,1.9*FT); }
g.fillStyle='rgba(225,232,230,0.65)'; g.fillRect(0,35.1*FT,CW,2);
}
function drawColumn(){
const px=GATE_X*FT, py=(WALL_Y-COL_D)*FT, r=COL_R*FT;
ctx.fillStyle='rgba(0,0,0,0.4)';
ctx.beginPath(); ctx.arc(px+2.5,py+3,r,0,TAU); ctx.fill();
ctx.fillStyle='#59626a';
ctx.beginPath(); ctx.arc(px,py,r,0,TAU); ctx.fill();
ctx.strokeStyle='#8b959c'; ctx.lineWidth=2; ctx.stroke();
ctx.fillStyle='#39424a';
ctx.beginPath(); ctx.arc(px,py,3.5,0,TAU); ctx.fill();
}
function render(){
if(bgDirty){ drawBG(); bgDirty=false; }
ctx.drawImage(bg,0,SB);
if(column) drawColumn();
for(let i=0;i<people.length;i++){
const p=people[i], px=p.x*FT, py=p.y*FT;
const s=Math.hypot(p.vx,p.vy), col=speedColor(s);
if(s>0.6){
ctx.globalAlpha=0.4; ctx.strokeStyle=col; ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(px-p.vx*0.085*FT,py-p.vy*0.085*FT);
ctx.lineTo(px,py); ctx.stroke();
ctx.globalAlpha=1;
}
ctx.fillStyle='rgba(0,0,0,0.34)';
ctx.beginPath(); ctx.arc(px+1.5,py+2.2,R*FT,0,TAU); ctx.fill();
ctx.fillStyle=col;
ctx.beginPath(); ctx.arc(px,py,R*FT,0,TAU); ctx.fill();
ctx.strokeStyle='rgba(8,10,10,0.55)'; ctx.lineWidth=1; ctx.stroke();
}
for(let i=0;i<ghosts.length;i++){
const g=ghosts[i];
ctx.globalAlpha=Math.max(0,g.a)*0.85;
ctx.fillStyle='#b7c2ae';
ctx.beginPath(); ctx.arc(g.x*FT,g.y*FT,R*FT*0.9,0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
ctx.fillStyle=vg;
ctx.fillRect(0,SB,CW,H*FT);
drawSB();
}
// ----- scoreboard: hand-drawn 7-segment displays -----
const DMAP={'0':'abcdef','1':'bc','2':'abged','3':'abgcd','4':'fgbc','5':'afgcd','6':'afgedc','7':'abc','8':'abcdefg','9':'abfgcd'};
const SEGS={a:[1.6,1.2,8.4,1.2],b:[9.2,2.4,9.2,7.6],c:[9.2,10.4,9.2,15.6],d:[1.6,16.8,8.4,16.8],e:[0.8,10.4,0.8,15.6],f:[0.8,2.4,0.8,7.6],g:[1.6,9,8.4,9]};
function drawDigit(x,y,h,ch){
const sc=h/18, on=DMAP[ch]||'';
ctx.lineCap='round';
ctx.lineWidth=2.4*sc;
ctx.strokeStyle='rgba(255,177,77,0.09)';
ctx.beginPath();
for(const s in SEGS) if(on.indexOf(s)<0){ const q=SEGS[s]; ctx.moveTo(x+q[0]*sc,y+q[1]*sc); ctx.lineTo(x+q[2]*sc,y+q[3]*sc); }
ctx.stroke();
ctx.strokeStyle='#ffb14d';
ctx.shadowColor='rgba(255,160,60,0.5)'; ctx.shadowBlur=6;
ctx.beginPath();
for(const s in SEGS) if(on.indexOf(s)>=0){ const q=SEGS[s]; ctx.moveTo(x+q[0]*sc,y+q[1]*sc); ctx.lineTo(x+q[2]*sc,y+q[3]*sc); }
ctx.stroke();
ctx.shadowBlur=0;
return 10*sc;
}
function drawColon(x,y,h){
const sc=h/18;
ctx.fillStyle='#ffb14d';
ctx.shadowColor='rgba(255,160,60,0.5)'; ctx.shadowBlur=6;
ctx.fillRect(x,y+5*sc,2.6*sc,2.6*sc);
ctx.fillRect(x,y+11*sc,2.6*sc,2.6*sc);
ctx.shadowBlur=0;
return 6*sc;
}
function drawDot(x,y,h){
const sc=h/18;
ctx.fillStyle='#ffb14d';
ctx.shadowColor='rgba(255,160,60,0.5)'; ctx.shadowBlur=6;
ctx.fillRect(x,y+14.5*sc,2.6*sc,2.6*sc);
ctx.shadowBlur=0;
return 6*sc;
}
function drawSB(){
ctx.fillStyle='#0a0c0b'; ctx.fillRect(0,0,CW,SB);
ctx.fillStyle='#222726'; ctx.fillRect(0,SB-1,CW,1);
ctx.fillStyle='#171c1b';
ctx.fillRect(238,12,1,40); ctx.fillRect(492,12,1,40); ctx.fillRect(760,12,1,40);
// status lamp
ctx.globalAlpha = finished ? 0.55+0.45*Math.abs(Math.sin(performance.now()/300)) : 1;
ctx.fillStyle = finished ? '#e8a24b' : (running ? '#5fc787' : '#66706b');
ctx.beginPath(); ctx.arc(30,27,5,0,TAU); ctx.fill();
ctx.globalAlpha=1;
ctx.font='10px Menlo, Consolas, monospace';
ctx.textAlign='left';
ctx.fillStyle='#8a958f';
ctx.fillText(finished?'CLEARED':(running?'RUNNING':'PAUSED'),44,31);
ctx.fillStyle='#4d5753';
ctx.fillText('GATE 7 · SECTION 114',22,49);
// section labels
ctx.font='9px Menlo, Consolas, monospace';
ctx.fillStyle='#5d6863';
ctx.textAlign='center';
ctx.fillText('ELAPSED',365,15);
ctx.fillText('OUT OF '+NP,626,15);
ctx.fillText('OUTFLOW · P/MIN',892,15);
const y=24;
// elapsed
const t=finished?finalT:simT;
const m=Math.min(9,Math.floor(t/60)), s10=Math.floor(t%60/10), s1=Math.floor(t%10), th=Math.floor(t*10)%10;
let x=301;
x+=drawDigit(x,y,30,''+m)+7;
x+=drawColon(x,y,30)+7;
x+=drawDigit(x,y,30,''+s10)+7;
x+=drawDigit(x,y,30,''+s1)+7;
x+=drawDot(x,y,30)+7;
drawDigit(x,y,30,''+th);
// out count
const oStr=String(Math.min(NP-people.length,999)).padStart(3,'0');
x=594;
for(let i=0;i<3;i++) x+=drawDigit(x,y,30,oStr[i])+7;
// outflow, lightly smoothed for the display; peak is raw
dispFlow+=(curFlow-dispFlow)*0.08;
const fStr=String(Math.min(999,Math.round(dispFlow))).padStart(3,'0');
x=860;
for(let i=0;i<3;i++) x+=drawDigit(x,y,30,fStr[i])+7;
ctx.font='8px Menlo, Consolas, monospace';
ctx.fillStyle='#4d5753';
ctx.fillText('PEAK '+Math.round(peakFlow)+' P/MIN',892,61);
}
// ----- controls & hash -----
const $=id=>document.getElementById(id);
const uRange=$('uRange'), uVal=$('uVal'), gRange=$('gRange'), gVal=$('gVal'),
cChk=$('cChk'), cVal=$('cVal'), pauseBtn=$('pauseBtn'), resetBtn=$('resetBtn');
function pushHash(){
try{ history.replaceState(null,'','#urgency='+urgency.toFixed(2)+'&gate='+gateYd.toFixed(1)+'&column='+(column?1:0)); }catch(e){}
}
(function parseHash(){
const q=new URLSearchParams(location.hash.replace(/^#/,''));
const num=(n,d,lo,hi)=>{ const v=parseFloat(q.get(n)); return Number.isFinite(v)?clamp(v,lo,hi):d; };
urgency=num('urgency',0.7,0,1);
gateYd=num('gate',1.2,0.5,3);
const c=(q.get('column')||'').trim().toLowerCase();
column=!(c===''||c==='0'||c==='false');
})();
function syncUI(){
uRange.value=urgency; uVal.textContent=urgency.toFixed(2);
gRange.value=gateYd; gVal.textContent=gateYd.toFixed(1)+' yd';
cChk.checked=column; cVal.textContent=column?'ON':'OFF';
}
uRange.addEventListener('input',()=>{ urgency=parseFloat(uRange.value); uVal.textContent=urgency.toFixed(2); pushHash(); });
gRange.addEventListener('input',()=>{ gateYd=parseFloat(gRange.value); gVal.textContent=gateYd.toFixed(1)+' yd'; buildWalls(); pushHash(); });
cChk.addEventListener('change',()=>{ column=cChk.checked; cVal.textContent=column?'ON':'OFF'; pushHash(); });
pauseBtn.addEventListener('click',()=>{ running=!running; pauseBtn.textContent=running?'Pause':'Resume'; });
resetBtn.addEventListener('click',()=>{ makeCrowd(); running=true; pauseBtn.textContent='Pause'; });
// ----- main loop -----
syncUI(); buildWalls(); makeCrowd();
let last=performance.now(), acc=0;
function frame(now){
let dt=(now-last)/1000; last=now;
if(dt>0.1)dt=0.1; if(dt<0)dt=0;
if(running){
acc+=dt;
let k=0;
while(acc>=STEP&&k<5){ simStep(STEP); acc-=STEP; k++; }
if(k===5)acc=0;
}
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it behaves like a real crowd
Where it doesn't, and everything I special-cased (all named)
To reproduce the two A/B tests: set the slider, hit RESET, read ELAPSED — the hash in the address bar tracks the settings, so each configuration is bookmarkable.