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
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 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
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You write simulations where the interesting behavior is supposed to emerge rather than be animated by hand.
Build me a single HTML page: a side-view escalator carrying 15 to 20 people. Then the escalator reverses, at high speed.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: how a person is represented, what makes one lose balance, and what happens when two of them occupy the same place.
The single fenced html block.
Having written it, what does the page do when the escalator reverses? Name anything you special-cased or nudged. Say what it does badly — I am going to press the button and watch.
Each person is an inverted pendulum with a head: a point-mass body of randomized height and mass whose centre of mass rides on a strut above the feet, moving on a 2-D plane of travel (along the escalator) × depth (a few invisible lanes; the camera is a pure side elevation, so depth only affects collisions and draw order). Upright, each runs the same balance controller with human flaws — a reaction delay before any corrective torque, capped ankle authority, and "capture steps" that rescue a lean only if there is footroom, which a crowd denies them. They lose balance when the surface under their feet accelerates harder than the ankles can answer: the belt's velocity flip enters the pendulum equation as a base acceleration, and once lean passes ~58° it is unrecoverable — angular momentum converts into slide/tumble velocity and the person becomes a bigger, softer circle governed by slope gravity, kinetic friction against the moving tread, and collision impulses. Two people in the same place resolve as soft inelastic circles: momentum is exchanged, an upright body converts a shove into pitch of its own pendulum, an upright hitting a downed body above a speed threshold pitches over it and joins it, and a downed figure stays an obstacle until it is nearly stationary relative to its floor and has a clear body-length of space to stand up into. Nobody is scheduled to fall; the lever only changes one number: the belt's target velocity.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator No. 4 — Uncontrolled Reversal</title>
<style>
:root{ --paper:#efe8d8; --ink:#26241f; --red:#b23a2e; }
*{ box-sizing:border-box; }
html,body{ margin:0; height:100%; overflow:hidden; background:var(--paper); color:var(--ink);
font-family:ui-monospace,'Cascadia Mono',Menlo,Consolas,monospace; }
#scene{ position:fixed; inset:0; display:block; cursor:crosshair; touch-action:none; }
#bar{ position:fixed; left:50%; bottom:14px; transform:translateX(-50%);
display:flex; align-items:center; gap:16px; padding:10px 16px;
background:var(--paper); border:2px solid var(--ink);
box-shadow:4px 4px 0 rgba(38,36,31,.22); white-space:nowrap; user-select:none; }
#brand{ font-size:10px; letter-spacing:.08em; line-height:1.6; }
#brand b{ font-size:11px; }
#lever{ position:relative; font:inherit; font-weight:700; letter-spacing:.12em; font-size:13px;
padding:11px 20px 11px 42px; background:var(--ink); color:var(--paper);
border:2px solid var(--ink); cursor:pointer; min-width:236px; text-align:left; }
#lever .lamp{ position:absolute; left:13px; top:50%; transform:translateY(-50%);
width:11px; height:11px; background:#5a564c; border:1px solid var(--paper); }
#lever.rev{ background:var(--red); }
#lever.rev .lamp{ background:#ffd9a0; animation:blink .5s steps(2,jump-none) infinite; }
@keyframes blink{ 50%{ background:#5e150e; } }
#lever small{ display:block; font-weight:400; letter-spacing:.06em; font-size:9px; opacity:.75; margin-top:3px; }
#lever:active{ transform:translate(1px,1px); }
#side{ display:flex; flex-direction:column; gap:6px; align-items:flex-start; }
#mute{ font:inherit; font-size:10px; letter-spacing:.1em; padding:5px 10px; background:none;
border:1.5px solid var(--ink); cursor:pointer; color:var(--ink); }
#hint{ font-size:9px; letter-spacing:.05em; opacity:.6; }
@media (max-width:820px){ #bar{ flex-direction:column; gap:8px; } }
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div id="bar">
<div id="brand"><b>BAY 4 — DIRECTION CONTROL</b><br>MOTOR ROOM · AUTHORISED USE</div>
<button id="lever" aria-pressed="false"><span class="lamp"></span><span id="lvT">SERVICE — UP</span>
<small id="lvS">press to throw into reverse</small></button>
<div id="side">
<button id="mute">SOUND: ON</button>
<div id="hint">SPACE — reverse · CLICK a figure to shove</div>
</div>
</div>
<script>
'use strict';
/* ================= constants ================= */
const TH=Math.PI/6, SIN=Math.sin(TH), COS=Math.cos(TH), TAN=Math.tan(TH);
const L=12, QT=L*COS, YTOP=L*SIN; // slope length, top-comb x, rise
const QMIN=-3.6, QMAX=QT+3.6; // horizontal extent of the scene (metres)
const G=9.81;
const BELT_UP=0.85, BELT_REV=-5.0; // belt speed along slope (m/s)
const BELT_RATE=26, FOOT_ACC=24; // belt slew rate, foot grip limit (m/s^2)
const HSTEP=1/240, P_TARGET=17;
const INK='#26241f', PAPER='#efe8d8', PAPER2='#e4dcc7', FAINT='#8b8577', RED='#b23a2e';
const SUITS=['#3d4a5c','#54402e','#31503f','#5b3535','#3e3a52','#6a5426','#2f3136','#59473b'];
const SKINS=['#e9c49c','#d3a074','#b07a4e','#8a5a38','#f0d3b3'];
const HAIRS=['#241f1a','#4a3826','#6e6257','#15130f','#7a4a2a'];
const MONO='ui-monospace, Menlo, Consolas, monospace';
/* ================= helpers ================= */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const ease=t=>t*t*(3-2*t);
const mod=(a,n)=>((a%n)+n)%n;
const R=Math.random;
const pick=a=>a[(R()*a.length)|0];
function rr(c,x,y,w,h,r){ r=Math.min(r,w/2,h/2); c.beginPath(); c.moveTo(x+r,y);
c.arcTo(x+w,y,x+w,y+h,r); c.arcTo(x+w,y+h,x,y+h,r); c.arcTo(x,y+h,x,y,r); c.arcTo(x,y,x+w,y,r); c.closePath(); }
function line(c,x1,y1,x2,y2){ c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke(); }
function ik(a,b,l1,l2,bend){ // 2-bone IK, returns joint + clamped end
let dx=b[0]-a[0], dy=b[1]-a[1], d=Math.hypot(dx,dy)||1e-6;
const dm=clamp(d, Math.abs(l1-l2)+0.01, l1+l2-0.01);
const ex=a[0]+dx/d*dm, ey=a[1]+dy/d*dm;
const base=Math.atan2(ey-a[1],ex-a[0]);
const A=Math.acos(clamp((l1*l1+dm*dm-l2*l2)/(2*l1*dm),-1,1));
const ja=base+A*bend;
return { j:[a[0]+Math.cos(ja)*l1, a[1]+Math.sin(ja)*l1], end:[ex,ey] };
}
/* ================= canvas / layout ================= */
const cv=document.getElementById('scene'), ctx=cv.getContext('2d');
const bg=document.createElement('canvas'), bctx=bg.getContext('2d');
let W=0,H=0,DPR=1,S=40,ox=0,oy=0;
const X=q=>ox+q*S, Y=y=>oy-y*S, sx=(q,z)=>X(q)+(z||0)*S*0.30;
function resize(){
W=innerWidth; H=innerHeight; DPR=Math.min(2,devicePixelRatio||1);
cv.width=W*DPR; cv.height=H*DPR; bg.width=W*DPR; bg.height=H*DPR;
S=clamp(Math.min((W-90)/21.4,(H-190)/9.9),20,64);
ox=W/2-(QMIN+QMAX)/2*S; oy=H-118;
drawStatic();
}
/* ================= world queries ================= */
const groundY=q=> q<=0?0 : q>=QT?YTOP : q*TAN;
function surfAng(q){ const s1=clamp((q+0.35)/0.7,0,1), s2=clamp((q-(QT-0.35))/0.7,0,1); return TH*s1*(1-s2); }
/* ================= state ================= */
let people=[], particles=[], stars=[];
let beltV=BELT_UP, beltTarget=BELT_UP, beltPhase=0, lastBeltAccQ=0, newelA=0;
let reversed=false, timeScale=1, slowmoT=0, acc=0, tSim=0, tReal=0, shake=0;
let stampT=0, signT=1, signCur='UP', signPrev='UP';
let chart=[], chartAcc=0, chartMarks=[], spawnCd=0;
/* ================= audio (all synthesised, optional) ================= */
let AC=null, master, mO1, mO2, mGain, mFilt, noiseBuf, muted=false, lastThud=0;
function initAudio(){
if(AC){ if(AC.state==='suspended')AC.resume(); return; }
try{
AC=new (window.AudioContext||window.webkitAudioContext)();
master=AC.createGain(); master.gain.value=0.9; master.connect(AC.destination);
mFilt=AC.createBiquadFilter(); mFilt.type='lowpass'; mFilt.frequency.value=260;
mGain=AC.createGain(); mGain.gain.value=0;
mO1=AC.createOscillator(); mO1.type='sawtooth'; mO1.frequency.value=50;
mO2=AC.createOscillator(); mO2.type='sawtooth'; mO2.frequency.value=51.5;
mO1.connect(mFilt); mO2.connect(mFilt); mFilt.connect(mGain); mGain.connect(master);
mO1.start(); mO2.start();
noiseBuf=AC.createBuffer(1,AC.sampleRate*0.4,AC.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
}catch(e){ AC=null; }
}
function klaxon(goingDown){
if(!AC||muted)return;
const t0=AC.currentTime;
const sq=AC.createOscillator(), g=AC.createGain(), f=AC.createBiquadFilter();
sq.type='square'; f.type='bandpass'; f.frequency.value=goingDown?540:420; f.Q.value=2;
g.gain.value=0.0001; sq.connect(f); f.connect(g); g.connect(master);
(goingDown?[620,470,620,470]:[470,620]).forEach((fr,i)=>{
sq.frequency.setValueAtTime(fr,t0+i*0.15);
g.gain.setValueAtTime(0.0001,t0+i*0.15);
g.gain.linearRampToValueAtTime(0.05,t0+i*0.15+0.02);
g.gain.exponentialRampToValueAtTime(0.0001,t0+i*0.15+0.14);
});
sq.start(t0); sq.stop(t0+0.8);
const o=AC.createOscillator(), g2=AC.createGain(); // mechanical clunk
o.type='sine'; o.frequency.setValueAtTime(120,t0); o.frequency.exponentialRampToValueAtTime(42,t0+0.22);
g2.gain.setValueAtTime(0.28,t0); g2.gain.exponentialRampToValueAtTime(0.001,t0+0.26);
o.connect(g2); g2.connect(master); o.start(t0); o.stop(t0+0.3);
const s=AC.createBufferSource(), bf=AC.createBiquadFilter(), g3=AC.createGain(); // gear grind
s.buffer=noiseBuf; bf.type='bandpass'; bf.frequency.value=900; bf.Q.value=1.2;
g3.gain.setValueAtTime(0.06,t0); g3.gain.exponentialRampToValueAtTime(0.001,t0+0.35);
s.connect(bf); bf.connect(g3); g3.connect(master); s.start(t0); s.stop(t0+0.4);
}
function thud(e){
if(!AC||muted)return;
const now=performance.now(); if(now-lastThud<70)return; lastThud=now;
const t0=AC.currentTime, k=clamp(e/7,0.1,1);
const o=AC.createOscillator(), g=AC.createGain();
o.type='sine'; o.frequency.setValueAtTime(92,t0); o.frequency.exponentialRampToValueAtTime(40,t0+0.12);
g.gain.setValueAtTime(0.20*k+0.02,t0); g.gain.exponentialRampToValueAtTime(0.001,t0+0.16);
o.connect(g); g.connect(master); o.start(t0); o.stop(t0+0.2);
const s=AC.createBufferSource(), f=AC.createBiquadFilter(), g2=AC.createGain();
s.buffer=noiseBuf; f.type='lowpass'; f.frequency.value=300;
g2.gain.setValueAtTime(0.11*k,t0); g2.gain.exponentialRampToValueAtTime(0.001,t0+0.09);
s.connect(f); f.connect(g2); g2.connect(master); s.start(t0); s.stop(t0+0.1);
}
function audioFrame(){
if(!AC)return;
const t=AC.currentTime;
mGain.gain.setTargetAtTime(muted?0:(0.04+(reversed?0.022:0)),t,0.08);
const fr=40+Math.abs(beltV)*13+(reversed?Math.sin(tReal*22)*5:0);
mO1.frequency.setTargetAtTime(fr,t,0.05);
mO2.frequency.setTargetAtTime(fr*1.008+1.3,t,0.05);
mFilt.frequency.setTargetAtTime(180+Math.abs(beltV)*30,t,0.1);
}
/* ================= people ================= */
function makePerson(q,z,fw){
const h=1.56+R()*0.34;
return {
q,z, vq:0, vz:0, up:true, onB:false, baseVq:0, walkV:0,
lam:(R()-0.5)*0.05, om:0,
hgt:h, m:18*h*h,
walk: fw? (0.78+R()*0.5) : (R()<0.45? 0.75+R()*0.55 : 0), // walkers vs standers
grip:R()<0.5,
Umax:3.9+R()*1.2, // ankle authority, rad/s^2
Kp:34+R()*20, Kd:9+R()*6,
delay:0.12+R()*0.22, // reaction delay, s
stepTh:0.24+R()*0.10, stepGain:0.17+R()*0.11, stepCdMax:0.22+R()*0.12,
stepCd:0, reactT:0, ramp:1,
ph:R()*6.28, seed:R()*100,
downT:0, stun:0, getT:-1, getDur:0.7, tryT:0, struggle:0,
fallAnim:1, fallDir:1, hhop:0, vhop:0, spin:0, spinA:0, lie:null,
suit:pick(SUITS), skin:pick(SKINS), hair:pick(HAIRS),
age:10, exit:false
};
}
function initial(){
people=[];
for(let i=0;i<13;i++){ // riding
const q=1.2+(i+R()*0.55)*((QT-2.4)/13);
const p=makePerson(q,(R()-0.5)*1.3);
p.onB=true; p.baseVq=beltV*COS;
if(p.walk>0)p.walkV=p.walk*0.6;
people.push(p);
}
for(let i=0;i<2;i++){ // boarding below
const p=makePerson(-1.2-i*1.1,(R()-0.5)*1.2,true);
p.walkV=p.walk; p.walk=0.9+R()*0.4; people.push(p);
}
for(let i=0;i<2;i++){ // leaving above
const p=makePerson(QT+0.5+i*0.9,(R()-0.5)*1.2,true);
p.walkV=p.walk; people.push(p);
}
}
/* ================= physics ================= */
function fallDown(p){
p.up=false;
p.fallDir=Math.sign(p.lam)||1;
p.vq=p.baseVq+Math.cos(p.lam)*p.om*0.52*p.hgt*0.85;
p.vz=p.vz*0.4+(R()-0.5)*0.5;
p.vhop=0.25+Math.min(1.3,Math.abs(p.om)*0.26)*(0.4+R()*0.6);
p.hhop=0.02;
p.spin=p.om*0.9+(R()-0.5)*2.4; p.spinA=0;
p.stun=0.45+R()*1.15; p.downT=0; p.getT=-1; p.tryT=0; p.struggle=0;
p.fallAnim=0;
const d=p.fallDir, hh=p.hgt;
p.lie={ f1:[-d*hh*(0.50+R()*0.22),0.05], f2:[-d*hh*(0.34+R()*0.18),0.10],
h1:[ d*hh*(0.10+R()*0.22),0.30+R()*0.14], h2:[d*hh*(-0.12+R()*0.2),0.07] };
dust(p.q,p.z,4); addShake(0.5);
}
function stepUp(p,h,beltQ){
p.age+=h;
let onB=p.onB; // comb hysteresis
if(onB){ if(p.q<-0.03||p.q>QT+0.22)onB=false; }
else if(p.q>0.04&&p.q<QT+0.2)onB=true;
p.onB=onB;
const targ=onB?beltQ:0; // feet adopt surface speed (rate-limited)
const nb=p.baseVq+clamp(targ-p.baseVq,-FOOT_ACC*h,FOOT_ACC*h);
const aB=(nb-p.baseVq)/h; p.baseVq=nb;
if(Math.abs(aB)>4){ p.reactT=Math.max(p.reactT,p.delay); p.ramp=0; } // jerk startles
if(p.reactT>0)p.reactT-=h; else p.ramp=Math.min(1,p.ramp+h/0.18);
// locomotion goal: everyone heads uphill; on landings everyone walks (min 0.78)
let vd=(p.q<0||p.q>QT)?Math.max(p.walk,0.78):p.walk;
if(vd>0){
let gap=9;
for(const o of people){ if(o===p)continue;
const dq=o.q-p.q;
if(dq>0.02&&dq<1.7&&Math.abs(o.z-p.z)<0.5){
const eff=dq-(o.up?0:0.62);
if(eff<gap)gap=eff;
}
}
vd*=clamp((gap-0.55)/0.45,0,1);
}
p.walkV+=clamp(vd-p.walkV,-3.2*h,3.2*h);
p.q+=(p.baseVq+p.walkV)*h;
// inverted pendulum, base acceleration = surface acceleration
const Lp=0.52*p.hgt;
let u=0;
if(p.ramp>0)u=clamp(-(p.Kp*p.lam+p.Kd*p.om),-p.Umax,p.Umax)*p.ramp;
if(p.grip&&Math.abs(aB)<2)p.om*=1-Math.min(1,1.7*h); // rail steadies the calm only
const nse=Math.sin(tSim*1.9+p.seed)*Math.sin(tSim*3.1+p.seed*1.7);
p.om+=((G*Math.sin(p.lam)-aB*Math.cos(p.lam))/Lp + u + nse*0.7)*h;
p.lam+=p.om*h;
// capture steps (need footroom)
p.stepCd-=h;
if(Math.abs(p.lam)>p.stepTh&&p.stepCd<=0){
const s=Math.sign(p.lam)||1;
let blocked=false;
for(const o of people){ if(o===p)continue;
const dq=(o.q-p.q)*s;
if(dq>0&&dq<0.4&&Math.abs(o.z-p.z)<0.5){blocked=true;break;}
}
p.lam-=s*p.stepGain*(blocked?0.22:1)*(0.75+0.5*R());
p.om*=blocked?0.85:0.32;
p.stepCd=p.stepCdMax*(blocked?1.5:1);
}
if(Math.abs(p.lam)>1.02){ fallDown(p); return; }
const f=clamp(Math.abs(p.walkV)/0.72,0,2.5);
p.ph+=6.283*f*h;
}
function stepDown(p,h,beltQ){
p.age+=h;
const onB=p.q>-0.02&&p.q<QT+0.02; p.onB=onB;
p.downT+=h; p.fallAnim=Math.min(1,p.fallAnim+h/0.32);
if(p.hhop>0||p.vhop!==0){ // cosmetic hop
p.vhop-=G*h; p.hhop+=p.vhop*h;
if(p.hhop<=0){ p.hhop=0;
if(p.vhop<-0.9){ p.vhop*=-0.3; dust(p.q,p.z,3); thud(2+Math.abs(p.vhop)*3); addShake(0.4); }
else p.vhop=0;
}
}
let aq=onB?-G*SIN*COS:0; // slope gravity, projected on travel axis
const vb=onB?beltQ:0, sig=p.vq-vb;
const amax=onB?0.5*G*COS*COS:0.62*G; // kinetic friction vs tread / floor
if(Math.abs(sig)<amax*h*1.5+1e-6)p.vq=vb; else p.vq-=Math.sign(sig)*amax*h;
p.vq+=aq*h;
p.vz*=1-Math.min(1,2.5*h);
p.q+=p.vq*h; p.z+=p.vz*h;
if(p.z>0.78){p.z=0.78;p.vz=-Math.abs(p.vz)*0.4;}
if(p.z<-0.78){p.z=-0.78;p.vz=Math.abs(p.vz)*0.4;}
p.spin*=1-Math.min(1,1.6*h); p.spinA+=p.spin*h;
// getting up: slow relative to what carries you + a clear body-length of space
if(p.getT<0&&p.downT>p.stun&&p.hhop<=0){
p.tryT-=h;
if(p.tryT<=0){ p.tryT=0.3;
const rel=Math.abs(p.vq-(onB?beltQ:0));
let clear=true;
for(const o of people){ if(o===p)continue;
const dq=Math.abs(o.q-p.q), dz=Math.abs(o.z-p.z);
if(o.up? (dq<0.5&&dz<0.4) : (dq<0.85&&dz<0.46)){clear=false;break;}
}
if(rel<0.45&&Math.abs(p.vz)<0.4&&clear){ p.getT=0; p.getDur=0.55+R()*0.35; p.struggle=0; }
else { p.struggle=1; p.vz+=(R()-0.5)*0.35; p.vq+=(R()-0.5)*0.16; } // wriggle free
}
}else if(p.getT>=0){
p.getT+=h;
if(p.getT>=p.getDur){
p.up=true; p.getT=-1;
p.baseVq=onB?beltQ:0; p.walkV=0; p.ramp=1; p.reactT=0;
p.lam=(R()-0.5)*0.08; p.om=(R()-0.5)*0.25;
p.fallAnim=1; p.spinA=0; p.hhop=0; p.vhop=0; p.stepCd=0.2;
}
}
}
function collide(){
for(let i=0;i<people.length;i++)for(let j=i+1;j<people.length;j++){
const a=people[i],b=people[j];
const ra=a.up?0.30:0.47, rb=b.up?0.30:0.47;
const dq=b.q-a.q, dz=b.z-a.z, rs=ra+rb, d2=dq*dq+dz*dz;
if(d2>=rs*rs||d2<1e-8)continue;
const d=Math.sqrt(d2), nx=dq/d, nz=dz/d;
const ia=1/a.m, ib=1/b.m, is=ia+ib;
const pen=Math.min(rs-d,0.14);
a.q-=nx*pen*(ia/is); a.z-=nz*pen*(ia/is);
b.q+=nx*pen*(ib/is); b.z+=nz*pen*(ib/is);
const rv=(b.vq-a.vq)*nx+(b.vz-a.vz)*nz;
if(rv>=0)continue;
const e=(a.up&&b.up)?0.15:0.06;
const jimp=-(1+e)*rv/is;
a.vq-=jimp*nx*ia; a.vz-=jimp*nz*ia;
b.vq+=jimp*nx*ib; b.vz+=jimp*nz*ib;
const hit=-rv;
if(a.up)a.om+=jimp*nx*ia*0.8/(0.52*a.hgt); // shove -> pitch
if(b.up)b.om-=jimp*nx*ib*0.8/(0.52*b.hgt);
if(a.up!==b.up&&hit>1.4){ // trip over a body
const u=a.up?a:b;
u.om+=Math.sign(u.vq||1)*hit*0.5/(0.52*u.hgt);
}
if(hit>2.0){
star((a.q+b.q)/2,(a.z+b.z)/2); thud(hit); addShake(Math.min(0.8,hit*0.12));
}
}
for(const p of people){
if(p.q<QMIN+0.32){p.q=QMIN+0.32; if(p.vq<0)p.vq*=-0.28;}
if(p.q>QMAX-0.32&&!p.up){p.q=QMAX-0.32; if(p.vq>0)p.vq*=-0.28;}
}
}
function physStep(h){
const pv=beltV;
beltV+=clamp(beltTarget-beltV,-BELT_RATE*h,BELT_RATE*h);
const beltAccQ=(beltV-pv)*COS/h; lastBeltAccQ=beltAccQ;
const beltQ=beltV*COS;
beltPhase+=beltV*h;
for(const p of people){ if(p.up)stepUp(p,h,beltQ); else stepDown(p,h,beltQ); }
collide();
for(const p of people)if(p.up&&p.q>QMAX-0.45)p.exit=true;
if(people.some(p=>p.exit))people=people.filter(p=>!p.exit);
spawnCd-=h;
if(people.length<P_TARGET&&spawnCd<=0){
let ok=true;
for(const o of people)if(Math.abs(o.q-(QMIN+0.6))<0.9&&Math.abs(o.z)<0.9){ok=false;break;}
if(ok){ const p=makePerson(QMIN+0.55,(R()-0.5)*1.2,true); p.age=0; p.walkV=p.walk;
people.push(p); spawnCd=0.9+R()*1.3; }
else spawnCd=0.5;
}
}
function addShake(v){ shake=Math.min(9,shake+v); }
/* ================= fx ================= */
function dust(q,z,n){ for(let i=0;i<n;i++){ if(particles.length>220)break;
particles.push({x:q+(R()-0.5)*0.3, y:groundY(q)+0.06+R()*0.25, vx:(R()-0.5)*0.7,
vy:0.25+R()*0.6, l:0.5+R()*0.5, r:0.025+R()*0.05}); } }
function star(q,z){ stars.push({q, y:groundY(q)+0.5, l:1, a:R()*6.28}); if(stars.length>40)stars.shift(); }
function drawFx(sdt){
for(const p of particles){ p.l-=sdt*1.1; p.x+=p.vx*sdt; p.y+=p.vy*sdt; p.vy*=1-1.5*sdt; p.r+=sdt*0.10; }
particles=particles.filter(p=>p.l>0);
for(const p of particles){
ctx.fillStyle='rgba(38,36,31,'+(Math.max(0,p.l)*0.28).toFixed(3)+')';
ctx.beginPath(); ctx.arc(sx(p.x,0),Y(p.y),Math.max(0.6,p.r*S),0,6.283); ctx.fill();
}
for(const s of stars){ s.l-=sdt*3.2; }
stars=stars.filter(s=>s.l>0);
ctx.strokeStyle=INK; ctx.lineWidth=1.2;
for(const s of stars){
const len=0.14*(0.4+0.6*s.l)*S, cx=sx(s.q,0), cy=Y(s.y);
ctx.globalAlpha=s.l*0.8; ctx.beginPath();
for(let k=0;k<5;k++){ const a=s.a+k*1.2566;
ctx.moveTo(cx+Math.cos(a)*len*0.4, cy+Math.sin(a)*len*0.4);
ctx.lineTo(cx+Math.cos(a)*len, cy+Math.sin(a)*len); }
ctx.stroke();
}
ctx.globalAlpha=1;
}
/* ================= poses & figure drawing ================= */
function poseUpright(p,o){
o=o||{};
const h=p.hgt, hipH=0.52*h, tor=0.29*h, hr=0.062*h;
const lam=o.lam!==undefined?o.lam:p.lam;
const vrel=o.vrel!==undefined?o.vrel:Math.max(0,p.walkV);
const panic=o.panic!==undefined?o.panic:clamp((Math.abs(p.om)-0.9)*1.1,0,1);
const b=o.brace!==undefined?o.brace:clamp((Math.abs(lam)-0.10)*2.4,0,1);
const crouch=(o.crouch||0)+b*0.18;
const s=Math.sin(lam), c=Math.cos(lam);
const m=clamp(vrel/0.25,0,1), wt=Math.min(vrel,1.2);
const A=0.10+0.13*wt;
const f1=[lerp(-0.10, A*Math.cos(p.ph),m)-b*0.14, Math.max(0,-Math.sin(p.ph))*0.07*Math.min(vrel*3,1)];
const f2=[lerp( 0.12, A*Math.cos(p.ph+Math.PI),m)+b*0.22, Math.max(0,Math.sin(p.ph))*0.07*Math.min(vrel*3,1)];
const mid=(f1[0]+f2[0])/2+Math.sin(tSim*0.8+p.seed)*0.012*(1-m);
const hip=[mid+s*hipH*(1-crouch*0.55), c*hipH*(1-crouch)];
const neck=[hip[0]+s*tor, hip[1]+c*tor];
const head=[neck[0]+s*(hr+0.02), neck[1]+c*(hr+0.02)];
const sh=[neck[0], neck[1]-0.045];
const legL=0.272*h;
const k1=ik(hip,f1,legL,legL,1), k2=ik(hip,f2,legL,legL,1);
const ua=0.205*h, fa=0.19*h;
let h1=[sh[0]-0.16*wt*Math.cos(p.ph)+b*0.18, sh[1]-0.30+b*0.14];
let h2=[sh[0]+0.16*wt*Math.cos(p.ph)+b*0.10, sh[1]-0.34];
h1=[lerp(h1[0],sh[0]+0.30,b), lerp(h1[1],hip[1]+0.14,b)];
h2=[lerp(h2[0],sh[0]+0.22,b), lerp(h2[1],hip[1]+0.20,b)];
if(p.grip&&p.onB&&!o.noRail){ // resting a hand on the rail
const w=clamp(1-Math.max(panic*2.5,b*3),0,1), ry=1.0*(h/1.75);
h2=[lerp(h2[0],sh[0]+0.16,w), lerp(h2[1],ry,w)];
}
if(panic>0){ // windmilling
const w1=tSim*11+p.seed, w2=-tSim*12.5+p.seed*1.7;
h1=[lerp(h1[0],sh[0]+Math.cos(w1)*0.42,panic), lerp(h1[1],sh[1]+Math.sin(w1)*0.42,panic)];
h2=[lerp(h2[0],sh[0]+Math.cos(w2)*0.42,panic), lerp(h2[1],sh[1]+Math.sin(w2)*0.42,panic)];
}
const e1=ik(sh,h1,ua,fa,-1), e2=ik(sh,h2,ua,fa,-1);
return {hip,neck,head,hr, f1,f2, k1:k1.j,k2:k2.j, e1:e1.j,ha1:e1.end, e2:e2.j,ha2:e2.end};
}
function poseLying(p){
const h=p.hgt, d=p.fallDir, hr=0.062*h, lie=p.lie||{f1:[-d*0.9,0.05],f2:[-d*0.6,0.1],h1:[0.2,0.3],h2:[-0.1,0.07]};
const slide=clamp(-Math.sign(p.vq||0)*Math.min(Math.abs(p.vq)*0.04,0.12),-0.12,0.12);
const wg=p.struggle?Math.sin(tSim*9+p.seed)*0.05:0;
const pel=[0,0.14];
const neck=[d*0.46*h+slide*0.5, 0.18+wg*0.4];
const head=[d*(0.46*h+0.10)+slide*0.5, 0.20];
const legL=0.26*h;
const f1=[lie.f1[0]+slide,lie.f1[1]+wg], f2=[lie.f2[0]+slide,lie.f2[1]-wg];
const k1=ik(pel,f1,legL,legL,-d), k2=ik(pel,f2,legL,legL,-d);
const ua=0.19*h, fa=0.18*h;
const h1=[lie.h1[0]+slide,lie.h1[1]+wg*2], h2=[lie.h2[0]+slide,lie.h2[1]-wg];
const e1=ik(neck,h1,ua,fa,1), e2=ik(neck,h2,ua,fa,1);
return {hip:pel,neck,head,hr, f1,f2, k1:k1.j,k2:k2.j, e1:e1.j,ha1:e1.end, e2:e2.j,ha2:e2.end};
}
const PK=['hip','neck','head','f1','f2','k1','k2','e1','ha1','e2','ha2'];
function mixPose(A,B,t){ const o={hr:A.hr};
for(const k of PK)o[k]=[lerp(A[k][0],B[k][0],t),lerp(A[k][1],B[k][1],t)]; return o; }
function limb(a,b,c,w){ ctx.lineWidth=w; ctx.beginPath(); ctx.moveTo(a[0],a[1]); ctx.lineTo(b[0],b[1]);
if(c)ctx.lineTo(c[0],c[1]); ctx.stroke(); }
function drawPerson(p){
const gy=groundY(p.q), zs=1+p.z*0.05, al=Math.min(1,p.age*2);
ctx.fillStyle='rgba(38,36,31,'+(0.13*(1-Math.min(0.7,p.hhop))*al).toFixed(3)+')';
const shr=(p.up?0.30:0.55)*zs*(1-Math.min(0.6,p.hhop*0.5));
ctx.beginPath(); ctx.ellipse(sx(p.q,p.z),Y(gy)+1.5,Math.max(1,shr*S),Math.max(0.5,shr*S*0.28),0,0,6.283); ctx.fill();
ctx.save();
ctx.translate(sx(p.q,p.z), Y(gy+p.hhop));
ctx.rotate(-surfAng(p.q));
ctx.scale(S*zs,-S*zs);
ctx.lineCap='round'; ctx.lineJoin='round'; ctx.strokeStyle=p.suit;
let pose;
if(p.up)pose=poseUpright(p,{});
else{
const lie=poseLying(p);
if(p.getT>=0){ const g=ease(clamp(p.getT/p.getDur,0,1));
pose=mixPose(lie, poseUpright(p,{lam:p.fallDir*0.1*(1-g), crouch:(1-g)*0.45, noRail:true, vrel:0}), g);
}else if(p.fallAnim<1){ const g=ease(p.fallAnim);
pose=mixPose(poseUpright(p,{lam:p.fallDir*Math.min(1,0.35+p.fallAnim), panic:1, noRail:true, vrel:0}), lie, g);
}else pose=lie;
}
if(!p.up&&p.getT<0&&Math.abs(p.spinA)>0.02){
ctx.translate(0,0.22); ctx.rotate(p.spinA); ctx.translate(0,-0.22);
}
ctx.globalAlpha=0.62*al;
limb(pose.neck,pose.e2,pose.ha2,0.038);
limb(pose.hip,pose.k2,pose.f2,0.05);
ctx.globalAlpha=al;
limb(pose.hip,pose.neck,null,0.055);
ctx.fillStyle=p.skin;
ctx.beginPath(); ctx.arc(pose.head[0],pose.head[1],pose.hr,0,6.283); ctx.fill();
ctx.lineWidth=0.018; ctx.stroke();
ctx.fillStyle=p.hair;
ctx.beginPath(); ctx.arc(pose.head[0],pose.head[1],pose.hr*0.98,0.2*Math.PI,1.3*Math.PI); ctx.closePath(); ctx.fill();
limb(pose.hip,pose.k1,pose.f1,0.05);
limb(pose.neck,pose.e1,pose.ha1,0.038);
if(p.up){ ctx.lineWidth=0.055; ctx.beginPath();
ctx.moveTo(pose.f1[0],pose.f1[1]+0.02); ctx.lineTo(pose.f1[0]+0.11,pose.f1[1]+0.01);
ctx.moveTo(pose.f2[0],pose.f2[1]+0.02); ctx.lineTo(pose.f2[0]+0.11,pose.f2[1]+0.01); ctx.stroke(); }
ctx.restore();
}
/* ================= static sheet ================= */
function ptOf(u,d){ return [X(u*COS-d*SIN), Y(u*SIN+d*COS)]; }
function slabPx(b,x1,x2,ysurf){
b.fillStyle=PAPER2; b.strokeStyle=INK; b.lineWidth=1.6;
b.beginPath(); b.rect(x1,ysurf,x2-x1,13); b.fill(); b.stroke();
b.save(); b.beginPath(); b.rect(x1,ysurf+2,x2-x1,9); b.clip();
b.strokeStyle='rgba(38,36,31,0.5)'; b.lineWidth=0.8; b.beginPath();
for(let s=x1-13;s<x2;s+=6){ b.moveTo(s,ysurf+13); b.lineTo(s+13,ysurf+2); }
b.stroke(); b.restore();
}
function leader(b,tx,ty,px,py,lab){
b.strokeStyle=FAINT; b.lineWidth=1; line(b,tx,ty,px,py);
b.fillStyle=FAINT; b.beginPath(); b.arc(tx,ty,2,0,6.283); b.fill();
b.textAlign='left'; b.fillText(lab,px+4,py+3);
}
function drawStatic(){
const b=bctx;
b.setTransform(DPR,0,0,DPR,0,0);
b.clearRect(0,0,W,H);
b.fillStyle=PAPER; b.fillRect(0,0,W,H);
for(let i=0;i<W*H/600;i++){
b.fillStyle=R()<0.5?'rgba(38,36,31,0.045)':'rgba(255,255,255,0.05)';
b.fillRect(R()*W,R()*H,1.2,1.2);
}
b.strokeStyle='rgba(38,36,31,0.06)'; b.lineWidth=1; b.beginPath();
for(let q=Math.ceil(QMIN);q<=QMAX;q++){ const x=X(q); b.moveTo(x,16); b.lineTo(x,H-16); }
for(let y=0;y<=8;y++){ const yy=Y(y); if(yy>16&&yy<H-16){ b.moveTo(16,yy); b.lineTo(W-16,yy); } }
b.stroke();
b.strokeStyle=INK; b.lineWidth=2; b.strokeRect(12.5,12.5,W-25,H-25);
b.lineWidth=0.8; b.strokeRect(17.5,17.5,W-35,H-35);
b.font='8px '+MONO; b.fillStyle=FAINT; b.textAlign='center';
for(let q=Math.ceil(QMIN*2)/2;q<=QMAX;q+=0.