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 rigid, three-circle body with mass, angular inertia, and two spring-loaded feet. Foot traction tries to match the escalator’s velocity; a limited balance controller tries to keep the torso upright. A sudden reversal pulls the feet out from under the body, and a person folds their legs when their actual tilt becomes too large. Overlapping bodies receive separation, collision, and friction impulses, so fallen people become physical obstacles. Recovery requires sustained support, low relative motion, and clear space overhead. The stair collision surface is simplified to an inclined moving belt; limbs are drawn but do not collide independently.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Against the Grain — Escalator Lab</title>
<style>
:root{color-scheme:dark;--bg:#10171e;--panel:#18232d;--ink:#edf3ed;--muted:#9dafb8;--accent:#efbf70}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:15px system-ui,sans-serif}
main{max-width:1280px;margin:auto;padding:24px}
header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:18px}
.eyebrow{color:var(--accent);letter-spacing:.2em;font-size:11px;font-weight:700}
h1{font-size:clamp(25px,4vw,42px);letter-spacing:-.045em;margin:6px 0}
header p{margin:0;color:var(--muted);line-height:1.5}
.controls{display:flex;gap:9px;flex-wrap:wrap}
button{border:1px solid #42535e;background:#263641;color:var(--ink);padding:12px 17px;border-radius:7px;font:600 14px system-ui;cursor:pointer}
button:hover{background:#344958}
button:focus-visible{outline:3px solid white;outline-offset:3px}
#reverse{background:var(--accent);color:#20252b;border-color:var(--accent);min-width:170px}
#reverse.engaged{background:#ec826c;border-color:#ec826c}
.scene{border:1px solid #354650;border-radius:12px;overflow:hidden;background:#18232d}
canvas{display:block;width:100%;aspect-ratio:12/7}
.dashboard{display:flex;flex-wrap:wrap;gap:15px 32px;padding:15px 20px;background:#142029;border-top:1px solid #354650;font-variant-numeric:tabular-nums}
.stat{display:flex;align-items:baseline;gap:9px}
.stat span{color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.stat strong{font-size:16px}
#direction{color:var(--accent)}
.below{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-top:20px;color:var(--muted);font-size:13px;line-height:1.65}
.below h2{font-size:12px;color:var(--ink);letter-spacing:.1em;text-transform:uppercase;margin:0 0 6px}
.below p{margin:0}
kbd{border:1px solid #42535e;border-radius:3px;padding:1px 5px;color:var(--ink)}
@media(max-width:750px){main{padding:14px}header{align-items:flex-start;flex-direction:column;gap:15px}.below{grid-template-columns:1fr;gap:15px}.dashboard{gap:12px 18px}}
</style>
</head>
<body>
<main>
<header>
<div>
<div class="eyebrow">MECHANICS STUDY / 018</div>
<h1>Against the grain.</h1>
<p>Eighteen riders. One belt. No fall schedule.</p>
</div>
<div class="controls">
<button id="reverse" type="button" aria-pressed="false">Throw into reverse</button>
<button id="pause" type="button" aria-pressed="false">Pause</button>
<button id="reset" type="button">Reset</button>
</div>
</header>
<div class="scene">
<canvas id="scene" width="1200" height="700" aria-label="Side-view physics simulation of eighteen people on an escalator"></canvas>
<div class="dashboard">
<div class="stat"><span>Drive</span><strong id="direction">UPHILL</strong></div>
<div class="stat"><span>Belt</span><strong id="speed">+24 px/s</strong></div>
<div class="stat"><span>Upright</span><strong id="standing">18 / 18</strong></div>
<div class="stat"><span>Body contacts</span><strong id="contacts">0</strong></div>
</div>
</div>
<div class="below">
<section>
<h2>Try it</h2>
<p>Reverse immediately, or let the riders reach the landing first.
Press again to restore the slower uphill drive. Recovery needs a gap:
a person pinned in a pile cannot simply stand through their neighbor.
<kbd>R</kbd> reverses; <kbd>Space</kbd> pauses.</p>
</section>
<section>
<h2>Model boundaries</h2>
<p>The visible treads share an inclined contact envelope, not individual riser collisions.
Bodies collide; illustrated limbs do not. Active feet and a limited righting motor
approximate human balance and getting up. Padded end stops keep the same eighteen
riders in view. Reset restores the initial arrangement, not a recorded outcome.</p>
</section>
</div>
</main>
<script>
"use strict";
(() => {
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const reverseButton = document.getElementById("reverse");
const pauseButton = document.getElementById("pause");
const directionEl = document.getElementById("direction");
const speedEl = document.getElementById("speed");
const standingEl = document.getElementById("standing");
const contactsEl = document.getElementById("contacts");
const W = 1200, H = 700;
const origin = {x:110, y:565};
const dx = 970, rise = 365;
const L = Math.hypot(dx, rise);
const C = dx/L, S = rise/L, slope = S/C;
const DT = 1/180, G = 420;
const colors = ["#efbd73","#7fc9c4","#df9182","#a8b6e3","#b5ca8b","#dba5c7"];
const clamp = (x,a,b) => Math.max(a,Math.min(b,x));
const wrap = x => Math.atan2(Math.sin(x),Math.cos(x));
let people = [], reversed = false, paused = false;
let belt = 24, phase = 0, accumulator = 0, last = 0, contactCount = 0;
function world(s,h) {
return {x:origin.x+C*s-S*h, y:origin.y-S*s-C*h};
}
function floorAt(s) {
if (s < 0) return {h:-s*slope, ns:S, nh:C, ts:C, th:-S, belt:0};
if (s > L) return {h:-(s-L)*slope, ns:S, nh:C, ts:C, th:-S, belt:0};
return {h:0, ns:0, nh:1, ts:1, th:0, belt:belt};
}
function bodyPoint(p, offset) {
const rs = Math.sin(p.a)*offset;
const rh = Math.cos(p.a)*offset;
return {s:p.s+rs,h:p.h+rh,rs,rh};
}
function velocityAt(p, rs, rh) {
return {s:p.vs+p.w*rh,h:p.vh-p.w*rs};
}
function impulse(p, js, jh, rs, rh) {
p.vs += js*p.inv;
p.vh += jh*p.inv;
p.w += (rh*js-rs*jh)*p.ii;
}
function effective(p, rs, rh, ns, nh) {
const arm = rh*ns-rs*nh;
return p.inv+arm*arm*p.ii;
}
function makePeople() {
people = [];
let seed = 71893;
function rand() {
seed = (Math.imul(seed,1664525)+1013904223) >>> 0;
return seed/4294967296;
}
for (let i=0;i<18;i++) {
const mass = .88+rand()*.28;
const leg = 31+rand()*5;
people.push({
id:i,s:52+i*49.5,h:leg-.16,
vs:24,vh:0,a:-.24+(rand()-.5)*.025,w:0,
mass,inv:1/mass,I:mass*230,ii:1/(mass*230),
leg,stance:1,down:false,ready:0,unsupported:0,
support:true,touched:false,footSlip:0,
color:colors[i%colors.length],
circles:[{o:-11,r:7},{o:0,r:8},{o:13,r:6.3}]
});
}
}
function reset() {
reversed = false; paused = false; belt = 24; phase = 0;
accumulator = 0; contactCount = 0;
makePeople(); syncButtons();
}
function syncButtons() {
reverseButton.classList.toggle("engaged",reversed);
reverseButton.textContent = reversed ? "Restore uphill" : "Throw into reverse";
reverseButton.setAttribute("aria-pressed",String(reversed));
pauseButton.textContent = paused ? "Resume" : "Pause";
pauseButton.setAttribute("aria-pressed",String(paused));
}
function toggleReverse() { reversed=!reversed; syncButtons(); }
function togglePause() { paused=!paused; accumulator=0; syncButtons(); }
reverseButton.addEventListener("click",toggleReverse);
pauseButton.addEventListener("click",togglePause);
document.getElementById("reset").addEventListener("click",reset);
document.addEventListener("keydown",e => {
if (e.repeat || /INPUT|TEXTAREA|SELECT/.test(e.target.tagName)) return;
if (e.code==="KeyR") {e.preventDefault();toggleReverse();}
if (e.code==="Space" && e.target.tagName!=="BUTTON") {
e.preventDefault();togglePause();
}
});
// A recovery decision uses the same support, clearance and motion tests for everyone.
function recoverySpace(p) {
const base = floorAt(p.s).h;
for (const q of people) {
if (q===p) continue;
for (const c of q.circles) {
const v = bodyPoint(q,c.o);
if (Math.abs(v.s-p.s)<19+c.r &&
v.h+c.r>base+14 && v.h-c.r<base+58)
return false;
}
}
return true;
}
function applyForces(p) {
const previousSupport = p.support;
p.support=false; p.touched=false; p.footSlip=0;
const fl = floorAt(p.s);
const relative = Math.hypot(p.vs-fl.belt,p.vh);
const tilted = Math.abs(wrap(p.a));
if (!p.down) {
if (previousSupport) p.unsupported=0;
else p.unsupported+=DT;
if (tilted>1.03 || p.unsupported>.24) {
p.down=true; p.ready=0;
}
} else {
const eligible = previousSupport && relative<105 &&
Math.abs(p.w)<1.8 && recoverySpace(p);
p.ready = eligible ? p.ready+DT : Math.max(0,p.ready-3*DT);
if (p.ready>.48) {
// This is a supported attempt to extend the legs, not a pose animation.
p.stance=Math.min(1,p.stance+DT*.9);
if (tilted<.42 && p.stance>.92) {
p.down=false; p.unsupported=0; p.ready=0;
}
} else {
p.stance=Math.max(0,p.stance-DT*4);
}
}
if (!p.down) p.stance=Math.min(1,p.stance+DT*2);
let fs = -G*S*p.mass;
let fh = -G*C*p.mass;
let torque = 0;
const extension = 9+(p.leg-9)*p.stance;
const footShift = 6*Math.sin(p.a);
if (p.stance>.015) {
for (const side of [-1,1]) {
// Articulated legs put their support points beneath the torso.
const rs = side*7+footShift, rh=-extension;
const footS=p.s+rs, footH=p.h+rh;
const f=floorAt(footS);
const penetration=-(footH-f.h)*f.nh;
const v=velocityAt(p,rs,rh);
const vn=v.s*f.ns+v.h*f.nh;
if (penetration>-.65) {
const normal=clamp(1900*(penetration+.3)-43*vn,0,1500*p.mass);
if (normal>0) {
const vt=(v.s-f.belt)*f.ts+v.h*f.th;
const friction=clamp(-36*p.mass*vt,-1.05*normal,1.05*normal);
const fx=f.ns*normal+f.ts*friction;
const fy=f.nh*normal+f.th*friction;
fs+=fx; fh+=fy;
torque+=rh*fx-rs*fy;
p.support=true;
p.footSlip=Math.max(p.footSlip,Math.abs(vt));
}
}
}
}
if ((previousSupport || p.support) && p.stance>.015) {
const motor=clamp(-15000*wrap(p.a)-1500*p.w,-6500,6500);
torque+=motor*p.stance*p.mass;
}
p.vs+=fs*p.inv*DT;
p.vh+=fh*p.inv*DT;
p.w+=torque*p.ii*DT;
// Small aerodynamic/angular damping; ground friction does most of the work.
p.vs*=Math.exp(-.035*DT);
p.vh*=Math.exp(-.035*DT);
p.w*=Math.exp(-.55*DT);
p.s+=p.vs*DT; p.h+=p.vh*DT; p.a=wrap(p.a+p.w*DT);
}
function groundContact(p,c) {
const v=bodyPoint(p,c.o), f=floorAt(v.s);
const depth=c.r-(v.h-f.h)*f.nh;
if (depth<=0) return;
p.support=true; p.touched=true;
const den=effective(p,v.rs,v.rh,f.ns,f.nh);
const amount=Math.max(0,depth-.035)*.64/den;
p.s+=f.ns*amount*p.inv; p.h+=f.nh*amount*p.inv;
p.a=wrap(p.a+(v.rh*f.ns-v.rs*f.nh)*amount*p.ii);
const vel=velocityAt(p,v.rs,v.rh);
const vn=vel.s*f.ns+vel.h*f.nh;
const jn=Math.max(0,-vn/den);
impulse(p,f.ns*jn,f.nh*jn,v.rs,v.rh);
const after=velocityAt(p,v.rs,v.rh);
const vt=(after.s-f.belt)*f.ts+after.h*f.th;
const td=effective(p,v.rs,v.rh,f.ts,f.th);
const jt=clamp(-vt/td,-.56*jn,.56*jn);
impulse(p,f.ts*jt,f.th*jt,v.rs,v.rh);
}
function wallContacts(p) {
// Physical padded end stops, outside both landings.
for (const c of p.circles) {
const v=bodyPoint(p,c.o), pt=world(v.s,v.h);
let ns,nh,depth;
if (pt.x-c.r<30) {ns=C;nh=-S;depth=30-(pt.x-c.r);}
else if (pt.x+c.r>1170) {ns=-C;nh=S;depth=pt.x+c.r-1170;}
else continue;
p.s+=ns*depth*.7; p.h+=nh*depth*.7;
const vel=velocityAt(p,v.rs,v.rh);
const vn=vel.s*ns+vel.h*nh;
if (vn<0) {
const j=-vn/effective(p,v.rs,v.rh,ns,nh);
impulse(p,j*ns,j*nh,v.rs,v.rh);
}
}
}
function collide(a,b,ca,cb,count) {
const pa=bodyPoint(a,ca.o), pb=bodyPoint(b,cb.o);
let ds=pb.s-pa.s, dh=pb.h-pa.h;
const radii=ca.r+cb.r, d2=ds*ds+dh*dh;
if (d2>=radii*radii) return;
let d=Math.sqrt(d2);
if (d<.0001) {ds=1;dh=0;d=1;}
const ns=ds/d,nh=dh/d;
if (count) contactCount++;
const den=effective(a,pa.rs,pa.rh,ns,nh)+effective(b,pb.rs,pb.rh,ns,nh);
const correction=Math.max(0,radii-d-.08)*.48/den;
a.s-=ns*correction*a.inv; a.h-=nh*correction*a.inv;
b.s+=ns*correction*b.inv; b.h+=nh*correction*b.inv;
a.a=wrap(a.a-(pa.rh*ns-pa.rs*nh)*correction*a.ii);
b.a=wrap(b.a+(pb.rh*ns-pb.rs*nh)*correction*b.ii);
const va=velocityAt(a,pa.rs,pa.rh),vb=velocityAt(b,pb.rs,pb.rh);
const vn=(vb.s-va.s)*ns+(vb.h-va.h)*nh;
const jn=Math.max(0,-1.04*vn/den);
impulse(a,-ns*jn,-nh*jn,pa.rs,pa.rh);
impulse(b,ns*jn,nh*jn,pb.rs,pb.rh);
const aa=velocityAt(a,pa.rs,pa.rh),bb=velocityAt(b,pb.rs,pb.rh);
const ts=nh,th=-ns;
const vt=(bb.s-aa.s)*ts+(bb.h-aa.h)*th;
const td=effective(a,pa.rs,pa.rh,ts,th)+effective(b,pb.rs,pb.rh,ts,th);
const jt=clamp(-vt/td,-.48*jn,.48*jn);
impulse(a,-ts*jt,-th*jt,pa.rs,pa.rh);
impulse(b,ts*jt,th*jt,pb.rs,pb.rh);
}
function step() {
const target=reversed ? -245 : 24;
belt+=clamp(target-belt,-2800*DT,2800*DT);
phase=(phase+belt*DT)%22;
for (const p of people) applyForces(p);
contactCount=0;
for (let iteration=0;iteration<6;iteration++) {
// Alternating order avoids always giving the same end of a pile priority.
const order=iteration%2 ? [...people].reverse() : people;
for (const p of order) {
for (const c of p.circles) groundContact(p,c);
wallContacts(p);
}
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.s-b.s)>56 || Math.abs(a.h-b.h)>56) continue;
for (const ca of a.circles)
for (const cb of b.circles) collide(a,b,ca,cb,iteration===5);
}
}
}
}
function line(points,color,width=2) {
ctx.beginPath();
for (let i=0;i<points.length;i++) {
const p=points[i];
if(i)ctx.lineTo(p.x,p.y);else ctx.moveTo(p.x,p.y);
}
ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke();
}
function dot(p,r,color) {
ctx.beginPath();ctx.arc(p.x,p.y,r,0,Math.PI*2);
ctx.fillStyle=color;ctx.fill();
}
function local(p,rs,rh) {
return world(p.s+rs,p.h+rh);
}
function rotated(p,rs,rh) {
return local(p,rs*Math.cos(p.a)+rh*Math.sin(p.a),
-rs*Math.sin(p.a)+rh*Math.cos(p.a));
}
function drawMachine() {
const gradient=ctx.createLinearGradient(0,0,0,H);
gradient.addColorStop(0,"#1d2d39");gradient.addColorStop(1,"#121d26");
ctx.fillStyle=gradient;ctx.fillRect(0,0,W,H);
ctx.strokeStyle="#ffffff06";ctx.lineWidth=1;
for (let x=40;x<W;x+=60) line([{x,y:70},{x,y:630}],"#ffffff06",1);
ctx.fillStyle="#92acb015";ctx.font="700 110px system-ui";
ctx.fillText("18",68,192);
ctx.fillStyle="#a7bac1";ctx.font="11px system-ui";
ctx.fillText("SHARED RULES / INDEPENDENT BODIES",75,219);
ctx.fillStyle="#748e9c";ctx.font="11px system-ui";
ctx.fillText("LOWER LANDING",42,646);
ctx.fillText("UPPER LANDING",1028,103);
const a=world(0,-16),b=world(L,-16),c=world(L,-63),d=world(0,-63);
ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);
ctx.lineTo(c.x,c.y);ctx.lineTo(d.x,d.y);ctx.closePath();
ctx.fillStyle="#283d49";ctx.fill();
line([d,c],"#44616d",3);
line([world(0,-42),world(L,-42)],"#152630",4);
// Moving tread drawing. Contacts use the mean inclined surface.
ctx.save();
const t0=world(0,10),t1=world(L,10),t2=world(L,-17),t3=world(0,-17);
ctx.beginPath();ctx.moveTo(t0.x,t0.y);ctx.lineTo(t1.x,t1.y);
ctx.lineTo(t2.x,t2.y);ctx.lineTo(t3.x,t3.y);ctx.closePath();ctx.clip();
const pitch=22;
for (let s=-pitch*2+phase;s<L+pitch*2;s+=pitch) {
const p=world(s,0),q=world(s+pitch,0);
ctx.fillStyle="#596d74";
ctx.fillRect(p.x,p.y,q.x-p.x,6);
line([{x:p.x,y:p.y},{x:q.x,y:p.y},{x:q.x,y:q.y}],"#a9b7b8",1.5);
for(let k=3;k<q.x-p.x;k+=4)
line([{x:p.x+k,y:p.y+1},{x:p.x+k,y:p.y+4}],"#344952",.7);
}
ctx.restore();
ctx.fillStyle="#52666e";
ctx.fillRect(30,565,80,13);
ctx.fillRect(1080,200,90,13);
line([{x:30,y:564},{x:110,y:564}],"#b4c0ba",2);
line([{x:1080,y:199},{x:1170,y:199}],"#b4c0ba",2);
// Far-side balustrade: deliberately not a collision wall.
line([world(-4,55),world(L+4,55)],"#66858d",5);
line([world(0,51),world(L,51)],"#14232d",3);
for(let s=0;s<=L;s+=130)
line([world(s,0),world(s,53)],"#6b858a55",2);
ctx.fillStyle="#dbad69";
ctx.fillRect(24,532,11,47);ctx.fillRect(1165,168,11,46);
ctx.fillStyle="#473e32";
for(let y=536;y<573;y+=9)ctx.fillRect(24,y,11,3);
for(let y=172;y<208;y+=9)ctx.fillRect(1165,y,11,3);
for(let s=90;s<L;s+=190) {
const mid=world(s,-35);
const sign=belt>=0 ? 1:-1;
const end={x:mid.x+sign*C*17,y:mid.y-sign*S*17};
line([{x:mid.x-sign*C*12,y:mid.y+sign*S*12},end],"#efbf70aa",2);
line([{x:end.x-sign*C*7-S*5,y:end.y+sign*S*7-C*5},
end,{x:end.x-sign*C*7+S*5,y:end.y+sign*S*7+C*5}],"#efbf70aa",2);
}
}
function drawPerson(p) {
const stance=p.stance;
const hip=rotated(p,0,-10),shoulder=rotated(p,0,7);
const head=rotated(p,0,14);
const extension=9+(p.leg-9)*stance;
const shift=6*Math.sin(p.a);
const feet=[];
for(const side of [-1,1]) {
const support=local(p,side*7+shift,-extension);
const folded=rotated(p,side*5+12,-20);
const foot={
x:support.x*stance+folded.x*(1-stance),
y:support.y*stance+folded.y*(1-stance)
};
const knee={
x:(hip.x+foot.x)/2+side*3+Math.sin(p.a)*7,
y:(hip.y+foot.y)/2-4*(1-stance)
};
line([hip,knee,foot],side<0?"#9fb6bd":p.color,3.2);
feet.push(foot);
}
line([hip,shoulder],p.color,8);
line([hip,shoulder],"#ffffff20",2);
const brace=clamp(p.footSlip/100+Math.abs(p.w)*.13,0,1);
for(const side of [-1,1]) {
const elbow=rotated(p,side*(7+brace*5),-3+brace*5);
const hand=rotated(p,side*(9+brace*10),-11+brace*13);
line([shoulder,elbow,hand],side<0?"#a7b9b8":p.color,2.7);
dot(hand,2,"#ebd3b4");
}
dot(head,6.1,"#ead3b7");
const hair=rotated(p,-1,17.5);
dot(hair,3.8,"#25333d");
const face=rotated(p,3.7,14.4);
dot(face,1,"#3a4245");
for(const foot of feet)
line([{x:foot.x-3,y:foot.y},{x:foot.x+5,y:foot.y}],"#dce4dd",2.5);
// Small ID tag follows the body, rather than marking a scripted role.
const label=rotated(p,-1,2);
ctx.save();ctx.translate(label.x,label.y);
ctx.fillStyle="#18262c";ctx.font="bold 6px system-ui";
ctx.textAlign="center";ctx.fillText(String(p.id+1).padStart(2,"0"),0,2);
ctx.restore();
}
function render() {
ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0);
ctx.lineCap="round";ctx.lineJoin="round";
drawMachine();
// Shadows describe occupancy, including bodies on the floor.
for(const p of people) {
const f=floorAt(p.s),pt=world(p.s,f.h);
ctx.save();ctx.translate(pt.x,pt.y);ctx.rotate(-Math.atan2(S,C));
ctx.fillStyle="#050b1035";ctx.beginPath();
ctx.ellipse(0,1,12+9*(1-p.stance),3,0,0,Math.PI*2);ctx.fill();ctx.restore();
}
for(const p of [...people].sort((a,b)=>a.h-b.h)) drawPerson(p);
ctx.fillStyle="#101b22cc";ctx.fillRect(73,285,225,56);
ctx.fillStyle=reversed?"#ee947c":"#efbf70";ctx.font="600 12px system-ui";
ctx.fillText(reversed?"REVERSE DRIVE ENGAGED":"NORMAL UPHILL DRIVE",88,308);
ctx.fillStyle="#9eb1bb";ctx.font="11px system-ui";
ctx.fillText("Target "+(reversed?"−245":"+24")+" px/s · actuator 2,800 px/s²",88,327);
if(paused) {
ctx.fillStyle="#10202c88";ctx.fillRect(0,0,W,H);
ctx.fillStyle="#eef2e9";ctx.font="600 24px system-ui";ctx.textAlign="center";
ctx.fillText("PAUSED",W/2,92);ctx.textAlign="left";
}
directionEl.textContent=reversed?"REVERSE":"UPHILL";
speedEl.textContent=(belt>=0?"+":"−")+Math.round(Math.abs(belt))+" px/s";
standingEl.textContent=people.filter(p=>!p.down&&Math.abs(p.a)<.8).length+" / 18";
contactsEl.textContent=contactCount;
}
function resize() {
const ratio=Math.min(window.devicePixelRatio||1,2);
const width=canvas.getBoundingClientRect().width;
canvas.width=Math.round(width*ratio);
canvas.height=Math.round(width*H/W*ratio);
render();
}
window.addEventListener("resize",resize);
document.addEventListener("visibilitychange",()=>{last=0;accumulator=0;});
function frame(now) {
if(!last) last=now;
const elapsed=Math.min((now-last)/1000,.06);
last=now;
if(!paused && !document.hidden) {
accumulator+=elapsed;
while(accumulator>=DT) {step();accumulator-=DT;}
}
render();requestAnimationFrame(frame);
}
reset();resize();requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The drive changes from +24 to −245 pixels/second in about a tenth of a second. Foot friction applies that change below each rider’s center of mass, generating tipping torque. Bodies then hit the belt and each other; contact friction carries fallen riders downhill, where they can obstruct arriving riders and collect near the lower stop. Reversing again pulls the pile uphill more slowly.
The nudges are explicit: slightly varied masses and leg lengths, tuned traction and balance strength, a supported recovery motor, and padded end boundaries. No rider has a scheduled fall or a designated place in the pile.
Its weaknesses: collisions use a smooth incline rather than stair risers; arms and legs can overlap; impacts are deliberately inelastic. Dense piles may jitter or remain jammed, and getting up looks mechanical. The initial group is evenly spaced, so the first loss of balance can look fairly synchronized. I have not browser-tested this file here.