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.
A person is two things: a point mass with momentum (the body) and a support point (the feet) that grips the step it stands on. The body is an inverted pendulum on those feet — offset the centre of mass from the point directly above the feet and gravity amplifies the offset — so staying up is an active task: the feet constantly chase the capture point (where a foot would have to land to kill the current fall) at a limited slide speed, and the legs can only push the body so hard. Losing balance is therefore never an event, it's a verdict from the same threshold that lets everyone ride: if the capture point is farther than the leg can reach, or the centre of mass sweeps past the foot, the figure is down. The ground talks to the feet through a limited-force grip, so when the steps change speed the feet are yanked first and the body lags — that lag is what a reversal is. Two upright figures softly repel each other; once someone is down they become ~1.4 m of obstacle: lying bodies are dragged by the steps, collide and impulse-resolve along the slope, vault over and stack on one another, trip moving feet, and get shoved by whatever arrives behind. A downed figure gets up only after a short personal daze, when nothing lies on or across it and the surface below is slow enough to stand on — a check a runaway escalator fails, so it stays down.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator No. 1 — side elevation</title>
<style>
:root{--paper:#f2ede2;--ink:#262218;--acc:#c73e1d;--dim:#6f695c}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--paper);color:var(--ink);
font-family:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,"Liberation Mono",monospace}
body{padding:16px clamp(12px,3vw,36px) 34px;max-width:1240px;margin:0 auto}
header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;
gap:10px 28px;border-bottom:2px solid var(--ink);padding-bottom:10px;margin-bottom:12px}
h1{font-size:clamp(16px,2.3vw,23px);letter-spacing:.07em}
.sub{display:block;font-size:10px;letter-spacing:.16em;color:var(--dim);margin-top:4px}
.readouts{display:flex;gap:clamp(12px,2.2vw,30px)}
.ro label{display:block;font-size:9px;letter-spacing:.2em;color:var(--dim)}
.ro span{font-size:15px;font-variant-numeric:tabular-nums}
.ro span.hot{color:var(--acc);font-weight:700}
.stage{border:2px solid var(--ink)}
canvas{display:block;width:100%;aspect-ratio:2.18/1;min-height:300px}
.panel{display:flex;align-items:center;gap:14px;margin-top:12px;flex-wrap:wrap}
button{font:inherit;font-size:13px;letter-spacing:.1em;padding:11px 22px;cursor:pointer;
background:var(--paper);color:var(--ink);border:2px solid var(--ink)}
button:hover{background:var(--ink);color:var(--paper)}
button.hot{background:var(--acc);border-color:var(--acc);color:var(--paper)}
button.hot:hover{background:var(--ink);border-color:var(--ink)}
button.ghost{font-size:10.5px;padding:8px 13px;border-width:1px;opacity:.85}
.hint{margin-left:auto;font-size:10.5px;letter-spacing:.08em;color:var(--dim)}
footer{margin-top:16px;font-size:11px;line-height:1.65;color:var(--dim);max-width:112ch}
</style>
</head>
<body>
<header>
<div><h1>ESCALATOR №1 — SIDE ELEVATION</h1>
<span class="sub">CROWD SIMULATION · EVERY FIGURE RUNS THE SAME RULES · NOTHING IS KEYFRAMED</span></div>
<div class="readouts">
<div class="ro"><label>SPEED</label><span id="roV">+0.90 m/s</span></div>
<div class="ro"><label>DIRECTION</label><span id="roS">UPWARD</span></div>
<div class="ro"><label>UPRIGHT</label><span id="roU">16</span></div>
<div class="ro"><label>DOWN</label><span id="roD">0</span></div>
<div class="ro"><label>SINCE TOGGLE</label><span id="roT">0.0 s</span></div>
</div>
</header>
<div class="stage"><canvas id="cv"></canvas></div>
<div class="panel">
<button id="btn">THROW IT INTO REVERSE</button>
<button id="snd" class="ghost">SOUND ON</button>
<span class="hint">SPACE or R toggles the reversal</span>
</div>
<footer>
A person is an inverted pendulum: the feet grip the steps with limited force, the body keeps its own
momentum, and the capture point decides everything. If the spot where a foot would have to land to save
the fall is out of reach, the figure goes down — same rule, every figure, every time. Figures that are
down are dragged by the steps, trip the ones still moving, and pile up; they get back up only when
nothing is lying on or across them and the surface is slow enough to stand on.
</footer>
<script>
'use strict';
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const sgn=v=>v>0?1:v<0?-1:0;
const frac=v=>v-Math.floor(v);
const INK='#262218',ACC='#c73e1d';
/* ---- geometry: a path coordinate s runs up the lower floor, the incline, the upper floor ---- */
const TH=Math.atan2(0.22,0.44),SIN=Math.sin(TH),COS=Math.cos(TH),TAN=Math.tan(TH);
const PITCH=Math.hypot(0.44,0.22);
const L=11,X0=3.2,FLO=-3.2,FHI=L+3.6;
const WALL_L=FLO+0.45,WALL_R=FHI-0.45;
const G=9.8,V_UP=0.9,V_REV=-2.8,NPOP=16;
/* ---- balance model ---- */
const NH=0.92,OM=Math.sqrt(G/NH),OM2=G/NH;
const REACH=0.62,DMAX=0.55,TF=0.07,AGRIP=45,ALEG=5.5,KCTRL=3.0,VSTEPM=2.6,KSTEP=7;
const HL=0.72,THK=0.26,SUPOV=1.22;
let Vstep=V_UP,Vtarget=V_UP,TV=0.16,reversed=false;
let sigAcc=0,railAcc=0,T=0,tToggle=0,timeScale=1,shake=0;
const people=[],dustP=[],spawnQ=[];
const blend=s=>clamp(Math.min(s+0.06,L+0.06-s)/0.35,0,1);
const surfV=s=>Vstep*blend(s);
const slopeT=s=>TAN*blend(s);
function pPt(s,h){
if(s<0)return{x:X0+s,y:h};
if(s<=L)return{x:X0+s*COS-h*SIN,y:s*SIN+h*COS};
return{x:X0+L*COS+(s-L),y:L*SIN+h};
}
const pAng=s=>TH*blend(s);
const surfY=s=>s<0?0:(s<=L?s*SIN:L*SIN);
function treadY(s){ /* height of the drawn tread under path position s */
const b=blend(s),base=surfY(s);
if(b<=0)return base;
const phi=((sigAcc%PITCH)+PITCH)%PITCH,k=Math.floor((s-phi)/PITCH);
return((k*PITCH+phi)*SIN)*b+base*(1-b);
}
function baseTraits(){return{vpref:1.05+Math.random()*0.45,walker:Math.random()<0.35,
daze:0.6+Math.random()*0.7,seed:Math.random()*100,mu:0.5+Math.random()*0.35};}
function spawnUp(s,vc,walker){
const t=baseTraits();if(walker!==undefined)t.walker=walker;
people.push({kind:1,s_c:s,v_c:vc,s_f:s-NH*slopeT(s),v_f:surfV(s),
riseT:1,fade:1,ph:Math.random()*TAU,need:0,d:0,w:0,ufd:0,...t});
}
function init(){for(let i=0;i<NPOP;i++){
const s=1.1+i*(9.6/(NPOP-1))+(Math.random()*0.3-0.15);
spawnUp(s,V_UP,i%3===0?true:undefined);
}}
function puff(s,h,n){const p=pPt(s,h);
for(let i=0;i<n;i++){if(dustP.length>90)dustP.shift();
dustP.push({x:p.x+(Math.random()-.5)*0.3,y:p.y+Math.random()*0.1,
vx:(Math.random()-.5)*0.9,vy:0.4+Math.random()*0.7,t:0,life:0.35+Math.random()*0.35});}}
function toDown(f,dir,vault,topH){
f.kind=0;
f.s=f.s_c+0.12*dir;
f.h=vault?topH:0.34;
f.v_s=f.v_c+0.55*dir;
f.v_h=vault?0.9:-0.35;
f.dir=dir;f.roll=0;f.rollV=0;f.downT=0;f.sup=0;f.supBody=null;f.stuck=false;f.ramT=-9;
puff(f.s,f.h+0.25,4);
}
/* ---- upright figure: inverted pendulum + capture-point feet ---- */
function stepUp(f,dt){
f.riseT+=dt;
const bl=blend(f.s_f),Vloc=Vstep*bl,tan_l=TAN*bl;
const d=f.s_c-f.s_f-NH*tan_l; // COM offset from neutral over the feet
const w=f.v_c-f.v_f; // COM velocity relative to the feet
const need=(f.s_c-NH*tan_l+w/OM)-f.s_f;// capture point minus where the feet are
f.d=d;f.w=w;f.need=need;
if(Math.abs(need)>REACH||Math.abs(d)>DMAX){ // the verdict
toDown(f,w!==0?sgn(w):(frac(f.seed*7.7)<0.5?1:-1),false,0);return;
}
const vdes=bl>0.5?((f.walker||Vstep<0.25)?Vloc+f.vpref:Vloc):f.vpref;
const ufd=clamp(need*KSTEP-1.15*(vdes-Vloc),-VSTEPM,VSTEPM); // chase capture; lag on purpose to walk
f.ufd=ufd;
const af=clamp((Vloc+ufd-f.v_f)/TF,-AGRIP,AGRIP); // the only way the ground talks to the feet
f.v_f+=af*dt;f.s_f+=f.v_f*dt;
const sway=0.20*Math.sin(T*(0.8+frac(f.seed)*1.2)+f.seed)
+0.13*Math.sin(T*(1.9+frac(f.seed*3.7))+f.seed*2.3);
const actrl=clamp((vdes-f.v_c)*KCTRL,-ALEG,ALEG);
f.v_c+=(OM2*d+actrl+sway)*dt;
f.s_c=clamp(f.s_c+f.v_c*dt,WALL_L+0.05,WALL_R+0.35);
f.ph+=(Math.abs(ufd)+0.3*Math.abs(w))*dt*6;
if(f.s_c>FHI-0.7){f.fade-=dt*2.2;
if(f.fade<=0){f.gone=true;spawnQ.push(T+0.8+Math.random()*0.8);}}
else f.fade=Math.min(1,f.fade+dt*2);
}
/* ---- contacts between figures ---- */
function interactions(dt){
for(let i=0;i<people.length;i++){const a=people[i];if(a.kind!==1)continue;
for(let j=i+1;j<people.length;j++){const c=people[j];if(c.kind!==1)continue;
const ds=a.s_c-c.s_c,ad=Math.abs(ds);
if(ad<0.55&&ad>1e-4){const p=(0.55-ad)*4*dt;a.v_c-=sgn(ds)*p;c.v_c+=sgn(ds)*p;}
}}
for(const f of people){if(f.kind!==1)continue;
for(const b of people){if(b.kind!==0)continue;
if(Math.abs(b.s-f.s_c)<0.62&&T-b.ramT>0.3){
const rel=b.v_s-f.v_c;
if(Math.abs(rel)>0.75){ // a sliding body slams into the legs
f.v_c+=clamp(rel*0.8,-2.6,2.6);
b.v_s-=clamp(rel*0.45,-1.5,1.5);
b.ramT=T;puff(b.s,0.3,2);
}
}
if(Math.abs(b.s-f.s_f)<0.5&&(b.h+THK)>0.13){
const moving=Math.abs(f.w)>0.35||Math.abs(f.ufd)>0.6;
if(moving){ // the foot swings into a lying body: trip
const dir=f.w!==0?sgn(f.w):sgn(f.ufd||1);
b.v_s+=0.3*dir;
toDown(f,dir,true,b.h+THK+0.02);
break;
}
}
}}
}
/* ---- downed bodies ---- */
function computeSupports(){
const idx=[];for(let i=0;i<people.length;i++)if(people[i].kind===0)idx.push(i);
idx.sort((a,b)=>people[a].h-people[b].h);
for(let n=0;n<idx.length;n++){const b=people[idx[n]];b.sup=0;b.supBody=null;
for(let m=0;m<n;m++){const o=people[idx[m]];
if(o.h+THK<=b.h+0.03&&Math.abs(o.s-b.s)<SUPOV&&o.h+THK>b.sup){
b.sup=o.h+THK;b.supBody=o;}}}
}
const wrapPi=r=>{r=(r%TAU+TAU)%TAU;return r>Math.PI?r-TAU:r;};
function stepDown(b,dt){
const bl=blend(b.s),Vloc=Vstep*bl,th_l=TH*bl;
if(b.h>b.sup+0.02){ // airborne
b.v_h-=G*Math.cos(th_l)*dt;
b.rollV*=Math.max(0,1-0.8*dt);
}else{
if(b.h<b.sup){if(b.v_h<-0.9)puff(b.s,b.sup+0.2,3);
b.h=b.sup;b.v_h=b.v_h<-0.9?-b.v_h*0.12:0;}
const rel=Vloc-b.v_s;
if(b.sup<=0.03){
if(bl>0.02){ // lying on the steps
if(Math.abs(rel)<0.05)b.v_s=Vloc; // carried
else b.v_s+=clamp(rel*7,-b.mu*G*COS,b.mu*G*COS)*dt-G*SIN*dt;
}else{ // lying on a floor
const r2=-b.v_s;
if(Math.abs(r2)<0.04)b.v_s=0;
else b.v_s+=clamp(r2*7,-0.62*G,0.62*G)*dt;
}
}else if(b.supBody){ // lying on somebody
b.v_s+=clamp((b.supBody.v_s-b.v_s)*6,-b.mu*G*COS,b.mu*G*COS)*dt;
b.v_s+=-G*Math.sin(th_l)*dt;
const e=b.supBody.s,dU=(e+0.55)-b.s,dD=b.s-(e-0.55);
let ed=dU<dD?1:-1;if(Math.abs(dU-dD)<0.35)ed=frac(b.seed*3.3)<0.5?1:-1;
if(Math.abs(b.v_s-b.supBody.v_s)<0.5)b.v_s+=ed*0.7*dt; // heaps shed themselves
}
if(b.downT>b.daze)b.v_s+=0.9*Math.sin(T*11+b.seed)*dt*(b.stuck?1:0.3);
b.rollV+=(rel*1.1-b.rollV)*Math.min(1,dt*6);
b.roll+=b.rollV*dt;
if(Math.abs(b.v_s-Vloc)<0.4){b.roll=wrapPi(b.roll);b.roll*=Math.max(0,1-5*dt);}
}
b.s+=b.v_s*dt;b.h+=b.v_h*dt;
if(b.s<WALL_L){b.s=WALL_L;if(b.v_s<0)b.v_s=0;}
if(b.s>WALL_R){b.s=WALL_R;if(b.v_s>0)b.v_s=0;}
b.downT+=dt;
}
function collideBodies(){
const OVL=2*HL-0.14;
for(let it=0;it<2;it++){
for(let i=0;i<people.length;i++){const a=people[i];if(a.kind!==0)continue;
for(let j=i+1;j<people.length;j++){const c=people[j];if(c.kind!==0)continue;
const ds=a.s-c.s,ad=Math.abs(ds);
if(ad>=OVL||Math.abs(a.h-c.h)>=THK*0.85)continue;
const rel=a.v_s-c.v_s;
if(ds*rel<0){
const behind=ds>0?a:c,front=ds>0?c:a;
if(Math.abs(rel)>0.95&&behind.h<0.75&&front.h<=behind.h+0.06&&behind.h<=behind.sup+0.08){
const rb=behind.v_s-front.v_s; // energetic rear-ender rides up over the front one
behind.h=front.h+THK+0.02;behind.v_h=0.35+0.28*Math.abs(rel);
behind.v_s=front.v_s+rb*0.7;front.v_s+=rb*0.2;
puff(behind.s,behind.h+0.2,3);
}else{
const jm=-1.06*rel/2;a.v_s+=jm;c.v_s-=jm;
}
}
const push=(OVL-ad)/2*0.12;
if(ds>0){a.s+=push;c.s-=push;}else{a.s-=push;c.s+=push;}
}}}
}
function risePass(){
for(const b of people){if(b.kind!==0)continue;
if(b.downT<b.daze){b.stuck=false;continue;}
let ok=b.h<=0.05&&b.sup<=0.04&&Math.abs(surfV(b.s))<=1.55;
if(ok)for(const o of people){if(o===b)continue;
if(o.kind===0){if(Math.abs(o.s-b.s)<0.95&&o.h>b.h-0.1){ok=false;break;}}
else if(Math.abs(o.s_c-b.s)<0.8){ok=false;break;}}
if(ok){
b.kind=1;b.s_c=b.s;b.v_c=0;
b.s_f=b.s-NH*slopeT(b.s);b.v_f=surfV(b.s);
b.riseT=0;b.ph=Math.random()*TAU;b.need=0;b.d=0;b.w=0;b.ufd=0;b.fade=1;b.stuck=false;
}else b.stuck=true;
}
}
function findSpot(){
for(let s=WALL_L+0.4;s<0;s+=0.25){
let ok=true;
for(const o of people){const p=o.kind===0?o.s:o.s_c;
if(Math.abs(p-s)<(o.kind===0?0.95:0.8)){ok=false;break;}}
if(ok)return s;
}
return null;
}
function maintain(){
for(let i=people.length-1;i>=0;i--)if(people[i].gone)people.splice(i,1);
for(let i=spawnQ.length-1;i>=0;i--)if(T>=spawnQ[i]){
const s=findSpot();
if(s!==null){spawnQ.splice(i,1);spawnUp(s,0.5);}
else spawnQ[i]+=0.5;
}
}
function step(dt){
Vstep+=(Vtarget-Vstep)*Math.min(1,dt/TV);
sigAcc+=Vstep*dt;railAcc=(railAcc+Vstep*dt)%6.4;
interactions(dt);
for(const p of people)if(p.kind===1)stepUp(p,dt);
computeSupports();
for(const p of people)if(p.kind===0)stepDown(p,dt);
collideBodies();
risePass();
maintain();
for(let i=dustP.length-1;i>=0;i--){const d=dustP[i];d.t+=dt;
if(d.t>d.life){dustP.splice(i,1);continue;}
d.vy-=3.5*dt;d.x+=d.vx*dt;d.y+=d.vy*dt;}
}
/* ---- rendering ---- */
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
const XMIN=-0.35,XMAX=17.05,YMIN=-0.75,YMAX=7.3;
let SC=1,OX=0,OY=0,DPR=1,CW=0,CH=0;
function resize(){
DPR=window.devicePixelRatio||1;
const r=cv.getBoundingClientRect();
CW=Math.max(1,Math.round(r.width*DPR));CH=Math.max(1,Math.round(r.height*DPR));
if(cv.width!==CW||cv.height!==CH){cv.width=CW;cv.height=CH;}
SC=Math.min(CW/(XMAX-XMIN),CH/(YMAX-YMIN));
OX=(CW-(XMAX-XMIN)*SC)/2;OY=(CH-(YMAX-YMIN)*SC)/2;
}
const sx=x=>OX+(x-XMIN)*SC,sy=y=>OY+(YMAX-y)*SC;
function lineW(x1,y1,x2,y2){ctx.beginPath();ctx.moveTo(sx(x1),sy(y1));ctx.lineTo(sx(x2),sy(y2));ctx.stroke();}
function lineW2p(a,b){lineW(a.x,a.y,b.x,b.y);}
function circF(x,y,r){ctx.beginPath();ctx.arc(sx(x),sy(y),r*SC,0,TAU);ctx.fill();}
function ell(x,y,rx,ry){ctx.beginPath();ctx.ellipse(sx(x),sy(y),rx*SC,ry*SC,0,0,TAU);ctx.fill();}
function quadF(a,b,c,d,st){ctx.fillStyle=st;ctx.beginPath();
ctx.moveTo(sx(a.x),sy(a.y));ctx.lineTo(sx(b.x),sy(b.y));ctx.lineTo(sx(c.x),sy(c.y));ctx.lineTo(sx(d.x),sy(d.y));
ctx.closePath();ctx.fill();}
function combPlate(x,y){
ctx.fillStyle='rgba(38,34,24,0.12)';ctx.fillRect(sx(x),sy(y+0.055),0.46*SC,0.055*SC);
ctx.strokeStyle='rgba(38,34,24,0.5)';ctx.lineWidth=1;ctx.beginPath();
for(let hx=x+0.03;hx<x+0.44;hx+=0.055){ctx.moveTo(sx(hx),sy(y));ctx.lineTo(sx(hx+0.04),sy(y+0.055));}
ctx.stroke();
ctx.strokeStyle=INK;ctx.lineWidth=1.5;ctx.strokeRect(sx(x),sy(y+0.055),0.46*SC,0.055*SC);
}
function drawSign(){
const px=X0-0.78,up=Vtarget>0,col=up?INK:ACC;
ctx.strokeStyle=INK;ctx.lineWidth=2;lineW(px,0.02,px,1.12);
ctx.fillStyle='#f7f3ea';ctx.fillRect(sx(px-0.37),sy(1.56),0.74*SC,0.44*SC);
ctx.strokeStyle=INK;ctx.lineWidth=1.5;ctx.strokeRect(sx(px-0.37),sy(1.56),0.74*SC,0.44*SC);
ctx.strokeStyle=col;ctx.lineWidth=2.5;
const ay=1.41,x1=px+(up?-0.22:0.22),x2=px+(up?0.22:-0.22);
lineW(x1,ay,x2,ay);
const hx=up?x2:x1,d=up?1:-1;
lineW(hx,ay,hx-0.09*d,ay+0.07);lineW(hx,ay,hx-0.09*d,ay-0.07);
ctx.fillStyle=col;
ctx.font=`700 ${Math.max(8*DPR,0.115*SC)}px ui-monospace,Menlo,Consolas,monospace`;
ctx.textAlign='center';ctx.fillText(up?'UP':'DOWN',sx(px),sy(1.20));
}
function drawMotor(){
const m=pPt(0.5,-0.64);
ctx.save();ctx.translate(sx(m.x),sy(m.y));ctx.rotate(-TH);
ctx.fillStyle='rgba(38,34,24,0.85)';ctx.fillRect(-0.55*SC,-0.26*SC,1.1*SC,0.52*SC);
ctx.strokeStyle='rgba(242,237,226,0.45)';ctx.lineWidth=1;ctx.beginPath();
for(let i=-1;i<=1;i++){ctx.moveTo(-0.34*SC,i*0.12*SC);ctx.lineTo(0.12*SC,i*0.12*SC);}
ctx.stroke();
const trans=Math.abs(Vstep-Vtarget)>0.06;
ctx.globalAlpha=trans?0.45+0.55*Math.abs(Math.sin(T*18)):1;
ctx.fillStyle=reversed?ACC:'#5a5648';
ctx.beginPath();ctx.arc(0.38*SC,0,0.055*SC,0,TAU);ctx.fill();
ctx.globalAlpha=1;ctx.restore();
}
function drawArchitecture(){
const Ht=L*SIN,Xtop=X0+L*COS;
ctx.lineCap='round';ctx.lineJoin='round';
ctx.fillStyle='rgba(38,34,24,0.13)';ctx.fillRect(sx(0.30),sy(0.95),0.15*SC,0.95*SC);
ctx.strokeStyle=INK;ctx.lineWidth=1.5;ctx.strokeRect(sx(0.30),sy(0.95),0.15*SC,0.95*SC);
ctx.strokeStyle=INK;ctx.lineWidth=Math.max(2,0.05*SC);
lineW(-0.2,0,X0,0);lineW(Xtop,Ht,XMAX-0.05,Ht);
ctx.strokeStyle='rgba(38,34,24,0.3)';ctx.lineWidth=1;ctx.beginPath();
for(let x=-0.1;x<X0-0.05;x+=0.33){ctx.moveTo(sx(x),sy(-0.03));ctx.lineTo(sx(x-0.13),sy(-0.16));}
for(let x=Xtop+0.12;x<XMAX-0.2;x+=0.33){ctx.moveTo(sx(x),sy(Ht-0.03));ctx.lineTo(sx(x-0.13),sy(Ht-0.16));}
ctx.stroke();
quadF(pPt(0,-0.16),pPt(L,-0.16),pPt(L,-0.64),pPt(0,-0.64),'rgba(38,34,24,0.05)');
ctx.strokeStyle='rgba(38,34,24,0.6)';ctx.lineWidth=Math.max(1.5,0.035*SC);
lineW2p(pPt(0,-0.16),pPt(L,-0.16));lineW2p(pPt(0,-0.64),pPt(L,-0.64));
ctx.strokeStyle='rgba(38,34,24,0.2)';ctx.lineWidth=1;ctx.beginPath();
for(let k=0;k<10;k++){if(0.9+k*1.1>L)break;
const p1=pPt(0.35+k*1.1,-0.16),p2=pPt(0.9+k*1.1,-0.64);
ctx.moveTo(sx(p1.x),sy(p1.y));ctx.lineTo(sx(p2.x),sy(p2.y));}
ctx.stroke();
quadF(pPt(0,1.0),pPt(L,1.0),pPt(L,1.17),pPt(0,1.17),'rgba(38,34,24,0.07)');
ctx.strokeStyle='rgba(38,34,24,0.5)';ctx.lineWidth=Math.max(1.2,0.028*SC);
lineW2p(pPt(0,1.0),pPt(L,1.0));lineW2p(pPt(0,1.17),pPt(L,1.17));
ctx.strokeStyle='rgba(38,34,24,0.2)';ctx.lineWidth=1;ctx.beginPath();
for(let k=0.6;k<L;k+=1.13){const p1=pPt(k,0.04),p2=pPt(k,1.0);
ctx.moveTo(sx(p1.x),sy(p1.y));ctx.lineTo(sx(p2.x),sy(p2.y));}
ctx.stroke();
ctx.strokeStyle=ACC;ctx.lineWidth=Math.max(3,0.07*SC);
ctx.setLineDash([0.3*SC,0.34*SC]);ctx.lineDashOffset=-railAcc*SC;
lineW2p(pPt(-0.15,1.42),pPt(L+0.15,1.42));
ctx.setLineDash([]);
ctx.fillStyle='rgba(38,34,24,0.4)';
ctx.font=`700 ${Math.max(9*DPR,0.13*SC)}px ui-monospace,Menlo,Consolas,monospace`;
ctx.textAlign='center';
ctx.fillText('EXIT',sx(Xtop+2.7),sy(Ht-0.14));
ctx.fillText('ENTRANCE',sx(1.55),sy(0.16));
drawSign();drawMotor();
}
function drawSteps(){
const Ht=L*SIN,Xtop=X0+L*COS;
ctx.save();ctx.beginPath();
ctx.rect(sx(X0-0.55),sy(Ht),(L*COS+1.1)*SC,Ht*SC);ctx.clip();
const phi=((sigAcc%PITCH)+PITCH)%PITCH,n=Math.ceil(L/PITCH)+1;
ctx.strokeStyle=INK;ctx.lineWidth=Math.max(1.2,0.03*SC);ctx.lineCap='butt';
ctx.beginPath();
for(let k=-2;k<=n;k++){
const xi=k*PITCH+phi,A=pPt(xi,0);
ctx.moveTo(sx(A.x),sy(A.y));
ctx.lineTo(sx(A.x+0.44),sy(A.y));
ctx.lineTo(sx(A.x+0.44),sy(A.y+0.22));
}
ctx.stroke();
ctx.strokeStyle='rgba(38,34,24,0.25)';ctx.lineWidth=Math.max(2.5,0.06*SC);ctx.beginPath();
for(let k=-2;k<=n;k++){
const xi=k*PITCH+phi,A=pPt(xi,0);
ctx.moveTo(sx(A.x+0.44),sy(A.y));ctx.lineTo(sx(A.x+0.44),sy(A.y+0.22));
}
ctx.stroke();
ctx.restore();
combPlate(X0-0.42,0);combPlate(Xtop-0.04,Ht);
}
function drawPeople(){
ctx.fillStyle='rgba(38,34,24,0.10)';
for(const p of people){
if(p.kind===1){const q=pPt(p.s_f,0);ell(q.x,treadY(p.s_f)+0.012,0.28,0.045);}
else if(p.h<0.06){const q=pPt(p.s,0);ell(q.x,treadY(p.s)+0.008,0.62,0.055);}
}
const downs=people.filter(p=>p.kind===0).sort((a,b)=>a.h-b.h);
for(const b of downs)drawDown(b);
for(const f of people)if(f.kind===1)drawUp(f);
}
function drawUp(f){
const rise=f.riseT<0.5?1-Math.pow(1-f.riseT/0.5,2):1;
const nE=NH*(0.30+0.70*rise);
const lean=clamp(f.d*0.8+f.w*0.12,-0.42,0.5);
const al=f.fade;
ctx.strokeStyle=`rgba(38,34,24,${(0.95*al).toFixed(3)})`;
ctx.fillStyle=ctx.strokeStyle;
ctx.lineWidth=Math.max(2,0.045*SC);ctx.lineCap='round';ctx.lineJoin='round';
const moving=Math.abs(f.w)>0.18||Math.abs(f.ufd)>0.25;
const stride=moving?clamp(0.10+Math.abs(f.w)*0.13,0.10,0.30):0.13;
const s1=f.s_f+stride*Math.sin(f.ph),s2=f.s_f-stride*Math.sin(f.ph);
const l1=moving?0.06*Math.max(0,Math.sin(f.ph+0.9)):0;
const l2=moving?0.06*Math.max(0,Math.sin(f.ph+0.9+Math.PI)):0;
const q1=pPt(s1,0),q2=pPt(s2,0);
const y1=treadY(s1)+l1,y2=treadY(s2)+l2;
const mx=(q1.x+q2.x)/2+COS*lean*0.35,my=(y1+y2)/2+nE*rise;
const nx=mx+COS*lean*0.75,ny=my+0.50;
const hx=nx+COS*lean*1.0,hy=ny+0.17;
leg(mx,my,q1.x,y1,COS);leg(mx,my,q2.x,y2,COS);
lineW(mx,my,nx,ny);
const flail=clamp(Math.abs(f.need)/REACH,0,1.25);
const sw=Math.sin(f.ph+Math.PI)*(moving?0.55:0.08);
const jg=flail>0.05?Math.sin(T*17+f.seed)*0.25*flail:0;
armV(nx,ny,-0.55+sw+flail*2.5+jg,COS);
armV(nx,ny,-0.55-sw+flail*2.5-jg,COS);
circF(hx,hy,0.105);
}
function leg(ax,ay,bx,by,Tx){
const kx=(ax+bx)/2+Tx*0.05,ky=(ay+by)/2;
ctx.beginPath();ctx.moveTo(sx(ax),sy(ay));ctx.lineTo(sx(kx),sy(ky));ctx.lineTo(sx(bx),sy(by));ctx.stroke();
}
function armV(x,y,a,Tx){
const d1x=Tx*Math.sin(a),d1y=-Math.cos(a);
const ex=x+d1x*0.20,ey=y+d1y*0.20;
const a2=a-0.35,d2x=Tx*Math.sin(a2),d2y=-Math.cos(a2);
ctx.beginPath();ctx.moveTo(sx(x),sy(y));ctx.lineTo(sx(ex),sy(ey));
ctx.lineTo(sx(ex+d2x*0.18),sy(ey+d2y*0.18));ctx.stroke();
}
function drawDown(b){
const bl=blend(b.s),hh=b.h+THK*0.5,base=pPt(b.s,0);
const cx=base.x-SIN*hh*bl,cy=treadY(b.s)+(COS*hh*bl+hh*(1-bl));
ctx.save();ctx.translate(sx(cx),sy(cy));ctx.rotate(-(pAng(b.s)+b.roll));
ctx.strokeStyle='rgba(38,34,24,0.92)';ctx.fillStyle=ctx.strokeStyle;
ctx.lineWidth=Math.max(2,0.045*SC);ctx.lineCap='round';ctx.lineJoin='round';
const D=b.dir,wig=b.stuck?1:0.35;
const w1=Math.sin(T*12+b.seed)*0.05*wig,w2=Math.sin(T*12+b.seed+2.1)*0.05*wig,
w3=Math.sin(T*10+b.seed+4.2)*0.06*wig;
L2(-0.42*D,0.02,0.34*D,0.03);
L2(-0.40*D,0.02,-0.62*D,0.09+w1);L2(-0.62*D,0.09+w1,-0.86*D,0.12+w2);
L2(-0.40*D,0.00,-0.60*D,-0.07+w2);L2(-0.60*D,-0.07+w2,-0.82*D,-0.11+w1);
L2(0.30*D,0.03,0.50*D,0.15+w3);L2(0.50*D,0.15+w3,0.64*D,0.06+w1);
L2(0.30*D,0.01,0.44*D,-0.14-w2);L2(0.44*D,-0.14-w2,0.58*D,-0.06-w3);
ctx.beginPath();ctx.arc(0.55*D*SC,-0.06*SC,0.105*SC,0,TAU);ctx.fill();
ctx.restore();
}
function L2(u1,v1,u2,v2){ctx.beginPath();ctx.moveTo(u1*SC,-v1*SC);ctx.lineTo(u2*SC,-v2*SC);ctx.stroke();}
function drawDust(){
for(const d of dustP){const a=(1-d.t/d.life)*0.3;
ctx.fillStyle=`rgba(38,34,24,${a.toFixed(3)})`;
ctx.beginPath();ctx.arc(sx(d.x),sy(d.y),(0.02+0.03*d.t/d.life)*SC,0,TAU);ctx.fill();}
}
function drawFrameText(){
ctx.strokeStyle='rgba(38,34,24,0.25)';ctx.lineWidth=1;
ctx.strokeRect(6*DPR,6*DPR,CW-12*DPR,CH-12*DPR);
ctx.fillStyle='rgba(38,34,24,0.55)';
ctx.font=`${Math.max(9*DPR,0.12*SC)}px ui-monospace,Menlo,Consolas,monospace`;
ctx.textAlign='right';
ctx.fillText('FIG. 1 — ESCALATOR WITH PASSENGERS, SIDE ELEVATION',CW-14*DPR,CH-26*DPR);
ctx.fillText('ALL MOTION PRODUCED BY THE MODEL — NO KEYFRAMES, NO TIMELINE',CW-14*DPR,CH-11*DPR);
}
function render(){
resize();
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#f2ede2';ctx.fillRect(0,0,CW,CH);
shake*=0.9;
ctx.translate((Math.random()*2-1)*shake*DPR,(Math.random()*2-1)*shake*DPR);
ctx.fillStyle='rgba(38,34,24,0.07)';
for(let gx=0;gx<=17;gx+=0.5)for(let gy=0;gy<=7;gy+=0.5)ctx.fillRect(sx(gx)-0.6,sy(gy)-0.6,1.2,1.2);
drawArchitecture();
drawSteps();
drawPeople();
drawDust();
drawFrameText();
}
/* ---- HUD, control, sound ---- */
const $=id=>document.getElementById(id);
const roV=$('roV'),roS=$('roS'),roU=$('roU'),roD=$('roD'),roT=$('roT'),btn=$('btn'),snd=$('snd');
let AC=null,humO=null,humG=null,muted=false;
function hud(){
let up=0,dn=0;for(const p of people)p.kind===1?up++:dn++;
roV.textContent=(Vstep<0?'-':'+')+Math.abs(Vstep).toFixed(2)+' m/s';
roU.textContent=up;roD.textContent=dn;
roT.textContent=Math.max(0,T-tToggle).toFixed(1)+' s';
}
function toggle(){
reversed=!reversed;
Vtarget=reversed?V_REV:V_UP;
TV=reversed?0.085:0.16;
timeScale=0.22;shake=7;tToggle=T;
btn.textContent=reversed?'RESTORE UPWARD':'THROW IT INTO REVERSE';
btn.classList.toggle('hot',reversed);
roS.textContent=reversed?'REVERSED':'UPWARD';
roS.classList.toggle('hot',reversed);
audioToggle();
}
btn.addEventListener('click',toggle);
snd.addEventListener('click',()=>{muted=!muted;snd.textContent=muted?'SOUND OFF':'SOUND ON';});
addEventListener('keydown',e=>{
if(e.repeat)return;
if(e.code==='Space'||e.code==='KeyR'){e.preventDefault();toggle();}
});
function audioInit(){
if(AC)return;
try{AC=new(window.AudioContext||window.webkitAudioContext)();}catch(e){return;}
humO=AC.createOscillator();humO.type='sawtooth';
const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.value=240;f.Q.value=2;
humG=AC.createGain();humG.gain.value=0;
humO.connect(f);f.connect(humG);humG.connect(AC.destination);humO.start();
}
function audioToggle(){audioInit();if(AC&&AC.state==='suspended')AC.resume();clunk();}
function clunk(){
if(!AC||muted)return;const t=AC.currentTime;
const o=AC.createOscillator();o.type='sine';
o.frequency.setValueAtTime(72,t);o.frequency.exponentialRampToValueAtTime(36,t+0.18);
const g=AC.createGain();g.gain.setValueAtTime(0.5,t);
g.gain.exponentialRampToValueAtTime(0.001,t+0.26);
o.connect(g);g.connect(AC.destination);o.start(t);o.stop(t+0.3);
const len=(AC.sampleRate*0.12)|0,buf=AC.createBuffer(1,len,AC.sampleRate),ch=buf.getChannelData(0);
for(let i=0;i<len;i++)ch[i]=(Math.random()*2-1)*(1-i/len);
const src=AC.createBufferSource();src.buffer=buf;
const bf=AC.createBiquadFilter();bf.type='bandpass';bf.frequency.value=190;bf.Q.value=1.2;
const g2=AC.createGain();g2.gain.value=0.5;
src.connect(bf);bf.connect(g2);g2.connect(AC.destination);src.start(t);
}
function audioFrame(){
if(!AC||!humG)return;
humG.gain.setTargetAtTime(muted?0:0.018+0.05*Math.min(1,Math.abs(Vstep)/3.2),AC.currentTime,0.08);
humO.frequency.setTargetAtTime(44+Math.abs(Vstep)*26,AC.currentTime,0.06);
}
init();
let last=performance.now();
function frame(now){
const dtR=Math.min(0.05,Math.max(0.001,(now-last)/1000));last=now;
timeScale+=(1-timeScale)*Math.min(1,dtR*2.2);
const dt=dtR*timeScale;
for(let i=0;i<3;i++)step(dt/3);
T+=dt;
render();hud();audioFrame();
requestAnimationFrame(frame);
}
requestAnimationFrame(t=>{last=t;requestAnimationFrame(frame);});
</script>
</body>
</html>
Press the button and the steps slam from +0.90 to −2.80 m/s in about a third of a second (there's a brief slow-motion beat, a camera shake and a synthesized clunk-and-hum — pure presentation, the clock scaling changes no outcome). Within roughly 0.2 s every figure's capture point lands farther uphill than a leg can reach, and the crowd topples backward, up-slope, in a staggered wave — the staggering comes from each figure's sway phase and whether it was walking, not from a schedule. The bodies skid up the rushing steps, lose the race against friction, and get carried down. The bottom landing receives them as a train: they slam into each other and the pit wall, the energetic rear-enders vault on top, and a 2–3 layer heap forms against the wall within ~6 s. Anyone standing on the landing gets bowled into it by arrivals. Figures thrown clear get up on the floors and immediately walk back and try to board — the steps yank their feet out from under them and throw them back down — so the entrance churns for as long as it stays reversed, because their only intent is "go up". Press restore: the steps ramp back to +0.9, footing checks start passing, the heap drains from the top down, bodies still on the steps either stand up mid-ride or get delivered over the top comb like luggage, and within ~20 s everyone is riding again. Rarely, a figure within a step of the top comb when it flips gets launched clear onto the upper landing.
Special-cased or nudged (all shared rules, applied to everyone): a body hitting another fast enough vaults over it — my bodies are 1-D-along-the-slope plus height, and this stands in for out-of-plane tumbling; a body supported by other bodies slowly slides toward the nearest edge of the one under it (a stand-in for 3-D side-collapse — without it flat stacks can deadlock); downed figures that want up but are blocked squirm sinusoidally, which untangles piles; a personal 0.6–1.3 s daze before rise attempts; a trip rule — a moving foot whose landing zone is occupied by a lying body goes down directly, because foot obstruction isn't representable in the pendulum state, so it's an explicit rule rather than an emergent verdict; feet slide continuously with a cosmetic gait drawn on top; surface drag at the combs is sampled at a body's centre point; intent is one line for everybody — walk/ride uphill, exit at the top. No fear, no helping, no handrails to grab.
What it does badly: everyone falls, every time — that's tuning, not law (the flip is ~3.7 m/s in ~0.1 s; recovery caps near 2 m/s). If you want survivors, raise TV in the toggle handler and the same rules will spare some. Piles are loose: bodies interpenetrate slightly and stack in neat layers rather than a tangle, and a body visibly drops when its support stands up. The figures are idiots — they re-board a visibly runaway machine forever, at full enthusiasm. Walkers tailgate standers, because a side view has no overtaking. A lying body can rest briefly against a standing figure's legs without consequence until something moves. Tumbling is a cosmetic spin, not rigid-body rotation. A badly settled heap can drain slowly after restore. And the motor hum is a crude synth.