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 one rigid capsule — a line segment from feet to crown with a mass, a moment of inertia, and a per-figure height and color. While balanced, a figure is an inverted pendulum pivoting on its planted feet: gravity tips it, the escalator's acceleration enters as base excitation, and a saturated ankle-torque controller plus an occasional shuffle-step (foot re-placement, limited by tread length) tries to hold a lean that matches the current acceleration; the feet also have a friction budget, and when the step's acceleration exceeds it the pivot starts to slide. A figure loses balance for no scripted reason — only when tilt, angular rate, friction, or support geometry exceed limits every figure shares. Once down, everyone becomes the same free rigid body under gravity, resolved by my own impulse solver: capsule-vs-capsule closest-point contacts between people, and circle-vs-moving-segment contacts against step treads, risers, and landings, with Coulomb friction — so a fallen figure is simply an obstacle, risers bulldoze loose bodies downhill, and piles compress and relax under their own weight. Overlaps are resolved by positional projection plus normal impulses, and tangential impulses clamped to μ times the normal impulse; standing figures hit by a body absorb the impulse as an angular kick and a shove of their foot placement, which can push them past their own limits. Getting up is a rule too: a downed figure waits until its speed is near zero, nothing overlaps the space its body would sweep, and the surface under it is calm, then rotates up about its feet — and is knocked back down if struck mid-rise.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESC-7 · Truss Field Overrun</title>
<style>
:root{
--ink:#e8e3d4; --dim:#8fa0a3; --yel:#ffd23f; --red:#ff4b3a;
--panel:#10171c; --edge:#2c3a42;
}
*{box-sizing:border-box; margin:0; padding:0}
html,body{height:100%}
body{
background:#0b1013; color:var(--ink); overflow:hidden;
font:14px/1.45 system-ui, "Segoe UI", sans-serif;
-webkit-user-select:none; user-select:none;
}
#stage{position:fixed; inset:0}
canvas{display:block; width:100%; height:100%; cursor:crosshair}
#sign{
position:fixed; top:18px; left:22px; pointer-events:none;
border-left:6px solid var(--yel); padding:2px 0 4px 14px;
text-shadow:0 2px 8px rgba(0,0,0,.6);
}
#sign h1{
font-family:Haettenschweiler, Impact, "Arial Narrow", sans-serif;
font-weight:400; font-size:clamp(30px,4.6vw,54px); line-height:.92;
letter-spacing:.045em; color:var(--ink);
}
#sign h1 em{font-style:normal; color:var(--yel)}
#sign p{
margin-top:6px; font-family:ui-monospace,Menlo,Consolas,monospace;
font-size:11px; letter-spacing:.14em; color:var(--dim); text-transform:uppercase;
}
#console{
position:fixed; left:22px; bottom:20px; display:flex; align-items:stretch; gap:14px;
background:linear-gradient(#141d23,#0d1418);
border:1px solid var(--edge); border-radius:10px; padding:12px 16px 12px 12px;
box-shadow:0 10px 30px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05);
}
#reverseBtn{
position:relative; width:118px; border-radius:8px; cursor:pointer;
border:2px solid #6b5510; color:#241b02;
background:linear-gradient(#ffe071,#f0b929 55%,#d99f16);
font-family:Haettenschweiler, Impact, "Arial Narrow", sans-serif;
font-size:23px; letter-spacing:.09em; padding:10px 6px 8px;
display:flex; flex-direction:column; align-items:center; gap:2px;
transition:transform .08s ease, filter .12s ease, box-shadow .12s ease;
box-shadow:0 5px 0 #7a5c07, 0 9px 18px rgba(0,0,0,.5);
}
#reverseBtn small{font-family:ui-monospace,monospace; font-size:9px; letter-spacing:.18em}
#reverseBtn:hover{filter:brightness(1.08)}
#reverseBtn:active{transform:translateY(4px); box-shadow:0 1px 0 #7a5c07, 0 4px 10px rgba(0,0,0,.5)}
#reverseBtn.engaged{
background:linear-gradient(#ff7a68,#e6392a 55%,#b3200f);
border-color:#5f1207; color:#fff;
box-shadow:0 5px 0 #6d1105, 0 9px 18px rgba(0,0,0,.5);
animation:btnpulse .5s steps(2,end) infinite;
}
@keyframes btnpulse{50%{filter:brightness(1.25)}}
.lamp{width:9px;height:9px;border-radius:50%;background:#5c2a22;box-shadow:inset 0 0 3px #000}
#reverseBtn.engaged .lamp{background:#fff; box-shadow:0 0 10px #fff, 0 0 18px var(--red)}
#readout{display:flex; flex-direction:column; justify-content:center; gap:3px;
font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12px; min-width:190px}
#readout .row{display:flex; justify-content:space-between; gap:14px}
#readout .k{color:var(--dim); letter-spacing:.12em; font-size:10px}
#readout .v{color:var(--yel)}
#readout .v.bad{color:var(--red)}
#hint{
position:fixed; right:22px; bottom:22px; max-width:230px; text-align:right;
font-family:ui-monospace,monospace; font-size:10.5px; letter-spacing:.06em;
color:var(--dim); pointer-events:none; line-height:1.6;
}
#hint b{color:var(--ink)}
@media (max-width:640px){ #hint{display:none} #readout{min-width:150px} }
</style>
</head>
<body>
<div id="stage"><canvas id="cv"></canvas></div>
<div id="sign">
<h1>ESC-7 <em>//</em> PLATFORM 2</h1>
<p>service up · step band nominal · hold handrail</p>
</div>
<div id="console">
<button id="reverseBtn" aria-pressed="false">
<span class="lamp"></span>REVERSE<small>DRIVE OVERRIDE</small>
</button>
<div id="readout">
<div class="row"><span class="k">BELT SPEED</span><span class="v" id="rSpeed">+0.50 m/s</span></div>
<div class="row"><span class="k">MODE</span><span class="v" id="rMode">SERVICE UP</span></div>
<div class="row"><span class="k">RIDING</span><span class="v" id="rRide">0</span></div>
<div class="row"><span class="k">DOWN</span><span class="v" id="rDown">0</span></div>
</div>
</div>
<div id="hint"><b>throw the override.</b> press again to restore service.<br>click the scene to poke the nearest rider.<br>every fall comes out of the solver — nothing is keyframed.</div>
<script>
(()=>{ "use strict";
/* ================================================================
ESC-7 — emergent escalator disaster.
One rigid-capsule person model. Balance = saturated inverted
pendulum + friction + stepping. Down = free rigid body in an
impulse solver vs steps (treads + risers), landings, each other.
================================================================ */
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const W = 13.0, H = 6.4, GRAV = 9.81;
const X0 = 3.0, ALPHA = Math.PI/6, CA = Math.cos(ALPHA), SA = Math.sin(ALPHA);
const LINC = 6.4, RISE = LINC*SA, XTOP = X0 + LINC*CA; // 3.0 .. 8.54
const PITCH = 0.4, RISER = PITCH*SA/CA, RANGE = 8.4, NSTEP = 21, SMIN = -1.0;
const VNORM = 0.5, VREV = -2.3, VSWITCH = 13.0; // m/s, m/s, m/s^2
const MU_BODY = 0.45, MU_STEP = 0.55, MU_GROUND = 0.7;
const SUB = 5, ITER = 4;
let scale=60, ox=0, oy=0, dpr=1;
function resize(){
dpr = Math.min(window.devicePixelRatio||1, 2);
cv.width = innerWidth*dpr; cv.height = innerHeight*dpr;
scale = Math.min(cv.width/W, cv.height/H);
ox = (cv.width - W*scale)/2; oy = (cv.height - H*scale)/2;
}
addEventListener('resize', resize); resize();
const X = x=>ox+x*scale, Y = y=>oy+(H-y)*scale;
/* ---------- escalator ---------- */
const esc = { off:0, v:VNORM, a:0, dir:1, phase:'norm' }; // phase: norm|switch|rev
function escStep(dt){
const target = esc.dir>0 ? VNORM : VREV;
const dv = target - esc.v, maxD = VSWITCH*dt;
esc.a = Math.abs(dv)<1e-6 ? 0 : Math.sign(dv)*Math.min(Math.abs(dv)/dt, VSWITCH);
esc.v += Math.abs(dv)<=maxD ? dv : Math.sign(dv)*maxD;
esc.off = (esc.off + esc.v*dt) % RANGE;
esc.phase = esc.dir<0 ? (esc.v < -0.2 ? 'rev':'switch') : (esc.v > 0.2 ? 'norm':'switch');
}
function stepS(i){ let s = SMIN + ((i*PITCH + esc.off)%RANGE+RANGE)%RANGE; return s; }
function stepGeom(i){
const s = stepS(i), cx = X0 + s*CA, ty = s*SA;
return { s, cx, ty, x0:cx-PITCH/2*CA, x1:cx+PITCH/2*CA, rx:cx-PITCH/2*CA, ry0:ty-RISER, ry1:ty };
}
function supportAt(x, y){ // {y, vx, vy, kind, idx}
let best=null;
if(x < X0+0.06 && y < 0.35) best={y:0,vx:0,vy:0,kind:'land'};
if(x > XTOP-0.06 && y > RISE-0.35) best={y:RISE,vx:0,vy:0,kind:'land'};
for(let i=0;i<NSTEP;i++){
const g=stepGeom(i);
if(g.s<-0.05||g.s>LINC+0.05) continue;
if(x>=g.x0-0.02 && x<=g.x1+0.02 && g.ty>=y-0.3 && (!best || g.ty>best.y))
best={y:g.ty, vx:esc.v*CA, vy:esc.v*SA, kind:'step', idx:i};
}
return best;
}
/* ---------- people ---------- */
const SKIN=['#e7b98f','#d9a06b','#b97a4e','#8a5a38','#6b4028'];
const CLOTH=['#c94f3d','#3f8f86','#c9962e','#5f7d52','#546a8f','#a54a67','#8a8f4f','#b0603a'];
const people=[];
let pid=0;
function makePerson(x){
const Ht=1.56+Math.random()*0.3, L=Ht*0.93;
return {
id:pid++, H:Ht, L, r:0.085, m:52+Ht*26+Math.random()*8, I:0,
x, y:L/2, vx:0, vy:0, phi:0, w:0,
state:'queue', theta:0, thw:0, footX:x, footY:0, fOff:0, stepIdx:-1,
sup:null, slip:0, stepCd:0, lastHit:-9, getT:0, getFrom:0, getDir:1,
targetX:x, walkSp:0.85+Math.random()*0.4, gait:Math.random()*6,
col:CLOTH[(Math.random()*CLOTH.length)|0], skin:SKIN[(Math.random()*SKIN.length)|0],
seed:Math.random()*10, flail:0,
};
}
function Iof(p){ return p.m*p.L*p.L/12; }
function axis(p){ return {x:Math.sin(p.phi), y:Math.cos(p.phi)}; }
function ends(p){ const u=axis(p), h=p.L/2;
return { fx:p.x-u.x*h, fy:p.y-u.y*h, hx:p.x+u.x*h, hy:p.y+u.y*h }; }
function capsuleOf(p){
if(p.state==='stand'){ const e=ends(p); return {x0:e.fx,y0:e.fy,x1:e.hx,y1:e.hy,r:p.r,m:p.m,I:Iof(p),p}; }
return {x0:p.x-Math.sin(p.phi)*p.L/2, y0:p.y-Math.cos(p.phi)*p.L/2,
x1:p.x+Math.sin(p.phi)*p.L/2, y1:p.y+Math.cos(p.phi)*p.L/2, r:p.r, m:p.m, I:Iof(p), p};
}
function velAt(p, rx, ry){ return {x:p.vx - p.w*ry, y:p.vy + p.w*rx}; }
const NF = 17;
for(let i=0;i<NF;i++){
const p = makePerson(0);
// pre-seed riders spread along the belt, already balanced
const s = 0.35 + (i%16)*PITCH + 0.02*Math.random();
p.state='stand'; p.stepIdx=-2; p.x = X0+s*CA; p.footX=p.x; p.footY=s*SA;
p.y = p.footY + p.L/2; p.theta=0; p.thw=0;
people.push(p);
}
// two more queue off-screen, walking in (17 riding + 2 = 19 total)
for(let i=0;i<2;i++){ const p=makePerson(-1.2-i*1.1); p.state='walkIn'; p.targetX=1.6; people.push(p); }
/* ---------- transitions ---------- */
function startFall(p, extraW){
const e=ends(p);
p.x=(e.fx+e.hx)/2; p.y=(e.fy+e.hy)/2;
p.phi=p.theta; p.w=p.thw+(extraW||0)+(Math.random()-0.5)*1.5;
const sv = p.sup ? p.sup : {vx:0,vy:0};
p.vx = sv.vx*0.4 + (Math.random()-0.5)*0.3;
p.vy = sv.vy*0.4 + 0.1;
p.state='fall'; p.flail=1; p.lastHit=timeNow;
}
function tryGetUp(p){
if(Math.hypot(p.vx,p.vy)>0.4 || Math.abs(p.w)>0.7) return;
if(timeNow - p.lastHit < 0.6) return;
if(Math.abs(esc.a) > 3.5) return;
const e=ends(p);
// feet = whichever end is lower; stand up rotating the head over them
const feetHead = (e.fy<=e.hy) ? {fx:e.fx,fy:e.fy,hx:e.hx,hy:e.hy} : {fx:e.hx,fy:e.hy,hx:e.fx,hy:e.fy};
const sup = supportAt(feetHead.fx, feetHead.fy);
if(!sup || sup.y > feetHead.fy+0.18 || sup.y < feetHead.fy-0.3) return;
if(Math.hypot(sup.vx,sup.vy) > 2.6) return;
// clearance: sweep three candidate torso angles, must overlap nobody
const cur = Math.atan2(feetHead.hx-feetHead.fx, feetHead.hy-feetHead.fy);
const goal = 0;
for(const t of [cur*0.66, cur*0.33, 0]){
const hx=feetHead.fx+Math.sin(t)*p.L, hy=feetHead.fy+Math.cos(t)*p.L;
for(const q of people){ if(q===p) continue;
const c=capsuleOf(q);
if(segDist(feetHead.fx,feetHead.fy,hx,hy,c.x0,c.y0,c.x1,c.y1).d < p.r+c.r+0.05) return;
}
}
p.state='getup'; p.getT=0; p.getFrom=cur; p.getDir=Math.sign(goal-cur)||1;
p.gFoot={x:feetHead.fx, y:sup.y}; p.gSup=sup; p.w=0; p.vx=0; p.vy=0;
}
/* ---------- balance ---------- */
function standPhysics(p, dt){
const sup = supportAt(p.footX, p.footY+0.05);
if(!sup){ startFall(p,0); return; }
p.sup=sup;
// friction-limited pivot tracking of its support
const ax = esc.a*CA, ay = esc.a*SA;
const need = Math.hypot(ax - 0, ay + 0);
const cap = MU_STEP*GRAV;
p.slip = need>cap ? (need-cap)/GRAV : 0;
const track = Math.min(1, dt*22);
p.footX += (sup.vx - (p.slip? p.slip*Math.sign(sup.vx||1):0))*dt*track + (sup.x!==undefined?0:0);
// plant foot on the surface point beneath (steps carry the foot)
if(sup.kind==='step'){
p.footX += sup.vx*dt*(1-track)*0; // foot rides the step:
p.footX = p.footX; // (tracking handled below)
}
// simpler & sturdier: foot follows support exactly, minus slip drift
p.footX += (sup.vx*dt) * (sup.kind==='step'?0:0);
// -- do the actual carry:
p.footX = p.footX; // no-op keeps intent clear; real carry:
p.footX += 0;
// (carry is applied once, here:)
p.footX = p.footX + sup.vx*dt - sup.vx*dt*track; // blend to support velocity
p.footY = sup.y;
if(p.slip>0) p.footX -= p.slip*Math.sign(esc.v||1)*dt*0.6;
// keep foot inside tread / landing
if(sup.kind==='step'){
const g=stepGeom(sup.idx);
p.footX = Math.max(g.x0+0.05, Math.min(g.x1-0.05, p.footX));
if(g.s<-0.02){ p.state='walkIn'; p.targetX=XTOP+1; p.walkSp=0.8; return; } // rode into comb -> step off
if(g.s>LINC-0.02){ p.state='walkOut'; p.targetX=XTOP+1.4; p.walkSp=0.9; p.y=RISE+p.L/2; p.footY=RISE; return; }
}
// pendulum with base excitation
const h = p.L/2 + p.L*0.05;
const thStar = Math.atan2(ax, GRAV+ay);
const mgh = p.m*GRAV*h, Qmax = 0.26*mgh;
let Q = -(4.2*mgh)*(p.theta-thStar) - (1.35*mgh)*p.thw;
Q = Math.max(-Qmax, Math.min(Qmax, Q));
const thAcc = (GRAV*Math.sin(p.theta) - Math.cos(p.theta)*ax + Math.sin(p.theta)*ay)/h + Q/(p.m*h*h);
p.thw += thAcc*dt; p.thw *= (1-0.12*dt);
p.theta += p.thw*dt;
// stepping strategy: shuffle toward the lean, if room
p.stepCd -= dt;
if(p.stepCd<=0 && Math.abs(p.theta-thStar)>0.16){
const dir=Math.sign(p.theta);
p.footX += dir*0.11; p.stepCd=0.22;
}
// sync rigid pose
p.phi=p.theta; p.x=p.footX+Math.sin(p.theta)*p.L/2; p.y=p.footY+Math.cos(p.theta)*p.L/2;
p.vx=sup.vx; p.vy=sup.vy; p.w=p.thw;
// fall criteria — identical for every figure
if(Math.abs(p.theta)>0.62 || Math.abs(p.thw)>3.4 || p.slip>0.75) startFall(p,0);
}
/* ---------- free-body physics ---------- */
function segDist(ax,ay,bx,by,cx,cy,dx,dy){
const ux=bx-ax,uy=by-ay,vx=dx-cx,vy=dy-cy,wx=ax-cx,wy=ay-cy;
const a=ux*ux+uy*uy,b=ux*vx+uy*vy,c=vx*vx+vy*vy,d=ux*wx+uy*wy,e=vx*wx+vy*wy;
const D=a*c-b*b; let s,t;
if(D<1e-9){s=0;t=e/c;}else{s=(b*e-c*d)/D;t=(a*e-b*d)/D;}
s=Math.max(0,Math.min(1,s)); t=Math.max(0,Math.min(1,t));
const px=ax+s*ux-(cx+t*vx), py=ay+s*uy-(cy+t*vy);
return {d:Math.hypot(px,py), nx:px, ny:py, s, t};
}
function applyImpulse(p, rx, ry, jx, jy){
const invM=1/p.m, invI=1/Iof(p);
p.vx+=jx*invM; p.vy+=jy*invM; p.w+=(rx*jy-ry*jx)*invI;
}
function effMass(p, rx, ry, nx, ny){ return 1/(1/p.m + (rx*ny-ry*nx)**2/Iof(p)); }
function collideEnv(p){
const u=axis(p), pts=[-p.L/2, 0, p.L/2];
for(const t of pts){
const px=p.x+u.x*t, py=p.y+u.y*t;
// landings
const floors=[{y:0,x0:-9,x1:X0+0.06},{y:RISE,x0:XTOP-0.06,x1:99}];
for(const f of floors){
if(px>f.x0&&px<f.x1&&py<f.y+p.r){
const pen=f.y+p.r-py, rx=px-p.x, ry=py-p.y;
p.y+=pen*0.8;
const vp=velAt(p,rx,ry), vn=vp.y;
if(vn<0){ const em=effMass(p,rx,ry,0,1), jn=-vn*em;
applyImpulse(p,rx,ry,0,jn);
let jt=-vp.x*em; const mx=MU_GROUND*jn; jt=Math.max(-mx,Math.min(mx,jt));
applyImpulse(p,rx,ry,jt,0);
}
p.lastHit=timeNow;
}
}
// steps: tread (top) + riser (both faces)
for(let i=0;i<NSTEP;i++){
const g=stepGeom(i); if(g.s<-0.1||g.s>LINC+0.1) continue;
if(px>g.x0&&px<g.x1&&py<g.ty+p.r&&py>g.ty-0.25){
const pen=g.ty+p.r-py, rx=px-p.x, ry=py-p.y;
p.y+=pen*0.8;
const vp=velAt(p,rx,ry), rvx=vp.x-esc.v*CA, rvy=vp.y-esc.v*SA, vn=rvy;
if(vn<0){ const em=effMass(p,rx,ry,0,1), jn=-vn*em;
applyImpulse(p,rx,ry,0,jn);
let jt=-rvx*em; const mx=MU_STEP*jn; jt=Math.max(-mx,Math.min(mx,jt));
applyImpulse(p,rx,ry,jt,0);
if(Math.abs(jn)/ (1/60/SUB) > 40) p.lastHit=timeNow;
}
}
if(py>g.ry0&&py<g.ry1+p.r&&Math.abs(px-g.rx)<p.r+0.03){
const side = px<g.rx?-1:1, pen=p.r+0.03-Math.abs(px-g.rx), rx=px-p.x, ry=py-p.y;
p.x+=side*pen*0.8;
const vp=velAt(p,rx,ry), rvx=vp.x-esc.v*CA, vn=rvx*side;
if(vn<0){ const em=effMass(p,rx,ry,side,0), jn=-vn*em;
applyImpulse(p,rx,ry,side*jn,0);
let jt=-(vp.y-esc.v*SA)*em; const mx=MU_STEP*jn; jt=Math.max(-mx,Math.min(mx,jt));
applyImpulse(p,rx,ry,0,jt);
p.lastHit=timeNow; // risers are rude
}
}
}
if(px<-p.r){p.x+=-p.r-px? 0:0;} // world edges
if(px<0.05){p.x=0.05; if(p.vx<0)p.vx*=-0.2;}
if(px>W-0.05){p.x=W-0.05; if(p.vx>0)p.vx*=-0.2;}
}
}
function collidePair(A,B){
const sd=segDist(A.x0,A.y0,A.x1,A.y1,B.x0,B.y0,B.x1,B.y1);
const pen=A.r+B.r-sd.d; if(pen<=0) return;
let nx,ny;
if(sd.d>1e-6){nx=sd.nx/sd.d; ny=sd.ny/sd.d;} else {nx=0;ny=1;}
const pa=A.p, pb=B.p;
const paStanding = pa.state==='stand', pbStanding = pb.state==='stand';
const axp=A.x0+sd.s*(A.x1-A.x0), ayp=A.y0+sd.s*(A.y1-A.y0);
const bxp=B.x0+sd.t*(B.x1-B.x0), byp=B.y0+sd.t*(B.y1-B.y0);
// positional projection
if(!paStanding) { pa.x+=nx*pen*0.4; pa.y+=ny*pen*0.4; }
else { pa.footX+=nx*pen*0.3; }
if(!pbStanding) { pb.x-=nx*pen*0.4; pb.y-=ny*pen*0.4; }
else { pb.footX-=nx*pen*0.3; }
// impulse
const ra={x:axp-pa.x,y:ayp-pa.y}, rb={x:bxp-pb.x,y:byp-pb.y};
const va = paStanding? {x:pa.vx-pa.thw*ra.y, y:pa.vy+pa.thw*ra.x} : velAt(pa,ra.x,ra.y);
const vb = pbStanding? {x:pb.vx-pb.thw*rb.y, y:pb.vy+pb.thw*rb.x} : velAt(pb,rb.x,rb.y);
const rvx=va.x-vb.x, rvy=va.y-vb.y, vn=rvx*nx+rvy*ny;
if(vn<0){
const emA = paStanding? pa.m*3 : effMass(pa,ra.x,ra.y,nx,ny);
const emB = pbStanding? pb.m*3 : effMass(pb,rb.x,rb.y,nx,ny);
const jn = -vn/(1/emA+1/emB);
if(!paStanding) applyImpulse(pa,ra.x,ra.y, nx*jn, ny*jn);
else { pa.thw += (ra.x*(ny*jn)-ra.y*(nx*jn))/(pa.m*(pa.L/2)**2); pa.lastHit=timeNow; }
if(!pbStanding) applyImpulse(pb,rb.x,rb.y,-nx*jn,-ny*jn);
else { pb.thw -= (rb.x*(ny*jn)-rb.y*(nx*jn))/(pb.m*(pb.L/2)**2); pb.lastHit=timeNow; }
// friction
const tvx=rvx-vn*nx, tvy=rvy-vn*ny, tl=Math.hypot(tvx,tvy);
if(tl>1e-4){
let jt=-tl/(1/emA+1/emB); const mx=MU_BODY*jn; jt=Math.max(-mx,Math.min(mx,jt));
const tx=tvx/tl, ty=tvy/tl;
if(!paStanding) applyImpulse(pa,ra.x,ra.y, tx*jt, ty*jt);
if(!pbStanding) applyImpulse(pb,rb.x,rb.y,-tx*jt,-ty*jt);
}
if(jn/(1/60/SUB) > 55){ pa.lastHit=timeNow; pb.lastHit=timeNow; }
// walkers trip over bodies at shin height
for(const [pw,oth] of [[pa,pb],[pb,pa]]){
if((pw.state==='walkIn'||pw.state==='walkOut') && oth.state!=='walkIn' && oth.state!=='walkOut'){
const cy = (pw===pa? ayp:byp);
if(cy < pw.y - pw.L*0.22 && Math.abs(jn)/(1/60/SUB) > 30){
pw.phi = 0; pw.theta=0.2*Math.sign(pw.walkSp||1);
pw.y = pw.footY + pw.L/2; startFall(pw, Math.sign(pw.targetX-pw.x)*2.2);
}
}
}
}
}
/* ---------- walkers ---------- */
function walkPhysics(p, dt, exiting){
const sup = supportAt(p.x, p.footY+0.05) || {y: p.x<X0?0:RISE, vx:0, vy:0, kind:'land'};
p.footY = sup.y; p.y = sup.y + p.L/2;
// spacing: slow for anything ahead
let clear = 0.55;
for(const q of people){ if(q===p) continue;
const dx=(q.x-p.x)*Math.sign(p.targetX-p.x);
if(dx>0 && dx<clear && (q.state==='stand'||q.state==='walkIn'||q.state==='walkOut')) clear=dx;
}
const want = Math.min(p.walkSp, Math.max(0,(clear-0.3)*3));
p.walkV = (p.walkV||0) + (Math.sign(p.targetX-p.x)*want - (p.walkV||0))*Math.min(1,dt*6);
p.x += p.walkV*dt; p.footX=p.x; p.phi=0; p.theta=0;
p.vx=p.walkV; p.vy=0; p.w=0;
p.gait += Math.abs(p.walkV)*dt*7;
if(exiting && Math.abs(p.x-p.targetX)<0.15){ p.state='gone'; p.respawn=1.5+Math.random()*2; }
if(!exiting && p.x>1.55){ p.state='queue'; } // wait at the comb for a step
}
function queuePhysics(p, dt){
p.y = p.L/2; p.footY=0; p.phi=0; p.theta=0; p.gait+=dt*2;
for(let i=0;i<NSTEP;i++){
const g=stepGeom(i);
if(g.s>0.06 && g.s<0.5 && Math.abs(p.x-(X0+g.s*CA))<0.6){
let taken=false;
for(const q of people) if(q!==p && q.state==='stand' && q.stepIdx===i) taken=true;
if(!taken){
p.state='stand'; p.stepIdx=i; p.footX=X0+g.s*CA; p.footY=g.s*SA;
p.x=p.footX; p.y=p.footY+p.L/2; p.theta=0; p.thw=0; return;
}
}
}
}
/* ---------- main step ---------- */
let timeNow=0;
function physics(dt){
escStep(dt);
const h=dt/SUB;
for(let s=0;s<SUB;s++){
for(const p of people){
switch(p.state){
case 'stand': standPhysics(p,h); break;
case 'fall':
p.vy-=GRAV*h; p.x+=p.vx*h; p.y+=p.vy*h; p.phi+=p.w*h;
p.w*=(1-0.25*h); p.flail=Math.max(0,p.flail-h*0.7);
break;
case 'fallen':
p.vy-=GRAV*h; p.x+=p.vx*h; p.y+=p.vy*h; p.phi+=p.w*h;
p.vx*=(1-1.5*h); p.w*=(1-1.5*h);
if(Math.hypot(p.vx,p.vy)<0.35 && Math.abs(p.w)<0.6 && Math.random()<h*0.8) tryGetUp(p);
break;
case 'getup':{
p.getT+=h;
const T=0.95, k=Math.min(1,p.getT/T), e=k*k*(3-2*k);
const ang=p.getFrom*(1-e);
p.phi=ang; p.w=(p.getFrom*(e-(Math.min(1,(p.getT-h)/T))**2*(3-2*Math.min(1,(p.getT-h)/T))))/h)||0;
p.x=p.gFoot.x+Math.sin(ang)*p.L/2; p.y=p.gFoot.y+Math.cos(ang)*p.L/2;
p.vx=0;p.vy=0;
if(k>=1){ p.state='stand'; p.theta=0; p.thw=0; p.footX=p.gFoot.x; p.footY=p.gFoot.y; p.stepIdx=-2; }
break; }
case 'walkIn': walkPhysics(p,h,false); break;
case 'walkOut': walkPhysics(p,h,true); break;
case 'queue': queuePhysics(p,h); break;
case 'gone':
p.respawn-=h;
if(p.respawn<=0){ p.state='walkIn'; p.x=-1-Math.random(); p.targetX=1.6; p.walkV=0; }
break;
}
if(p.state==='fall'||p.state==='fallen'||p.state==='getup') collideEnv(p);
}
// pair contacts, a few iterations so piles settle
for(let it=0; it<ITER; it++){
const caps = people.filter(p=>p.state!=='gone'&&p.state!=='queue').map(capsuleOf);
for(let i=0;i<caps.length;i++) for(let j=i+1;j<caps.length;j++)
collidePair(caps[i],caps[j]);
}
for(const p of people){
if(p.state==='fall' && Math.hypot(p.vx,p.vy)<0.3 && Math.abs(p.w)<0.8){ p.state='fallen'; }
if(p.state==='stand' && p.stepIdx>=0){ // keep claim honest
const g=stepGeom(p.stepIdx);
if(g.s<-0.05||g.s>LINC+0.05) p.stepIdx=-2;
}
}
}
}
/* ---------- rendering ---------- */
function drawScene(t){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#0b1013'; ctx.fillRect(0,0,cv.width,cv.height);
ctx.setTransform(1,0,0,1,ox,oy);
const S=scale, mx=x=>x*S, my=y=>(H-y)*S;
// wall + tiles
const wg=ctx.createLinearGradient(0,0,0,cv.height);
wg.addColorStop(0,'#101a20'); wg.addColorStop(1,'#18262d');
ctx.fillStyle=wg; ctx.fillRect(-ox,-oy,cv.width,cv.height);
ctx.strokeStyle='rgba(255,255,255,.035)'; ctx.lineWidth=1;
for(let x=0;x<W;x+=0.62){ctx.beginPath();ctx.moveTo(mx(x),0);ctx.lineTo(mx(x),cv.height-oy);ctx.stroke();}
for(let y=0;y<H;y+=0.31){ctx.beginPath();ctx.moveTo(-ox,my(y));ctx.lineTo(cv.width-ox,my(y));ctx.stroke();}
// warm light pool
const lg=ctx.createRadialGradient(mx(6),my(5.4),10,mx(6),my(5.4),S*7);
lg.addColorStop(0,'rgba(255,196,110,.10)'); lg.addColorStop(1,'rgba(255,196,110,0)');
ctx.fillStyle=lg; ctx.fillRect(-ox,-oy,cv.width,cv.height);
// wall sign
ctx.fillStyle='#1d2f33'; ctx.fillRect(mx(9.6),my(4.9),S*2.4,S*0.72);
ctx.strokeStyle='#3a525a'; ctx.strokeRect(mx(9.6),my(4.9),S*2.4,S*0.72);
ctx.fillStyle='#ffd23f'; ctx.font=`${S*0.34}px Haettenschweiler, Impact, sans-serif`;
ctx.textBaseline='middle'; ctx.fillText('PLATFORM 2 →', mx(9.75), my(4.55));
// truss under the escalator
ctx.strokeStyle='#233037'; ctx.lineWidth=Math.max(2,S*0.05);
ctx.beginPath();
ctx.moveTo(mx(X0-0.5),my(-0.55)); ctx.lineTo(mx(XTOP+0.5),my(RISE-0.55));
ctx.moveTo(mx(X0-0.5),my(0)); ctx.lineTo(mx(XTOP+0.5),my(RISE));
ctx.stroke();
for(let s=0;s<LINC;s+=0.8){
ctx.beginPath();
ctx.moveTo(mx(X0+s*CA),my(s*SA)); ctx.lineTo(mx(X0+(s+0.8)*CA),my((s+0.8)*SA-0.55));
ctx.moveTo(mx(X0+(s+0.8)*CA),my((s+0.8)*SA)); ctx.lineTo(mx(X0+s*CA),my(s*SA-0.55));
ctx.stroke();
}
// landings
ctx.fillStyle='#2b363c';
ctx.fillRect(-ox,my(0),mx(X0)-(-ox),S*0.5);
ctx.fillRect(mx(XTOP),my(RISE),cv.width-ox-mx(XTOP),S*0.5);
ctx.fillStyle='#39464d';
ctx.fillRect(-ox,my(0),mx(X0)-(-ox),S*0.06);
ctx.fillRect(mx(XTOP),my(RISE),cv.width-ox-mx(XTOP),S*0.06);
// hazard chevrons at both combs
ctx.save();
for(const [cx0,cy0] of [[X0-0.55,0],[XTOP+0.02,RISE]]){
for(let i=0;i<5;i++){
ctx.fillStyle=i%2?'#c8a02a':'#20262b';
ctx.fillRect(mx(cx0+i*0.11),my(cy0+0.0),S*0.11,S*0.05);
}
}
ctx.restore();
// steps
for(let i=0;i<NSTEP;i++){
const g=stepGeom(i); if(g.s<-0.55||g.s>LINC+0.55) continue;
const grd=ctx.createLinearGradient(0,my(g.ty),0,my(g.ty-0.5));
grd.addColorStop(0,'#5a656e'); grd.addColorStop(1,'#39434b');
ctx.fillStyle=grd;
ctx.beginPath();
ctx.moveTo(mx(g.x0),my(g.ty)); ctx.lineTo(mx(g.x1),my(g.ty));
ctx.lineTo(mx(g.x1),my(g.ty-0.5)); ctx.lineTo(mx(g.rx),my(g.ry0-0.27)); ctx.closePath();
ctx.fill();
ctx.strokeStyle='#20282d'; ctx.lineWidth=1; ctx.stroke();
// grooves
ctx.strokeStyle='rgba(0,0,0,.28)';
for(let k=1;k<5;k++){const gx=g.x0+k*PITCH/5*CA;
ctx.beginPath();ctx.moveTo(mx(gx),my(g.ty));ctx.lineTo(mx(gx),my(g.ty-0.05));ctx.stroke();}
// yellow demarcation
ctx.fillStyle='#ffd23f';
ctx.fillRect(mx(g.x0),my(g.ty),S*0.035,S*0.028);
ctx.fillRect(mx(g.x1)-S*0.035,my(g.ty),S*0.035,S*0.028);
}
// balustrade glass + handrail
ctx.beginPath();
ctx.moveTo(mx(X0-0.7),my(0.9)); ctx.lineTo(mx(X0-0.2),my(0.9));
ctx.lineTo(mx(X0+LINC*CA*0.5),my(LINC*SA*0.5+0.9)); ctx.lineTo(mx(XTOP+0.2),my(RISE+0.9));
ctx.lineTo(mx(XTOP+0.7),my(RISE+0.9));
ctx.strokeStyle='rgba(160,200,210,.16)'; ctx.lineWidth=S*0.5; ctx.stroke();
const hr=[[X0-0.75,0.98],[X0-0.15,0.98],[XTOP+0.15,RISE+0.98],[XTOP+0.75,RISE+0.98]];
ctx.beginPath(); ctx.moveTo(mx(hr[0][0]),my(hr[0][1]));
ctx.lineTo(mx(hr[1][0]),my(hr[1][1])); ctx.lineTo(mx(hr[2][0]),my(hr[2][1])); ctx.lineTo(mx(hr[3][0]),my(hr[3][1]));
ctx.strokeStyle='#11161a'; ctx.lineWidth=S*0.09; ctx.lineCap='round'; ctx.stroke();
ctx.strokeStyle='rgba(255,255,255,.12)'; ctx.lineWidth=S*0.02; ctx.stroke();
// handrail motion ticks
ctx.fillStyle='rgba(255,255,255,.25)';
for(let k=0;k<6;k++){
const f=((k/6 + esc.off/LINC*0.5)%1+1)%1;
const hx=hr[1][0]+(hr[2][0]-hr[1][0])*f, hy=hr[1][1]+(hr[2][1]-hr[1][1])*f;
ctx.fillRect(mx(hx),my(hy)-S*0.03,S*0.02,S*0.06);
}
// people (sorted so piles layer sanely)
const order=[...people].filter(p=>p.state!=='gone').sort((a,b)=>a.y-b.y);
for(const p of order) drawPerson(p,t);
// alarm beacon
const alarm = esc.phase!=='norm';
ctx.fillStyle='#2c3a42'; ctx.fillRect(mx(0.55),my(5.1),S*0.1,S*0.35);
ctx.beginPath(); ctx.arc(mx(0.6),my(5.2),S*0.12,0,7);
ctx.fillStyle=alarm? (Math.sin(t*14)>0?'#ff4b3a':'#7a1d14') : '#3d4a50';
ctx.fill();
if(alarm && Math.sin(t*14)>0){
const ag=ctx.createRadialGradient(mx(0.6),my(5.2),2,mx(0.6),my(5.2),S*1.4);
ag.addColorStop(0,'rgba(255,75,58,.28)'); ag.addColorStop(1,'rgba(255,75,58,0)');
ctx.fillStyle=ag; ctx.fillRect(mx(0.6)-S*1.4,my(5.2)-S*1.4,S*2.8,S*2.8);
}
// vignette
ctx.setTransform(1,0,0,1,0,0);
const vg=ctx.createRadialGradient(cv.width/2,cv.height*0.45,cv.height*0.3,cv.width/2,cv.height*0.45,cv.height*0.95);
vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,.42)');
ctx.fillStyle=vg; ctx.fillRect(0,0,cv.width,cv.height);
}
function drawPerson(p,t){
const S=scale, mx=x=>x*S, my=y=>(H-y)*S;
const e=ends(p), u=axis(p);
const hip={x:e.fx+u.x*p.L*0.47,y:e.fy+u.y*p.L*0.47};
const sh ={x:e.fx+u.x*p.L*0.80,y:e.fy+u.y*p.L*0.80};
const perp={x:u.y,y:-u.x};
const down = p.state==='fall'||p.state==='fallen'||p.state==='getup';
ctx.lineCap='round'; ctx.lineJoin='round';
// shadow
const sup=supportAt(p.x, p.y-p.L/2);
if(sup){ ctx.fillStyle='rgba(0,0,0,.25)';
ctx.beginPath(); ctx.ellipse(mx(p.x),my(sup.y)+2,S*0.22,S*0.045,0,0,7); ctx.fill(); }
// legs
ctx.strokeStyle=p.col; ctx.lineWidth=S*0.075;
const kneep = down? 0.32 : 0.08+0.05*Math.sin(p.gait||0);
for(const sgn of [-1,1]){
const kx=(e.fx+hip.x)/2+perp.x*(sgn*kneep*p.L*0.3+(down?Math.sin(p.seed+sgn)*0.06:0));
const ky=(e.fy+hip.y)/2+perp.y*(sgn*kneep*p.L*0.3);
const fx = down? e.fx+perp.x*sgn*0.07 : e.fx+perp.x*sgn*0.06 + u.x*Math.sin((p.gait||0)+sgn)*0.02;
ctx.beginPath(); ctx.moveTo(mx(hip.x),my(hip.y)); ctx.lineTo(mx(kx),my(ky)); ctx.lineTo(mx(fx),my(e.fy)); ctx.stroke();
}
// torso
ctx.lineWidth=S*0.11; ctx.beginPath(); ctx.moveTo(mx(hip.x),my(hip.y)); ctx.lineTo(mx(sh.x),my(sh.y)); ctx.stroke();
// arms
ctx.strokeStyle=p.col; ctx.lineWidth=S*0.055;
const fl = down? p.flail*1.4+0.3 : 0.18;
for(const sgn of [-1,1]){
const swing = down? Math.sin(t*11+p.seed+sgn*2)*fl : Math.sin((p.gait||0)+sgn*Math.PI)*0.12 + p.thw*0.15*sgn;
const ex=sh.x+perp.x*sgn*p.L*0.16 + u.x*swing*p.L*0.2 - (down?0:0);
const ey=sh.y+perp.y*sgn*p.L*0.16 - (down? Math.abs(swing)*p.L*0.18 : p.L*0.22);
ctx.beginPath(); ctx.moveTo(mx(sh.x),my(sh.y)); ctx.lineTo(mx(ex),my(ey)); ctx.stroke();
}
// head
const hx=e.hx+u.x*p.L*0.09, hy=e.hy+u.y*p.L*0.09;
ctx.fillStyle=p.skin; ctx.beginPath(); ctx.arc(mx(hx),my(hy),S*0.095,0,7); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,.35)'; ctx.lineWidth=1; ctx.stroke();
}
/* ---------- UI / sound ---------- */
const btn=document.getElementById('reverseBtn');
const rSpeed=document.getElementById('rSpeed'), rMode=document.getElementById('rMode'),
rRide=document.getElementById('rRide'), rDown=document.getElementById('rDown');
let AC=null, humOsc=null, humGain=null;
function audio(){
if(AC) return;
try{
AC=new (window.AudioContext||window.webkitAudioContext)();
humOsc=AC.createOscillator(); humGain=AC.createGain();
humOsc.type='sawtooth'; humOsc.frequency.value=40; humGain.gain.value=0.0;
const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.value=180;
humOsc.connect(f); f.connect(humGain); humGain.connect(AC.destination); humOsc.start();
}catch(e){}
}
function alarmBeep(){
if(!AC) return;
try{
const o=AC.createOscillator(), g=AC.createGain();
o.type='square'; o.frequency.setValueAtTime(880,AC.currentTime);
o.frequency.exponentialRampToValueAtTime(440,AC.currentTime+0.35);
g.gain.setValueAtTime(0.08,AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.4);
o.connect(g); g.connect(AC.destination); o.start(); o.stop(AC.currentTime+0.42);
}catch(e){}
}
btn.addEventListener('click',()=>{
audio(); if(AC&&AC.state==='suspended')AC.resume();
esc.dir*=-1;
btn.classList.toggle('engaged', esc.dir<0);
btn.setAttribute('aria-pressed', esc.dir<0);
btn.innerHTML = esc.dir<0
? '<span class="lamp"></span>RESTORE<small>DRIVE OVERRIDE</small>'
: '<span class="lamp"></span>REVERSE<small>DRIVE OVERRIDE</small>';
alarmBeep(); setTimeout(alarmBeep, 450);
});
cv.addEventListener('pointerdown',(ev)=>{
const wx=((ev.clientX*dpr)-ox)/scale, wy=(H-((ev.clientY*dpr)-oy)/scale);
let best=null,bd=1.2;
for(const p of people){ if(p.state==='gone')continue;
const d=Math.hypot(p.x-wx,p.y-wy); if(d<bd){bd=d;best=p;} }
if(best){
if(best.state==='stand'){ best.thw += (Math.random()<0.5?-1:1)*(1.6+Math.random()); best.lastHit=timeNow; }
else { best.vx += (Math.random()-0.5)*2; best.vy += 1.2; best.w += (Math.random()-0.5)*4; best.lastHit=timeNow;
if(best.state==='fallen') best.state='fall'; }
}
});
/* ---------- loop ---------- */
let last=performance.now(), acc=0, uiT=0;
function frame(now){
let dt=Math.min(0.045,(now-last)/1000); last=now; timeNow+=dt;
physics(dt);
drawScene(timeNow);
if(humGain){ humGain.gain.value=Math.min(0.05,Math.abs(esc.v)*0.018);
humOsc.frequency.value=34+Math.abs(esc.v)*26; }
uiT+=dt;
if(uiT>0.12){ uiT=0;
rSpeed.textContent=(esc.v>=0?'+':'')+esc.v.toFixed(2)+' m/s';
rSpeed.classList.toggle('bad', esc.phase!=='norm');
rMode.textContent = esc.phase==='norm'?'SERVICE UP': esc.phase==='rev'?'!! REVERSE !!':'SWITCHING';
rMode.classList.toggle('bad', esc.phase==='rev');
let ride=0,down=0;
for(const p of people){ if(p.state==='stand'||p.state==='walkIn'||p.state==='walkOut'||p.state==='queue')ride++;
if(p.state==='fall'||p.state==='fallen'||p.state==='getup')down++; }
rRide.textContent=ride; rDown.textContent=down; rDown.classList.toggle('bad',down>0);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Press REVERSE and the drive swings from +0.5 m/s to −2.3 m/s at 13 m/s² — over 1.3 g along the truss. Every rider's pendulum gets the same base-excitation kick, their friction budget blows immediately, and they pitch uphill while the steps run out from under them downhill. What follows is the solver: bodies tumble, land on treads and risers, get bulldozed downhill by the riser faces (two-sided colliders), eject out of the bottom comb onto the lower landing, and pile there — the pile breathes because contacts are impulses with position projection, not constraints. Walkers entering from the left trip over the wreckage at shin height (contact-impulse rule, same for both walkers), and riders who were near the top when you pressed it ride the whole length down. Downed figures attempt to rise after ~0.6 s of calm, sweeping three candidate torso angles for clearance first; mid-rise hits dump them back down. Press the button again mid-chaos and the reversal acceleration flattens whoever had stood back up. Clicking the scene shoves the nearest figure — a debug poke, disclosed here.
Special-cased / nudged, honestly: the pre-seeded riders start planted and balanced (the ride state itself is initial conditions, not animation); the boarding gate claims one rider per passing step; get-up is a prescribed eased rotation (collidable and interruptible, but not torque-driven); walkers are kinematic until contact trips them; flailing arms and bent knees are cosmetic overlays on the rigid pose; and the bottom world edge is a soft wall so the pile stays in frame.
What it does badly, since you're going to press it: piles jitter under compression at 4 iterations — bodies occasionally pop upright out of a heap; a fallen figure whose feet end up upslope stands up rotating the "wrong" way over them; bodies can tunnel through a riser on the very first frame of reversal if caught exactly at a comb edge; and because standing figures are heavy pivots, a fast body can bounce off a standing rider instead of bowling them over. All of it is the same five states and one solver — no figure falls because the clock said so.