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 a single 2D rigid rectangle (14×44 px, mass 1, I≈183) with position, velocity, angle and angular velocity; its four corners are the collision shape and the two bottom corners are the feet. While standing a PD controller applies torque −kp·angle −kd·w clamped to maxTorque to keep the body vertical, and the feet are coupled to the moving escalator plane by a Coulomb friction impulse limited to μ·N (μ≈0.85 standing, 0.35 when fallen, N≈m·g·cos30°). If the friction needed to follow the belt’s acceleration or the torque needed to stay upright exceeds those limits, or if |angle|>0.55 rad or |w|>3.5 rad/s, the controller is disabled and the body becomes a passive ragdoll. Person–person contact is resolved with SAT on the two OBBs: the minimum-penetration axis gives the normal, penetration is position-corrected, and a normal impulse (restitution 0.05) plus a clamped friction impulse is applied at the averaged contact point; the same impulse code handles the escalator plane and the bottom comb wall, so a fallen body is simply another colliding rectangle that blocks, is pushed, and slides under those arriving behind it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator Reversal</title>
<style>
html,body{margin:0;padding:0;overflow:hidden;background:#0a0a0f;height:100%}
canvas{display:block;width:100%;height:100%}
#ui{position:absolute;top:12px;left:12px;background:rgba(255,255,255,0.92);padding:12px 14px;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,0.3);font-family:system-ui,sans-serif;min-width:230px}
#ui h1{margin:0 0 8px 0;font-size:13px;letter-spacing:0.6px;color:#222}
#ui button{appearance:none;border:none;padding:10px 16px;border-radius:8px;font-weight:800;cursor:pointer;font-size:14px;transition:transform .05s,background .2s;width:100%}
#reverseBtn{background:#e63946;color:#fff}
#reverseBtn.reversed{background:#457b9d}
#reverseBtn:active{transform:scale(0.98)}
#resetBtn{background:#e9e9e9;color:#333;margin-top:8px}
#stats{margin-top:10px;font-size:12px;color:#333;line-height:1.4}
#hint{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.72);color:#fff;padding:8px 14px;border-radius:20px;font-family:system-ui,sans-serif;font-size:12px;pointer-events:none;text-align:center}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<h1>ESCALATOR — SIDE VIEW</h1>
<button id="reverseBtn">● REVERSE</button>
<button id="resetBtn">Reset</button>
<div id="stats"></div>
</div>
<div id="hint">18 riders. Press REVERSE to throw the belt into fast reverse.</div>
<script>
const canvas=document.getElementById('c'),ctx=canvas.getContext('2d');
let W,H,dpr;
let x0,y0,x1,y1,L,angleA,cosA,sinA,nx,ny,tx,ty;
let beltVel=70,beltTarget=70,beltPos=0;
const normalVel=70, reverseVel=-320;
let isReversed=false;
const g=900;
const muStanding=0.85, muFallen=0.35;
const restitution=0.05;
const kp=1800, kd=120, maxTorque=7000;
const fallAngle=0.55, fallW=3.5, getUpDelay=1.2;
function initEscalator(){
angleA=30*Math.PI/180; cosA=Math.cos(angleA); sinA=Math.sin(angleA);
tx=cosA; ty=-sinA; nx=sinA; ny=cosA;
L=Math.min(W*0.88, 920);
const cx=W/2, cy=H/2+35;
x0=cx - tx*L/2; y0=cy - ty*L/2;
x1=cx + tx*L/2; y1=cy + ty*L/2;
}
function resize(){
dpr=window.devicePixelRatio||1;
W=window.innerWidth; H=window.innerHeight;
canvas.width=W*dpr; canvas.height=H*dpr;
canvas.style.width=W+'px'; canvas.style.height=H+'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
initEscalator();
}
window.addEventListener('resize',resize);
class Person{
constructor(s){
this.s=s;
this.x=x0+tx*s;
this.y=y0+ty*s - 22;
this.vx=tx*beltVel; this.vy=ty*beltVel;
this.angle=(Math.random()-0.5)*0.08;
this.w=0;
this.mass=1; this.invMass=1;
this.wHalf=7; this.hHalf=22;
this.inertia=((this.wHalf*2)**2 + (this.hHalf*2)**2)/12;
this.invInertia=1/this.inertia;
this.state='standing';
this.fallTime=0; this.riseTimer=0; this.standTime=0;
this.color=`hsl(${200+Math.random()*40},72%,58%)`;
this.skin=`hsl(${22+Math.random()*18},62%,76%)`;
this.phase=Math.random()*Math.PI*2;
}
}
let people=[];
function initPeople(){
people=[];
const n=18;
for(let i=0;i<n;i++){
const s=75 + (L-150)*(i+0.5)/n + (Math.random()-0.5)*18;
const p=new Person(s);
p.x+=(Math.random()-0.5)*3; p.y+=(Math.random()-0.5)*3;
people.push(p);
}
}
function getCorners(p){
const c=Math.cos(p.angle), s=Math.sin(p.angle);
const w=p.wHalf, h=p.hHalf;
const pts=[{x:-w,y:-h},{x:w,y:-h},{x:w,y:h},{x:-w,y:h}];
return pts.map(pt=>({x:p.x + pt.x*c - pt.y*s, y:p.y + pt.x*s + pt.y*c}));
}
function updateBelt(dt){
const maxA=2500;
if(Math.abs(beltVel-beltTarget)<0.5){ beltVel=beltTarget; }
else{
const dir=Math.sign(beltTarget-beltVel);
beltVel+=dir*maxA*dt;
if(dir>0 && beltVel>beltTarget) beltVel=beltTarget;
if(dir<0 && beltVel<beltTarget) beltVel=beltTarget;
}
beltPos+=beltVel*dt;
}
function handleEscalatorContact(p,dt){
const corners=getCorners(p);
let deepest=0, dc=null, rx=0, ry=0;
for(const c of corners){
const d=(c.x - x0)*nx + (c.y - y0)*ny;
if(d<0){
const pen=-d;
if(pen>deepest){ deepest=pen; dc=c; rx=c.x-p.x; ry=c.y-p.y; }
}
}
if(deepest>0){
p.x+=nx*deepest; p.y+=ny*deepest;
const vc_x=p.vx + p.w*(-ry), vc_y=p.vy + p.w*(rx);
const v_n=vc_x*nx + vc_y*ny;
const v_t=vc_x*tx + vc_y*ty;
const vrel_t=v_t - beltVel;
if(v_n<0){
const rCrossN=rx*ny - ry*nx;
const denom=p.invMass + rCrossN*rCrossN*p.invInertia;
const j=-(1+restitution)*v_n/denom;
p.vx+=j*nx*p.invMass; p.vy+=j*ny*p.invMass; p.w+=j*rCrossN*p.invInertia;
const mu=(p.state==='standing'||p.state==='rising')?muStanding:muFallen;
const rCrossT=rx*ty - ry*tx;
const denomT=p.invMass + rCrossT*rCrossT*p.invInertia;
let jf=-vrel_t/denomT;
const maxF=mu*Math.abs(j);
jf=Math.max(-maxF,Math.min(maxF,jf));
p.vx+=jf*tx*p.invMass; p.vy+=jf*ty*p.invMass; p.w+=jf*rCrossT*p.invInertia;
} else {
const mu=(p.state==='standing'||p.state==='rising')?muStanding:muFallen;
const rCrossT=rx*ty - ry*tx;
const denomT=p.invMass + rCrossT*rCrossT*p.invInertia;
let jf=-vrel_t*0.35/denomT;
const maxF=mu*900*dt*4;
jf=Math.max(-maxF,Math.min(maxF,jf));
p.vx+=jf*tx*p.invMass; p.vy+=jf*ty*p.invMass; p.w+=jf*rCrossT*p.invInertia;
}
if(p.state==='fallen') p.w*=0.92;
}
}
function handleWallContact(p){
const corners=getCorners(p);
for(const c of corners){
const s=(c.x - x0)*tx + (c.y - y0)*ty;
if(s<0){
const pen=-s;
p.x+=tx*pen; p.y+=ty*pen;
const rx=c.x-p.x, ry=c.y-p.y;
const vc_x=p.vx + p.w*(-ry), vc_y=p.vy + p.w*(rx);
const v_n=vc_x*tx + vc_y*ty;
if(v_n<0){
const rCrossN=rx*ty - ry*tx;
const denom=p.invMass + rCrossN*rCrossN*p.invInertia;
const j=-(1+restitution*0.5)*v_n/denom;
p.vx+=j*tx*p.invMass; p.vy+=j*ty*p.invMass; p.w+=j*rCrossN*p.invInertia;
const wx=nx, wy=ny;
const v_t=vc_x*wx + vc_y*wy;
const rCrossT=rx*wy - ry*wx;
const denomT=p.invMass + rCrossT*rCrossT*p.invInertia;
let jf=-v_t/denomT;
const maxF=0.45*Math.abs(j);
jf=Math.max(-maxF,Math.min(maxF,jf));
p.vx+=jf*wx*p.invMass; p.vy+=jf*wy*p.invMass; p.w+=jf*rCrossT*p.invInertia;
}
}
}
}
function handlePersonCollision(a,b){
const ca=getCorners(a), cb=getCorners(b);
const axes=[];
let c=Math.cos(a.angle), s=Math.sin(a.angle); axes.push({x:c,y:s}); axes.push({x:-s,y:c});
c=Math.cos(b.angle); s=Math.sin(b.angle); axes.push({x:c,y:s}); axes.push({x:-s,y:c});
let minOverlap=Infinity, minAxis=null;
for(const ax of axes){
let minA=Infinity,maxA=-Infinity,minB=Infinity,maxB=-Infinity;
for(const p of ca){ const pr=p.x*ax.x+p.y*ax.y; if(pr<minA)minA=pr; if(pr>maxA)maxA=pr; }
for(const p of cb){ const pr=p.x*ax.x+p.y*ax.y; if(pr<minB)minB=pr; if(pr>maxB)maxB=pr; }
const o=Math.min(maxA,maxB)-Math.max(minA,minB);
if(o<0) return;
if(o<minOverlap){ minOverlap=o; minAxis=ax; }
}
const dx=a.x-b.x, dy=a.y-b.y;
if(dx*minAxis.x+dy*minAxis.y<0) minAxis={x:-minAxis.x,y:-minAxis.y};
const nx=minAxis.x, ny=minAxis.y, tx=-ny, ty=nx;
const tot=a.invMass+b.invMass;
const ma=minOverlap*(a.invMass/tot), mb=minOverlap*(b.invMass/tot);
a.x+=nx*ma*0.85; a.y+=ny*ma*0.85;
b.x-=nx*mb*0.85; b.y-=ny*mb*0.85;
function inside(px,py,o){
const co=Math.cos(o.angle), si=Math.sin(o.angle);
const dx=px-o.x, dy=py-o.y;
const lx=dx*co+dy*si, ly=-dx*si+dy*co;
return Math.abs(lx)<o.wHalf+0.5 && Math.abs(ly)<o.hHalf+0.5;
}
let cx=0,cy=0,cnt=0;
for(const p of ca) if(inside(p.x,p.y,b)){ cx+=p.x; cy+=p.y; cnt++; }
for(const p of cb) if(inside(p.x,p.y,a)){ cx+=p.x; cy+=p.y; cnt++; }
if(cnt===0){ cx=(a.x+b.x)/2; cy=(a.y+b.y)/2; } else { cx/=cnt; cy/=cnt; }
const rxA=cx-a.x, ryA=cy-a.y, rxB=cx-b.x, ryB=cy-b.y;
const vAx=a.vx + a.w*(-ryA), vAy=a.vy + a.w*(rxA);
const vBx=b.vx + b.w*(-ryB), vBy=b.vy + b.w*(rxB);
const vrX=vAx-vBx, vrY=vAy-vBy;
const vrN=vrX*nx+vrY*ny;
if(vrN<0){
const rCN_A=rxA*ny - ryA*nx, rCN_B=rxB*ny - ryB*nx;
const denom=a.invMass+b.invMass + rCN_A*rCN_A*a.invInertia + rCN_B*rCN_B*b.invInertia;
const j=-(1+restitution)*vrN/denom;
a.vx+=j*nx*a.invMass; a.vy+=j*ny*a.invMass; a.w+=j*rCN_A*a.invInertia;
b.vx-=j*nx*b.invMass; b.vy-=j*ny*b.invMass; b.w-=j*rCN_B*b.invInertia;
const vrT=vrX*tx+vrY*ty;
const rCT_A=rxA*ty - ryA*tx, rCT_B=rxB*ty - ryB*tx;
const denomT=a.invMass+b.invMass + rCT_A*rCT_A*a.invInertia + rCT_B*rCT_B*b.invInertia;
let jf=-vrT/denomT;
const maxF=0.5*Math.abs(j);
jf=Math.max(-maxF,Math.min(maxF,jf));
a.vx+=jf*tx*a.invMass; a.vy+=jf*ty*a.invMass; a.w+=jf*rCT_A*a.invInertia;
b.vx-=jf*tx*b.invMass; b.vy-=jf*ty*b.invMass; b.w-=jf*rCT_B*b.invInertia;
}
a.w*=0.98; b.w*=0.98;
}
function updateState(p,dt){
const dist=(p.x - x0)*nx + (p.y - y0)*ny;
const onEsc=dist<32 && dist>-12 && p.s>-12 && p.s<L+12;
if(p.state==='standing'){
p.standTime+=dt;
if(Math.abs(p.angle)>fallAngle || Math.abs(p.w)>fallW){
p.state='falling'; p.fallTime=0; p.w+=(beltVel<0?1:-1)*1.8;
}
} else if(p.state==='falling'){
p.fallTime+=dt;
if(Math.abs(p.angle)>1.0 && onEsc && Math.abs(p.w)<2.2) { p.state='fallen'; p.fallTime=0; p.w*=0.5; }
else if(p.fallTime>0.85 && onEsc){ p.state='fallen'; p.fallTime=0; }
} else if(p.state==='fallen'){
p.fallTime+=dt;
const spd=Math.hypot(p.vx,p.vy);
let nearby=0;
for(const o of people) if(o!==p){ const d=Math.hypot(o.x-p.x,o.y-p.y); if(d<36) nearby++; }
if(p.fallTime>getUpDelay && spd<32 && Math.abs(p.w)<1.1 && nearby<2 && onEsc){
let blocked=false;
for(const o of people) if(o!==p){
const ds=Math.abs(o.s-p.s), dn=Math.abs((o.x-p.x)*nx+(o.y-p.y)*ny);
if(ds<28 && dn<20){ blocked=true; break; }
}
if(!blocked){ p.state='rising'; p.riseTimer=0; }
}
p.vx*=0.996; p.w*=0.992;
} else if(p.state==='rising'){
p.riseTimer+=dt;
if(Math.abs(p.angle)<0.26 && Math.abs(p.w)<1.2 && p.riseTimer>0.6 && onEsc){
p.state='standing'; p.angle*=0.5; p.w*=0.5; p.standTime=0;
}
if(Math.abs(p.angle)>0.85 || Math.abs(p.w)>4){ p.state='falling'; p.fallTime=0; }
if(p.riseTimer>2.0){ p.state='fallen'; p.fallTime=0; }
}
}
function physicsStep(dt){
for(const p of people){
p.vy+=g*dt;
if(p.state==='standing'){
let tq=-kp*p.angle - kd*p.w;
tq=Math.max(-maxTorque,Math.min(maxTorque,tq));
p.w+=tq*p.invInertia*dt;
p.w*=0.995;
} else if(p.state==='rising'){
let tq=-kp*0.7*p.angle - kd*0.5*p.w;
tq=Math.max(-maxTorque*0.8,Math.min(maxTorque*0.8,tq));
p.w+=tq*p.invInertia*dt;
p.vy-=180*dt;
} else { p.w*=0.998; p.vx*=0.999; p.vy*=0.999; }
p.x+=p.vx*dt; p.y+=p.vy*dt; p.angle+=p.w*dt;
p.s=(p.x - x0)*tx + (p.y - y0)*ty;
}
for(const p of people) handleEscalatorContact(p,dt);
for(const p of people) handleWallContact(p);
for(let it=0;it<3;it++) for(let i=0;i<people.length;i++) for(let j=i+1;j<people.length;j++) handlePersonCollision(people[i],people[j]);
for(const p of people){
if(beltVel>0 && p.s>L+42){
p.s=-18; p.x=x0+tx*p.s+nx*26; p.y=y0+ty*p.s+ny*26;
p.vx=tx*beltVel; p.vy=ty*beltVel; p.angle=0; p.w=0; p.state='standing';
}
if(p.y>H+110 || p.x<-110 || p.x>W+110){
p.s=60+Math.random()*(L-120); p.x=x0+tx*p.s+nx*28; p.y=y0+ty*p.s+ny*28;
p.vx=tx*beltVel; p.vy=ty*beltVel; p.angle=0; p.w=0; p.state='standing';
}
}
for(const p of people) updateState(p,dt);
}
function drawEscalator(){
const thick=38;
const p0x=x0,p0y=y0,p1x=x1,p1y=y1;
const offx=nx*thick, offy=ny*thick;
ctx.fillStyle='#1e2130'; ctx.beginPath();
ctx.moveTo(p0x,p0y); ctx.lineTo(p1x,p1y); ctx.lineTo(p1x-offx,p1y-offy); ctx.lineTo(p0x-offx,p0y-offy); ctx.closePath(); ctx.fill();
ctx.strokeStyle='#2f3347'; ctx.lineWidth=2; ctx.stroke();
const stepCount=30, spacing=L/stepCount;
const off=beltPos%spacing;
for(let i=-2;i<stepCount+3;i++){
const s=i*spacing + off;
if(s<-30||s>L+30) continue;
const x=x0+tx*s, y=y0+ty*s;
ctx.save(); ctx.translate(x,y); ctx.rotate(-angleA);
ctx.fillStyle='#4a4e62'; ctx.fillRect(-9,-6,18,6);
ctx.fillStyle='#6a6e82'; ctx.fillRect(-9,-6,18,2);
ctx.restore();
}
const rail=14;
ctx.strokeStyle='#0b0c12'; ctx.lineWidth=5; ctx.beginPath();
ctx.moveTo(p0x+nx*rail,p0y+ny*rail); ctx.lineTo(p1x+nx*rail,p1y+ny*rail); ctx.stroke();
ctx.beginPath(); ctx.moveTo(p0x-nx*(thick+rail-10),p0y-ny*(thick+rail-10)); ctx.lineTo(p1x-nx*(thick+rail-10),p1y-ny*(thick+rail-10)); ctx.stroke();
ctx.strokeStyle='#e63946'; ctx.lineWidth=6; ctx.lineCap='round'; ctx.beginPath();
ctx.moveTo(p0x+nx*rail,p0y+ny*rail); ctx.lineTo(p1x+nx*rail,p1y+ny*rail); ctx.stroke();
ctx.beginPath(); ctx.moveTo(p0x-nx*(thick+rail-10),p0y-ny*(thick+rail-10)); ctx.lineTo(p1x-nx*(thick+rail-10),p1y-ny*(thick+rail-10)); ctx.stroke();
ctx.fillStyle='#c9c9d6'; ctx.beginPath(); ctx.arc(p0x,p0y,11,0,Math.PI*2); ctx.fill(); ctx.strokeStyle='#777'; ctx.lineWidth=2; ctx.stroke();
ctx.beginPath(); ctx.arc(p1x,p1y,11,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#ffbe0b'; ctx.fillRect(p0x-3,p0y-32,6,64);
ctx.fillStyle='rgba(255,190,11,0.25)'; ctx.fillRect(p0x-12,p0y-42,24,84);
}
function drawPerson(p){
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.angle);
const h=p.hHalf, headR=7, headY=-h+headR, torsoTop=headY+headR, hipY=h-10, footY=h, shoulderY=torsoTop+6;
ctx.strokeStyle='#1a1a1a'; ctx.lineWidth=3; ctx.lineCap='round';
if(p.state==='standing'||p.state==='rising'){
ctx.beginPath(); ctx.moveTo(-3,hipY); ctx.lineTo(-4,footY); ctx.moveTo(3,hipY); ctx.lineTo(4,footY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0,shoulderY); ctx.lineTo(-6,shoulderY+10); ctx.moveTo(0,shoulderY); ctx.lineTo(6,shoulderY+10); ctx.stroke();
} else {
const t=performance.now()/1000 + p.phase, f=Math.sin(t*13)*0.6;
ctx.beginPath(); ctx.moveTo(0,hipY); ctx.lineTo(-5+f*4,footY-1); ctx.moveTo(0,hipY); ctx.lineTo(5-f*4,footY-1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0,shoulderY); ctx.lineTo(-7+f*5,shoulderY+8); ctx.moveTo(0,shoulderY); ctx.lineTo(7-f*5,shoulderY+8); ctx.stroke();
}
let col=p.color; if(p.state==='falling'||p.state==='fallen') col='#ff5a5a'; else if(p.state==='rising') col='#ffd166';
ctx.strokeStyle=col; ctx.lineWidth=6; ctx.beginPath(); ctx.moveTo(0,torsoTop); ctx.lineTo(0,hipY); ctx.stroke();
ctx.strokeStyle='#000'; ctx.lineWidth=1; ctx.stroke();
ctx.fillStyle=p.skin; ctx.strokeStyle='#000'; ctx.lineWidth=1.4; ctx.beginPath(); ctx.arc(0,headY,headR,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#000'; ctx.beginPath(); ctx.arc(2,headY-1,1.1,0,Math.PI*2); ctx.fill();
ctx.restore();
if(p.state!=='standing'){
ctx.save(); ctx.translate(p.x,p.y); ctx.fillStyle=p.state==='fallen'?'#ff6b6b':p.state==='falling'?'#ff9f1c':'#ffd166';
ctx.font='700 9px system-ui,sans-serif'; ctx.textAlign='center'; ctx.fillText(p.state.toUpperCase(),0,-p.hHalf-13); ctx.restore();
}
}
function render(){
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#0f111a'; ctx.fillRect(0,0,W,H);
drawEscalator();
for(const p of people) drawPerson(p);
}
function updateStats(){
const fallen=people.filter(p=>p.state==='fallen').length, falling=people.filter(p=>p.state==='falling').length, standing=people.filter(p=>p.state==='standing').length;
document.getElementById('stats').innerHTML=`Belt: ${beltVel>0?'UP':'DOWN'} ${Math.abs(beltVel|0)} px/s<br>Standing: ${standing} Falling: ${falling} Fallen: ${fallen}`;
}
let last=0;
function loop(t){
requestAnimationFrame(loop);
if(!last) last=t;
let dt=(t-last)/1000; last=t; dt=Math.min(dt,1/30);
updateBelt(dt);
const subs=4, sdt=dt/subs;
for(let i=0;i<subs;i++) physicsStep(sdt);
render(); updateStats();
}
document.getElementById('reverseBtn').addEventListener('click',()=>{
isReversed=!isReversed; beltTarget=isReversed?reverseVel:normalVel;
const b=document.getElementById('reverseBtn');
b.textContent=isReversed?'◀ REVERSE (DOWN)':'● REVERSE';
b.classList.toggle('reversed',isReversed);
});
document.getElementById('resetBtn').addEventListener('click',initPeople);
resize(); initPeople(); requestAnimationFrame(loop);
</script>
</body>
</html>
When you press REVERSE the belt target jumps from +70 px/s up to −320 px/s down and accelerates at 2500 px/s², so the reversal completes in ~0.2 s. The Coulomb limit μ·N (≈0.85·N standing) cannot supply the ~1700 px/s² needed to keep feet with the belt, feet slip, the friction impulse at the foot (offset ~22 px below the COM) creates a large overturning torque, the PD upright controller saturates at 7000, and most riders exceed 0.55 rad / 3.5 rad/s and become passive within 0.3–0.6 s. Fallen rectangles have μ=0.35, so gravity alone (g·sin30° > μ·g·cos30°) drags them downhill; the static yellow comb wall at s=0 stops them and the next riders hit them, their SAT impulse adds torque and they topple, forming a 2–4-deep domino pile that creeps downhill as a frictional granular flow. Pressing REVERSE again drives the pile uphill and it usually breaks apart. Special-cased/nudged: only the bottom wall is hard — the top is open and riders who exit the top while moving up are wrapped to the bottom to keep 18 on the belt; standing vs fallen use different μ/restitution; getting up is gated (≥1.2 s fallen, speed<32 px/s, <2 neighbours within 36 px and no one within 28 px along the belt) and uses a weaker PD plus a small lift; angular damping and 4× substeps are added for stability. What it does badly: thin OBBs tunnel 1–2 px at high belt speed and the averaged contact point jitters on edge-edge contacts; the pile has no true static friction so a tall stack slowly oozes instead of locking; rising is fragile — a riser in a dense pile stays blocked forever and a riser while the belt is still fast is immediately re-toppled; handrails are visual only so limbs clip through them.