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 rider is a single 2D rigid capsule (a 1.5 m segment with a 0.15 m radius, 72 kg, inertia ≈ mL²/10) with position, angle θ from vertical, linear and angular velocity. Balance is an inverted pendulum driven by a saturating PD torque (max 200 N·m) that holds θ near zero; the same torque is what fails. When the band accelerates, friction at the foot contact drags the feet down-slope, the body's inertia lags, and the resulting torque exceeds what the controller can produce — θ runs away past ~60° and the rider is flagged down. Two riders in the same place are resolved by sequential impulses on the closest points of their capsules (normal + Coulomb friction, μ≈0.55), with a translation-only position pass; a downed rider is simply a horizontal capsule, so it is an obstacle by construction, not by rule.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator Reversal</title>
<style>
html,body{margin:0;height:100%;background:#070a0e;overflow:hidden;
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;}
#c{display:block;width:100vw;height:100vh;}
#ui{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);
display:flex;gap:16px;align-items:center;pointer-events:none;z-index:5;}
#btn{pointer-events:auto;font:600 13px/1 inherit;letter-spacing:.09em;
text-transform:uppercase;color:#e8eef7;background:#1b2432;
border:1px solid #33445c;border-radius:999px;padding:14px 28px;
cursor:pointer;transition:background .15s,border-color .15s,transform .08s,color .15s;}
#btn:hover{background:#243044;border-color:#4a6285;}
#btn:active{transform:scale(.97);}
#btn.rev{background:#3a1620;border-color:#8a2f42;color:#ffd7de;}
#hud{position:fixed;left:18px;top:16px;color:#5d6f88;
font:500 12px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;
letter-spacing:.05em;pointer-events:none;z-index:5;}
#hud b{color:#9fb4cf;font-weight:600;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud"></div>
<div id="ui"><button id="btn">Reverse</button></div>
<script>
(function () {
'use strict';
/* ------------------------------------------------------------------ */
/* canvas / camera */
/* ------------------------------------------------------------------ */
var cv = document.getElementById('c');
var ctx = cv.getContext('2d');
var hud = document.getElementById('hud');
var btn = document.getElementById('btn');
var DPR = Math.min(2, window.devicePixelRatio || 1);
var W = 1, H = 1;
var VIEW = { x0: -4.4, y0: -1.4, w: 17.6, h: 9.0 };
var scale = 1, ox = 0, oy = 0;
function resize() {
W = window.innerWidth; H = window.innerHeight;
cv.width = Math.round(W * DPR);
cv.height = Math.round(H * DPR);
cv.style.width = W + 'px';
cv.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
scale = Math.min(W / VIEW.w, H / VIEW.h);
ox = (W - VIEW.w * scale) / 2 - VIEW.x0 * scale;
oy = (H + VIEW.h * scale) / 2 + VIEW.y0 * scale;
}
function sx(x) { return ox + x * scale; }
function sy(y) { return oy - y * scale; }
/* ------------------------------------------------------------------ */
/* geometry */
/* ------------------------------------------------------------------ */
var G = 9.81;
var ANG = Math.PI / 6;
var CA = Math.cos(ANG), SA = Math.sin(ANG);
var INCL_LEN = 10.4;
var TOP_X = INCL_LEN * CA; // 9.0067
var TOP_Y = INCL_LEN * SA; // 5.2
var BOT_X = -3.5;
var TOP_END_X = TOP_X + 3.0;
var PATH_LEN = (0 - BOT_X) + INCL_LEN + (TOP_END_X - TOP_X);
/* band state */
var bandSpeed = 0.45;
var bandTarget = 0.45;
var bandPhase = 0;
var reversed = false;
/* surface query — returns a shared object, read immediately */
var SURF = { y: 0, nx: 0, ny: 1, tx: 1, ty: 0, vx: 0, vy: 0 };
function surfaceAt(x) {
if (x < 0) {
SURF.y = 0; SURF.nx = 0; SURF.ny = 1; SURF.tx = 1; SURF.ty = 0;
SURF.vx = bandSpeed; SURF.vy = 0;
} else if (x <= TOP_X) {
SURF.y = x * SA / CA;
SURF.nx = -SA; SURF.ny = CA; SURF.tx = CA; SURF.ty = SA;
SURF.vx = bandSpeed * CA; SURF.vy = bandSpeed * SA;
} else {
SURF.y = TOP_Y; SURF.nx = 0; SURF.ny = 1; SURF.tx = 1; SURF.ty = 0;
SURF.vx = bandSpeed; SURF.vy = 0;
}
return SURF;
}
function surfDist(px, py) {
if (px < 0) return py;
if (px <= TOP_X) return -px * SA + py * CA;
return py - TOP_Y;
}
/* ------------------------------------------------------------------ */
/* tuning */
/* ------------------------------------------------------------------ */
var KP = 900, KD = 220, TMAX = 200; // normal balance
var KP_UP = 900, KD_UP = 220, TMAX_UP = 800; // standing back up
var MU_SURF = 0.85, MU_BODY = 0.55, MU_WALL = 0.5;
var DOWN_ANGLE = 1.05; // rad from vertical -> down
var UP_ANGLE = 0.50; // rad from vertical -> recovered
/* ------------------------------------------------------------------ */
/* person */
/* ------------------------------------------------------------------ */
var PALETTE = [
['#d9534f','#f0c8a0'], ['#4a90d9','#e8b98a'], ['#e0a83a','#f0c8a0'],
['#5cb85c','#d9a878'], ['#9b6bd6','#f0c8a0'], ['#e08a4a','#e8b98a'],
['#3fb8b0','#f0c8a0'], ['#d05a8a','#d9a878'], ['#7a8fe0','#f0c8a0'],
['#b8b83a','#e8b98a'], ['#c96a3a','#f0c8a0'], ['#4aa8a0','#d9a878']
];
function Person(x, y, th) {
this.cx = x; this.cy = y; this.th = th || 0;
this.vx = 0; this.vy = 0; this.om = 0;
this.m = 72;
this.r = 0.15;
this.L = 1.5;
this.invM = 1 / this.m;
this.I = this.m * this.L * this.L / 10;
this.invI = 1 / this.I;
this.downed = false;
this.downTime = 0;
this.phase = Math.random() * 6.2832;
var c = PALETTE[(Math.random() * PALETTE.length) | 0];
this.color = c[0];
this.skin = c[1];
this.alpha = 1;
this.A = { x: 0, y: 0 };
this.B = { x: 0, y: 0 };
this.updateEndpoints();
}
Person.prototype.updateEndpoints = function () {
var dx = Math.sin(this.th), dy = Math.cos(this.th), h = this.L * 0.5;
this.A.x = this.cx - dx * h; this.A.y = this.cy - dy * h;
this.B.x = this.cx + dx * h; this.B.y = this.cy + dy * h;
};
var people = [];
var pendingSpawn = 0;
/* ------------------------------------------------------------------ */
/* closest points between two segments (shared result) */
/* ------------------------------------------------------------------ */
var SEGRES = { c1x: 0, c1y: 0, c2x: 0, c2y: 0 };
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function closestSegSeg(p1, q1, p2, q2) {
var d1x = q1.x - p1.x, d1y = q1.y - p1.y;
var d2x = q2.x - p2.x, d2y = q2.y - p2.y;
var rx = p1.x - p2.x, ry = p1.y - p2.y;
var a = d1x * d1x + d1y * d1y;
var e = d2x * d2x + d2y * d2y;
var f = d2x * rx + d2y * ry;
var s = 0, t = 0, EPS = 1e-9;
if (a <= EPS && e <= EPS) { s = 0; t = 0; }
else if (a <= EPS) { s = 0; t = clamp(f / e, 0, 1); }
else {
var c = d1x * rx + d1y * ry;
if (e <= EPS) { t = 0; s = clamp(-c / a, 0, 1); }
else {
var b = d1x * d2x + d1y * d2y;
var denom = a * e - b * b;
if (denom > EPS) s = clamp((b * f - c * e) / denom, 0, 1);
else s = 0;
t = (b * s + f) / e;
if (t < 0) { t = 0; s = clamp(-c / a, 0, 1); }
else if (t > 1) { t = 1; s = clamp((b - c) / a, 0, 1); }
}
}
SEGRES.c1x = p1.x + d1x * s; SEGRES.c1y = p1.y + d1y * s;
SEGRES.c2x = p2.x + d2x * t; SEGRES.c2y = p2.y + d2y * t;
return SEGRES;
}
/* ------------------------------------------------------------------ */
/* contacts */
/* ------------------------------------------------------------------ */
var contacts = [];
function buildContacts() {
contacts.length = 0;
var n = people.length, i, j, e, pt, s, nx, ny, svx, svy, d, pen, cpx, cpy;
for (i = 0; i < n; i++) {
var p = people[i];
/* --- band surface --- */
for (e = 0; e < 2; e++) {
pt = e === 0 ? p.A : p.B;
s = surfaceAt(pt.x);
nx = s.nx; ny = s.ny; svx = s.vx; svy = s.vy;
d = surfDist(pt.x, pt.y);
pen = p.r - d;
if (pen > -0.02) {
cpx = pt.x - nx * p.r; cpy = pt.y - ny * p.r;
contacts.push({
a: p, b: null, nx: nx, ny: ny,
rax: cpx - p.cx, ray: cpy - p.cy,
rbx: 0, rby: 0,
pen: pen > 0 ? pen : 0,
svx: svx, svy: svy, mu: MU_SURF, jn: 0, jt: 0
});
}
}
/* --- bottom wall --- */
for (e = 0; e < 2; e++) {
pt = e === 0 ? p.A : p.B;
if (pt.x - p.r < BOT_X) {
pen = BOT_X - (pt.x - p.r);
contacts.push({
a: p, b: null, nx: 1, ny: 0,
rax: (pt.x - p.r) - p.cx, ray: pt.y - p.cy,
rbx: 0, rby: 0,
pen: pen, svx: 0, svy: 0, mu: MU_WALL, jn: 0, jt: 0
});
}
}
}
/* --- body vs body --- */
for (i = 0; i < n; i++) {
var A = people[i];
for (j = i + 1; j < n; j++) {
var B = people[j];
var ddx = A.cx - B.cx, ddy = A.cy - B.cy;
var rr = A.L * 0.5 + B.L * 0.5 + A.r + B.r;
if (ddx * ddx + ddy * ddy > rr * rr) continue;
var res = closestSegSeg(A.A, A.B, B.A, B.B);
var ux = res.c1x - res.c2x, uy = res.c1y - res.c2y;
var dist = Math.sqrt(ux * ux + uy * uy);
var minD = A.r + B.r;
if (dist < minD) {
var nnx, nny;
if (dist > 1e-6) { nnx = ux / dist; nny = uy / dist; }
else { nnx = 1; nny = 0; }
var mx = (res.c1x + res.c2x) * 0.5;
var my = (res.c1y + res.c2y) * 0.5;
contacts.push({
a: A, b: B, nx: nnx, ny: nny,
rax: mx - A.cx, ray: my - A.cy,
rbx: mx - B.cx, rby: my - B.cy,
pen: minD - dist,
svx: 0, svy: 0, mu: MU_BODY, jn: 0, jt: 0
});
}
}
}
}
/* ------------------------------------------------------------------ */
/* sequential impulse solver */
/* ------------------------------------------------------------------ */
function solveContacts(iters) {
var nc = contacts.length;
for (var it = 0; it < iters; it++) {
for (var k = 0; k < nc; k++) {
var c = contacts[k];
var a = c.a, b = c.b;
var nx = c.nx, ny = c.ny;
var vax = a.vx - a.om * c.ray;
var vay = a.vy + a.om * c.rax;
var vbx = c.svx, vby = c.svy;
if (b) { vbx = b.vx - b.om * c.rby; vby = b.vy + b.om * c.rbx; }
var rvx = vax - vbx, rvy = vay - vby;
var vn = rvx * nx + rvy * ny;
var rnA = c.rax * ny - c.ray * nx;
var kn = a.invM + rnA * rnA * a.invI;
if (b) {
var rnB = c.rbx * ny - c.rby * nx;
kn += b.invM + rnB * rnB * b.invI;
}
var dj = -vn / kn;
var jn0 = c.jn;
c.jn = Math.max(0, c.jn + dj);
dj = c.jn - jn0;
a.vx += nx * dj * a.invM;
a.vy += ny * dj * a.invM;
a.om += (c.rax * ny - c.ray * nx) * dj * a.invI;
if (b) {
b.vx -= nx * dj * b.invM;
b.vy -= ny * dj * b.invM;
b.om -= (c.rbx * ny - c.rby * nx) * dj * b.invI;
}
/* friction */
vax = a.vx - a.om * c.ray;
vay = a.vy + a.om * c.rax;
vbx = c.svx; vby = c.svy;
if (b) { vbx = b.vx - b.om * c.rby; vby = b.vy + b.om * c.rbx; }
rvx = vax - vbx; rvy = vay - vby;
var tx = -ny, ty = nx;
var vt = rvx * tx + rvy * ty;
var rtA = c.rax * ty - c.ray * tx;
var kt = a.invM + rtA * rtA * a.invI;
if (b) {
var rtB = c.rbx * ty - c.rby * tx;
kt += b.invM + rtB * rtB * b.invI;
}
var djt = -vt / kt;
var maxF = c.mu * c.jn;
var jt0 = c.jt;
c.jt = Math.max(-maxF, Math.min(maxF, c.jt + djt));
djt = c.jt - jt0;
a.vx += tx * djt * a.invM;
a.vy += ty * djt * a.invM;
a.om += (c.rax * ty - c.ray * tx) * djt * a.invI;
if (b) {
b.vx -= tx * djt * b.invM;
b.vy -= ty * djt * b.invM;
b.om -= (c.rbx * ty - c.rby * tx) * djt * b.invI;
}
}
}
}
function correctPositions() {
for (var it = 0; it < 4; it++) {
for (var k = 0; k < contacts.length; k++) {
var c = contacts[k];
if (c.pen <= 0.002) continue;
var a = c.a, b = c.b;
var invSum = a.invM + (b ? b.invM : 0);
if (invSum <= 0) continue;
var corr = Math.min(c.pen - 0.002, 0.25) * 0.45;
var s = corr / invSum;
a.cx += c.nx * s * a.invM;
a.cy += c.ny * s * a.invM;
if (b) {
b.cx -= c.nx * s * b.invM;
b.cy -= c.ny * s * b.invM;
}
c.pen -= corr;
}
}
}
/* ------------------------------------------------------------------ */
/* getting up */
/* ------------------------------------------------------------------ */
var TMP_A = { x: 0, y: 0 }, TMP_B = { x: 0, y: 0 };
function canGetUp(p) {
if (p.downTime < 0.75) return false;
if (Math.abs(p.om) > 3.5) return false;
if (Math.abs(bandSpeed) > 0.7) return false; // footing
var s = surfaceAt(p.cx);
var feetY = s.y + p.r;
TMP_A.x = p.cx; TMP_A.y = feetY;
TMP_B.x = p.cx; TMP_B.y = feetY + p.L;
for (var i = 0; i < people.length; i++) {
var q = people[i];
if (q === p) continue;
var res = closestSegSeg(TMP_A, TMP_B, q.A, q.B);
var dx = res.c1x - res.c2x, dy = res.c1y - res.c2y;
var lim = p.r + q.r + 0.04;
if (dx * dx + dy * dy < lim * lim) return false;
}
return true;
}
/* ------------------------------------------------------------------ */
/* physics step */
/* ------------------------------------------------------------------ */
function physicsStep(dt) {
/* band ramp */
var rate = 22;
if (bandSpeed < bandTarget) bandSpeed = Math.min(bandTarget, bandSpeed + rate * dt);
else if (bandSpeed > bandTarget) bandSpeed = Math.max(bandTarget, bandSpeed - rate * dt);
bandPhase += bandSpeed * dt;
var i, p;
/* forces + balance */
for (i = 0; i < people.length; i++) {
p = people[i];
p.vy -= G * dt;
if (p.downed) {
p.downTime += dt;
if (canGetUp(p)) {
var tq = -KP_UP * p.th - KD_UP * p.om;
if (tq > TMAX_UP) tq = TMAX_UP; else if (tq < -TMAX_UP) tq = -TMAX_UP;
p.om += tq * p.invI * dt;
}
if (Math.abs(p.th) < UP_ANGLE) { p.downed = false; p.downTime = 0; }
} else {
var t2 = -KP * p.th - KD * p.om;
if (t2 > TMAX) t2 = TMAX; else if (t2 < -TMAX) t2 = -TMAX;
p.om += t2 * p.invI * dt;
if (Math.abs(p.th) > DOWN_ANGLE) { p.downed = true; p.downTime = 0; }
}
var ld = 1 - 0.25 * dt;
p.vx *= ld; p.vy *= ld;
p.om *= 1 - 0.5 * dt;
if (p.alpha < 1) p.alpha = Math.min(1, p.alpha + dt * 2.5);
}
for (i = 0; i < people.length; i++) people[i].updateEndpoints();
buildContacts();
solveContacts(10);
for (i = 0; i < people.length; i++) {
p = people[i];
p.cx += p.vx * dt;
p.cy += p.vy * dt;
p.th += p.om * dt;
if (p.th > Math.PI) p.th -= 6.283185307;
else if (p.th < -Math.PI) p.th += 6.283185307;
var sp = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
if (sp > 25) { p.vx *= 25 / sp; p.vy *= 25 / sp; }
if (p.om > 25) p.om = 25; else if (p.om < -25) p.om = -25;
p.updateEndpoints();
}
correctPositions();
for (i = 0; i < people.length; i++) people[i].updateEndpoints();
}
/* ------------------------------------------------------------------ */
/* population flow */
/* ------------------------------------------------------------------ */
function managePopulation() {
for (var i = people.length - 1; i >= 0; i--) {
if (people[i].cx > TOP_END_X + 0.6) {
people.splice(i, 1);
pendingSpawn++;
}
}
if (pendingSpawn > 0 && people.length < 24) {
var x = BOT_X + 0.75, y = 0.15 + 0.75;
var clear = true;
for (var k = 0; k < people.length; k++) {
var q = people[k];
var dx = q.cx - x, dy = q.cy - y;
if (dx * dx + dy * dy < 0.64) { clear = false; break; }
}
if (clear) {
var np = new Person(x, y, 0);
np.alpha = 0;
people.push(np);
pendingSpawn--;
}
}
}
/* ------------------------------------------------------------------ */
/* drawing */
/* ------------------------------------------------------------------ */
function pathPoint(s) {
if (s < 3.5) return { x: BOT_X + s, y: 0, tx: 1, ty: 0 };
s -= 3.5;
if (s < INCL_LEN) return { x: s * CA, y: s * SA, tx: CA, ty: SA };
s -= INCL_LEN;
return { x: TOP_X + s, y: TOP_Y, tx: 1, ty: 0 };
}
function drawEscalator() {
var P = [
{ x: BOT_X, y: 0 },
{ x: 0, y: 0 },
{ x: TOP_X, y: TOP_Y },
{ x: TOP_END_X, y: TOP_Y }
];
var i;
/* glass balustrade */
ctx.beginPath();
ctx.moveTo(sx(P[0].x), sy(P[0].y));
for (i = 1; i < P.length; i++) ctx.lineTo(sx(P[i].x), sy(P[i].y));
for (i = P.length - 1; i >= 0; i--) ctx.lineTo(sx(P[i].x), sy(P[i].y + 1.15));
ctx.closePath();
ctx.fillStyle = 'rgba(110,160,215,0.055)';
ctx.fill();
/* truss */
ctx.beginPath();
ctx.moveTo(sx(P[0].x), sy(P[0].y));
for (i = 1; i < P.length; i++) ctx.lineTo(sx(P[i].x), sy(P[i].y));
for (i = P.length - 1; i >= 0; i--) ctx.lineTo(sx(P[i].x), sy(P[i].y - 0.62));
ctx.closePath();
ctx.fillStyle = '#161d27';
ctx.fill();
ctx.strokeStyle = '#232e3d';
ctx.lineWidth = 1;
ctx.stroke();
/* step ticks */
var pitch = 0.42;
var nSteps = Math.ceil(PATH_LEN / pitch) + 1;
ctx.strokeStyle = '#2c3a4d';
ctx.lineWidth = Math.max(1, scale * 0.022);
ctx.beginPath();
for (var k = 0; k < nSteps; k++) {
var s = k * pitch + bandPhase;
s = ((s % PATH_LEN) + PATH_LEN) % PATH_LEN;
var pt = pathPoint(s);
var ux = -pt.ty, uy = pt.tx;
ctx.moveTo(sx(pt.x), sy(pt.y));
ctx.lineTo(sx(pt.x - ux * 0.17), sy(pt.y - uy * 0.17));
}
ctx.stroke();
/* top edge */
ctx.beginPath();
ctx.moveTo(sx(P[0].x), sy(P[0].y));
for (i = 1; i < P.length; i++) ctx.lineTo(sx(P[i].x), sy(P[i].y));
ctx.strokeStyle = '#3d4f68';
ctx.lineWidth = Math.max(1.5, scale * 0.035);
ctx.stroke();
/* handrail */
ctx.beginPath();
ctx.moveTo(sx(P[0].x), sy(P[0].y + 1.15));
for (i = 1; i < P.length; i++) ctx.lineTo(sx(P[i].x), sy(P[i].y + 1.15));
ctx.strokeStyle = '#2a3646';
ctx.lineWidth = Math.max(2, scale * 0.075);
ctx.lineCap = 'round';
ctx.stroke();
ctx.strokeStyle = '#4a5f7d';
ctx.lineWidth = Math.max(1, scale * 0.025);
ctx.stroke();
/* posts */
ctx.strokeStyle = 'rgba(90,120,155,0.30)';
ctx.lineWidth = Math.max(1, scale * 0.02);
ctx.beginPath();
for (var s2 = 0.6; s2 < PATH_LEN; s2 += 2.2) {
var pp = pathPoint(s2);
ctx.moveTo(sx(pp.x), sy(pp.y));
ctx.lineTo(sx(pp.x), sy(pp.y + 1.15));
}
ctx.stroke();
/* bottom wall */
ctx.fillStyle = '#1a2230';
ctx.fillRect(sx(BOT_X) - scale * 0.14, sy(2.6), scale * 0.14, sy(0) - sy(2.6));
ctx.fillStyle = '#2b3a4e';
ctx.fillRect(sx(BOT_X) - scale * 0.14, sy(2.6), scale * 0.14, scale * 0.05);
}
function drawPeople(t) {
for (var i = 0; i < people.length; i++) {
var p = people[i];
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.translate(sx(p.cx), sy(p.cy));
ctx.rotate(p.th);
ctx.scale(scale, scale);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
var panic = Math.min(1,
Math.abs(p.om) / 3.0 + Math.max(0, Math.abs(p.th) - 0.6) / 1.5);
var sw = Math.sin(t * 11 + p.phase);
/* legs */
ctx.strokeStyle = '#232c38';
ctx.lineWidth = 0.085;
ctx.beginPath();
ctx.moveTo(0, 0.08); ctx.lineTo(-0.11, 0.74);
ctx.moveTo(0, 0.08); ctx.lineTo(0.11, 0.74);
ctx.stroke();
/* torso */
ctx.strokeStyle = p.color;
ctx.lineWidth = 0.19;
ctx.beginPath();
ctx.moveTo(0, -0.44); ctx.lineTo(0, 0.10);
ctx.stroke();
/* arms */
ctx.lineWidth = 0.07;
var aL = -0.20 - panic * (1.45 + 0.5 * sw);
var aR = 0.20 + panic * (1.45 + 0.5 * Math.sin(t * 11 + p.phase + 2.1));
ctx.beginPath();
ctx.moveTo(0, -0.40);
ctx.lineTo(0.56 * Math.sin(aL), -0.40 + 0.56 * Math.cos(aL));
ctx.moveTo(0, -0.40);
ctx.lineTo(0.56 * Math.sin(aR), -0.40 + 0.56 * Math.cos(aR));
ctx.stroke();
/* head */
ctx.beginPath();
ctx.arc(0, -0.62, 0.135, 0, 6.283185307);
ctx.fillStyle = p.skin;
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.30)';
ctx.lineWidth = 0.022;
ctx.stroke();
ctx.restore();
}
}
function draw(t) {
var bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#10161f');
bg.addColorStop(1, '#070a0e');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
drawEscalator();
drawPeople(t);
}
/* ------------------------------------------------------------------ */
/* hud */
/* ------------------------------------------------------------------ */
var hudTick = 0;
function updateHud() {
if (hudTick++ % 8) return;
var down = 0;
for (var i = 0; i < people.length; i++) if (people[i].downed) down++;
hud.innerHTML = 'band <b>' + bandSpeed.toFixed(2) + '</b> m/s · ' +
'riders <b>' + people.length + '</b> · ' +
'down <b>' + down + '</b>';
}
/* ------------------------------------------------------------------ */
/* main loop */
/* ------------------------------------------------------------------ */
var last = 0, acc = 0;
var FIXED = 1 / 120;
function loop(now) {
var dt = (now - last) / 1000;
last = now;
if (dt > 0.25) dt = 0.25;
acc += dt;
var steps = 0;
while (acc >= FIXED && steps < 10) {
physicsStep(FIXED);
acc -= FIXED;
steps++;
}
if (steps >= 10) acc = 0;
managePopulation();
draw(now / 1000);
updateHud();
requestAnimationFrame(loop);
}
/* ------------------------------------------------------------------ */
/* controls */
/* ------------------------------------------------------------------ */
function toggle() {
reversed = !reversed;
bandTarget = reversed ? -3.6 : 0.45;
btn.textContent = reversed ? 'Restore' : 'Reverse';
btn.classList.toggle('rev', reversed);
}
btn.addEventListener('click', toggle);
window.addEventListener('keydown', function (e) {
if (e.code === 'Space') { e.preventDefault(); toggle(); }
});
/* ------------------------------------------------------------------ */
/* boot */
/* ------------------------------------------------------------------ */
function init() {
resize();
window.addEventListener('resize', resize);
people = [];
for (var i = 0; i < 17; i++) {
var s = 0.9 + i * 0.56;
var x = s * CA;
var ys = s * SA;
var p = new Person(x, ys + 0.15 + 0.75, (Math.random() - 0.5) * 0.05);
people.push(p);
}
last = performance.now();
requestAnimationFrame(loop);
}
init();
})();
</script>
</body>
</html>
Pressing the button ramps the band from +0.45 m/s to −3.6 m/s at 22 m/s², so the reversal is complete in about 0.18 s. The band is kinematic — it does not care what is standing on it. Friction at each foot contact (μ = 0.85) is capped at roughly 520 N, so the feet get yanked down-slope while the torso's inertia lags; the resulting torque about the centre of mass (~400 N·m) blows straight through the 200 N·m balance ceiling, θ runs past 60°, and the rider is flagged down. From that moment the same capsule is just a horizontal obstacle lying on the ramp. Riders above slide into it, the contact solver stacks them, and the whole column is carried down to the flat bottom landing and jammed against the wall at x = −3.5. Nobody is told to fall; the only thing that changed is the surface velocity.
Getting up is gated on four things: at least 0.75 s down, low spin, |band speed| < 0.7 m/s, and a clear vertical column where the body would stand. If all four hold, the same PD controller is re-engaged with a raised torque ceiling and the capsule rotates up off its foot contact — the ground normal does the lifting. If the pile is too tight, the clearance test fails and they stay down until the people above them clear out.
Things I special-cased or nudged, so you can judge them: the get-up torque ceiling is 800 N·m versus 200 N·m for normal balance (a rigid capsule cannot push with its legs, so standing up needs a bigger lever); the clearance test is a heuristic vertical-capsule probe, not a swept volume; the band-speed gate for standing up is a hand-picked "footing" threshold; the bottom wall is an artificial boundary that exists purely to give the pile something to stack against; and riders who ride off the top are deleted and a replacement is dropped in at the bottom, which is population bookkeeping rather than physics.
What it does badly: the pile jitters — the solver is ten iterations of sequential impulses with translation-only position correction, so a deep stack breathes and occasionally a capsule will pop a few centimetres. Standing up looks snappy and slightly robotic because it is one saturated torque, not a leg extension. The escalator surface is a smooth ramp, so the step treads are pure decoration and feet never actually catch on a riser. Limbs do not articulate — arms flail on a sine wave keyed to angular velocity, which reads as panic but is not simulated. And because the whole body is one capsule, a rider who goes down on the incline tends to slide head-first down the slope rather than tumbling, which is a bit tidier than the real thing.