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 commuter is three point-masses — foot, hip, head — joined by two rigid stick constraints and drawn as a stick figure, with the three points also acting as collision circles so a lying figure is a real obstacle. Everyone runs the same loop: gravity, stick constraints, circle–circle and circle–ground contacts (impulse solver with Coulomb friction and position projection), plus a "muscle" controller that springs the hip above the foot and the head above the hip with a hard force cap. Balance is lost purely physically: the escalator treads are a moving stepped heightfield whose grip on the feet is friction-limited, so when the drive reverses faster than μ·g can drag the feet along, the feet slip, the lean angle grows past what the capped muscles can recover, and the figure goes ragdoll. It stands back up only when its foot is on the ground, the local surface isn't accelerating hard, and a query finds free space at its would-be head/hip positions — so buried figures wait until the pile opens up.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator №9 — reversal study</title>
<style>
:root{--bg:#0e0f12;--ink:#c7ccd4;--dim:#6d7684;--acc:#d9a441}
*{box-sizing:border-box;margin:0}
html,body{background:var(--bg);color:var(--ink);
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,"Liberation Mono",monospace}
body{display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:18px 12px 34px}
header{width:100%;max-width:1500px;display:flex;align-items:baseline;gap:14px;padding:2px 4px 10px;flex-wrap:wrap}
h1{font-size:15px;letter-spacing:.18em;font-weight:700;color:#e8ebef}
.sub{font-size:11px;color:var(--dim);letter-spacing:.08em}
canvas{width:100%;max-width:1500px;height:auto;display:block;border:1px solid #262a31;
background:#191b1f;border-radius:4px}
#bar{width:100%;max-width:1500px;display:flex;align-items:center;gap:12px;padding:12px 4px 0;flex-wrap:wrap}
button{font:inherit;font-weight:700;letter-spacing:.14em;cursor:pointer;border-radius:3px;
padding:12px 22px;border:1px solid #4a515b;background:#22262d;color:#e8ebef}
button:hover{background:#2a2f38}
#rev{border-color:#7a3b31;background:#8f3226}
#rev:hover{background:#a53a2b}
#rev.on{border-color:#4f6b4a;background:#3f5d3c}
#reset{padding:12px 14px;color:var(--dim)}
#speed{min-width:150px;font-size:13px;color:var(--acc);letter-spacing:.08em}
#hint{font-size:11px;color:var(--dim);letter-spacing:.06em;margin-left:auto}
label.snd{font-size:11px;color:var(--dim);display:flex;gap:6px;align-items:center;cursor:pointer;letter-spacing:.08em}
</style>
</head>
<body>
<header>
<h1>ESCALATOR №9 — REVERSAL STUDY</h1>
<span class="sub">side view · 19 commuters · no scripted disaster, only rules</span>
</header>
<canvas id="c" width="1500" height="620"></canvas>
<div id="bar">
<button id="rev" type="button">REVERSE ▼</button>
<button id="reset" type="button">RESET</button>
<div id="speed">1.10 m/s ↑</div>
<label class="snd"><input type="checkbox" id="snd" checked>sound</label>
<div id="hint">press REVERSE — press again to put it back</div>
</div>
<script>
'use strict';
/* ---------- world constants (pixels, seconds, y up) ---------- */
const PPM=60, G=9.81*PPM, DT=1/240;
const ANG=27*Math.PI/180, CA=Math.cos(ANG), SA=Math.sin(ANG), TA=Math.tan(ANG);
const XB=320, RUN=780, XT=XB+RUN, YB=0, YT=RUN*TA;
const TREAD_X=40, TREAD_S=TREAD_X/CA, STEP_H=TREAD_X*TA, L_INCL=RUN/CA;
const V_FWD=1.1*PPM, V_REV=-3.0*PPM, RATE=25*PPM; /* 1.1 -> -3.0 m/s in ~0.16 s */
const BETA=0.35, SLOP=0.6, MU_PP=0.3;
let escV=V_FWD, escTgt=V_FWD, escOff=0, escAcc=0, simT=0;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const mod=(a,n)=>((a%n)+n)%n;
/* walking surface: static floors at both ends, moving stepped heightfield between */
function surfaceAt(x){
if(x<=XB) return {y:YB,svx:0,svy:0,esc:false};
if(x>=XT) return {y:YT,svx:0,svy:0,esc:false};
const s=(x-XB)/CA, local=mod(s-escOff,TREAD_S);
let y=YB+(s-local+TREAD_S*0.5)*SA;
const B=TREAD_S*0.7; /* comb-plate blend near the ends */
if(s<B) y=YB+(y-YB)*(s/B);
else if(s>L_INCL-B) y=YT+(y-YT)*((L_INCL-s)/B);
return {y,svx:escV*CA,svy:escV*SA,esc:true};
}
/* ---------- people ---------- */
const COL=['#c94f3d','#3d7a8c','#c99a3d','#6a8c3d','#8c4d6a','#4a6a8c','#b0603a',
'#5a8c7a','#946a9e','#a8574e','#4e8a6a','#7a6a4a'];
const SKIN=['#e8b98a','#d09a66','#b0784a','#8a5a3a','#f0cda6'];
let people=[], parts=[], impacts=[], dead=[], uid=0;
function Pt(x,y,m,r,mu){return {x,y,vx:0,vy:0,im:1/m,r,mu,contact:false,owner:null};}
class Person{
constructor(x,facing,mode){
this.id=uid++;
const sc=0.92+Math.random()*0.18;
this.L1=48*sc; this.L2=42*sc; this.headR=9*sc; this.hipR=8; this.footR=6;
const gy=surfaceAt(x).y;
this.foot=Pt(x,gy+this.footR,1.2,this.footR,0.75);
this.hip =Pt(x,gy+this.footR+this.L1,2.6,this.hipR,0.32);
this.head=Pt(x,gy+this.footR+this.L1+this.L2,1.2,this.headR,0.32);
this.circ=[this.foot,this.hip,this.head];
for(const pt of this.circ) pt.owner=this;
this.facing=facing; this.mode=mode;
this.goal=(mode==='walk')?'board':null;
this.phase=Math.random()*6.28; this.idle=Math.random()*6.28;
this.sat=0; this.air=0; this.downUntil=0; this.riseCheck=0; this.wasOnEsc=false;
this.jacket=COL[this.id%COL.length];
this.pants=this.id%3?'#3a4048':'#4a4038';
this.skin=SKIN[this.id%SKIN.length];
this.hat=Math.random()<0.28;
}
get lean(){return Math.atan2(this.hip.x-this.foot.x,this.hip.y-this.foot.y);}
}
/* ---------- muscles: capped springs; walking is a tuned shimmy ---------- */
const FMAXH=1900, FMAXT=1100;
function muscles(p,dt){
if(p.mode==='down') return;
const rise=p.mode==='rise';
const f=p.foot,h=p.hip,d=p.head;
const KH=rise?1500:900, DH=rise?75:55, KT=rise?1100:750, DT2=rise?55:38;
const FH=rise?5200:FMAXH, FT=rise?3000:FMAXT;
let tx=f.x, ty=f.y+p.L1*0.985;
if(p.mode==='ride') tx+=Math.sin(simT*1.7+p.idle)*2+Math.sin(simT*0.83+p.idle*1.7)*1.4;
if(p.mode==='walk'){
const des=p.facing*58;
h.vx+=clamp((des-h.vx)*14,-520,520)*h.im*dt; /* forward drive */
p.phase+=dt*(3.2+Math.abs(h.vx)*0.075); /* stepping targets */
const sw=Math.sin(p.phase);
const lift=9*Math.max(0,p.facing*Math.cos(p.phase));
const ftx=h.x+p.facing*15*sw;
const fty=surfaceAt(f.x).y+f.footR+lift;
f.vx+=clamp(420*(ftx-f.x)-26*(f.vx-h.vx),-1100,1100)*f.im*dt;
if(fty>f.y) f.vy+=clamp(500*(fty-f.y),0,900)*f.im*dt;
}
let Fx=KH*(tx-h.x)-DH*(h.vx-f.vx);
let Fy=KH*(ty-h.y)-DH*(h.vy-f.vy);
const m=Math.hypot(Fx,Fy);
if(m>FH){Fx*=FH/m;Fy*=FH/m;p.sat+=dt;} /* muscle saturation */
h.vx+=Fx*h.im*dt; h.vy+=Fy*h.im*dt;
f.vx-=Fx*f.im*dt*0.55; f.vy-=Fy*f.im*dt*0.55;
const tx2=h.x+Math.sin(simT*2.1+p.idle)*1.1, ty2=h.y+p.L2;
let Gx=KT*(tx2-d.x)-DT2*(d.vx-h.vx);
let Gy=KT*(ty2-d.y)-DT2*(d.vy-h.vy);
const m2=Math.hypot(Gx,Gy);
if(m2>FT){Gx*=FT/m2;Gy*=FT/m2;}
d.vx+=Gx*d.im*dt; d.vy+=Gy*d.im*dt;
h.vx-=Gx*h.im*dt*0.3; h.vy-=Gy*h.im*dt*0.3;
}
/* ---------- contacts ---------- */
function groundVel(pt){
const s=surfaceAt(pt.x);
const pen=s.y+pt.r-pt.y;
pt.contact=false;
if(pen>0){
pt.contact=true;
const vn=pt.vy-s.svy;
let Jn=(-vn+BETA*Math.max(0,pen-SLOP)/DT)/pt.im;
if(Jn<0)Jn=0;
pt.vy+=Jn*pt.im;
const mu=(pt.owner&&pt.owner.mode==='down')?0.3:pt.mu;
const vt=pt.vx-s.svx;
const Jmax=mu*Jn;
let Jt=clamp(-vt/pt.im,-Jmax,Jmax);
if(Math.abs(vt)<6) Jt=-vt/pt.im; /* static stick at low slip */
pt.vx+=Jt*pt.im;
if(Jn*pt.im>110) impacts.push({x:pt.x,y:s.y,s:Jn*pt.im});
}
}
function wallVel(pt){
const pen=pt.x+pt.r-1420;
if(pen>0){
const dvn=pt.vx+BETA*Math.max(0,pen-SLOP)/DT;
if(dvn>0){ pt.vx-=dvn; pt.vy-=clamp(pt.vy,-0.25*dvn,0.25*dvn); }
}
}
function pairVel(a,b){
const dx=b.x-a.x, dy=b.y-a.y, rs=a.r+b.r, d2=dx*dx+dy*dy;
if(d2>=rs*rs||d2===0) return;
const d=Math.sqrt(d2), nx=dx/d, ny=dy/d, pen=rs-d;
const vn=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
const mE=1/(a.im+b.im);
let Jn=(-vn+BETA*Math.max(0,pen-SLOP)/DT)*mE;
if(Jn<0)Jn=0;
a.vx-=Jn*a.im*nx; a.vy-=Jn*a.im*ny;
b.vx+=Jn*b.im*nx; b.vy+=Jn*b.im*ny;
const tx=-ny, ty=nx;
const vt=(b.vx-a.vx)*tx+(b.vy-a.vy)*ty;
const Jt=clamp(-vt*mE,-MU_PP*Jn,MU_PP*Jn);
a.vx-=Jt*a.im*tx; a.vy-=Jt*a.im*ty;
b.vx+=Jt*b.im*tx; b.vy+=Jt*b.im*ty;
if(Jn*a.im>110) impacts.push({x:(a.x+b.x)/2,y:(a.y+b.y)/2,s:Jn*a.im});
}
function stickVel(a,b,len){
const dx=b.x-a.x, dy=b.y-a.y;
const d=Math.hypot(dx,dy)||1e-4;
const nx=dx/d, ny=dy/d;
const vr=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
const J=-(vr+BETA*(d-len)/DT*0.5)/(a.im+b.im);
a.vx-=J*nx*a.im; a.vy-=J*ny*a.im;
b.vx+=J*nx*b.im; b.vy+=J*ny*b.im;
}
function stickPos(a,b,len){
const dx=b.x-a.x, dy=b.y-a.y;
const d=Math.hypot(dx,dy)||1e-4;
const c=(d-len)/d, wa=a.im/(a.im+b.im), wb=b.im/(a.im+b.im);
a.x+=dx*c*wa; a.y+=dy*c*wa;
b.x-=dx*c*wb; b.y-=dy*c*wb;
}
function groundPos(pt){
const pen=surfaceAt(pt.x).y+pt.r-pt.y;
if(pen>SLOP) pt.y+=pen-SLOP;
}
function pairPos(a,b){
const dx=b.x-a.x, dy=b.y-a.y, rs=a.r+b.r, d2=dx*dx+dy*dy;
if(d2>=rs*rs||d2===0) return;
const d=Math.sqrt(d2), pen=rs-d;
if(pen<=SLOP) return;
const nx=dx/d, ny=dy/d, c=pen-SLOP;
const wa=a.im/(a.im+b.im), wb=b.im/(a.im+b.im);
a.x-=nx*c*wa; a.y-=ny*c*wa;
b.x+=nx*c*wb; b.y+=ny*c*wb;
}
/* ---------- one physics substep ---------- */
function substep(dt){
const pv=escV;
escV+=clamp(escTgt-escV,-RATE*dt,RATE*dt);
escAcc=(escV-pv)/dt;
escOff=mod(escOff+escV*dt,TREAD_S);
for(const p of people){
for(const pt of p.circ) pt.contact=false;
p.foot.vy-=G*dt; p.hip.vy-=G*dt; p.head.vy-=G*dt;
muscles(p,dt);
}
for(let it=0;it<4;it++){
for(const p of people){
stickVel(p.foot,p.hip,p.L1); stickVel(p.hip,p.head,p.L2);
groundVel(p.foot); groundVel(p.hip); groundVel(p.head);
wallVel(p.foot); wallVel(p.hip); wallVel(p.head);
}
for(let i=0;i<people.length;i++)for(let j=i+1;j<people.length;j++){
const A=people[i],B=people[j];
if(Math.abs(A.hip.x-B.hip.x)>260) continue;
for(const a of A.circ)for(const b of B.circ) pairVel(a,b);
}
}
for(const p of people)for(const pt of p.circ){pt.x+=pt.vx*dt;pt.y+=pt.vy*dt;}
for(let it=0;it<2;it++){
for(const p of people){
stickPos(p.foot,p.hip,p.L1); stickPos(p.hip,p.head,p.L2);
groundPos(p.foot); groundPos(p.hip); groundPos(p.head);
}
for(let i=0;i<people.length;i++)for(let j=i+1;j<people.length;j++){
const A=people[i],B=people[j];
if(Math.abs(A.hip.x-B.hip.x)>260) continue;
for(const a of A.circ)for(const b of B.circ) pairPos(a,b);
}
}
}
/* ---------- behaviour state machine (same rules for everyone) ---------- */
function canRise(p){
if(!p.foot.contact) return false;
if(surfaceAt(p.foot.x).esc && Math.abs(escAcc)>2.2*PPM) return false; // need calm footing
const hx=p.foot.x, hy1=p.foot.y+p.L1, hy2=hy1+p.L2;
for(const q of people){
if(q===p) continue;
if(Math.abs(q.hip.x-hx)>200) continue;
for(const pt of q.circ){
if(Math.hypot(pt.x-hx,pt.y-hy1)<pt.r+p.hipR+5) return false; // no room to stand
if(Math.hypot(pt.x-hx,pt.y-hy2)<pt.r+p.headR+5) return false;
}
}
return true;
}
function updateStates(dtF){
for(const p of people){
const lean=p.lean, hipH=p.hip.y-p.foot.y;
if(p.mode==='ride'||p.mode==='walk'){
if(surfaceAt(p.foot.x).esc) p.wasOnEsc=true;
if(!p.foot.contact) p.air+=dtF; else p.air=0;
let fell=false;
if(Math.abs(lean)>0.9) fell=true; // toppled too far
if(hipH<0.55*p.L1) fell=true; // legs folded
if(p.air>0.35) fell=true; // airborne stumble
if(p.sat>0.22) fell=true; // muscles maxed too long
if(fell){
p.mode='down'; p.downUntil=simT+0.7+Math.random()*1.1;
p.sat=0; p.air=0; p.riseCheck=simT+0.1;
burst(p.hip.x,p.hip.y,6,120);
shake=Math.min(10,shake+2.5);
continue;
}
p.sat=Math.max(0,p.sat-dtF*1.5);
if(p.mode==='ride'){
if(p.foot.x>XT+14&&p.wasOnEsc){p.mode='walk';p.goal='topExit';p.facing=1;p.wasOnEsc=false;}
else if(p.foot.x<XB-14&&p.wasOnEsc){p.mode='walk';p.goal='bottomExit';p.facing=-1;p.wasOnEsc=false;}
}else{
if(p.goal==='topExit'&&p.foot.x>1380){dead.push(p);}
else if(p.goal==='bottomExit'&&p.foot.x<-55){dead.push(p);}
else if(p.goal==='board'&&p.foot.x>XB+12){p.mode='ride';p.goal=null;}
else if(p.goal==='pace'){
if(p.foot.x>270)p.facing=-1;
if(p.foot.x<120)p.facing=1;
}
}
}else if(p.mode==='down'){
if(Math.random()<dtF*1.6){ // wriggle so piles can relax
const w=(Math.random()*2-1)*26;
p.hip.vx+=w; p.head.vx+=w*0.6;
}
if(simT>p.downUntil&&simT>p.riseCheck){
p.riseCheck=simT+0.15;
if(canRise(p)) p.mode='rise';
}
}else if(p.mode==='rise'){
if(Math.abs(lean)>1.25||hipH<0.2*p.L1){p.mode='down';p.downUntil=simT+0.5+Math.random();}
else if(Math.abs(lean)<0.16&&hipH>0.92*p.L1){p.mode='ride';}
}
}
if(dead.length){people=people.filter(p=>!dead.includes(p));dead=[];}
}
/* ---------- casting & arrivals ---------- */
let spawnT=4;
function spawnInitial(){
people=[];dead=[];parts=[];impacts=[];simT=0;spawnT=4;
for(let i=0;i<17;i++) people.push(new Person(XB+(60+i*45)*CA,1,'ride'));
people.push(new Person(190,1,'ride'));
const b=new Person(240,-1,'walk'); b.goal='pace'; people.push(b);
}
function spawner(dtF){
spawnT-=dtF;
if(spawnT<=0){
spawnT=2.8+Math.random()*2.4;
if(people.length<14) people.push(new Person(-30,1,'walk'));
}
}
/* ---------- foley (cosmetic) ---------- */
let AC=null;
const sndEl=document.getElementById('snd');
function ac(){try{if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();
if(AC.state==='suspended')AC.resume();return AC;}catch(e){return null;}}
function thud(strength){
if(!sndEl.checked)return; const a=ac(); if(!a)return;
try{
const dur=0.14, buf=a.createBuffer(1,Math.floor(a.sampleRate*dur),a.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2.2);
const src=a.createBufferSource();src.buffer=buf;
const f=a.createBiquadFilter();f.type='lowpass';f.frequency.value=140+Math.min(500,strength*0.9);
const g=a.createGain();g.gain.value=Math.min(0.4,strength/2600);
src.connect(f);f.connect(g);g.connect(a.destination);src.start();
}catch(e){}
}
function clunk(){
if(!sndEl.checked)return; const a=ac(); if(!a)return;
try{
const t=a.currentTime,o=a.createOscillator(),g=a.createGain();
o.type='square';o.frequency.setValueAtTime(90,t);o.frequency.exponentialRampToValueAtTime(38,t+0.28);
g.gain.setValueAtTime(0.22,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
o.connect(g);g.connect(a.destination);o.start(t);o.stop(t+0.32);
}catch(e){}
}
function burst(x,y,n,sp){
for(let i=0;i<n;i++){
if(parts.length>220)return;
parts.push({x:x+(Math.random()*10-5),y:y+Math.random()*6,
vx:(Math.random()*2-1)*sp,vy:Math.random()*sp*0.8+20,
life:0.35+Math.random()*0.35,t:0,r:1.5+Math.random()*2.5});
}
}
/* ---------- rendering ---------- */
const cv=document.getElementById('c'), ctx=cv.getContext('2d');
const W=1500,H=620;
const sx=x=>x+30, sy=y=>H-(y+90);
let shake=0;
function line(x1,y1,x2,y2){ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke();}
function line2(a,b){ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();}
function roundRect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function drawArm(sh,a,ul,fl,bend,fc){
const d1x=Math.sin(a)*fc, d1y=Math.cos(a);
const el={x:sh.x+d1x*ul,y:sh.y+d1y*ul};
const d2x=Math.sin(a+bend)*fc, d2y=Math.cos(a+bend);
line2(sh,el); line2(el,{x:el.x+d2x*fl,y:el.y+d2y*fl});
}
function drawPerson(p){
const F=p.foot,Hp=p.hip,D=p.head, fc=p.facing, down=p.mode==='down';
const gs=surfaceAt(F.x).y;
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(sx(Hp.x),sy(Math.min(gs,Hp.y-4))+3,down?34:16,4,0,0,7);
ctx.fill();
const f={x:sx(F.x),y:sy(F.y)}, h={x:sx(Hp.x),y:sy(Hp.y)}, hd={x:sx(D.x),y:sy(D.y)};
ctx.lineCap='round';
/* legs */
ctx.strokeStyle=p.pants; ctx.lineWidth=3.5;
let f2;
if(p.mode==='walk'){
const sw2=Math.sin(p.phase+Math.PI);
const lift2=9*Math.max(0,fc*Math.cos(p.phase+Math.PI));
f2={x:h.x+fc*15*sw2,y:sy(surfaceAt(F.x).y+p.footR+lift2)};
}else if(down){f2={x:f.x-fc*8,y:f.y-7};}
else{f2={x:f.x+fc*5,y:f.y-1};}
line2(h,f2); line2(h,f);
/* torso */
ctx.strokeStyle=p.jacket; ctx.lineWidth=4;
line2(h,{x:hd.x-(hd.x-h.x)*0.18,y:hd.y-(hd.y-h.y)*0.18});
/* arms */
const sh={x:h.x+(hd.x-h.x)*0.72,y:h.y+(hd.y-h.y)*0.72};
let a1,a2,bend=0.45;
if(down){a1=2.3+Math.sin(simT*3+p.idle)*0.15;a2=-2.3-Math.sin(simT*3.7+p.idle)*0.15;bend=0.9;}
else if(p.air>0.08||Math.abs(p.lean)>0.5){
a1=2.7+Math.sin(simT*24+p.idle)*0.5;a2=-2.7-Math.sin(simT*24+p.idle)*0.5;bend=0.25;}
else if(p.mode==='walk'){const sw=Math.sin(p.phase)*0.55;a1=0.3+sw;a2=-0.3+sw;}
else if(p.mode==='rise'){a1=0.7;a2=-0.7;}
else{const sw=Math.sin(simT*1.6+p.idle)*0.12;a1=0.18+sw;a2=-0.18+sw;}
ctx.strokeStyle=p.jacket; ctx.lineWidth=3;
drawArm(sh,a1,15,14,bend,fc); drawArm(sh,a2,15,14,bend,fc);
/* head */
ctx.fillStyle=p.skin;
ctx.beginPath();ctx.arc(hd.x+fc*1.5,hd.y,p.headR,0,7);ctx.fill();
if(p.hat){ctx.fillStyle=p.jacket;ctx.fillRect(hd.x+fc*1.5-p.headR,hd.y-p.headR-2,p.headR*2,4);}
}
function render(){
ctx.save();
ctx.translate((Math.random()*2-1)*shake,(Math.random()*2-1)*shake);
ctx.fillStyle='#1b1e23'; ctx.fillRect(-20,-20,W+40,H+40);
ctx.strokeStyle='rgba(255,255,255,0.03)'; ctx.lineWidth=1;
for(let yy=40;yy<H;yy+=90) line(0,yy,W,yy);
ctx.fillStyle='#8a6a2f'; ctx.fillRect(0,86,W,7);
/* roundel + status sign */
ctx.strokeStyle='#c9a23d'; ctx.lineWidth=3;
ctx.beginPath();ctx.arc(sx(120),sy(475),26,0,7);ctx.stroke();
ctx.fillStyle='#c9a23d';ctx.font='700 24px ui-monospace,Menlo,Consolas,monospace';
ctx.textAlign='center';ctx.fillText('9',sx(120),sy(475)+9);
ctx.fillStyle='#252a31';ctx.strokeStyle='#3a4048';ctx.lineWidth=1.5;
roundRect(210,96,224,58,6);ctx.fill();ctx.stroke();
ctx.fillStyle='#9aa3ad';ctx.font='11px ui-monospace,Menlo,Consolas,monospace';ctx.textAlign='left';
ctx.fillText('ESCALATOR 9 — DIRECTION',222,114);
const up=escV>2, rev=escV<-2;
ctx.font='700 16px ui-monospace,Menlo,Consolas,monospace';
ctx.fillStyle=up?'#79b47a':rev?'#e06a4f':'#d9a441';
ctx.fillText(up?'UP ▲':rev?'REVERSED ▼':'CHANGING …',222,140);
ctx.globalAlpha=rev?0.35+0.65*(Math.sin(simT*7)*0.5+0.5):1;
ctx.beginPath();ctx.arc(414,134,5,0,7);ctx.fill();
ctx.globalAlpha=1;
/* pit + landings */
ctx.fillStyle='#14161a'; ctx.fillRect(0,sy(0),W,H-sy(0)+20);
ctx.fillStyle='#2e333b'; ctx.fillRect(sx(0),sy(0),XB,13); ctx.fillRect(sx(XT),sy(YT),1420-XT,13);
ctx.fillStyle='#3d434d'; ctx.fillRect(sx(0),sy(0),XB,3); ctx.fillRect(sx(XT),sy(YT),1420-XT,3);
/* truss */
const n1x=-SA,n1y=CA,n2x=SA,n2y=-CA;
ctx.fillStyle='#232830';
ctx.beginPath();
ctx.moveTo(sx(XB),sy(0)); ctx.lineTo(sx(XT),sy(YT));
ctx.lineTo(sx(XT+n2x*88),sy(YT+n2y*88)); ctx.lineTo(sx(XB+n2x*88),sy(n2y*88));
ctx.closePath();ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.05)'; ctx.lineWidth=2;
for(let u=30;u<L_INCL;u+=64){
const x1=XB+u*CA,y1=YB+u*SA;
line(sx(x1),sy(y1),sx(x1+n2x*88),sy(y1+n2y*88));
}
/* far balustrade + handrail */
ctx.fillStyle='rgba(255,255,255,0.045)';
ctx.beginPath();
ctx.moveTo(sx(XB+n1x*52),sy(n1y*52)); ctx.lineTo(sx(XT+n1x*52),sy(YT+n1y*52));
ctx.lineTo(sx(XT+n1x*80),sy(YT+n1y*80)); ctx.lineTo(sx(XB+n1x*80),sy(n1y*80));
ctx.closePath();ctx.fill();
ctx.strokeStyle='#8f6d33'; ctx.lineWidth=5;
line(sx(XB+n1x*86),sy(n1y*86),sx(XT+n1x*86),sy(YT+n1y*86));
/* moving treads */
const nT=Math.ceil(L_INCL/TREAD_S);
for(let k=0;k<=nT;k++){
const s0=k*TREAD_S+escOff;
if(s0>=L_INCL) break;
const x0=XB+s0*CA, x1=Math.min(x0+TREAD_X,XT);
if(x1<=x0) continue;
const y=YB+(s0+TREAD_S*0.5)*SA, B=TREAD_S*0.7, sC=s0+TREAD_S/2;
let yd=y;
if(sC<B) yd=YB+(y-YB)*(sC/B); else if(sC>L_INCL-B) yd=YT+(y-YT)*((L_INCL-sC)/B);
ctx.fillStyle='#262b33'; ctx.fillRect(sx(x0),sy(yd),x1-x0,STEP_H+3);
ctx.fillStyle='#4b525d'; ctx.fillRect(sx(x0),sy(yd)-4,x1-x0,5);
ctx.fillStyle='#5a6270'; ctx.fillRect(sx(x0),sy(yd)-4,x1-x0,2);
}
ctx.fillStyle='#c9a23d';
for(let i=0;i<5;i++){
ctx.fillRect(sx(XB-34+i*7),sy(YB)-8,4,10);
ctx.fillRect(sx(XT+2+i*7),sy(YT)-8,4,10);
}
/* people + dust */
for(const p of people) drawPerson(p);
for(const q of parts){
ctx.globalAlpha=Math.max(0,1-q.t/q.life)*0.5;
ctx.fillStyle='#b8ad9c';
ctx.beginPath();ctx.arc(sx(q.x),sy(q.y),q.r,0,7);ctx.fill();
}
ctx.globalAlpha=1;
ctx.restore();
}
/* ---------- controls & main loop ---------- */
let reversed=false;
const btn=document.getElementById('rev'), speedEl=document.getElementById('speed');
btn.addEventListener('click',()=>{
reversed=!reversed;
escTgt=reversed?V_REV:V_FWD;
btn.textContent=reversed?'RESUME ▲':'REVERSE ▼';
btn.classList.toggle('on',reversed);
shake=Math.min(12,shake+7);
clunk();
});
document.getElementById('reset').addEventListener('click',()=>{
spawnInitial(); escV=V_FWD; escTgt=V_FWD; escOff=0; escAcc=0; reversed=false;
btn.textContent='REVERSE ▼'; btn.classList.remove('on');
});
let last=performance.now(), accT=0;
function frame(now){
const dtF=Math.min(0.05,(now-last)/1000); last=now; simT+=dtF;
accT+=dtF; let n=0;
while(accT>=DT&&n<10){substep(DT);accT-=DT;n++;}
updateStates(dtF);
spawner(dtF);
let played=0;
for(const im of impacts){
if(im.s>150) burst(im.x,im.y,2,im.s*0.25);
if(played<3&&im.s>170){thud(im.s);played++;}
}
impacts.length=0;
for(const q of parts){q.t+=dtF;q.x+=q.vx*dtF;q.y+=q.vy*dtF;q.vy-=300*dtF;}
parts=parts.filter(q=>q.t<q.life);
shake=Math.max(0,shake-dtF*14);
render();
speedEl.textContent=(escV/PPM).toFixed(2)+' m/s '+(escV>2?'↑':escV<-2?'▼':'·');
requestAnimationFrame(frame);
}
spawnInitial();
requestAnimationFrame(frame);
</script>
</body>
</html>
Before you touch anything: seventeen figures stand spaced along the upward escalator, swaying idly as the treads scroll under them; a commuter walks in from the left every few seconds and boards, riders shuffle off at the top and leave right, and one person paces the lower landing. Press REVERSE and the drive slams from +1.1 to −3.0 m/s in about 0.16 s (≈25 m/s², rate-limited — that ramp is the only scripted input). The treads drop away vertically and shoot down-slope faster than foot friction can follow, every balance controller saturates at once, and each figure topples on their own physics — then slides, tumbles, and collides mid-slide while the escalator itself drags the heap toward the bottom comb, where arriving bodies slam into earlier ones and the pile builds. Buried figures wriggle and wait; when a downed figure has ground contact, a calm surface, and clear space at its would-be hip/head, it stands up wobbly, rides or steps to the bottom, and walks off left. Press RESUME and the same violence runs the other way — standing figures go down again, and loose bodies on the lower treads get conveyed back up and dumped at the top landing.
Special-casing I'll own up to: walking is a tuned shimmy (procedural foot-swing targets, landings only — nobody walks on the escalator); downed figures get small random "wriggle" impulses so piles can relax and unlock; tread heights are blended into the floors near the comb plates to remove a lip; feet "statically stick" below a tiny slip speed; a spawner tops the population back up and leavers despawn off-screen; thuds and dust are cosmetic foley, and sound only starts after your first button press (browser autoplay rules). No fall is keyframed — the state machine only switches muscles on/off based on the physics.
What it does badly: deep piles slump and creep rather than sitting dead still, wedged figures can twitch while waiting, nobody grabs the handrail or heroically climbs the down escalator, standing up can shove neighbors (the room check has margins, not guarantees), the walking shuffle looks mechanical in slow motion, downed postures are approximated by three circles, and rapid toggle-spamming can whipsaw anyone standing at the comb plates. Watch the bottom-left corner — that's where the story is.