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 the same three-particle chain — foot, hip and head joined by two inextensible segments — integrated with position-based dynamics under gravity, so a standing figure is an actively balanced inverted pendulum: a limited-strength controller pulls hip and head toward the upright position above the foot, a limited stepping reflex moves the foot toward the head's projection when the lean grows, and friction with the step surface (itself limited) drags the foot with the escalator. Losing balance is never triggered directly; it happens when the surface accelerates faster than friction and the balance controller can correct, so the head lags, the lean passes the angle the controller and a catch-step can recover from, and gravity finishes the fall. When particles from different people overlap they push each other apart positionally, so a downed figure's foot, hip and head are simply three obstacles lying across the slope that later arrivals trip over, slide into and stack against. A fallen figure whose particles have slowed, who has been down briefly, and whose upright target space is not occupied by anyone else's particles re-enables a stronger version of the same upright controller to get back up; if the space is occupied, 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 Reversal — an emergent pile-up</title>
<style>
:root{ --ink:#1c2430; --paper:#f4efe6; --accent:#c8342a; --accent2:#1f6f4a; }
*{ box-sizing:border-box; }
body{ margin:0; background:#20242c; color:var(--ink);
font-family: Georgia, 'Times New Roman', serif; }
.wrap{ max-width:1040px; margin:0 auto; padding:18px 14px 26px; }
header{ color:#f2ead9; display:flex; flex-wrap:wrap; align-items:baseline; gap:10px 18px; }
h1{ font-size:26px; margin:0; letter-spacing:.5px; font-weight:normal; }
header p{ margin:0; font-style:italic; color:#b9c0cc; font-size:14px; }
.panel{ background:var(--paper); border-radius:10px; padding:12px; margin-top:12px;
box-shadow:0 10px 30px rgba(0,0,0,.45); }
.controls{ display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom:10px; }
button{ font-family:inherit; font-size:17px; cursor:pointer; border-radius:8px;
border:2px solid #14181f; padding:10px 18px; background:var(--accent2); color:#fff;
box-shadow:0 3px 0 #14181f; }
button:active{ transform:translateY(2px); box-shadow:0 1px 0 #14181f; }
button.reverse{ background:var(--accent); }
button.small{ font-size:13px; padding:7px 12px; background:#e7dfcd; color:var(--ink); }
.readout{ font-size:14px; background:#fffdf6; border:1px solid #d8cfb8; border-radius:8px;
padding:8px 12px; min-width:230px; }
.readout b{ font-variant-numeric:tabular-nums; }
.hint{ font-size:13px; color:#5a5344; font-style:italic; }
canvas{ display:block; width:100%; height:auto; background:#cfdde8; border-radius:8px;
border:1px solid #9fb2c2; }
.legend{ display:flex; flex-wrap:wrap; gap:6px 20px; font-size:13px; color:#44403a; margin-top:9px; }
.sw{ display:inline-block; width:11px; height:11px; border-radius:50%; margin-right:5px; vertical-align:-1px; }
footer{ color:#9aa3b1; font-size:12.5px; margin-top:12px; line-height:1.5; }
kbd{ background:#333a45; border:1px solid #59636f; border-bottom-width:2px; border-radius:4px;
padding:0 5px; color:#eee; font-family:inherit; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>The Escalator Reverses</h1>
<p>seventeen inverted pendulums, one bad afternoon</p>
</header>
<div class="panel">
<div class="controls">
<button id="btnReverse">⚠ THROW INTO REVERSE</button>
<button id="btnReset" class="small">↺ Reset riders</button>
<div class="readout" id="readout">steps: —</div>
</div>
<canvas id="cv" width="1000" height="620"></canvas>
<div class="legend">
<span><span class="sw" style="background:#3f7fbf"></span>standing / riding</span>
<span><span class="sw" style="background:#e09a26"></span>stumbling</span>
<span><span class="sw" style="background:#c8342a"></span>down</span>
<span><span class="sw" style="background:#7a4fa3"></span>getting up</span>
<span class="hint">Press <kbd>Space</kbd> to reverse. Nobody is told to fall — watch the feet.</span>
</div>
</div>
<footer>
Model, in brief: every rider is a foot–hip–head chain held up by a strength-limited balance
controller and a catch-step reflex, standing on a surface whose friction is also limited.
Reverse the surface fast enough and the feet are dragged out from under the heads.
Bodies collide as circles; a body on the slope is just an obstacle. Riders who find clear
space above them and a moment of stillness haul themselves upright again.
</footer>
</div>
<script>
'use strict';
/* ============================== world ============================== */
var W = 1000, H = 620;
var GRAV = 950; // px / s^2
var DT = 1/120; // physics substep
var INCL_X0 = 200, INCL_X1 = 640, FLOOR_B = 520, FLOOR_T = 230;
var IDX = INCL_X1-INCL_X0, IDY = FLOOR_T-FLOOR_B;
var ILEN = Math.hypot(IDX, IDY), IUX = IDX/ILEN, IUY = IDY/ILEN; // up-slope unit
function surfY(x){
if (x < INCL_X0) return FLOOR_B;
if (x > INCL_X1) return FLOOR_T;
return FLOOR_B + IDY * (x - INCL_X0) / IDX;
}
function surfVel(x, out){
if (x < INCL_X0 || x > INCL_X1){ out.x = curSpeed; out.y = 0; }
else { out.x = curSpeed * IUX; out.y = curSpeed * IUY; }
return out;
}
/* ============================== escalator drive ============================== */
var SPEED_NORMAL = 38, SPEED_REVERSE = -175, DRIVE_ACCEL = 820;
var curSpeed = SPEED_NORMAL, targetSpeed = SPEED_NORMAL, reversed = false;
var travel = 0; // integrated step travel, for drawing the steps
/* ============================== people ============================== */
var SEG1 = 26, SEG2 = 28, BODY = SEG1 + SEG2;
var HUES = [210, 16, 130, 265, 35, 190, 330, 90, 0, 240, 55, 160, 290, 110, 20, 200, 310];
var people = [];
function mkParticle(x, y, r, vx, vy){
return { x:x, y:y, px:x - vx*DT, py:y - vy*DT, r:r };
}
function makePerson(x, i){
var y = surfY(x), sv = surfVel(x, {x:0,y:0});
return {
p: [ mkParticle(x, y-4, 6, sv.x, sv.y),
mkParticle(x, y-4-SEG1, 9, sv.x, sv.y),
mkParticle(x, y-4-BODY, 10, sv.x, sv.y) ],
hue: HUES[i % HUES.length],
phase: Math.random()*Math.PI*2,
fallenTime: 0, getUp: 0, stepTime: 0, stepCd: 0, stepTarget: x,
grounded: false
};
}
function resetPeople(){
people = [];
for (var i = 0; i < 17; i++) people.push(makePerson(212 + i*27, i));
}
resetPeople();
function velX(p){ return (p.x - p.px)/DT; }
function velY(p){ return (p.y - p.py)/DT; }
function leanOf(per){ // signed angle of foot->head from vertical, radians
return Math.atan2(per.p[2].x - per.p[0].x, -(per.p[2].y - per.p[0].y));
}
/* ============================== physics ============================== */
var tmpV = {x:0, y:0};
function applyController(per){
var f = per.p[0], hip = per.p[1], head = per.p[2];
var lean = leanOf(per), aLean = Math.abs(lean);
per.stepCd -= DT;
if (per.getUp > 0) per.getUp -= DT;
if (per.stepTime > 0) per.stepTime -= DT;
// --- stepping reflex: shuffle the foot toward under the head ---
if (per.grounded && aLean > 0.20 && aLean < 1.0 && per.stepCd <= 0 && per.getUp <= 0){
per.stepCd = 0.28; per.stepTime = 0.15;
per.stepTarget = head.x + Math.sign(lean)*8;
}
if (per.stepTime > 0){
var dx = per.stepTarget - f.x, mx = 260*DT;
f.x += Math.max(-mx, Math.min(mx, dx));
f.y -= 26*DT; // little lift while stepping
}
// --- balance / get-up controller (limited strength) ---
var rising = per.getUp > 0;
if (!per.grounded && !rising) return;
if (aLean > 1.15 && !rising) return; // too far gone: controller gives up
var fade = rising ? 1 : Math.max(0, 1 - aLean/1.25);
var kP = (rising ? 210 : 120) * fade, kD = rising ? 14 : 11;
var capH = (rising ? 2600 : 1500) * Math.max(fade, rising?1:0.15);
var capM = capH * 0.6;
var ex = (f.x) - head.x, ey = (f.y - BODY) - head.y;
var ax = ex*kP - (velX(head)-velX(f))*kD;
var ay = ey*kP - (velY(head)-velY(f))*kD;
ax = Math.max(-capH, Math.min(capH, ax)); ay = Math.max(-capH, Math.min(capH, ay));
head.px -= ax*DT*DT; head.py -= ay*DT*DT;
var ex2 = (f.x) - hip.x, ey2 = (f.y - SEG1) - hip.y;
var bx = ex2*kP*0.8 - (velX(hip)-velX(f))*kD;
var by = ey2*kP*0.8 - (velY(hip)-velY(f))*kD;
bx = Math.max(-capM, Math.min(capM, bx)); by = Math.max(-capM, Math.min(capM, by));
hip.px -= bx*DT*DT; hip.py -= by*DT*DT;
if (rising){ // plant the foot while hauling up
surfVel(f.x, tmpV);
f.px = f.x - tmpV.x*DT; f.py = f.y - tmpV.y*DT;
}
}
function clearanceFor(per){
var f = per.p[0];
var tx = f.x, tyH = f.y - BODY, tyM = f.y - SEG1;
for (var i = 0; i < people.length; i++){
var o = people[i]; if (o === per) continue;
for (var j = 0; j < 3; j++){
var q = o.p[j];
if (Math.hypot(q.x-tx, q.y-tyH) < 30) return false;
if (Math.hypot(q.x-tx, q.y-tyM) < 24) return false;
}
}
return true;
}
function maybeGetUp(per){
var aLean = Math.abs(leanOf(per));
if (aLean > 1.05){
per.fallenTime += DT;
if (per.fallenTime > 0.6 && per.getUp <= 0){
var sp = 0;
for (var j = 0; j < 3; j++) sp += Math.hypot(velX(per.p[j]), velY(per.p[j]));
if (sp/3 < 60 && clearanceFor(per)) per.getUp = 0.9;
}
} else per.fallenTime = 0;
}
function integrate(per){
for (var j = 0; j < 3; j++){
var p = per.p[j];
var vx = (p.x - p.px) * 0.999, vy = (p.y - p.py) * 0.999;
p.px = p.x; p.py = p.y;
p.x += vx; p.y += vy + GRAV*DT*DT;
}
}
function constrainSegment(a, b, rest){
var dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy) || 0.0001;
var diff = (d - rest)/d * 0.5;
a.x += dx*diff; a.y += dy*diff;
b.x -= dx*diff; b.y -= dy*diff;
}
function groundParticle(per, idx){
var p = per.p[idx];
if (idx === 0 && per.stepTime > 0) return; // foot is mid-step
var y0 = surfY(p.x);
var m = (surfY(p.x+3) - surfY(p.x-3)) / 6;
var inv = 1/Math.hypot(1, m);
var nx = m*inv, ny = -inv; // surface normal (points up)
var dist = (p.y - y0) * ny; // approx signed distance
if (dist < p.r){
p.x += nx * (p.r - dist);
p.y += ny * (p.r - dist);
}
}
function applyGroundFriction(per, idx){
var p = per.p[idx];
var y0 = surfY(p.x);
var m = (surfY(p.x+3) - surfY(p.x-3)) / 6;
var inv = 1/Math.hypot(1, m);
var tx = inv, ty = m*inv, nx = m*inv, ny = -inv;
if ((p.y - y0) * ny > p.r + 1.5) { if (idx===0) per.grounded = false; return; }
if (idx === 0) per.grounded = true;
if (idx === 0 && per.stepTime > 0) return;
var vx = velX(p), vy = velY(p);
var vn = vx*nx + vy*ny, vt = vx*tx + vy*ty;
surfVel(p.x, tmpV);
var svn = tmpV.x*nx + tmpV.y*ny, svt = tmpV.x*tx + tmpV.y*ty;
if (vn < svn) vn = svn; // no sinking into the step
var maxF = (idx === 0 ? 1150 : 520) * DT; // friction limit: feet grip, bodies skid
var dvt = Math.max(-maxF, Math.min(maxF, svt - vt));
vt += dvt;
var nvx = tx*vt + nx*vn, nvy = ty*vt + ny*vn;
p.px = p.x - nvx*DT; p.py = p.y - nvy*DT;
}
function collide(){
for (var i = 0; i < people.length; i++)
for (var j = i+1; j < people.length; j++)
for (var a = 0; a < 3; a++)
for (var b = 0; b < 3; b++){
var p = people[i].p[a], q = people[j].p[b];
var dx = q.x-p.x, dy = q.y-p.y, min = (p.r+q.r)*0.92;
var d2 = dx*dx + dy*dy;
if (d2 < min*min && d2 > 0.01){
var d = Math.sqrt(d2), push = (min-d)/d * 0.5;
p.x -= dx*push; p.y -= dy*push;
q.x += dx*push; q.y += dy*push;
}
}
}
function stepPhysics(){
// drive the steps toward the target speed with a hard, finite acceleration
var dv = targetSpeed - curSpeed;
var mx = DRIVE_ACCEL * DT;
curSpeed += Math.max(-mx, Math.min(mx, dv));
travel += curSpeed * DT;
var i, per;
for (i = 0; i < people.length; i++){ per = people[i]; applyController(per); maybeGetUp(per); }
for (i = 0; i < people.length; i++) integrate(people[i]);
for (var it = 0; it < 4; it++){
for (i = 0; i < people.length; i++){
per = people[i];
constrainSegment(per.p[0], per.p[1], SEG1);
constrainSegment(per.p[1], per.p[2], SEG2);
}
collide();
for (i = 0; i < people.length; i++){
per = people[i];
for (var j = 0; j < 3; j++) groundParticle(per, j);
for (j = 0; j < 3; j++){
var p = per.p[j];
if (p.x < 10){ p.x = 10; } if (p.x > W-10){ p.x = W-10; }
if (p.y > H-6){ p.y = H-6; }
}
}
}
for (i = 0; i < people.length; i++){
per = people[i];
for (var k = 0; k < 3; k++) applyGroundFriction(per, k);
}
}
/* riders who walk off a landing re-enter at the other end, so the car stays full */
function wrapRiders(){
for (var i = 0; i < people.length; i++){
var per = people[i], f = per.p[0];
if (Math.abs(leanOf(per)) > 0.45) continue; // only the upright leave
if (f.x > W-16 && f.y < FLOOR_T + 40) placeAt(per, 24);
else if (f.x < 16 && f.y > FLOOR_B - 40) placeAt(per, W-24);
}
}
function placeAt(per, x){
var y = surfY(x), sv = surfVel(x, {x:0,y:0});
var ys = [y-4, y-4-SEG1, y-4-BODY];
for (var j = 0; j < 3; j++){
per.p[j].x = x; per.p[j].y = ys[j];
per.p[j].px = x - sv.x*DT; per.p[j].py = ys[j] - sv.y*DT;
}
per.fallenTime = 0; per.getUp = 0;
}
/* ============================== drawing ============================== */
var cv = document.getElementById('cv'), ctx = cv.getContext('2d');
function drawEscalator(){
// truss / side panel
ctx.fillStyle = '#8d99a6';
ctx.beginPath();
ctx.moveTo(0, FLOOR_B+8); ctx.lineTo(INCL_X0, FLOOR_B+8);
ctx.lineTo(INCL_X1, FLOOR_T+8); ctx.lineTo(W, FLOOR_T+8);
ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#6e7a88';
ctx.fillRect(0, FLOOR_B+8, INCL_X0, H-FLOOR_B);
ctx.fillRect(INCL_X1, FLOOR_T+8, W-INCL_X1, H-FLOOR_T);
// landings
ctx.fillStyle = '#aeb9c4';
ctx.fillRect(0, FLOOR_B, INCL_X0, 8); ctx.fillRect(INCL_X1, FLOOR_T, W-INCL_X1, 8);
ctx.fillStyle = '#7c8894';
for (var x = 6; x < INCL_X0-8; x += 10) ctx.fillRect(x, FLOOR_B, 5, 3);
for (x = INCL_X1+6; x < W-8; x += 10) ctx.fillRect(x, FLOOR_T, 5, 3);
// moving steps along the incline
var step = 30, off = ((travel % step) + step) % step;
ctx.fillStyle = '#5d6873';
ctx.fillRect(INCL_X0, FLOOR_B+34, 0, 0); // (keeps path state simple)
ctx.beginPath(); // return run, underneath
ctx.moveTo(INCL_X0 + (-IDY/ILEN)*46, FLOOR_B + (IDX/ILEN)*46);
ctx.lineTo(INCL_X1 + (-IDY/ILEN)*46, FLOOR_T + (IDX/ILEN)*46);
ctx.strokeStyle = '#77828e'; ctx.lineWidth = 16; ctx.stroke();
for (var s = -off; s < ILEN; s += step){
var bx = INCL_X0 + IUX*s, by = FLOOR_B + IUY*s;
ctx.fillStyle = '#93a1ae';
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx + IUX*step*0.92, by + IUY*step*0.92);
ctx.lineTo(bx + IUX*step*0.92, by + IUY*step*0.92 + 13);
ctx.lineTo(bx, by + 13);
ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#4c565f'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.beginPath(); // tread line
ctx.moveTo(bx, by); ctx.lineTo(bx + IUX*step*0.92, by + IUY*step*0.92);
ctx.strokeStyle = '#39424b'; ctx.lineWidth = 3; ctx.stroke();
}
// handrail with moving grip marks
ctx.strokeStyle = '#2e3742'; ctx.lineWidth = 7; ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(8, FLOOR_B-84); ctx.lineTo(INCL_X0, FLOOR_B-84);
ctx.lineTo(INCL_X1, FLOOR_T-84); ctx.lineTo(W-8, FLOOR_T-84);
ctx.stroke();
ctx.strokeStyle = 'rgba(240,240,230,.55)'; ctx.lineWidth = 2.5;
var hoff = ((travel % 46) + 46) % 46;
ctx.beginPath();
for (var hx = -hoff + 20; hx < INCL_X0; hx += 46){ ctx.moveTo(hx, FLOOR_B-84); ctx.lineTo(hx+16, FLOOR_B-84); }
for (var hs = -hoff; hs < ILEN; hs += 46){
var hxp = INCL_X0 + IUX*hs, hyp = FLOOR_B-84 + IUY*hs;
ctx.moveTo(hxp, hyp); ctx.lineTo(hxp + IUX*16, hyp + IUY*16);
}
for (hx = INCL_X1 - hoff; hx < W-20; hx += 46){ ctx.moveTo(hx, FLOOR_T-84); ctx.lineTo(hx+16, FLOOR_T-84); }
ctx.stroke();
// balustrade posts
ctx.strokeStyle = 'rgba(46,55,66,.5)'; ctx.lineWidth = 3;
ctx.beginPath();
for (var px2 = 60; px2 < W; px2 += 120){
ctx.moveTo(px2, surfY(px2)); ctx.lineTo(px2, surfY(px2)-84);
}
ctx.stroke(); ctx.lineCap = 'butt';
}
function drawPerson(per, t){
var f = per.p[0], hip = per.p[1], head = per.p[2];
var lean = leanOf(per), aLean = Math.abs(lean);
var state = per.getUp > 0 ? 3 : (aLean < 0.38 ? 0 : (aLean < 1.0 ? 1 : 2));
var shirt = 'hsl(' + per.hue + ',55%,' + (state===2 ? '38%' : '45%') + ')';
// shadow
ctx.fillStyle = 'rgba(30,40,50,.18)';
ctx.beginPath();
ctx.ellipse(hip.x, surfY(hip.x)+5, 16, 4, 0, 0, Math.PI*2); ctx.fill();
var kneeX = (hip.x+f.x)/2 + Math.sin(lean)*6 + (per.stepTime>0 ? Math.sign(lean||1)*5 : 0);
var kneeY = (hip.y+f.y)/2 - (per.stepTime>0 ? 5 : 0);
var neckX = hip.x + (head.x-hip.x)*0.72, neckY = hip.y + (head.y-hip.y)*0.72;
var hx = head.x + (head.x-hip.x)*0.16, hy = head.y + (head.y-hip.y)*0.16;
ctx.lineCap = 'round';
ctx.strokeStyle = '#2b2f36'; ctx.lineWidth = 5; // legs
ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(kneeX, kneeY); ctx.lineTo(f.x, f.y); ctx.stroke();
ctx.strokeStyle = shirt; ctx.lineWidth = 8; // torso
ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(neckX, neckY); ctx.stroke();
// arms: swing with lean rate and flail when down — purely cosmetic, driven by the sim state
var flail = state===2 ? Math.sin(t*13 + per.phase)*0.7 : 0;
var armA = lean*1.5 + flail + Math.sin(t*2 + per.phase)*0.06;
ctx.strokeStyle = shirt; ctx.lineWidth = 4.5;
ctx.beginPath();
ctx.moveTo(neckX, neckY);
ctx.lineTo(neckX + Math.sin(armA)*21, neckY + Math.cos(armA)*21);
ctx.moveTo(neckX, neckY);
ctx.lineTo(neckX + Math.sin(armA+2.6)*19, neckY + Math.cos(armA+2.6)*19);
ctx.stroke();
ctx.fillStyle = '#eec39a'; // head
ctx.beginPath(); ctx.arc(hx, hy, 9, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#2b2f36'; ctx.lineWidth = 1.6; ctx.stroke();
ctx.fillStyle = '#2b2f36'; // eye, on the downhill-awareness side
ctx.beginPath(); ctx.arc(hx + (lean>=0?3:-3), hy-1, 1.4, 0, Math.PI*2); ctx.fill();
return state;
}
function draw(t){
ctx.clearRect(0, 0, W, H);
var g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#d7e5ef'); g.addColorStop(1, '#b9cbd9');
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(255,255,255,.35)'; // distant windows
for (var i = 0; i < 7; i++) ctx.fillRect(40 + i*140, 40, 84, 110);
drawEscalator();
var counts = [0,0,0,0];
for (var i = 0; i < people.length; i++) counts[drawPerson(people[i], t)]++;
// direction arrows on the truss
ctx.fillStyle = reversed ? '#c8342a' : '#1f6f4a';
ctx.font = 'bold 15px Georgia';
var label = reversed ? '◀◀ STEPS RUNNING DOWNHILL — FAST' : '▶ steps running uphill — normal';
ctx.fillText(label, 18, H-14);
return counts;
}
/* ============================== ui / main loop ============================== */
var btn = document.getElementById('btnReverse');
var readout = document.getElementById('readout');
function setReversed(r){
reversed = r;
targetSpeed = r ? SPEED_REVERSE : SPEED_NORMAL;
btn.textContent = r ? '↩ PUT IT BACK' : '⚠ THROW INTO REVERSE';
btn.className = r ? '' : 'reverse';
}
btn.addEventListener('click', function(){ setReversed(!reversed); });
document.getElementById('btnReset').addEventListener('click', function(){
setReversed(false); curSpeed = SPEED_NORMAL; travel = 0; resetPeople();
});
window.addEventListener('keydown', function(e){
if (e.code === 'Space'){ e.preventDefault(); setReversed(!reversed); }
});
setReversed(false);
var last = performance.now(), acc = 0, uiTick = 0;
function frame(now){
var ft = Math.min(0.05, (now - last)/1000); last = now;
acc += ft;
while (acc >= DT){ stepPhysics(); acc -= DT; }
wrapRiders();
var counts = draw(now/1000);
if (++uiTick % 6 === 0){
readout.innerHTML =
'step speed <b>' + Math.round(curSpeed) + ' px/s</b> ' +
(curSpeed > 4 ? '(uphill)' : curSpeed < -4 ? '(DOWNHILL)' : '(stopped)') +
' · standing <b>' + counts[0] + '</b>' +
' · stumbling <b>' + counts[1] + '</b>' +
' · down <b>' + counts[2] + '</b>' +
' · getting up <b>' + counts[3] + '</b>';
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
Press the button and the step speed slews from +38 px/s to −175 px/s at 820 px/s², so the reversal itself takes about a quarter of a second. The feet, gripped by the stronger foot friction, are dragged downhill with the steps; the heads are not, so every rider leans uphill, the balance controllers saturate, a few catch-steps fire and mostly fail, and the whole line goes over more or less together — the uphill riders tend to land on the slope and slide, the ones lower down get arrived-into by the people above them, and a heap forms and keeps shuffling toward the bottom landing, where bodies stack against the end of the truss and each other. Riders in the heap who find stillness and clear space above their feet haul themselves up (they turn purple while doing it), often just in time to be knocked down again by the next arrival; riders who recover on the moving slope get carried down and may fall a second time. Pressing the button again restores the uphill drive, and the survivors gradually sort themselves out and ride up. Special cases, honestly declared: the only true teleport is at the landings — a rider who is upright when they walk off one end is re-placed upright at the other, purely to keep the escalator populated; the get-up is a stronger, temporarily re-enabled version of the standing controller gated by a speed-and-clearance check, not an animation; the stepping reflex is a scripted reflex rule (same for everyone, triggered by lean, not by the clock); and the arms, knees and eye are cosmetic, driven by lean and state but feeding nothing back into the physics. What it does badly: the three-circle body has no real limbs, so figures in a dense pile can interpenetrate at the joints, fold into improbable pretzels, and jitter as the constraint solver argues with itself; there is no head-impact or injury model, so everyone is indestructibly cheerful and will keep trying to stand in a space a real person would give up on; the friction at the two landing corners is approximate, so bodies occasionally snag or pop slightly at the comb plates; and because the balance controller is tuned to be just competent, on the normal setting riders sway a little drunkenly and, if you leave it running long enough, someone will eventually trip over a neighbour's foot at the top landing for no dramatic reason at all.