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 small rigid humanoid skeleton made of Verlet particles: feet, knees, hip, chest, head, and dangling arms. While riding, the feet are pinned to the moving step by a grip constraint, and the skeleton is stiffened into a standing pose by distance constraints. A balance controller rotates the rigid body around the midpoint of the feet toward vertical; its torque is deliberately limited, so when the escalator reverses violently the feet are dragged away faster than the torso can follow, the body leans past a threshold, the rigid pose is released, and the person becomes a ragdoll that can slump, trip, and collide. Overlapping particles are pushed apart with inverse-mass weighting, so a downed person is a collection of solid obstacles to everyone arriving behind them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Escalator Reversal</title>
<style>
body { margin:0; background:#101418; color:#eef; font-family:system-ui,sans-serif; display:flex; align-items:center; justify-content:center; min-height:100vh; }
#wrap { text-align:center; }
canvas { background:#1b222c; border:1px solid #000; display:block; }
.toolbar { margin-top:10px; display:flex; gap:12px; justify-content:center; align-items:center; }
button { padding:10px 24px; font-size:16px; background:#b3382c; color:#fff; border:0; border-radius:6px; cursor:pointer; }
button.go { background:#2a7a4b; }
span { font-size:16px; letter-spacing:1px; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="c" width="1000" height="700"></canvas>
<div class="toolbar">
<button id="btn">REVERSE</button><span id="status">FORWARD</span>
</div>
</div>
<script>
"use strict";
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
const btn=document.getElementById('btn');
const statusSpan=document.getElementById('status');
const W=1000, H=700;
const GRAV=1700;
const DT=1/60;
const STEP_W=50, STEP_H=28, STEP_SPACING=50;
const NORMAL_SPEED=45, REVERSE_SPEED=-700, MAX_ACCEL=7000;
const FOOT_R=3.5;
const LEG_LEN=36, TORSO_LEN=24, HEAD_LEN=12;
const BALANCE_K=0.08, BALANCE_MAX_ROT=0.005;
const pathPts=[{x:-600,y:480},{x:330,y:480},{x:670,y:300},{x:1600,y:300}];
const segs=[], cum=[0];
for(let i=0;i<pathPts.length-1;i++){
const a=pathPts[i], b=pathPts[i+1];
const dx=b.x-a.x, dy=b.y-a.y;
const len=Math.hypot(dx,dy);
segs.push({ax:a.x, ay:a.y, dx, dy, len});
cum.push(cum[i]+len);
}
const PATH_LEN=cum[cum.length-1];
function pathPoint(s){
if(s<0){
const t=s/segs[0].len;
return {x:segs[0].ax+segs[0].dx*t, y:segs[0].ay+segs[0].dy*t};
}
if(s>PATH_LEN){
const last=segs[segs.length-1];
const t=(s-cum[cum.length-2])/last.len;
return {x:last.ax+last.dx*t, y:last.ay+last.dy*t};
}
for(let i=0;i<segs.length;i++){
if(s<=cum[i+1]){
const seg=segs[i], t=(s-cum[i])/seg.len;
return {x:seg.ax+seg.dx*t, y:seg.ay+seg.dy*t};
}
}
return {x:pathPts[pathPts.length-1].x, y:pathPts[pathPts.length-1].y};
}
function pathTangent(s){
if(s<0) return {x:segs[0].dx/segs[0].len, y:segs[0].dy/segs[0].len};
if(s>PATH_LEN){ const last=segs[segs.length-1]; return {x:last.dx/last.len, y:last.dy/last.len}; }
for(let i=0;i<segs.length;i++){
if(s<=cum[i+1]){
const seg=segs[i];
return {x:seg.dx/seg.len, y:seg.dy/seg.len};
}
}
return {x:1,y:0};
}
const floors=[{x1:-600,x2:400,y:480},{x1:600,x2:1600,y:300}];
let steps=[];
const numSteps=Math.ceil(PATH_LEN/STEP_SPACING)+2;
for(let i=0;i<numSteps;i++) steps.push({idx:i,s:0,x:0,y:0,left:0,right:0,top:0,bottom:0,vx:0,vy:0});
let stepOffset=0, currentSpeed=NORMAL_SPEED, reversing=false;
function updateSteps(){
for(const st of steps){
st.s=st.idx*STEP_SPACING+stepOffset;
const p=pathPoint(st.s), t=pathTangent(st.s);
st.x=p.x;
st.y=p.y+STEP_H/2;
st.top=p.y;
st.bottom=p.y+STEP_H;
st.left=p.x-STEP_W/2;
st.right=p.x+STEP_W/2;
st.vx=t.x*currentSpeed;
st.vy=t.y*currentSpeed;
}
}
function groundYAt(x){
let best=Infinity;
for(const f of floors) if(x>=f.x1 && x<=f.x2 && f.y<best) best=f.y;
for(const st of steps) if(x>=st.left && x<=st.right && st.top<best) best=st.top;
return isFinite(best)?best:null;
}
class Particle{
constructor(x,y,r){ this.x=x; this.y=y; this.px=x; this.py=y; this.r=r; this.im=1; }
}
function distP(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }
class Person{
constructor(x, footY, stepIdx){
this.mode='standing';
this.coreActive=true;
this.fallTimer=0; this.downTimer=0; this.recoverT=0;
this.prevLean=0; this.leanAngle=0; this.leanTimer=0;
this.getupTargets=null;
this.color=`hsl(${Math.floor(Math.random()*360)},65%,70%)`;
const P=[];
P.push(new Particle(x, footY-LEG_LEN, 5));
P.push(new Particle(x, footY-LEG_LEN-TORSO_LEN, 5));
P.push(new Particle(x, footY-LEG_LEN-TORSO_LEN-HEAD_LEN, 7));
P.push(new Particle(x-4, footY-18, 3.5));
P.push(new Particle(x-7, footY, 3.5));
P.push(new Particle(x+4, footY-18, 3.5));
P.push(new Particle(x+7, footY, 3.5));
P.push(new Particle(x-7, footY-LEG_LEN-TORSO_LEN+12.124, 3.5));
P.push(new Particle(x-14, footY-LEG_LEN-TORSO_LEN+24.248, 3.5));
P.push(new Particle(x+7, footY-LEG_LEN-TORSO_LEN+12.124, 3.5));
P.push(new Particle(x+14, footY-LEG_LEN-TORSO_LEN+24.248, 3.5));
this.P=P;
this.bones=[[0,1],[1,2],[0,3],[3,4],[0,5],[5,6],[1,7],[7,8],[1,9],[9,10]];
this.boneRest=this.bones.map(([i,j])=>distP(P[i],P[j]));
this.coreIdxs=[0,1,2,3,4,5,6];
this.corePairs=[];
for(let i=0;i<this.coreIdxs.length;i++)
for(let j=i+1;j<this.coreIdxs.length;j++)
this.corePairs.push([this.coreIdxs[i],this.coreIdxs[j]]);
this.coreRest=this.corePairs.map(([i,j])=>distP(P[i],P[j]));
this.anchors=[
{foot:4, active:true, stepIdx:stepIdx, localX:P[4].x-steps[stepIdx].x, target:{x:0,y:0,im:0}},
{foot:6, active:true, stepIdx:stepIdx, localX:P[6].x-steps[stepIdx].x, target:{x:0,y:0,im:0}}
];
}
}
function updateAnchors(person){
for(const a of person.anchors){
if(!a.active) continue;
const st=steps[a.stepIdx];
a.target.x=st.x+a.localX;
a.target.y=st.top-FOOT_R;
}
}
function solveDist(p1,p2, rest, k){
const dx=p2.x-p1.x, dy=p2.y-p1.y;
const d=Math.hypot(dx,dy);
if(d<1e-6) return;
const w=p1.im+p2.im;
if(w===0) return;
const diff=(d-rest)/d;
const corr=diff*k/w;
const ox=dx*corr, oy=dy*corr;
p1.x+=ox*p1.im; p1.y+=oy*p1.im;
p2.x-=ox*p2.im; p2.y-=oy*p2.im;
}
function solveBones(person){
for(let b=0;b<person.bones.length;b++){
const [i,j]=person.bones[b];
solveDist(person.P[i],person.P[j], person.boneRest[b], 1);
}
}
function solveCore(person){
for(let b=0;b<person.corePairs.length;b++){
const [i,j]=person.corePairs[b];
solveDist(person.P[i],person.P[j], person.coreRest[b], 0.85);
}
}
function solveAnchors(person){
for(const a of person.anchors){
if(!a.active) continue;
solveDist(person.P[a.foot], a.target, 0, 0.7);
}
}
function solveBalance(person){
const P=person.P;
const fmx=(P[4].x+P[6].x)/2, fmy=(P[4].y+P[6].y)/2;
const ax=P[2].x-fmx, ay=P[2].y-fmy;
const len=Math.hypot(ax,ay);
if(len<1) return;
const theta=Math.atan2(ax, -ay);
const omega=theta-person.prevLean;
person.prevLean=theta;
let rot=-theta*BALANCE_K - omega*0.08;
rot=clamp(rot,-BALANCE_MAX_ROT,BALANCE_MAX_ROT);
const cos=Math.cos(rot), sin=Math.sin(rot);
for(const idx of person.coreIdxs){
const p=P[idx];
const rx=p.x-fmx, ry=p.y-fmy;
p.x=fmx+rx*cos-ry*sin;
p.y=fmy+rx*sin+ry*cos;
}
}
function solvePersonPhysics(person){
for(let it=0;it<8;it++){
solveBones(person);
if(person.mode==='standing' && person.coreActive){
solveCore(person);
solveBalance(person);
solveAnchors(person);
}
}
if(person.mode==='standing' && person.coreActive) solveAnchors(person);
if(person.mode==='gettingup' && person.getupTargets){
const k=Math.min(1,person.recoverT);
const s=k*k*(3-2*k);
for(let i=0;i<person.P.length;i++){
const t=person.getupTargets[i], p=person.P[i];
p.x+=(t.x-p.x)*0.10*s;
p.y+=(t.y-p.y)*0.10*s;
}
}
}
function segmentCollide(p, ax,ay,bx,by, nx,ny, svx,svy){
const abx=bx-ax, aby=by-ay;
const len2=abx*abx+aby*aby;
if(len2<1e-6) return;
let t=((p.x-ax)*abx+(p.y-ay)*aby)/len2;
t=clamp(t,0,1);
const qx=ax+t*abx, qy=ay+t*aby;
const dx=p.x-qx, dy=p.y-qy;
const rr=p.r;
const d2=dx*dx+dy*dy;
if(d2>=rr*rr) return;
const d=Math.sqrt(d2);
if(d>1e-6){
p.x=qx+dx/d*rr;
p.y=qy+dy/d*rr;
} else {
p.x=qx+nx*rr;
p.y=qy+ny*rr;
}
const vx=p.x-p.px, vy=p.y-p.py;
let rvx=vx-svx, rvy=vy-svy;
let vn=rvx*nx+rvy*ny;
if(vn<0){ rvx-=nx*vn; rvy-=ny*vn; }
const tx=-ny, ty=nx;
let vt=rvx*tx+rvy*ty;
const fr=0.5;
rvx-=tx*vt*fr; rvy-=ty*vt*fr;
p.px=p.x-(svx+rvx);
p.py=p.y-(svy+rvy);
}
function rectCollide(p, st){
const left=st.left, right=st.right, top=st.top, bottom=st.bottom;
const cx=clamp(p.x, left, right);
const cy=clamp(p.y, top, bottom);
const dx=p.x-cx, dy=p.y-cy;
const rr=p.r;
const d2=dx*dx+dy*dy;
if(d2>=rr*rr) return;
let nx,ny;
if(d2>1e-6){
const d=Math.sqrt(d2);
nx=dx/d; ny=dy/d;
p.x=cx+nx*rr;
p.y=cy+ny*rr;
} else {
const dl=p.x-left, dr=right-p.x, dt=p.y-top, db=bottom-p.y;
const m=Math.min(dl,dr,dt,db);
if(m===dl){ nx=-1; ny=0; p.x=left-rr; }
else if(m===dr){ nx=1; ny=0; p.x=right+rr; }
else if(m===dt){ nx=0; ny=-1; p.y=top-rr; }
else { nx=0; ny=1; p.y=bottom+rr; }
}
const vx=p.x-p.px, vy=p.y-p.py;
let rvx=vx-st.vx, rvy=vy-st.vy;
let vn=rvx*nx+rvy*ny;
if(vn<0){ rvx-=nx*vn; rvy-=ny*vn; }
const tx=-ny, ty=nx;
let vt=rvx*tx+rvy*ty;
const fr=0.5;
rvx-=tx*vt*fr; rvy-=ty*vt*fr;
p.px=p.x-(st.vx+rvx);
p.py=p.y-(st.vy+rvy);
}
function wallCollide(p,x,y1,y2){
if(p.x-p.r<x && p.y>y1 && p.y<y2){
segmentCollide(p,x,y1,x,y2,1,0,0,0);
}
}
function solveContacts(){
for(const person of people){
for(const p of person.P){
for(const f of floors){
if(p.x>=f.x1 && p.x<=f.x2 && p.y+p.r>f.y){
p.y=f.y-p.r;
const vx=p.x-p.px, vy=p.y-p.py;
if(vy>0) p.py=p.y;
p.px=p.x-vx*0.7;
}
}
for(const st of steps){
if(p.x+p.r<st.left || p.x-p.r>st.right || p.y+p.r<st.top || p.y-p.r>st.bottom) continue;
rectCollide(p,st);
}
wallCollide(p,20,200,550);
wallCollide(p,980,200,550);
}
}
}
function solveInterPersonCollisions(){
for(let a=0;a<people.length;a++){
for(let b=a+1;b<people.length;b++){
const A=people[a].P, B=people[b].P;
for(let i=0;i<A.length;i++){
for(let j=0;j<B.length;j++){
const p=A[i], q=B[j];
const dx=q.x-p.x, dy=q.y-p.y;
const rr=p.r+q.r;
const d2=dx*dx+dy*dy;
if(d2>=rr*rr) continue;
const d=Math.sqrt(d2);
let nx,ny;
if(d>1e-6){ nx=dx/d; ny=dy/d; }
else { nx=Math.random()-0.5; ny=Math.random()-0.5; const l=Math.hypot(nx,ny)||1; nx/=l; ny/=l; }
const overlap=rr-d;
const w=p.im+q.im;
if(w===0) continue;
const corr=overlap/w;
p.x-=nx*corr*p.im; p.y-=ny*corr*p.im;
q.x+=nx*corr*q.im; q.y+=ny*corr*q.im;
}
}
}
}
}
function leanAngle(person){
const P=person.P;
const fmx=(P[4].x+P[6].x)/2, fmy=(P[4].y+P[6].y)/2;
return Math.atan2(P[2].x-fmx, -(P[2].y-fmy));
}
function clearSpace(person){
const hip=person.P[0];
const gy=groundYAt(hip.x);
if(gy===null) return false;
const footY=gy-FOOT_R;
const hipY=footY-LEG_LEN;
for(const other of people){
if(other===person) continue;
for(const p of other.P){
if(Math.abs(p.x-hip.x)<22 && p.y>hipY-85 && p.y<footY+8) return false;
}
}
return true;
}
function beginGettingUp(person){
person.mode='gettingup';
person.recoverT=0;
const hip=person.P[0];
const gy=groundYAt(hip.x);
const footY=(gy!==null?gy:hip.y+LEG_LEN)-FOOT_R;
const bx=hip.x;
const hipY=footY-LEG_LEN;
const chestY=hipY-TORSO_LEN;
const headY=chestY-HEAD_LEN;
const targets=[];
targets[0]={x:bx, y:hipY};
targets[1]={x:bx, y:chestY};
targets[2]={x:bx, y:headY};
targets[3]={x:bx-4, y:footY-18};
targets[4]={x:bx-7, y:footY};
targets[5]={x:bx+4, y:footY-18};
targets[6]={x:bx+7, y:footY};
targets[7]={x:bx-7, y:chestY+12.124};
targets[8]={x:bx-14, y:chestY+24.248};
targets[9]={x:bx+7, y:chestY+12.124};
targets[10]={x:bx+14, y:chestY+24.248};
person.getupTargets=targets;
}
function reattachAnchors(person){
for(const a of person.anchors){
a.active=false;
const foot=person.P[a.foot];
for(const st of steps){
if(foot.x>=st.left && foot.x<=st.right && Math.abs(foot.y-(st.top-FOOT_R))<25){
a.active=true; a.stepIdx=st.idx; a.localX=foot.x-st.x; break;
}
}
}
}
function updatePerson(person,dt){
if(person.mode==='standing'){
person.leanAngle=leanAngle(person);
if(Math.abs(person.leanAngle)>0.5){
person.leanTimer+=dt;
if(person.leanTimer>0.06){
person.mode='falling';
person.fallTimer=0;
person.coreActive=false;
person.anchors.forEach(a=>a.active=false);
person.prevLean=0;
return;
}
} else person.leanTimer=0;
} else if(person.mode==='falling'){
person.fallTimer+=dt;
let spd=0;
for(const p of person.P) spd+=Math.hypot(p.x-p.px,p.y-p.py);
spd/=person.P.length;
if(person.fallTimer>0.9 && spd<12){
person.mode='down';
person.downTimer=0;
}
} else if(person.mode==='down'){
person.downTimer+=dt;
if(person.downTimer>1.2 && clearSpace(person) && groundYAt(person.P[0].x)!==null){
beginGettingUp(person);
}
} else if(person.mode==='gettingup'){
person.recoverT+=dt/1.8 * (clearSpace(person)?1:0.25);
if(person.recoverT>=1){
person.mode='standing';
person.coreActive=true;
person.leanTimer=0;
person.prevLean=0;
reattachAnchors(person);
person.getupTargets=null;
}
}
}
function stepSim(dt){
updateSteps();
for(const person of people) updateAnchors(person);
for(const person of people){
for(const p of person.P){
const vx=(p.x-p.px)*0.998;
const vy=(p.y-p.py)*0.998;
p.px=p.x; p.py=p.y;
p.x+=vx;
p.y+=vy+GRAV*dt*dt;
}
}
for(let pass=0; pass<2; pass++){
for(const person of people) solvePersonPhysics(person);
solveInterPersonCollisions();
solveContacts();
}
for(const person of people) updatePerson(person,dt);
}
function updateSpeed(dt){
const target=reversing?REVERSE_SPEED:NORMAL_SPEED;
const dv=clamp(target-currentSpeed,-MAX_ACCEL*dt,MAX_ACCEL*dt);
currentSpeed+=dv;
stepOffset+=currentSpeed*dt;
}
function drawPerson(ctx, person){
const P=person.P;
ctx.lineCap='round';
ctx.lineJoin='round';
ctx.strokeStyle=person.color;
ctx.lineWidth=4;
for(const [i,j] of person.bones){
ctx.beginPath();
ctx.moveTo(P[i].x,P[i].y);
ctx.lineTo(P[j].x,P[j].y);
ctx.stroke();
}
ctx.fillStyle=person.color;
ctx.beginPath();
ctx.arc(P[2].x,P[2].y,P[2].r,0,Math.PI*2);
ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.35)';
for(let i=0;i<P.length;i++){
if(i===2) continue;
ctx.beginPath(); ctx.arc(P[i].x,P[i].y,2,0,Math.PI*2); ctx.fill();
}
}
function draw(){
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#1b222c';
ctx.fillRect(0,0,W,H);
ctx.fillStyle='#2a3440';
ctx.fillRect(0,480,W,220);
ctx.fillRect(0,300,W,400);
for(const st of steps){
if(st.right<0 || st.left>W) continue;
ctx.fillStyle='#3a4b5a';
ctx.fillRect(st.left, st.top, STEP_W, STEP_H);
ctx.fillStyle='#6a8295';
ctx.fillRect(st.left, st.top, STEP_W, 5);
ctx.strokeStyle='#1c252e';
ctx.lineWidth=1;
ctx.strokeRect(st.left, st.top, STEP_W, STEP_H);
}
ctx.strokeStyle='#7c8c9c';
ctx.lineWidth=4;
ctx.beginPath();
let started=false;
for(let x=0;x<=W;x+=10){
const gy=groundYAt(x);
if(gy===null) continue;
const y=gy-75;
if(!started){ ctx.moveTo(x,y); started=true; } else ctx.lineTo(x,y);
}
ctx.stroke();
for(const person of people) drawPerson(ctx,person);
}
updateSteps();
const people=[];
const visibleSteps=[];
for(let i=0;i<steps.length;i++){
const st=steps[i];
if(st.x>80 && st.x<920) visibleSteps.push(i);
}
const count=Math.min(17, visibleSteps.length);
for(let k=0;k<count;k++){
const idx=visibleSteps[Math.floor(k*visibleSteps.length/count)];
const st=steps[idx];
const x=st.x+(Math.random()-0.5)*10;
const footY=st.top-FOOT_R;
people.push(new Person(x, footY, idx));
}
let last=performance.now(), acc=0;
function frame(t){
const ft=Math.min((t-last)/1000,0.05);
last=t;
acc+=ft;
while(acc>=DT){
updateSpeed(DT);
stepSim(DT);
acc-=DT;
}
draw();
requestAnimationFrame(frame);
}
btn.addEventListener('click',()=>{
reversing=!reversing;
btn.textContent=reversing?'GO FORWARD':'REVERSE';
btn.className=reversing?'go':'';
statusSpan.textContent=reversing?'REVERSING':'FORWARD';
});
requestAnimationFrame(frame);
</script>
</body>
</html>
When the button is pressed, the escalator reverses with a rapid acceleration. The people’s feet are pulled backward by the moving steps; their rigid standing pose resists, and the limited balance torque cannot keep the torso above the feet, so they lean, lose their grip, and topple. The impact of a falling person shoves the people around them, and since a downed person is just a set of collidable particles, the people behind pile onto them. Once the chaos settles, a downed person waits until the space above them is clear and there is a surface beneath them, then uses a procedural get-up controller to interpolate back to a standing pose and reattaches to the escalator if it is still underfoot.
I special-cased the recovery: it is a deliberate controller, not part of the accident physics. I also placed walls at the visible edges of the walkway so people pile up on-screen instead of riding off into the void. What it does badly is that the ragdolls are stiff and springy; they do not crumple like real bodies, and the recovery is visibly a “stand-up blend” rather than a biomechanical effort. The balance limit is tuned to make the high-speed reversal reliably knock people down, which means a particularly violent collision can also make someone fall even when the escalator is running normally.