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.
A person is two equal masses, hip and chest, tied by a distance constraint that shortens as balance fails. Steps act only on whichever particle is actually touching them: horizontal-tread friction and the step’s vertical motion, stronger while balance is high and weaker once it is gone. A capped spring tries to keep the chest over the hip; that spring is balance, and it drains when the lean needs more force than the cap, or when the torso is already whipping relative to the hip. It refills only if the hip is supported, nearly matched to the belt, and the column above the hip is empty. If two particles overlap, they are pushed apart and trade a soft impulse with friction, so a down body is just another collider the next hip can strike.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Escalator</title>
<style>
html, body { margin: 0; height: 100%; background: #101114; color: #e6e1d8; }
body { display: flex; align-items: center; justify-content: center; font-family: sans-serif; }
#wrap { position: relative; width: min(100vw, 1200px); }
canvas { width: 100%; height: auto; display: block; background: #17191e; }
#ui {
position: absolute; left: 12px; top: 12px; display: flex; gap: 10px;
align-items: center; flex-wrap: wrap; max-width: calc(100% - 24px);
}
button {
font: 16px/1.2 sans-serif; padding: 9px 16px; cursor: pointer;
background: #f2efe6; color: #1a140c; border: 2px solid #1a140c; border-radius: 4px;
}
button.hot { background: #e23b2f; color: #fff; border-color: #6d100c; }
button:focus { outline: 2px solid #ffd36a; outline-offset: 2px; }
#stat { font-size: 13px; letter-spacing: 0.02em; text-shadow: 0 1px 2px #000; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="c" width="1200" height="640"></canvas>
<div id="ui">
<button id="btn" type="button" aria-pressed="false">Reverse</button>
<span id="stat"></span>
</div>
</div>
<script>
(function () {
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const W = 1200, H = 640;
const G = 1600;
const UP = 30;
const REV = -480;
const ACC = 2800;
const N = 18;
const xL = 42, xR = 1158;
const incl0 = 270, incl1 = 990;
const yBot = 524, yTop = 156;
const nSteps = 20;
const RUN = (incl1 - incl0) / nSteps;
const RISE = (yBot - yTop) / nSteps;
const ALONG = Math.hypot(RUN, RISE);
const COS = RUN / ALONG;
const SIN = RISE / ALONG;
const K = 64;
const FMAX = 600;
const R = 10;
const COLORS = [
"#c4493a", "#d9782d", "#e0b04a", "#7ea04a", "#3f8f78", "#3d7ea6",
"#5c5aa8", "#8d4d8b", "#a33d55", "#b5653b", "#6e8b3d", "#2f8f9a",
"#c46b8a", "#8a6a3b", "#4d6f8a", "#b23a2f", "#6a7d4a", "#a85a3a"
];
let belt = UP;
let reversed = false;
let phase = 0;
let landPhase = 0;
let shake = 0;
let dt = 1 / 90;
const steps = [];
const people = [];
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function mod(a, n) { return ((a % n) + n) % n; }
function beltVelAt(x) {
const along = { vx: belt * COS, vy: -belt * SIN };
const flat = { vx: belt, vy: 0 };
const blend = 28;
let t = -1;
if (x < incl0 - blend) return flat;
if (x < incl0 + blend) t = (x - (incl0 - blend)) / (2 * blend);
else if (x > incl1 + blend) return flat;
else if (x > incl1 - blend) t = (x - (incl1 - blend)) / (2 * blend);
else return along;
const s = t * t * (3 - 2 * t);
const from = x < incl1 ? flat : along;
const to = x < incl1 ? along : flat;
return { vx: from.vx + (to.vx - from.vx) * s, vy: from.vy + (to.vy - from.vy) * s };
}
function envelope(x) {
if (x <= incl0) return yBot;
if (x >= incl1) return yTop;
const t = (x - incl0) / (incl1 - incl0);
return yBot + t * (yTop - yBot);
}
function rebuildSteps() {
steps.length = 0;
const shift = mod(phase, ALONG) / ALONG;
for (let i = -3; i < nSteps + 4; i++) {
steps.push({
x: incl0 + (i + shift) * RUN,
y: yBot - (i + shift) * RISE
});
}
}
function spawn() {
people.length = 0;
for (let i = 0; i < N; i++) {
const ti = i + 1;
const hx = incl0 + (ti + 0.5) * RUN;
const sy = yBot - ti * RISE;
const hy = sy - R;
const bv = beltVelAt(hx);
people.push({
hx: hx, hy: hy, hvx: bv.vx, hvy: bv.vy,
cx: hx, cy: hy - 46, cvx: bv.vx, cvy: bv.vy,
len: 46, bal: 1, hipGround: false, col: COLORS[i]
});
}
}
function applySurface(x, y, vx, vy, sy, x0, x1, bvx, bvy, mu, sticky) {
if (x < x0 || x > x1) return { x: x, y: y, vx: vx, vy: vy, hit: false };
const feet = y + R;
if (feet < sy - (sticky ? 4 : 0) || feet > sy + 20) return { x: x, y: y, vx: vx, vy: vy, hit: false };
let hit = false;
if (feet > sy) {
y -= feet - sy;
hit = true;
} else if (sticky) {
hit = true;
if (vy >= bvy - 80) y -= (feet - sy) * 0.45;
}
if (!hit) return { x: x, y: y, vx: vx, vy: vy, hit: false };
if (vy > bvy) vy = bvy;
else if (sticky) vy += (bvy - vy) * 0.55;
const maxDv = mu * G * dt;
vx -= clamp(vx - bvx, -maxDv, maxDv);
return { x: x, y: y, vx: vx, vy: vy, hit: true };
}
function groundParticle(p, hip) {
let x = hip ? p.hx : p.cx;
let y = hip ? p.hy : p.cy;
let vx = hip ? p.hvx : p.cvx;
let vy = hip ? p.hvy : p.cvy;
const sticky = hip;
const mu = hip ? 0.34 + 0.74 * p.bal : 0.32;
let grounded = false;
const bv = beltVelAt(x);
if (x < incl0 || x > incl1) {
const sy = x < incl0 ? yBot : yTop;
const x0 = x < incl0 ? xL : incl1;
const x1 = x < incl0 ? incl0 : xR;
const res = applySurface(x, y, vx, vy, sy, x0, x1, bv.vx, bv.vy, mu, sticky);
x = res.x; y = res.y; vx = res.vx; vy = res.vy;
grounded = res.hit;
} else {
let best = null;
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
if (x < s.x - 1 || x > s.x + RUN + 1) continue;
const pen = (y + R) - s.y;
if (pen < -5 || pen > 18) continue;
if (!best || Math.abs(pen) < Math.abs(best.pen)) best = { s: s, pen: pen };
}
if (best) {
const res = applySurface(x, y, vx, vy, best.s.y, best.s.x, best.s.x + RUN, bv.vx, bv.vy, mu, sticky);
x = res.x; y = res.y; vx = res.vx; vy = res.vy;
grounded = res.hit;
}
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
if (y + R < s.y || y - R > s.y + RISE) continue;
if (x < s.x && x + R > s.x && (vx - bv.vx) > 30) {
x -= x + R - s.x;
vx = bv.vx;
grounded = true;
shake += 0.45;
}
}
}
if (x < xL + R) { x = xL + R; if (vx < 0) { vx = 0; shake += 0.2; } }
if (x > xR - R) { x = xR - R; if (vx > 0) vx = 0; }
if (y < 18) { y = 18; if (vy < 0) vy = 0; }
const env = envelope(x);
if (y + R > env + 18) {
y = env + 18 - R;
if (vy > bv.vy) vy = bv.vy;
const maxDv = mu * G * dt;
vx -= clamp(vx - bv.vx, -maxDv, maxDv);
grounded = true;
}
if (hip) {
p.hx = x; p.hy = y; p.hvx = vx; p.hvy = vy;
if (grounded) p.hipGround = true;
} else {
p.cx = x; p.cy = y; p.cvx = vx; p.cvy = vy;
}
}
function rod(p) {
let dx = p.cx - p.hx;
let dy = p.cy - p.hy;
let d = Math.hypot(dx, dy) || 0.001;
const diff = (d - p.len) / d;
const ox = dx * diff * 0.5;
const oy = dy * diff * 0.5;
p.hx += ox; p.hy += oy;
p.cx -= ox; p.cy -= oy;
const nx = dx / d, ny = dy / d;
const relv = (p.cvx - p.hvx) * nx + (p.cvy - p.hvy) * ny;
p.hvx += relv * 0.5 * nx; p.hvy += relv * 0.5 * ny;
p.cvx -= relv * 0.5 * nx; p.cvy -= relv * 0.5 * ny;
}
function separate(ax, ay, avx, avy, bx, by, bvx, bvy) {
let dx = bx - ax, dy = by - ay;
let d = Math.hypot(dx, dy) || 0.001;
const min = R + R;
if (d >= min) return null;
let pen = min - d;
if (pen > 14) pen = 14;
const nx = dx / d, ny = dy / d;
ax -= nx * pen * 0.5; ay -= ny * pen * 0.5;
bx += nx * pen * 0.5; by += ny * pen * 0.5;
const reln = (bvx - avx) * nx + (bvy - avy) * ny;
if (reln < 0) {
const j = reln * 0.55;
avx += j * nx; avy += j * ny;
bvx -= j * nx; bvy -= j * ny;
shake += Math.min(2, -reln * 0.004 + pen * 0.02);
}
const tx = -ny, ty = nx;
const relt = (bvx - avx) * tx + (bvy - avy) * ty;
avx += relt * 0.18 * tx; avy += relt * 0.18 * ty;
bvx -= relt * 0.18 * tx; bvy -= relt * 0.18 * ty;
return { ax: ax, ay: ay, avx: avx, avy: avy, bx: bx, by: by, bvx: bvx, bvy: bvy };
}
function collideAll() {
const nodes = [];
for (let i = 0; i < people.length; i++) {
nodes.push({ i: i, k: "h" });
nodes.push({ i: i, k: "c" });
}
for (let a = 0; a < nodes.length; a++) {
for (let b = a + 1; b < nodes.length; b++) {
if (nodes[a].i === nodes[b].i) continue;
const A = people[nodes[a].i], B = people[nodes[b].i];
const ah = nodes[a].k === "h", bh = nodes[b].k === "h";
const res = separate(
ah ? A.hx : A.cx, ah ? A.hy : A.cy, ah ? A.hvx : A.cvx, ah ? A.hvy : A.cvy,
bh ? B.hx : B.cx, bh ? B.hy : B.cy, bh ? B.hvx : B.cvx, bh ? B.hvy : B.cvy
);
if (!res) continue;
if (ah) { A.hx = res.ax; A.hy = res.ay; A.hvx = res.avx; A.hvy = res.avy; }
else { A.cx = res.ax; A.cy = res.ay; A.cvx = res.avx; A.cvy = res.avy; }
if (bh) { B.hx = res.bx; B.hy = res.by; B.hvx = res.bvx; B.hvy = res.bvy; }
else { B.cx = res.bx; B.cy = res.by; B.cvx = res.bvx; B.cvy = res.bvy; }
}
}
}
function columnClear(p) {
const spots = [[p.hx, p.hy - p.len * 0.5], [p.hx, p.hy - p.len * 0.95]];
for (let i = 0; i < people.length; i++) {
const o = people[i];
if (o === p) continue;
for (let s = 0; s < spots.length; s++) {
if (Math.hypot(o.hx - spots[s][0], o.hy - spots[s][1]) < 22) return false;
if (Math.hypot(o.cx - spots[s][0], o.cy - spots[s][1]) < 22) return false;
}
}
return true;
}
function capV(p) {
let s = Math.hypot(p.hvx, p.hvy);
if (s > 1100) { p.hvx *= 1100 / s; p.hvy *= 1100 / s; }
s = Math.hypot(p.cvx, p.cvy);
if (s > 1100) { p.cvx *= 1100 / s; p.cvy *= 1100 / s; }
}
function substep(h) {
dt = h;
const target = reversed ? REV : UP;
belt += clamp(target - belt, -ACC * h, ACC * h);
phase += belt * h;
landPhase += belt * h;
rebuildSteps();
shake *= Math.exp(-2.4 * h);
for (let i = 0; i < people.length; i++) {
const p = people[i];
p.hipGround = false;
p.hvy += G * h;
p.cvy += G * h;
const drag = Math.exp(-0.22 * h);
p.hvx *= drag; p.hvy *= drag; p.cvx *= drag; p.cvy *= drag;
if (p.bal > 0.02) {
const ex = p.hx - p.cx;
let ax = clamp(ex * K * p.bal, -FMAX, FMAX);
p.cvx += ax * h;
p.hvx -= ax * h * 0.5;
const rel = clamp(p.cvx - p.hvx, -180, 180);
const d = rel * p.bal * h * 3.2;
p.cvx -= d;
p.hvx += d * 0.35;
}
p.hx += p.hvx * h; p.hy += p.hvy * h;
p.cx += p.cvx * h; p.cy += p.cvy * h;
const want = 28 + 18 * p.bal;
p.len += (want - p.len) * Math.min(1, h * 2.4);
}
for (let k = 0; k < 8; k++) {
for (let i = 0; i < people.length; i++) rod(people[i]);
collideAll();
for (let i = 0; i < people.length; i++) {
groundParticle(people[i], true);
groundParticle(people[i], false);
}
}
for (let i = 0; i < people.length; i++) {
const p = people[i];
const ex = p.hx - p.cx;
const need = Math.abs(ex) * K;
if (need > FMAX * 1.2) p.bal -= h * (6 + (need - FMAX) / 80);
const whip = Math.abs(p.cvx - p.hvx);
if (whip > 120 && Math.abs(ex) > 8) p.bal -= h * 6;
if (p.bal < 0) p.bal = 0;
if (p.hipGround) {
const bv = beltVelAt(p.hx);
const rv = Math.hypot(p.hvx - bv.vx, p.hvy - bv.vy);
const quiet = rv < 120 && Math.hypot(p.cvx - p.hvx, p.cvy - p.hvy) < 170;
if (quiet && columnClear(p)) p.bal = Math.min(1, p.bal + h * 1.05);
}
capV(p);
}
if (shake > 8) shake = 8;
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#2a3038");
g.addColorStop(0.45, "#23272e");
g.addColorStop(1, "#14161a");
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#0e1013";
ctx.fillRect(0, yBot + 8, incl0, H - yBot);
ctx.beginPath();
ctx.moveTo(incl0, yBot + 8);
ctx.lineTo(incl1, yTop + 8);
ctx.lineTo(incl1, H);
ctx.lineTo(incl0, H);
ctx.fill();
ctx.fillRect(incl1, yTop + 8, W - incl1, H - yTop);
}
function drawStructure() {
ctx.fillStyle = "#3c4148";
ctx.beginPath();
ctx.moveTo(incl0, yBot);
ctx.lineTo(incl1, yTop);
ctx.lineTo(incl1, yTop + 58);
ctx.lineTo(incl0, yBot + 58);
ctx.fill();
ctx.fillStyle = "#4a5058";
ctx.fillRect(xL - 8, yBot, incl0 - xL + 14, 46);
ctx.fillRect(incl1 - 6, yTop, xR - incl1 + 20, 46);
ctx.strokeStyle = "#2b2f34";
ctx.lineWidth = 3;
for (let i = 0; i < 6; i++) {
const t = i / 5;
const x = incl0 + t * (incl1 - incl0);
const y = yBot + t * (yTop - yBot);
ctx.beginPath();
ctx.moveTo(x, y + 8);
ctx.lineTo(x, y + 54);
ctx.stroke();
}
ctx.fillStyle = "#d7a322";
ctx.fillRect(xL, yBot - 3, incl0 - xL, 5);
ctx.fillRect(incl1, yTop - 3, xR - incl1, 5);
ctx.fillStyle = "#1c1e22";
const gap = 16;
let off = mod(landPhase, gap);
ctx.globalAlpha = 0.45;
for (let x = xL + off; x < incl0; x += gap) {
ctx.fillRect(x, yBot - 2, 2, 4);
}
off = mod(landPhase, gap);
for (let x = incl1 + off; x < xR; x += gap) {
ctx.fillRect(x, yTop - 2, 2, 4);
}
ctx.globalAlpha = 1;
}
function drawSteps() {
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
if (s.x + RUN < incl0 - 4 || s.x > incl1 + 4) continue;
const x0 = Math.max(s.x, incl0);
const x1 = Math.min(s.x + RUN, incl1);
if (x1 <= x0) continue;
ctx.fillStyle = "#c5c8cc";
ctx.fillRect(x0, s.y, x1 - x0, RISE);
ctx.fillStyle = "#eceff2";
ctx.fillRect(x0, s.y, x1 - x0, 3);
ctx.fillStyle = "#e2b31f";
ctx.fillRect(x0, s.y, x1 - x0, 2);
ctx.strokeStyle = "rgba(60,64,70,0.35)";
ctx.lineWidth = 1;
const grooves = 3;
for (let g = 1; g <= grooves; g++) {
const gx = s.x + (g / (grooves + 1)) * RUN;
if (gx < x0 || gx > x1) continue;
ctx.beginPath();
ctx.moveTo(gx, s.y + 4);
ctx.lineTo(gx, s.y + RISE - 2);
ctx.stroke();
}
}
ctx.strokeStyle = "#6d737a";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(incl0, yBot);
ctx.lineTo(incl1, yTop);
ctx.stroke();
}
function drawRail() {
ctx.lineCap = "round";
ctx.strokeStyle = "rgba(190, 206, 214, 0.18)";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(xL, yBot - 34);
ctx.lineTo(incl0, yBot - 34);
ctx.lineTo(incl1, yTop - 34);
ctx.lineTo(xR, yTop - 34);
ctx.stroke();
ctx.strokeStyle = "#1a1c1f";
ctx.lineWidth = 6;
ctx.setLineDash([18, 12]);
ctx.lineDashOffset = -landPhase * 0.55;
ctx.beginPath();
ctx.moveTo(xL, yBot - 46);
ctx.lineTo(incl0, yBot - 46);
ctx.stroke();
ctx.lineDashOffset = -phase * 0.55;
ctx.beginPath();
ctx.moveTo(incl0, yBot - 46);
ctx.lineTo(incl1, yTop - 46);
ctx.stroke();
ctx.lineDashOffset = -landPhase * 0.55;
ctx.beginPath();
ctx.moveTo(incl1, yTop - 46);
ctx.lineTo(xR, yTop - 46);
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = "#3a3d42";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(xL, yBot - 46);
ctx.lineTo(incl0, yBot - 46);
ctx.lineTo(incl1, yTop - 46);
ctx.lineTo(xR, yTop - 46);
ctx.stroke();
}
function drawPerson(p) {
const dx = p.cx - p.hx, dy = p.cy - p.hy;
const d = Math.hypot(dx, dy) || 1;
const ux = dx / d, uy = dy / d;
const px = -uy, py = ux;
const mx = (p.hx + p.cx) / 2, my = (p.hy + p.cy) / 2;
const headX = p.cx + ux * 8, headY = p.cy + uy * 8;
const leg = 12 + 6 * p.bal;
const f1x = p.hx - ux * leg + px * 5, f1y = p.hy - uy * leg + py * 5;
const f2x = p.hx - ux * leg - px * 5, f2y = p.hy - uy * leg - py * 5;
const a1x = mx + px * 12 - ux * 2, a1y = my + py * 12 - uy * 2;
const a2x = mx - px * 12 - ux * 2, a2y = my - py * 12 - uy * 2;
ctx.lineCap = "round";
ctx.strokeStyle = "#1b140f";
ctx.lineWidth = 4.5;
ctx.beginPath();
ctx.moveTo(f1x, f1y); ctx.lineTo(p.hx, p.hy);
ctx.moveTo(f2x, f2y); ctx.lineTo(p.hx, p.hy);
ctx.moveTo(a1x, a1y); ctx.lineTo(mx, my);
ctx.moveTo(a2x, a2y); ctx.lineTo(mx, my);
ctx.moveTo(p.hx, p.hy); ctx.lineTo(p.cx, p.cy);
ctx.stroke();
ctx.strokeStyle = p.col;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(p.hx, p.hy); ctx.lineTo(p.cx, p.cy);
ctx.moveTo(a1x, a1y); ctx.lineTo(mx, my);
ctx.moveTo(a2x, a2y); ctx.lineTo(mx, my);
ctx.stroke();
ctx.fillStyle = "#f1c7a8";
ctx.beginPath();
ctx.arc(headX, headY, 6.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#1b140f";
ctx.beginPath();
ctx.arc(headX + px * 2.2, headY + py * 2.2, 1.1, 0, Math.PI * 2);
ctx.fill();
}
function draw() {
const ox = Math.sin(performance.now() * 0.041) * shake;
const oy = Math.cos(performance.now() * 0.063) * shake * 0.45;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.setTransform(1, 0, 0, 1, ox, oy);
drawBackground();
drawStructure();
drawSteps();
drawRail();
for (let i = 0; i < people.length; i++) drawPerson(people[i]);
ctx.setTransform(1, 0, 0, 1, 0, 0);
let up = 0, down = 0;
for (let i = 0; i < people.length; i++) {
const p = people[i];
if (p.bal > 0.72 && Math.abs(p.cx - p.hx) < 12) up++;
else if (p.bal < 0.28) down++;
}
const dir = reversed ? "reverse" : "up";
document.getElementById("stat").textContent =
dir + " · belt " + belt.toFixed(0) + " px/s · " + up + " upright · " + down + " down";
}
const btn = document.getElementById("btn");
function toggle() {
reversed = !reversed;
btn.textContent = reversed ? "Run upward" : "Reverse";
btn.classList.toggle("hot", reversed);
btn.setAttribute("aria-pressed", reversed ? "true" : "false");
}
btn.addEventListener("click", toggle);
window.addEventListener("keydown", function (e) {
if (e.code === "Space") { e.preventDefault(); toggle(); }
});
spawn();
let last = performance.now();
let acc = 0;
const STEP = 1 / 90;
function frame(now) {
let elapsed = (now - last) / 1000;
last = now;
if (elapsed > 0.08) elapsed = 0.08;
acc += elapsed;
let n = 0;
while (acc >= STEP && n < 5) {
substep(STEP);
acc -= STEP;
n++;
}
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Pressing the button only changes the belt’s target speed. The belt slews to about −480 px/s fast enough that hip friction cannot keep up, so hips lag, catch the next riser, and the chest spring saturates. Everyone starts that topple together — same jerk, same equations — then the left wall and whoever is already down sort them: sliders arrive, hit the stack, and stay down while the column above them is occupied. At constant speed a figure with a clear column and a hip that has matched the belt stands back up, including while still reversed; the packed end of the pile does not, because the wall keeps those hips from matching the belt. Press again and the belt runs upward, the stack stretches, and the ones who get a gap climb back onto their feet.
Nudges, all shared, none of them a fall script: hip contact sticks to a nearby tread so cruise riding does not drift off the step, chest contact only blocks penetration, shoe friction scales up with balance, a limp body shortens its rod, velocity is blended over a few dozen pixels at each comb, and a safety surface sits under the treads if a particle falls through a gap. End walls are geometry. Space toggles the same control as the button. Colors and which step they start on are the only differences between figures.
What it does badly: the first half-second looks synchronized, because it is. Getting up is a chest lever, not a kneel, and it can stall or twitch if a neighbor sits in the measured column but not in the arc the chest actually swings through. The wall-versus-belt fight jitters the bottom of the pile. Risers can snag late or not at all if the slip does not cover those last pixels, and under a heavy stack the eight correction passes still leave some overlap. The handrail is scenery. I have not watched this run; those are the failure modes the solver is built to have.