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 2-D ragdoll: 11 point masses (head, chest, hip, two knees, two feet, two elbows, two hands) totalling about 70 kg. They are joined by rigid distance constraints plus a few range limits (spine stiffness, knee fold, arm fold), integrated with position-based Verlet at 240 Hz. A person has one of three states. Standing runs a balance controller. That controller applies clamped PD accelerations that hold the hip over the average position of whichever feet are touching a surface. The hip tracks the velocity of the surface under those feet. Chest, head, knees and arms hold a posture relative to the hip. The controller also takes recovery steps toward a capture point, hip + relative velocity × 0.32 s. Down means limp. Rising is the same controller, fading in over 1.4 s. The key limit is that the hip controller can only push horizontally at 4.5 m/s².
A person loses balance by measurement, never by timer. They fall when any of these holds:
The steps are an exact moving sawtooth. Treads and risers carry the belt velocity, and friction is Coulomb, based on penetration depth. So when the belt reverses at 16 m/s², the feet are dragged and kicked by risers faster than the hip can follow, and the person topples. When two people occupy the same place, every particle of one is pushed out of every bone (capsule) of the other, split by inverse mass, with tangential friction. A body on the steps is therefore solid ground for whoever slides into it. A person gets up only when all of these hold:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Escalator reversal</title>
<style>
html,body{margin:0;height:100%;background:#12151b;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#dde}
canvas{display:block;width:100%;height:100%}
#ui{position:fixed;left:12px;top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
button{font:600 14px system-ui,sans-serif;padding:9px 14px;border-radius:8px;border:1px solid #445;background:#262b33;color:#e8eaf0;cursor:pointer}
button:hover{filter:brightness(1.15)}
#rev{background:#8a1f1f;border-color:#d44;min-width:190px}
#rev.on{background:#1f6a36;border-color:#4c4}
#hud{position:fixed;left:12px;top:60px;font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;color:#9aa3b2;background:rgba(10,12,16,.6);padding:6px 10px;border-radius:6px;white-space:pre}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<button id="rev">REVERSE (space)</button>
<button id="slow">Slow-mo: off (S)</button>
<button id="dbg">State rings: off (D)</button>
<button id="reset">Reset (R)</button>
</div>
<div id="hud"></div>
<script>
'use strict';
// ---------------- world constants (metres, seconds, y up) ----------------
const G=9.81, DT=1/240;
const TH=30*Math.PI/180, COS=Math.cos(TH), SIN=Math.sin(TH), TAN=Math.tan(TH);
const D=0.40, HS=D*TAN, NST=32, X0=0, X1=X0+NST*D, H=(X1-X0)*TAN;
const FLAT=0.8, XA=X0-FLAT, XB=X1+FLAT, WL=XA-4.6, WR=XB+4.6;
const V_FWD=0.6, V_REV=-2.8, ACC_REV=16, ACC_FWD=1.5;
const AMAX=4.5, LOAD=G*DT*DT*6, MUB=0.35;
const NPER=18, MAXP=20;
let phase=0, beltV=V_FWD, beltTarget=V_FWD, reversed=false, simT=0, railOff=0, totalFalls=0;
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
// Terrain query: which surface segment is under x, its height, extent and velocity.
function seg(x,o){
if(x<XA){o.y=0;o.xl=-1e9;o.xr=XA;o.vx=0;o.vy=0;o.m=0;return o;}
if(x>=XB){o.y=H;o.xl=XB;o.xr=1e9;o.vx=0;o.vy=0;o.m=0;return o;}
const k=Math.floor((x-X0-phase)/D), xn=X0+phase+k*D, raw=(xn-X0)*TAN;
if(raw<=0){o.y=0;o.vx=beltV*COS;o.vy=0;}
else if(raw>=H){o.y=H;o.vx=beltV*COS;o.vy=0;}
else{o.y=raw;o.vx=beltV*COS;o.vy=beltV*SIN;}
o.xl=xn<XA?XA:xn; o.xr=xn+D>XB?XB:xn+D; o.m=1; return o;
}
const Sg={},Ng={},Sa={},Tb={};
// ---------------- bodies ----------------
// 0 head,1 chest,2 hip,3 kneeL,4 footL,5 kneeR,6 footR,7 elbowL,8 handL,9 elbowR,10 handR
const PR=[.11,.10,.10,.06,.05,.06,.05,.05,.045,.05,.045];
const PMASS=[5,20,20,5,2.5,5,2.5,2,1.5,2,1.5];
const PMU=[.6,.55,.55,.55,1.1,.55,1.1,.55,.8,.55,.8];
const CONS=[[0,1,.25],[1,2,.5],[2,3,.45],[3,4,.45],[2,5,.45],[5,6,.45],[1,7,.28],[7,8,.27],[1,9,.28],[9,10,.27]];
const RANGE=[[0,2,.66,.76],[2,4,.26,.91],[2,6,.26,.91],[1,8,.14,1],[1,10,.14,1],[4,6,.08,2]];
const NPART=MAXP*11;
const X=new Float64Array(NPART),Y=new Float64Array(NPART),PX=new Float64Array(NPART),PY=new Float64Array(NPART);
const AX=new Float64Array(NPART),AY=new Float64Array(NPART),IM=new Float64Array(NPART),RAD=new Float64Array(NPART);
const SVX=new Float64Array(NPART),SVY=new Float64Array(NPART);
const GND=new Uint8Array(NPART), OWN=new Int16Array(NPART);
const P=[];
function makePerson(id,fx,fy,vx,vy){
const b=id*11, face=1, rf=PR[4];
const p={base:b,id,state:'stand',face,swing:-1,s:0,sx0:0,sy0:0,cool:0.3,air:0,downT:0,minDown:2,r:0,vd:0,
walk:0.9+Math.random()*0.35,hue:(id*137.508+15)%360,minx:0,maxx:0,miny:0,maxy:0};
const hipY=fy+rf+0.84;
const pos=[
[fx+0.05,hipY+0.75],[fx+0.03,hipY+0.5],[fx,hipY],
[fx-0.09+0.16,fy+rf+0.42],[fx-0.09,fy+rf],[fx+0.09+0.16,fy+rf+0.42],[fx+0.09,fy+rf],
[fx+0.02,hipY+0.22],[fx+0.07,hipY-0.03],[fx+0.04,hipY+0.22],[fx+0.09,hipY-0.03]];
for(let l=0;l<11;l++){
const i=b+l; X[i]=pos[l][0];Y[i]=pos[l][1];PX[i]=X[i]-vx*DT;PY[i]=Y[i]-vy*DT;
IM[i]=1/PMASS[l];RAD[i]=PR[l];OWN[i]=id;GND[i]=0;SVX[i]=0;SVY[i]=0;
}
P.push(p); return p;
}
function reset(){
P.length=0; phase=0; beltV=V_FWD; beltTarget=V_FWD; reversed=false; simT=0; totalFalls=0;
updateRevBtn();
const spots=[];
let k=1+Math.floor(Math.random()*2);
while(spots.length<13 && k<=30){ spots.push([X0+k*D+0.2,k*HS,true]); k+=2+(Math.random()<0.28?1:0); }
const bottom=[XA-0.9,XA-1.8,XA-2.7,XA-3.6];
let bi=0;
spots.push([XB+1.3,H,false],[XB+2.6,H,false]);
while(spots.length<NPER && bi<bottom.length) spots.push([bottom[bi++],0,false]);
for(let i=0;i<spots.length;i++){
const s=spots[i];
makePerson(i,s[0],s[1],s[2]?V_FWD*COS:0,s[2]?V_FWD*SIN:0);
}
}
// ---------------- helpers ----------------
function fall(p){p.state='down';p.downT=0;p.swing=-1;p.minDown=1.0+Math.random()*1.6;totalFalls++;}
let KTX=0,KTY=0;
function kneeTarget(hx,hy,fx,fy,face){
const mx=(hx+fx)*0.5,my=(hy+fy)*0.5,dx=hx-fx,dy=hy-fy,d=Math.hypot(dx,dy)||1e-6;
const half=d*0.5,bb=Math.sqrt(Math.max(0,0.2025-half*half));
let nx=-dy/d,ny=dx/d; if(nx*face<0){nx=-nx;ny=-ny;}
KTX=mx+nx*bb; KTY=my+ny*bb;
}
function pd(i,tx,ty,kp,kd,rvx,rvy,lim,gc){
const vx=(X[i]-PX[i])/DT, vy=(Y[i]-PY[i])/DT;
AX[i]+=clamp(kp*(tx-X[i])+kd*(rvx-vx),-lim,lim);
AY[i]+=clamp(kp*(ty-Y[i])+kd*(rvy-vy),-lim,lim)+gc;
}
// ---------------- controllers (identical for every figure) ----------------
function ctrlStand(p){
const b=p.base,hip=b+2,ch=b+1,face=p.face;
let n=0,sx=0,sy=0,svx=0,svy=0;
for(let f=0;f<2;f++){ if(p.swing===f)continue; const i=b+4+2*f;
if(GND[i]){n++;sx+=X[i];sy+=Y[i];svx+=SVX[i];svy+=SVY[i];} }
if(n){sx/=n;sy/=n;svx/=n;svy/=n;p.air=0;}
else{ p.air+=DT; if(p.air>0.3){fall(p);return;} sx=(X[b+4]+X[b+6])*.5; sy=Math.min(Y[b+4],Y[b+6]); }
const g=n?1:0;
const hvx=(X[hip]-PX[hip])/DT,hvy=(Y[hip]-PY[hip])/DT;
const off=X[hip]-sx, tilt=Math.abs(Math.atan2(X[ch]-X[hip],Y[ch]-Y[hip]));
if(Math.abs(off)>0.42||Y[hip]-sy<0.45||tilt>0.85){fall(p);return;}
// hip: over support, moving with support (+ walking velocity), limited authority
AX[hip]+=clamp(110*(sx-X[hip])+22*(svx+p.vd-hvx),-AMAX,AMAX)*g;
AY[hip]+=(clamp(110*(sy+0.84-Y[hip])+22*(svy-hvy),-12,12)+G)*g;
// posture relative to hip
pd(ch,X[hip]+face*0.03+p.vd*0.04,Y[hip]+0.5,220,26,hvx,hvy,30,G*g);
const cvx=(X[ch]-PX[ch])/DT,cvy=(Y[ch]-PY[ch])/DT;
pd(b,X[ch]+face*0.03,Y[ch]+0.25,200,24,cvx,cvy,30,G*g);
for(let f=0;f<2;f++){ const ft=b+4+2*f,kn=b+3+2*f;
kneeTarget(X[hip],Y[hip],X[ft],Y[ft],face);
const fvx=(X[ft]-PX[ft])/DT,fvy=(Y[ft]-PY[ft])/DT;
pd(kn,KTX,KTY,160,16,(hvx+fvx)*.5,(hvy+fvy)*.5,40,G*g); }
const reach=clamp(off/0.3,-1,1), ar=Math.abs(reach);
for(let s=0;s<2;s++){ const el=b+7+2*s,hd=b+8+2*s;
const ex=X[ch]+face*0.02+reach*0.14+(s?0.03:-0.03), ey=Y[ch]-0.27+ar*0.18;
pd(el,ex,ey,70,8,cvx,cvy,40,G*g);
pd(hd,ex+face*0.06+reach*0.2,ey-0.25+ar*0.25,70,8,cvx,cvy,40,G*g); }
// stepping toward the capture point (walking and recovery use the same rule)
const cap=X[hip]+(hvx-svx)*0.32+p.vd*0.08;
if(p.swing<0){
p.cool-=DT;
if(n&&p.cool<=0&&Math.abs(cap-sx)>0.1){
p.swing=Math.abs(X[b+4]-cap)>Math.abs(X[b+6]-cap)?0:1;
const f=b+4+2*p.swing; p.s=0; p.sx0=X[f]; p.sy0=Y[f];
}
}
if(p.swing>=0){
const f=b+4+2*p.swing,o=b+4+2*(1-p.swing);
p.s+=DT/0.28;
const tx=clamp(cap,X[o]-0.6,X[o]+0.6);
seg(tx,Tb);
const e=Math.min(1,p.s),sm=e*e*(3-2*e);
const dx=p.sx0+(tx-p.sx0)*sm, dy=p.sy0+(Tb.y+PR[4]-p.sy0)*sm+0.15*Math.sin(Math.PI*e);
pd(f,dx,dy,500,40,svx,svy,90,G);
if(p.s>=1&&(GND[f]||p.s>1.6)){p.swing=-1;p.cool=0.05;}
}
}
function ctrlRise(p){
const b=p.base,hip=b+2,ch=b+1,face=p.face;
p.r+=DT/1.4;
seg(X[hip],Sa);
const svx=Sa.vx,svy=Sa.vy,gy=Sa.y;
const gain=Math.min(1,p.r*3), e=clamp((p.r-0.3)/0.7,0,1), sm=e*e*(3-2*e);
const hvx=(X[hip]-PX[hip])/DT,hvy=(Y[hip]-PY[hip])/DT;
if((hvx-svx)*(hvx-svx)+(hvy-svy)*(hvy-svy)>2.6){fall(p);return;} // knocked while getting up
for(let f=0;f<2;f++){ const i=b+4+2*f, tx=X[hip]+(f?0.1:-0.1); seg(tx,Tb);
const ty=Tb.y+PR[4]+0.15*(1-sm);
pd(i,tx,ty,260*gain,28*gain,svx,svy,50,0); }
const fx=(X[b+4]+X[b+6])*.5;
const hy=gy+PR[4]+0.2+0.64*sm;
AX[hip]+=clamp(gain*(120*(fx-X[hip])+22*(svx-hvx)),-8,8);
AY[hip]+=clamp(gain*(120*(hy-Y[hip])+22*(svy-hvy)),-14,14)+G*gain;
pd(ch,X[hip]+face*0.2*(1-sm),Y[hip]+0.48,160*gain,20*gain,hvx,hvy,35,G*gain);
const cvx=(X[ch]-PX[ch])/DT,cvy=(Y[ch]-PY[ch])/DT;
pd(b,X[ch]+face*0.05,Y[ch]+0.25,160*gain,20*gain,cvx,cvy,35,G*gain);
for(let f=0;f<2;f++){ const ft=b+4+2*f,kn=b+3+2*f;
kneeTarget(X[hip],Y[hip],X[ft],Y[ft],face);
pd(kn,KTX,KTY,160*gain,16*gain,hvx,hvy,40,G*gain); }
for(let s=0;s<2;s++){ const el=b+7+2*s,hd=b+8+2*s;
let hx,hyy;
if(p.r<0.55){hx=X[hip]+face*(0.32+s*0.06);hyy=gy+0.06;}
else{hx=X[ch]+face*0.08;hyy=Y[ch]-0.5;}
pd(hd,hx,hyy,60*gain,8*gain,svx,svy,40,G*gain*0.5);
pd(el,(X[ch]+hx)*.5+face*0.05,(Y[ch]+hyy)*.5,60*gain,8*gain,cvx,cvy,40,G*gain*0.5); }
if(p.r>=1){p.state='stand';p.swing=-1;p.cool=0.15;p.air=0;}
}
// ---------------- contacts ----------------
function fric(i,nx,ny,pen,svx,svy,mu,ox,oy){
const vx=ox-PX[i], vy=oy-PY[i];
const rx=vx-svx*DT, ry=vy-svy*DT;
let vn=rx*nx+ry*ny;
let tx=rx-vn*nx, ty=ry-vn*ny;
if(vn<0)vn=0;
const tl=Math.hypot(tx,ty), bud=mu*pen;
if(tl<=bud){tx=0;ty=0;} else {const f=(tl-bud)/tl;tx*=f;ty*=f;}
PX[i]=X[i]-(svx*DT+tx+vn*nx); PY[i]=Y[i]-(svy*DT+ty+vn*ny);
}
function corner(i,cx,cy,svx,svy,mu,load){
const r=RAD[i], dx=X[i]-cx, dy=Y[i]-cy, d2=dx*dx+dy*dy;
if(d2>=r*r) return;
const d=Math.sqrt(d2)||1e-6, nx=dx/d, ny=dy/d, pen=r-d, ox=X[i], oy=Y[i];
X[i]+=nx*pen; Y[i]+=ny*pen;
fric(i,nx,ny,pen+(ny>0.5?load:0),svx,svy,mu,ox,oy);
if(ny>0.3){GND[i]=1;SVX[i]=svx;SVY[i]=svy;}
}
function ground(i,mode){
const p=P[OWN[i]], loc=i-p.base, r=RAD[i];
const mu=mode?PMU[loc]:0;
const load=((loc===4||loc===6)&&p.state==='stand')?LOAD:0;
let ox,oy;
if(X[i]<WL+r){ox=X[i];oy=Y[i];const pen=WL+r-X[i];X[i]=WL+r;fric(i,1,0,pen,0,0,mu,ox,oy);}
else if(X[i]>WR-r){ox=X[i];oy=Y[i];const pen=X[i]-(WR-r);X[i]=WR-r;fric(i,-1,0,pen,0,0,mu,ox,oy);}
seg(X[i],Sg);
if(X[i]+r>Sg.xr){
seg(Sg.xr+1e-6,Ng);
if(Ng.y>Sg.y+1e-6){
if(Y[i]<Ng.y){ox=X[i];oy=Y[i];const pen=X[i]+r-Sg.xr;X[i]-=pen;fric(i,-1,0,pen,Ng.vx,Ng.vy,mu,ox,oy);}
else corner(i,Sg.xr,Ng.y,Ng.vx,Ng.vy,mu,load);
}
}
seg(X[i],Sg);
if(X[i]-r<Sg.xl){
seg(Sg.xl-1e-6,Ng);
if(Ng.y>Sg.y+1e-6){
if(Y[i]<Ng.y){ox=X[i];oy=Y[i];const pen=Sg.xl-(X[i]-r);X[i]+=pen;fric(i,1,0,pen,Ng.vx,Ng.vy,mu,ox,oy);}
else corner(i,Sg.xl,Ng.y,Ng.vx,Ng.vy,mu,load);
}
}
seg(X[i],Sg);
if(Y[i]<Sg.y+r){
ox=X[i];oy=Y[i];const pen=Sg.y+r-Y[i];Y[i]=Sg.y+r;
fric(i,0,1,pen+load,Sg.vx,Sg.vy,mu,ox,oy);
GND[i]=1;SVX[i]=Sg.vx;SVY[i]=Sg.vy;
}
}
// step noses poking into limbs/torso that lie across them
const BX=-Math.SQRT1_2, BY=Math.SQRT1_2;
function cornerBones(p){
const b=p.base;
for(let c=0;c<CONS.length;c++){
const a=b+CONS[c][0], k=b+CONS[c][1];
const ax=X[a],ay=Y[a],ex=X[k]-ax,ey=Y[k]-ay;
const mnx=Math.min(ax,X[k])-0.12, mxx=Math.max(ax,X[k])+0.12;
if(mxx<XA||mnx>XB) continue;
const L2=ex*ex+ey*ey; if(L2<1e-6) continue;
const s0=Math.ceil((mnx-X0-phase)/D), s1=Math.floor((mxx-X0-phase)/D);
for(let s=s0;s<=s1;s++){
const cx=X0+phase+s*D; if(cx<=XA||cx>=XB) continue;
const raw=(cx-X0)*TAN, cy=clamp(raw,0,H), cyp=clamp(raw-HS,0,H);
if(cy-cyp<1e-4) continue;
const t=((cx-ax)*ex+(cy-ay)*ey)/L2; if(t<=0.03||t>=0.97) continue;
const qx=ax+t*ex, qy=ay+t*ey, dx=qx-cx, dy=qy-cy, d=Math.hypot(dx,dy);
const rr=RAD[a]*(1-t)+RAD[k]*t;
let nx,ny,pen;
if(d<1e-6){nx=-SIN;ny=COS;pen=rr;}
else if(dx*BX+dy*BY>=0){ if(d>=rr) continue; nx=dx/d;ny=dy/d;pen=rr-d; }
else { if(d>0.2) continue; nx=-dx/d;ny=-dy/d;pen=rr+d; }
const wa=IM[a]*(1-t), wk=IM[k]*t, W=wa*(1-t)+wk*t, lam=pen/W;
const oax=X[a],oay=Y[a],okx=X[k],oky=Y[k];
X[a]+=nx*lam*wa;Y[a]+=ny*lam*wa;X[k]+=nx*lam*wk;Y[k]+=ny*lam*wk;
const inc=raw>0&&raw<H, svx=beltV*COS, svy=inc?beltV*SIN:0;
fric(a,nx,ny,pen*(1-t),svx,svy,0.55,oax,oay);
fric(k,nx,ny,pen*t,svx,svy,0.55,okx,oky);
}
}
}
// particle of one person vs bone (capsule) of another
function pb(i,j,k){
const xj=X[j],yj=Y[j],ex=X[k]-xj,ey=Y[k]-yj,L2=ex*ex+ey*ey;
let t=L2>1e-9?((X[i]-xj)*ex+(Y[i]-yj)*ey)/L2:0; t=t<0?0:(t>1?1:t);
const qx=xj+t*ex,qy=yj+t*ey,dx=X[i]-qx,dy=Y[i]-qy;
const rr=RAD[i]+RAD[j]*(1-t)+RAD[k]*t, d2=dx*dx+dy*dy;
if(d2>=rr*rr||d2<1e-12) return;
const d=Math.sqrt(d2),nx=dx/d,ny=dy/d,pen=rr-d;
const wi=IM[i],wj=IM[j]*(1-t),wk=IM[k]*t,W=wi+wj*(1-t)+wk*t,lam=pen/W;
X[i]+=nx*lam*wi;Y[i]+=ny*lam*wi;
X[j]-=nx*lam*wj;Y[j]-=ny*lam*wj;
X[k]-=nx*lam*wk;Y[k]-=ny*lam*wk;
const vix=X[i]-PX[i],viy=Y[i]-PY[i];
const vqx=(X[j]-PX[j])*(1-t)+(X[k]-PX[k])*t, vqy=(Y[j]-PY[j])*(1-t)+(Y[k]-PY[k])*t;
const rx=vix-vqx,ry=viy-vqy,rn=rx*nx+ry*ny;
let tx=rx-rn*nx,ty=ry-rn*ny;
const tl=Math.hypot(tx,ty); if(tl<1e-9) return;
const bud=MUB*pen, red=tl<=bud?1:bud/tl;
tx*=red;ty*=red;
const a=1/W;
PX[i]+=tx*wi*a;PY[i]+=ty*wi*a;
PX[j]-=tx*wj*a;PY[j]-=ty*wj*a;
PX[k]-=tx*wk*a;PY[k]-=ty*wk*a;
}
function bodyCollide(){
for(const p of P){
let a=1e9,b=-1e9,c=1e9,d=-1e9;
for(let i=p.base;i<p.base+11;i++){ if(X[i]<a)a=X[i]; if(X[i]>b)b=X[i]; if(Y[i]<c)c=Y[i]; if(Y[i]>d)d=Y[i]; }
p.minx=a-0.16;p.maxx=b+0.16;p.miny=c-0.16;p.maxy=d+0.16;
}
for(let m=0;m<P.length;m++){ const A=P[m];
for(let n=m+1;n<P.length;n++){ const B=P[n];
if(A.maxx<B.minx||B.maxx<A.minx||A.maxy<B.miny||B.maxy<A.miny) continue;
collidePair(A,B); collidePair(B,A);
}
}
}
function collidePair(A,B){
const bb=B.base;
for(let i=A.base;i<A.base+11;i++){
const xi=X[i],yi=Y[i];
if(xi<B.minx||xi>B.maxx||yi<B.miny||yi>B.maxy) continue;
for(let c=0;c<CONS.length;c++) pb(i,bb+CONS[c][0],bb+CONS[c][1]);
}
}
function solveCons(){
for(const p of P){ const b=p.base;
for(let c=0;c<CONS.length;c++){
const a=b+CONS[c][0],k=b+CONS[c][1],L=CONS[c][2];
const dx=X[k]-X[a],dy=Y[k]-Y[a],d=Math.sqrt(dx*dx+dy*dy)||1e-9;
const w=IM[a]+IM[k],f=(d-L)/(d*w);
X[a]+=dx*f*IM[a];Y[a]+=dy*f*IM[a];X[k]-=dx*f*IM[k];Y[k]-=dy*f*IM[k];
}
for(let c=0;c<RANGE.length;c++){
const a=b+RANGE[c][0],k=b+RANGE[c][1];
const dx=X[k]-X[a],dy=Y[k]-Y[a],d=Math.sqrt(dx*dx+dy*dy)||1e-9;
let L; if(d<RANGE[c][2])L=RANGE[c][2]; else if(d>RANGE[c][3])L=RANGE[c][3]; else continue;
const w=IM[a]+IM[k],f=(d-L)/(d*w);
X[a]+=dx*f*IM[a];Y[a]+=dy*f*IM[a];X[k]-=dx*f*IM[k];Y[k]-=dy*f*IM[k];
}
}
}
// ---------------- simulation step ----------------
function step(){
const a=reversed?ACC_REV:ACC_FWD;
beltV+=clamp(beltTarget-beltV,-a*DT,a*DT);
phase=((phase+beltV*COS*DT)%D+D)%D;
railOff+=beltV*DT; simT+=DT;
const np=P.length*11;
for(let i=0;i<np;i++){AX[i]=0;AY[i]=0;}
for(const p of P){
if(p.state==='stand') ctrlStand(p);
else if(p.state==='rise') ctrlRise(p);
else p.downT+=DT;
}
const dt2=DT*DT;
for(let i=0;i<np;i++){
let vx=(X[i]-PX[i])*0.9995, vy=(Y[i]-PY[i])*0.9995;
const sp=vx*vx+vy*vy; if(sp>0.04){const f=0.2/Math.sqrt(sp);vx*=f;vy*=f;}
PX[i]=X[i];PY[i]=Y[i];
X[i]+=vx+AX[i]*dt2; Y[i]+=vy+(AY[i]-G)*dt2;
if(!(X[i]===X[i])||!(Y[i]===Y[i])){X[i]=PX[i]=0;Y[i]=PY[i]=1;}
}
solveCons();solveCons();solveCons();
bodyCollide();
solveCons();solveCons();
bodyCollide();
solveCons();
for(let i=0;i<np;i++)GND[i]=0;
for(let i=0;i<np;i++)ground(i,1);
for(const p of P)cornerBones(p);
solveCons();
for(let i=0;i<np;i++)ground(i,0);
}
// ---------------- per-frame decisions (same rules for everyone) ----------------
function blocked(p,dir){
const b=p.base,hx=X[b+2],hy=Y[b+2];
for(const q of P){ if(q===p)continue; const qb=q.base;
const dx=(X[qb+2]-hx)*dir;
if(dx>0&&dx<0.85&&Math.abs(Y[qb+2]-hy)<1.0) return true;
for(let i=qb;i<qb+11;i++){ const d=(X[i]-hx)*dir; if(d>0.12&&d<0.5&&Y[i]<hy+0.9&&Y[i]>hy-1.0) return true; }
}
return false;
}
function spawnClear(){
for(const q of P){ const qb=q.base; for(let i=qb;i<qb+11;i++) if(X[i]<WL+1.6&&Y[i]<2.2) return false; }
return true;
}
function teleport(p){
const b=p.base,dx=WL+0.7-X[b+2],dy=-H;
for(let i=b;i<b+11;i++){X[i]+=dx;PX[i]+=dx;Y[i]+=dy;PY[i]+=dy;}
}
function decide(p){
const hx=X[p.base+2];
seg(hx,Sa);
let vd=0;
if(!Sa.m){
if(hx<XA){ if(beltV>0.2) vd=p.walk; else if(beltV<-0.2&&hx>WL+0.9) vd=-p.walk*0.8; }
else{ if(hx<WR-1.0) vd=p.walk; else if(beltV>0.2&&spawnClear()){teleport(p);return;} }
} else {
if(Sa.y>=H-1e-6&&beltV>0.2) vd=p.walk*0.8;
}
if(vd!==0&&blocked(p,Math.sign(vd))) vd=0;
p.vd=vd; if(vd) p.face=Math.sign(vd);
}
function tryRise(p){
if(p.downT<p.minDown) return;
const b=p.base,hip=b+2,ch=b+1;
seg(X[hip],Sa);
if(Sa.m&&Math.abs(beltV)>1.0) return;
const rvx=(X[hip]-PX[hip])/DT-Sa.vx, rvy=(Y[hip]-PY[hip])/DT-Sa.vy;
if(rvx*rvx+rvy*rvy>0.09) return;
const cvx=(X[ch]-PX[ch])/DT-Sa.vx, cvy=(Y[ch]-PY[ch])/DT-Sa.vy;
if(cvx*cvx+cvy*cvy>0.16) return;
if(Y[hip]-Sa.y>0.35) return;
const hx=X[hip]; if(hx<WL+0.35||hx>WR-0.35) return;
const gy=Sa.y;
for(const q of P){ if(q===p)continue; const qb=q.base;
for(let i=qb;i<qb+11;i++) if(X[i]>hx-0.42&&X[i]<hx+0.42&&Y[i]>gy-0.05&&Y[i]<gy+1.75) return; }
p.state='rise'; p.r=0; p.face=X[b]>=hx?1:-1;
}
// ---------------- rendering ----------------
const canvas=document.getElementById('c'), ctx=canvas.getContext('2d');
function resize(){const r=Math.min(2,window.devicePixelRatio||1);canvas.width=Math.floor(innerWidth*r);canvas.height=Math.floor(innerHeight*r);}
addEventListener('resize',resize); resize();
let showStates=false;
function limb(pts,b,color,w,outline){
ctx.beginPath(); ctx.moveTo(X[b+pts[0]],Y[b+pts[0]]);
for(let n=1;n<pts.length;n++) ctx.lineTo(X[b+pts[n]],Y[b+pts[n]]);
ctx.strokeStyle=outline; ctx.lineWidth=w+0.035; ctx.stroke();
ctx.strokeStyle=color; ctx.lineWidth=w; ctx.stroke();
}
function drawPerson(p){
const b=p.base,h=p.hue;
const near=`hsl(${h},62%,60%)`, far=`hsl(${h},42%,38%)`, ol='rgba(8,10,14,0.85)';
limb([1,7,8],b,far,0.06,ol);
limb([2,3,4],b,far,0.075,ol);
limb([0,1,2],b,near,0.11,ol);
limb([2,5,6],b,near,0.075,ol);
limb([1,9,10],b,near,0.06,ol);
ctx.beginPath(); ctx.arc(X[b],Y[b],0.125,0,Math.PI*2);
ctx.fillStyle=ol; ctx.fill();
ctx.beginPath(); ctx.arc(X[b],Y[b],0.105,0,Math.PI*2);
ctx.fillStyle=`hsl(${h},55%,70%)`; ctx.fill();
if(showStates){
ctx.beginPath(); ctx.arc(X[b],Y[b],0.2,0,Math.PI*2);
ctx.strokeStyle=p.state==='stand'?'#3f3':p.state==='rise'?'#fc3':'#f44';
ctx.lineWidth=0.03; ctx.stroke();
}
}
function draw(){
const cw=canvas.width,chh=canvas.height;
ctx.setTransform(1,0,0,1,0,0);
const bg=ctx.createLinearGradient(0,0,0,chh); bg.addColorStop(0,'#252b36'); bg.addColorStop(1,'#12151b');
ctx.fillStyle=bg; ctx.fillRect(0,0,cw,chh);
const vx0=WL-0.5,vx1=WR+0.5,vy0=-1.5,vy1=H+2.4;
const sc=Math.min(cw/(vx1-vx0),chh/(vy1-vy0));
const ox=(cw-(vx1-vx0)*sc)/2-vx0*sc, oy=(chh-(vy1-vy0)*sc)/2+vy1*sc;
ctx.setTransform(sc,0,0,-sc,ox,oy);
const px=1/sc;
ctx.strokeStyle='rgba(255,255,255,0.035)'; ctx.lineWidth=px;
for(let x=WL;x<WR;x+=1.2){ctx.beginPath();ctx.moveTo(x,-1.5);ctx.lineTo(x,H+3);ctx.stroke();}
// floors, pit, walls
ctx.fillStyle='#30353e'; ctx.fillRect(WL-4,-3,XA-(WL-4),3);
ctx.fillStyle='#22262d'; ctx.fillRect(XA,-3,WR+4-XA,2.05);
ctx.fillStyle='#30353e'; ctx.fillRect(XB,H-0.6,WR+4-XB,0.6);
ctx.fillStyle='#1a1d23'; ctx.fillRect(WL-4,-3,4,H+8); ctx.fillRect(WR,-3,4,H+8);
ctx.strokeStyle='#6b7380'; ctx.lineWidth=0.04;
ctx.beginPath();ctx.moveTo(WL,0);ctx.lineTo(XA,0);ctx.moveTo(XB,H);ctx.lineTo(WR,H);ctx.stroke();
// truss
const k1=0.2/TAN;
ctx.beginPath();
ctx.moveTo(XA,0);ctx.lineTo(X0,0);ctx.lineTo(X1,H);ctx.lineTo(XB,H);ctx.lineTo(XB,H-0.9);
ctx.lineTo(X1+k1,H-0.9);ctx.lineTo(X0+k1,-0.9);ctx.lineTo(XA,-0.9);ctx.closePath();
ctx.fillStyle='#2b3038'; ctx.fill(); ctx.strokeStyle='#3c424c'; ctx.lineWidth=0.03; ctx.stroke();
// balustrade glass (behind the riders)
ctx.beginPath();
ctx.moveTo(XA-0.45,0);ctx.lineTo(X0,0);ctx.lineTo(X1,H);ctx.lineTo(XB+0.45,H);
ctx.lineTo(XB+0.45,H+0.95);ctx.lineTo(X1,H+0.95);ctx.lineTo(X0,0.95);ctx.lineTo(XA-0.45,0.95);ctx.closePath();
ctx.fillStyle='rgba(150,195,230,0.07)'; ctx.fill();
ctx.fillStyle='#1b1e24'; ctx.fillRect(XA-0.55,0,0.12,0.95); ctx.fillRect(XB+0.43,H,0.12,0.95);
ctx.beginPath();ctx.moveTo(XA-0.5,0.95);ctx.lineTo(X0,0.95);ctx.lineTo(X1,H+0.95);ctx.lineTo(XB+0.5,H+0.95);
ctx.strokeStyle='#15171b'; ctx.lineWidth=0.08; ctx.setLineDash([]); ctx.stroke();
ctx.strokeStyle='#454b55'; ctx.lineWidth=0.035; ctx.setLineDash([0.25,0.25]); ctx.lineDashOffset=-railOff; ctx.stroke();
ctx.setLineDash([]);
// steps
const s0=Math.floor((XA-X0-phase)/D)-1, s1=Math.ceil((XB-X0-phase)/D)+1;
for(let k=s0;k<=s1;k++){
const xn=X0+phase+k*D, xl=Math.max(xn,XA), xr=Math.min(xn+D,XB);
if(xr<=xl) continue;
const y=clamp((xn-X0)*TAN,0,H);
ctx.fillStyle='#4a5059'; ctx.fillRect(xl,y-0.32,xr-xl,0.32);
ctx.fillStyle='#383d45'; ctx.fillRect(xl,y-0.32,0.03,0.285);
ctx.fillStyle='#a3aab4'; ctx.fillRect(xl,y-0.035,xr-xl,0.035);
if(xn>=XA){ctx.fillStyle='#d9a91f'; ctx.fillRect(xn,y-0.035,Math.min(0.07,xr-xn),0.035);}
}
ctx.fillStyle='#8b7a3c'; ctx.fillRect(XA-0.35,-0.03,0.35,0.035); ctx.fillRect(XB,H-0.03,0.35,0.035);
for(const p of P) if(p.state==='down') drawPerson(p);
for(const p of P) if(p.state!=='down') drawPerson(p);
}
// ---------------- UI & loop ----------------
const revBtn=document.getElementById('rev'), slowBtn=document.getElementById('slow'),
dbgBtn=document.getElementById('dbg'), hud=document.getElementById('hud');
let timeScale=1;
function updateRevBtn(){ revBtn.textContent=reversed?'RESTORE (space)':'REVERSE (space)'; revBtn.classList.toggle('on',reversed); }
function toggleRev(){ reversed=!reversed; beltTarget=reversed?V_REV:V_FWD; updateRevBtn(); }
function toggleSlow(){ timeScale=timeScale===1?0.25:1; slowBtn.textContent='Slow-mo: '+(timeScale===1?'off':'on')+' (S)'; }
function toggleDbg(){ showStates=!showStates; dbgBtn.textContent='State rings: '+(showStates?'on':'off')+' (D)'; }
revBtn.onclick=toggleRev; slowBtn.onclick=toggleSlow; dbgBtn.onclick=toggleDbg;
document.getElementById('reset').onclick=reset;
addEventListener('keydown',e=>{
if(e.code==='Space'){e.preventDefault();toggleRev();}
else if(e.key==='s'||e.key==='S')toggleSlow();
else if(e.key==='d'||e.key==='D')toggleDbg();
else if(e.key==='r'||e.key==='R')reset();
});
reset();
let acc=0,last=performance.now(),frameN=0;
function frame(now){
const ft=Math.min(0.05,(now-last)/1000); last=now;
acc+=ft*timeScale;
let n=0;
while(acc>=DT&&n<10){step();acc-=DT;n++;}
if(n>=10)acc=0;
for(const p of P){ if(p.state==='stand')decide(p); else if(p.state==='down')tryRise(p); }
draw();
if((frameN++%6)===0){
let st=0,dn=0,ri=0; for(const p of P){ if(p.state==='stand')st++; else if(p.state==='down')dn++; else ri++; }
hud.textContent=`belt ${beltV>=0?' ':''}${beltV.toFixed(2)} m/s (motor target ${beltTarget.toFixed(1)})\n`+
`people ${P.length}: standing ${st} down ${dn} getting up ${ri}\n`+
`falls ${totalFalls} t = ${simT.toFixed(1)} s`;
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
I could not run this here. What follows is what the model should produce, based on its rules and numbers, plus the places I expect it to go wrong. Treat it as a prediction to check against the screen.
Normal running. 18 figures:
Anyone reaching the right wall is teleported back to the bottom-left queue, but only while the belt runs forward and the entry is clear. That keeps the stream going.
On REVERSE. The motor slews from +0.6 to −2.8 m/s in about 0.2 s.
On RESTORE. The belt returns to +0.6, and everyone lying on the steps is carried up. They stand once the belt is below 1 m/s, they are settled, and they have room. The pile slowly unpacks as people stand, walk and board.
Special-cased or nudged — please count these:
What it does badly: