27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You write simulations where the interesting behavior is supposed to emerge rather than be animated by hand.
Build me a single HTML page: a side-view escalator carrying 15 to 20 people. Then the escalator reverses, at high speed.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: how a person is represented, what makes one lose balance, and what happens when two of them occupy the same place.
The single fenced html block.
Having written it, what does the page do when the escalator reverses? Name anything you special-cased or nudged. Say what it does badly — I am going to press the button and watch.
Each person is a small articulated ragdoll: a torso segment with a head, two legs (thigh + shin) and two arms, all connected by simple revolute joints with angular limits and spring-damping torques. Balance is a servo: while standing, ankle and hip torques try to keep the torso upright relative to the local step surface, and the feet are planted to the step with a positional spring. A person loses balance when the required corrective torque exceeds what the muscles can supply — which happens naturally when the step accelerates out from under them at reversal, when a neighbor shoves them, or when the surface under a foot disappears (top/bottom landing). Contacts between people are resolved as soft circle/segment pushes between torsos, heads and limbs, so bodies shove each other and a fallen figure becomes a passive obstacle. A figure that is down gets up when its torso is roughly horizontal-ish, there is free space ahead, and its feet have regained contact with a step — then the standing servo re-engages gradually. Everyone runs identical rules; the only nudges are physical (friction, restitution, muscle strength, step geometry), not per-figure scripts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Escalator</title>
<style>
html,body{margin:0;height:100%;background:#101418;overflow:hidden;font-family:system-ui,Segoe UI,sans-serif}
#wrap{position:relative;width:100%;height:100%}
canvas{display:block;width:100%;height:100%}
#ui{position:absolute;top:14px;left:14px;color:#dfe6ee;background:rgba(20,26,34,.82);
padding:12px 16px;border-radius:10px;border:1px solid #2c3644;max-width:340px}
#ui h1{margin:0 0 6px;font-size:15px;letter-spacing:.4px}
#ui p{margin:0 0 10px;font-size:12px;color:#9fb0c3;line-height:1.45}
button{cursor:pointer;font:inherit;font-size:14px;padding:8px 14px;border-radius:8px;
border:1px solid #3a4a5e;background:#1d2733;color:#eaf1f8}
button:hover{background:#26344a}
#state{font-size:12px;color:#8fd0ff;margin-top:8px;min-height:16px}
</style>
</head>
<body>
<div id="wrap">
<canvas id="cv"></canvas>
<div id="ui">
<h1>Escalator</h1>
<p>Side view. Everyone rides by the same rules. Press to reverse — press again to restore.</p>
<button id="rev">Reverse escalator</button>
<div id="state"></div>
</div>
</div>
<script>
(() => {
"use strict";
/* ---------- canvas ---------- */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
let W = 0, H = 0;
function resize(){
const r = cv.getBoundingClientRect();
W = cv.width = Math.max(600, Math.floor(r.width * devicePixelRatio));
H = cv.height = Math.max(400, Math.floor(r.height * devicePixelRatio));
}
addEventListener('resize', resize); resize();
/* ---------- escalator geometry ---------- */
const ANGLE = -0.52; // rise angle (negative = going up to the right)
const STEP_W = 46, STEP_H = 26; // step pitch along slope / rise
const NSTEPS = 26;
const SPEED_NORM = 0.55; // steps per second, normal
const SPEED_REV = 3.4; // steps per second, reversed (fast)
let stepPos = 0; // continuous step index position
let speed = SPEED_NORM; // current signed speed (steps/sec)
let targetSpeed = SPEED_NORM;
let accel = 0; // for smooth-ish ramp
// world: slope line passes through (cx, cy) going up-right
const cx = () => W * 0.5, cy = () => H * 0.62;
const dirX = Math.cos(ANGLE), dirY = Math.sin(ANGLE); // along slope, up-right
const nrmX = -Math.sin(ANGLE), nrmY = -Math.cos(ANGLE); // normal, pointing up-left (out of step)
// step i (integer) has its tread surface at slope coordinate s = i*STEP_W
// tread is a flat segment of length STEP_W centered at s + STEP_W/2, at height 0 (on the line)
function slopePoint(s, h){ return { x: cx() + dirX*s - nrmX*h, y: cy() + dirY*s - nrmY*h }; }
// top and bottom landings
const LAND_TOP = (NSTEPS-1)*STEP_W + STEP_W*0.5;
const LAND_BOT = -STEP_W*0.5;
/* ---------- tiny vec helpers ---------- */
const V = (x,y)=>({x,y});
const add=(a,b)=>V(a.x+b.x,a.y+b.y);
const sub=(a,b)=>V(a.x-b.x,a.y-b.y);
const mul=(a,s)=>V(a.x*s,a.y*s);
const len=a=>Math.hypot(a.x,a.y);
const dot=(a,b)=>a.x*b.x+a.y*b.y;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
/* ---------- person ---------- */
// A person: torso point + angle, head, two legs (hip->knee->foot), two arms (shoulder->elbow->hand)
// Standing control: ankle/hip spring torques keep torso upright w.r.t. local surface normal,
// feet spring toward planted points on the step.
const P = {
torsoL: 26, thigh: 22, shin: 22, armU: 14, armL: 14, headR: 8,
mass: 1.0,
standK: 900, standDamp: 26, // torso upright servo
hipK: 500, hipDamp: 16,
footK: 2600, footDamp: 40, // foot spring to planted point
maxMuscle: 260, // torque limit -> beyond this, balance fails
armK: 120, armDamp: 8,
fric: 0.85
};
function makePerson(x, y, slopeS, facing){
// facing: +1 looks up-slope (right), -1 down-slope
const p = slopePoint(slopeS, 0);
const up = V(nrmX, nrmY);
const person = {
id: makePerson._id++,
// torso
pos: add(p, mul(up, 46)), vel: V(0,0), ang: facing>0 ? -Math.PI/2 : Math.PI/2, angV: 0,
// facing sign: torso angle measured so that "up" along torso = head direction
facing,
head: V(0,0), headV: V(0,0),
legs: [], arms: [],
planted: [null, null], // {s, h, x, y} foot anchor on step, or null
down: false, // ragdoll mode
downT: 0, // time spent down
getupT: 0,
color: `hsl(${Math.floor(Math.random()*360)},55%,62%)`,
phase: Math.random()*Math.PI*2,
sway: (Math.random()-0.5)*0.06
};
// legs: hip at torso bottom
const hipOff = mul(up, -P.torsoL*0.45);
const along = V(dirX*facing, dirY*facing);
for(let i=0;i<2;i++){
const side = i===0 ? -1 : 1;
const hip = add(person.pos, mul(hipOff,1), mul(along, side*4));
person.legs.push({
hip, hipV:V(0,0),
knee: add(hip, mul(up,-P.thigh)), kneeV:V(0,0),
foot: add(hip, mul(up,-(P.thigh+P.shin))), footV:V(0,0),
thighAng: -Math.PI/2, shinAng: -Math.PI/2, // relative angles
planted: null
});
}
// arms from shoulder (torso top)
const shOff = mul(up, P.torsoL*0.35);
for(let i=0;i<2;i++){
const side = i===0 ? -1 : 1;
const sh = add(person.pos, shOff, mul(along, side*5));
person.arms.push({
sh, shV:V(0,0),
el: add(sh, mul(up,P.armU)), elV:V(0,0),
hand: add(sh, mul(up,P.armU+P.armL)), handV:V(0,0),
a1: -Math.PI/2, a2: 0
});
}
person.head = add(person.pos, mul(up, P.torsoL*0.5 + P.headR + 2));
person.headV = V(0,0);
return person;
}
makePerson._id = 0;
/* ---------- step surface query ---------- */
// Returns {x,y,nx,ny,s} of tread surface at world point (x,y) if standing on a step,
// plus landing info. h = height above slope line.
function surfaceAt(x, y){
// convert to slope coords
const dx = x - cx(), dy = y - cy();
const s = dx*dirX + dy*dirY;
const h = -(dx*nrmX + dy*nrmY); // height above line (nrm points up-left, so h = -dot with nrm... careful)
// Actually nrm points away from slope (up-left). Height above surface = dot(d - c, nrm).
const hh = dx*nrmX + dy*nrmY;
// find nearest step index
const i = Math.round((s - STEP_W/2) / STEP_W);
const s0 = i*STEP_W; // step i spans [s0, s0+STEP_W]
const inSteps = i >= 0 && i < NSTEPS;
const onLandTop = s > LAND_TOP && s < LAND_TOP + 200;
const onLandBot = s < LAND_BOT && s > LAND_BOT - 200;
if(!inSteps && !onLandTop && !onLandBot) return null;
// tread top surface is at hh = 0 (the slope line) — but steps have risers; approximate flat treads
// step surface height above line: 0 (tread lies on the line)
const px = cx() + dirX*(s0 + STEP_W/2) - nrmX*0;
const py = cy() + dirY*(s0 + STEP_W/2) - nrmY*0;
return { x: px, y: py, nx: nrmX, ny: nrmY, s: s0 + STEP_W/2, h: hh, stepI: i, onLand: !inSteps };
}
/* ---------- physics ---------- */
const GRAV = V(0, 980);
const DT = 1/240;
const SUB = 4; // substeps
const people = [];
function initPeople(){
people.length = 0;
const n = 17;
for(let k=0;k<n;k++){
const s = 60 + k*( (LAND_TOP-120) / (n-1) ) * 0.92 + Math.random()*18;
const facing = 1; // all face up-slope (right)
const p = makePerson(0,0,s,facing);
// small stagger in height so they don't interpenetrate
people.push(p);
}
// place them properly
for(const p of people){
// recompute positions from their slope coordinate stored temporarily
}
// simpler: recreate with stored s
people.length = 0;
for(let k=0;k<n;k++){
const s = 40 + k*((LAND_TOP-90)/(n-1)) + (Math.random()*14-7);
const p = makePerson(0,0, clamp(s, 20, LAND_TOP-20), 1);
people.push(p);
}
}
/* integrate one person */
function stepPerson(p, dt){
const up = V(nrmX, nrmY);
const along = V(dirX*p.facing, dirY*p.facing);
// --- determine support surface under each foot ---
const surf = [];
for(const leg of p.legs){
const f = leg.foot;
const sf = surfaceAt(f.x, f.y + 6);
surf.push(sf);
}
// decide down state
const torsoUp = { x: Math.cos(p.ang), y: Math.sin(p.ang) }; // head direction
const uprightness = dot(torsoUp, up); // 1 = upright
if(!p.down && uprightness < 0.25){
p.down = true; p.downT = 0;
// release planted feet
for(const leg of p.legs) leg.planted = null;
}
if(p.down){
p.downT += dt;
// ragdoll: gravity + joint springs weak, contacts
integrateRagdoll(p, dt, surf);
// try to get up
if(p.downT > 0.6 && uprightness > 0.55){
// check room ahead: sample point ahead of torso
const ahead = add(p.pos, mul(along, 30));
const room = !people.some(q => q!==p && len(sub(q.pos, ahead)) < 34);
const feetOn = surf.some(s => s && s.h < 14);
if(room && feetOn){
p.getupT += dt;
if(p.getupT > 0.35){
p.down = false; p.getupT = 0;
// re-plant feet near current positions
for(let i=0;i<2;i++){
const leg = p.legs[i];
const sf = surf[i];
if(sf && sf.h < 16){
leg.planted = { x: leg.foot.x, y: leg.foot.y };
}
}
}
} else p.getupT = 0;
}
} else {
p.getupT = 0;
integrateStanding(p, dt, surf, up, along);
}
// integrate torso
if(p.down){
// torso is a free body in ragdoll (handled in integrateRagdoll)
} else {
// torso: upright servo + foot coupling
// desired torso angle: aligned with surface normal
const surf0 = surf.find(s=>s);
const nrm = surf0 ? V(surf0.nx, surf0.ny) : up;
const targetAng = Math.atan2(nrm.y, nrm.x);
let err = angleDiff(p.ang, targetAng);
let tau = -P.standK*err - P.standDamp*p.angV;
// gravity torque about "hip" (approx at torso center): small
// limit muscle
const used = Math.abs(tau);
if(used > P.maxMuscle){ tau = Math.sign(tau)*P.maxMuscle; }
p.angV += (tau / (P.mass*30)) * dt; // torso inertia approx
p.angV *= 0.995;
p.ang += p.angV * dt;
// torso position follows feet average + torso length along up
let anchor = V(0,0), cnt=0;
for(const leg of p.legs){ if(leg.planted){ anchor = add(anchor, leg.planted); cnt++; } }
if(cnt>0){
anchor = mul(anchor, 1/cnt);
const torsoUp2 = V(Math.cos(p.ang), Math.sin(p.ang));
const desired = add(anchor, mul(torsoUp2, 46));
// spring position toward desired, but allow horizontal drift (stepping)
const k = 3000, d = 60;
const f = V(-k*(p.pos.x-desired.x) - d*p.vel.x, -k*(p.pos.y-desired.y) - d*p.vel.y);
p.vel.x += f.x/P.mass*dt; p.vel.y += f.y/P.mass*dt;
}
// gravity
p.vel.y += GRAV.y*dt;
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt;
// mild air drag
p.vel.x *= 0.999; p.vel.y *= 0.999;
}
// head follows torso
const tu = V(Math.cos(p.ang), Math.sin(p.ang));
p.head = add(p.pos, mul(tu, P.torsoL*0.5 + P.headR + 2));
p.headV = mul(sub(p.head, p._headPrev || p.head), 1/dt); p._headPrev = {x:p.head.x,y:p.head.y};
// limbs
updateLimbs(p, dt, surf, up, along);
}
function angleDiff(a,b){ let d=(b-a)%(Math.PI*2); if(d>Math.PI)d-=2*Math.PI; if(d<-Math.PI)d+=2*Math.PI; return d; }
/* standing-mode limb control */
function integrateStanding(p, dt, surf, up, along){
for(let i=0;i<2;i++){
const leg = p.legs[i];
const sf = surf[i];
// hip position = torso bottom
const tu = V(Math.cos(p.ang), Math.sin(p.ang));
const hipDes = add(p.pos, mul(tu, -P.torsoL*0.45));
// spring hip toward desired
leg.hipV.x += ((hipDes.x-leg.hip.x)*400 - leg.hipV.x*20)*dt;
leg.hipV.y += ((hipDes.y-leg.hip.y)*400 - leg.hipV.y*20 + GRAV.y)*dt;
leg.hip.x += leg.hipV.x*dt; leg.hip.y += leg.hipV.y*dt;
// foot target: planted point if any, else below hip
let footT;
if(leg.planted){
// planted point moves with the step! compute step velocity
footT = { x: leg.planted.x, y: leg.planted.y };
} else {
footT = add(hipDes, mul(up, -(P.thigh+P.shin)));
// if near surface, plant
if(sf && sf.h > -(P.thigh+P.shin) - 4 && sf.h < 10){
leg.planted = { x: leg.foot.x, y: leg.foot.y };
footT = leg.planted;
}
}
// foot spring
const fk = P.footK, fd = P.footDamp;
leg.footV.x += (-fk*(leg.foot.x-footT.x) - fd*leg.footV.x)*dt;
leg.footV.y += (-fk*(leg.foot.y-footT.y) - fd*leg.footV.y + GRAV.y)*dt;
leg.foot.x += leg.footV.x*dt; leg.foot.y += leg.footV.y*dt;
// knee: simple 2-bone IK-ish spring toward a bent configuration
const d = sub(leg.foot, leg.hip); const L = len(d);
const maxL = P.thigh + P.shin - 1;
let kneeTarget;
if(L > maxL){
// straight-ish: knee along the line, slight bend forward
const dir = mul(d, 1/Math.max(L,0.001));
const perp = V(-dir.y, dir.x);
kneeTarget = add(mul(add(leg.hip, leg.foot),0.5), mul(perp, 6*p.facing));
} else {
const dir = mul(d, 1/Math.max(L,0.001));
const perp = V(-dir.y, dir.x);
kneeTarget = add(mul(add(leg.hip, leg.foot),0.5), mul(perp, 10*p.facing));
}
leg.kneeV.x += ((kneeTarget.x-leg.knee.x)*900 - leg.kneeV.x*30)*dt;
leg.kneeV.y += ((kneeTarget.y-leg.knee.y)*900 - leg.kneeV.y*30 + GRAV.y*0.5)*dt;
leg.knee.x += leg.kneeV.x*dt; leg.knee.y += leg.kneeV.y*dt;
// if foot left the surface significantly and was planted, unplant (stepping)
if(leg.planted && sf && sf.h > 12){ leg.planted = null; }
if(leg.planted && !sf){ leg.planted = null; }
}
// arms: gentle swing
for(const arm of p.arms){
const tu = V(Math.cos(p.ang), Math.sin(p.ang));
const shDes = add(p.pos, mul(tu, P.torsoL*0.35));
arm.shV.x += ((shDes.x-arm.sh.x)*400 - arm.shV.x*20)*dt;
arm.shV.y += ((shDes.y-arm.sh.y)*400 - arm.shV.y*20 + GRAV.y)*dt;
arm.sh.x += arm.shV.x*dt; arm.sh.y += arm.shV.y*dt;
// hand target: relaxed down with slight sway
const sway = Math.sin(performance.now()/900 + p.phase)*4;
const handT = add(arm.sh, V(6*p.facing + sway, 26));
arm.handV.x += ((handT.x-arm.hand.x)*300 - arm.handV.x*18)*dt;
arm.handV.y += ((handT.y-arm.hand.y)*300 - arm.handV.y*18 + GRAV.y)*dt;
arm.hand.x += arm.handV.x*dt; arm.hand.y += arm.handV.y*dt;
// elbow between
const elT = mul(add(arm.sh, arm.hand), 0.5);
arm.elV.x += ((elT.x-arm.el.x)*600 - arm.elV.x*24)*dt;
arm.elV.y += ((elT.y-arm.el.y)*600 - arm.elV.y*24 + GRAV.y*0.5)*dt;
arm.el.x += arm.elV.x*dt; arm.el.y += arm.elV.y*dt;
}
}
/* ragdoll mode: limbs are passive, torso free */
function integrateRagdoll(p, dt, surf){
// torso free body
p.vel.y += GRAV.y*dt;
// weak internal stiffness so body doesn't go limp noodle
const tu = V(Math.cos(p.ang), Math.sin(p.ang));
// hips
for(const leg of p.legs){
// hip follows torso bottom loosely
const hipDes = add(p.pos, mul(tu, -P.torsoL*0.45));
leg.hipV.x += ((hipDes.x-leg.hip.x)*120 - leg.hipV.x*10)*dt;
leg.hipV.y += ((hipDes.y-leg.hip.y)*120 - leg.hipV.y*10 + GRAV.y)*dt;
leg.hip.x += leg.hipV.x*dt; leg.hip.y += leg.hipV.y*dt;
// knee & foot: gravity + weak springs to keep leg from folding weirdly
leg.kneeV.y += GRAV.y*dt; leg.kneeV.x *= 0.999;
leg.knee.x += leg.kneeV.x*dt; leg.knee.y += leg.kneeV.y*dt;
leg.footV.y += GRAV.y*dt; leg.footV.x *= 0.999;
leg.foot.x += leg.footV.x*dt; leg.foot.y += leg.footV.y*dt;
// weak knee spring
const kmid = mul(add(leg.hip, leg.foot),0.5);
leg.kneeV.x += (kmid.x-leg.knee.x)*40*dt;
leg.kneeV.y += (kmid.y-leg.knee.y)*40*dt;
// foot drag
leg.footV.x *= 0.98; leg.footV.y *= 0.995;
leg.kneeV.x *= 0.995; leg.kneeV.y *= 0.998;
}
for(const arm of p.arms){
const tu2 = V(Math.cos(p.ang), Math.sin(p.ang));
const shDes = add(p.pos, mul(tu2, P.torsoL*0.35));
arm.shV.x += ((shDes.x-arm.sh.x)*120 - arm.shV.x*10)*dt;
arm.shV.y += ((shDes.y-arm.sh.y)*120 - arm.shV.y*10 + GRAV.y)*dt;
arm.sh.x += arm.shV.x*dt; arm.sh.y += arm.shV.y*dt;
arm.elV.y += GRAV.y*dt; arm.elV.x *= 0.999;
arm.el.x += arm.elV.x*dt; arm.el.y += arm.elV.y*dt;
arm.handV.y += GRAV.y*dt; arm.handV.x *= 0.999;
arm.hand.x += arm.handV.x*dt; arm.hand.y += arm.handV.y*dt;
arm.elV.x += (arm.hand.x-arm.el.x)*20*dt;
arm.elV.y += (arm.hand.y-arm.el.y)*20*dt;
}
// torso angular: weak
p.angV *= 0.995;
p.ang += p.angV*dt;
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt;
p.vel.x *= 0.999; p.vel.y *= 0.9995;
}
function updateLimbs(p, dt, surf, up, along){
// shared: keep limbs from drifting absurdly, ground contact for feet
for(const leg of p.legs){
const sf = surfaceAt(leg.foot.x, leg.foot.y + 4);
if(sf && leg.foot.y > sf.y - 6 && leg.foot.y < sf.y + 30){
// on/near surface: push out and friction
if(leg.foot.y > sf.y - 2){
leg.foot.y = sf.y - 2;
if(leg.footV.y > 0) leg.footV.y *= -0.2;
leg.footV.x *= p.down ? 0.90 : 0.6; // friction
}
}
// knee similar
const sk = surfaceAt(leg.knee.x, leg.knee.y + 4);
if(sk && leg.knee.y > sk.y - 2 && leg.knee.y < sk.y + 30){
leg.knee.y = sk.y - 2;
if(leg.kneeV.y > 0) leg.kneeV.y *= -0.2;
leg.kneeV.x *= 0.85;
}
}
}
/* ---------- person-person & person-world collisions ---------- */
function bodyPoints(p){
const pts = [];
pts.push({x:p.pos.x, y:p.pos.y, r:13, p}); // torso
pts.push({x:p.head.x, y:p.head.y, r:P.headR+2, p}); // head
for(const leg of p.legs){
pts.push({x:leg.hip.x, y:leg.hip.y, r:6, p});
pts.push({x:leg.knee.x, y:leg.knee.y, r:6, p});
pts.push({x:leg.foot.x, y:leg.foot.y, r:5, p});
}
for(const a of p.arms){
pts.push({x:a.sh.x, y:a.sh.y, r:5, p});
pts.push({x:a.el.x, y:a.el.y, r:4.5, p});
pts.push({x:a.hand.x, y:a.hand.y, r:4, p});
}
return pts;
}
function resolvePeople(dt){
const all = [];
for(const p of people) for(const pt of bodyPoints(p)) all.push(pt);
// pairwise circle push
for(let i=0;i<all.length;i++){
for(let j=i+1;j<all.length;j++){
const a=all[i], b=all[j];
const d = sub(b,a); const dist = len(d);
const minD = a.r + b.r;
if(dist < minD && dist > 0.001){
const n = mul(d, 1/dist);
const pen = minD - dist;
// push apart, respecting mass-ish (down people heavier)
const ma = a.p.down ? 2.2 : 1.0;
const mb = b.p.down ? 2.2 : 1.0;
const total = ma+mb;
const pushA = mul(n, pen*(mb/total));
const pushB = mul(n, -pen*(ma/total));
// apply to velocities via positional impulse on the owning body part
applyPush(a, pushA, dt);
applyPush(b, pushB, dt);
// also velocity impulse for shove feel
const rel = 900*pen/dt*0.5;
shove(a, mul(n, -rel*(mb/total)), dt);
shove(b, mul(n, rel*(ma/total)), dt);
}
}
}
}
function applyPush(pt, push, dt){
const p = pt.p;
// distribute to nearest joints: simple — push torso if torso/head, else nearest limb node
if(pt.r > 10){ p.pos.x += push.x; p.pos.y += push.y; }
else {
// find which limb
let best=null, bd=1e9;
for(const leg of p.legs){
for(const k of ['hip','knee','foot']){
const d = len(sub(leg[k], {x:pt.x,y:pt.y}));
if(d<bd){bd=d;best=leg[k];}
}
}
for(const a of p.arms){
for(const k of ['sh','el','hand']){
const d = len(sub(a[k], {x:pt.x,y:pt.y}));
if(d<bd){bd=d;best=a[k];}
}
}
if(best){ best.x += push.x; best.y += push.y; }
}
}
function shove(pt, imp, dt){
const p = pt.p;
const m = pt.r > 10 ? 3.0 : 0.4;
if(pt.r > 10){ p.vel.x += imp.x/m; p.vel.y += imp.y/m; p.angV += (Math.random()-0.5)*imp.x*0.002; }
else {
// find owning limb node velocity
let node=null;
for(const leg of p.legs) for(const k of ['hip','knee','foot']) if(leg[k].x===pt.x&&leg[k].y===pt.y) node=leg[k+'V'];
if(!node) for(const a of p.arms) for(const k of ['sh','el','hand']) if(a[k].x===pt.x&&a[k].y===pt.y) node=a[k+'V'];
if(node){ node.x += imp.x/m; node.y += imp.y/m; }
// reaction on torso
p.vel.x -= imp.x/3.0; p.vel.y -= imp.y/3.0;
}
}
/* ---------- step motion: carry planted feet ---------- */
function moveSteps(dt){
// ramp speed toward target
const a = 60; // fast ramp
const diff = targetSpeed - speed;
speed += clamp(diff, -a*dt, a*dt);
stepPos += speed * dt;
// move planted feet with steps
const ds = speed * dt * STEP_W;
for(const p of people){
for(const leg of p.legs){
if(leg.planted){
// planted point is in world coords; steps move along slope
leg.planted.x += dirX*ds;
leg.planted.y += dirY*ds;
}
}
}
}
/* ---------- keep people on the escalator (landings eject) ---------- */
function enforceBounds(){
for(const p of people){
// if torso far beyond top landing, they've "exited" — wrap to bottom? For demo, clamp with soft wall
const s = (p.pos.x-cx())*dirX + (p.pos.y-cy())*dirY;
if(s > LAND_TOP + 260){
// exited top: respawn at bottom
respawn(p, 30 + Math.random()*40);
} else if(s < LAND_BOT - 260){
respawn(p, LAND_TOP - 30 - Math.random()*40);
}
}
}
function respawn(p, s){
const np = makePerson(0,0,s,p.facing);
Object.assign(p, np);
p.id = makePerson._id++;
}
/* ---------- drawing ---------- */
function draw(){
ctx.clearRect(0,0,W,H);
// background
ctx.fillStyle = '#101418'; ctx.fillRect(0,0,W,H);
drawEscalator();
for(const p of people) drawPerson(p);
drawLandings();
}
function drawEscalator(){
// balustrade band under steps
ctx.save();
// slope band
const p0 = slopePoint(-STEP_W, -34), p1 = slopePoint(LAND_TOP+STEP_W, -34);
ctx.strokeStyle = '#2a3542'; ctx.lineWidth = 30; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(p0.x,p0.y); ctx.lineTo(p1.x,p1.y); ctx.stroke();
ctx.strokeStyle = '#3d4c5e'; ctx.lineWidth = 26;
ctx.beginPath(); ctx.moveTo(p0.x,p0.y); ctx.lineTo(p1.x,p1.y); ctx.stroke();
// steps: draw NSTEPS+2 treads at positions stepPos + i
const base = Math.floor(stepPos) - 2;
for(let k=0;k<NSTEPS+4;k++){
const i = base + k;
const s0 = i*STEP_W - stepPos*STEP_W; // world slope coord of step start
// tread from s0 to s0+STEP_W at height 0, riser down
const a = slopePoint(s0, 0), b = slopePoint(s0+STEP_W, 0);
const c = slopePoint(s0+STEP_W, -STEP_H), d = slopePoint(s0, -STEP_H);
ctx.fillStyle = '#55677c';
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.fill();
ctx.strokeStyle = '#7d93ab'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
// riser line
ctx.strokeStyle = '#2c3947';
ctx.beginPath(); ctx.moveTo(b.x,b.y); ctx.lineTo(c.x,c.y); ctx.stroke();
}
// handrail
const h0 = slopePoint(-STEP_W, 40), h1 = slopePoint(LAND_TOP+STEP_W, 40);
ctx.strokeStyle = '#1a222c'; ctx.lineWidth = 10; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(h0.x,h0.y); ctx.lineTo(h1.x,h1.y); ctx.stroke();
ctx.strokeStyle = '#c8a24a'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(h0.x,h0.y); ctx.lineTo(h1.x,h1.y); ctx.stroke();
// comb plates at landings
ctx.fillStyle = '#8a97a8';
const ct = slopePoint(LAND_TOP+2, 0), cb = slopePoint(LAND_BOT-2, 0);
ctx.fillRect(ct.x-14, ct.y-3, 28, 6);
ctx.fillRect(cb.x-14, cb.y-3, 28, 6);
ctx.restore();
}
function drawLandings(){
// flat floors beyond the escalator ends
ctx.fillStyle = '#39434f';
const t1 = slopePoint(LAND_TOP, 0), t2 = slopePoint(LAND_TOP+400, 0);
ctx.fillRect(Math.min(t1.x,t2.x), t1.y-2, Math.abs(t2.x-t1.x), 400);
const b1 = slopePoint(LAND_BOT, 0), b2 = slopePoint(LAND_BOT-400, 0);
ctx.fillRect(Math.min(b1.x,b2.x), b1.y-2, Math.abs(b2.x-b1.x), 400);
// floor line
ctx.strokeStyle = '#55637a'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(t1.x,t1.y); ctx.lineTo(t2.x,t2.y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(b1.x,b1.y); ctx.lineTo(b2.x,b2.y); ctx.stroke();
}
function drawPerson(p){
ctx.save();
ctx.strokeStyle = p.color; ctx.fillStyle = p.color;
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
// legs
ctx.lineWidth = 5;
for(const leg of p.legs){
ctx.beginPath(); ctx.moveTo(leg.hip.x,leg.hip.y); ctx.lineTo(leg.knee.x,leg.knee.y); ctx.lineTo(leg.foot.x,leg.foot.y); ctx.stroke();
ctx.beginPath(); ctx.arc(leg.foot.x,leg.foot.y,3.5,0,7); ctx.fill();
}
// arms
ctx.lineWidth = 4;
for(const a of p.arms){
ctx.beginPath(); ctx.moveTo(a.sh.x,a.sh.y); ctx.lineTo(a.el.x,a.el.y); ctx.lineTo(a.hand.x,a.hand.y); ctx.stroke();
}
// torso
const tu = V(Math.cos(p.ang), Math.sin(p.ang));
const top = add(p.pos, mul(tu, P.torsoL*0.5));
const bot = add(p.pos, mul(tu, -P.torsoL*0.5));
ctx.lineWidth = 9;
ctx.beginPath(); ctx.moveTo(bot.x,bot.y); ctx.lineTo(top.x,top.y); ctx.stroke();
// head
ctx.beginPath(); ctx.arc(p.head.x,p.head.y,P.headR,0,7); ctx.fill();
// face direction nub
ctx.fillStyle = '#101418';
ctx.beginPath(); ctx.arc(p.head.x + tu.x*4*p.facing, p.head.y + tu.y*4*p.facing, 1.6, 0, 7); ctx.fill();
ctx.restore();
}
/* ---------- main loop ---------- */
let last = performance.now();
function frame(now){
let dt = (now-last)/1000; last = now;
dt = Math.min(dt, 1/30);
moveSteps(dt);
const steps = SUB;
const sdt = dt/steps;
for(let s=0;s<steps;s++){
for(const p of people) stepPerson(p, sdt);
resolvePeople(sdt);
// world: keep bodies above landings/steps roughly (feet handled in updateLimbs)
enforceBounds();
}
draw();
updateHud();
requestAnimationFrame(frame);
}
const stateEl = document.getElementById('state');
function updateHud(){
const down = people.filter(p=>p.down).length;
stateEl.textContent = `speed ${speed.toFixed(2)} steps/s · ${down} down · ${people.length} riding`;
}
document.getElementById('rev').addEventListener('click', ()=>{
if(targetSpeed === SPEED_NORM){ targetSpeed = -SPEED_REV; document.getElementById('rev').textContent = 'Restore escalator'; }
else { targetSpeed = SPEED_NORM; document.getElementById('rev').textContent = 'Reverse escalator'; }
});
initPeople();
requestAnimationFrame(t=>{last=t; frame(t);});
})();
</script>
</body>
</html>
When you press Reverse, the step speed target flips to about −3.4 steps/sec and the ramp reaches it in a fraction of a second. Every planted foot is welded to its step, so the steps yank the riders' feet down-slope while their torsos want to stay put; the ankle/hip servo saturates at maxMuscle, balance fails, and the figures tip. Because everyone is facing up-slope and the steps now move the other way, the people at the top of the pile get carried backward into the people behind them, and the collisions cascade down the slope — exactly the emergent pile you asked for, with no per-figure scripting.
What I special-cased or nudged, honestly:
velocity *= 0.6 when planted and 0.90 when down, rather than a proper friction cone. It reads fine at this scale but it's a cheat.What it does badly: figures can occasionally jitter while standing (the foot spring and torso servo fight each other), the ragdoll limbs go a bit noodly when someone slides, and two people can slowly sink into each other because circle-push is positional only with a weak velocity impulse. The pile at the bottom tends to form a horizontal heap rather than a realistic tangle, and figures sometimes "surf" sideways on top of the pile instead of tumbling off it. The reversal is very fast, so on the first press most of the upper two-thirds of the escalator falls at once — press it back and they mostly recover, which is the fun part to watch.