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 foot-anchored inverted pendulum while balanced, with a capture-point test combining lean and velocity; belt acceleration or a collision can move that point outside the foot’s support area. The rider then becomes the same rigid body used for falling and sliding: a mass, moment of inertia, and five overlapping collision discs, driven by gravity, moving-step friction, and contact impulses. When two riders overlap, positional correction separates them while normal and friction impulses exchange momentum; a standing rider remains foot-anchored but receives destabilizing torque, so a prone body is a real obstacle rather than a decorative one.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator Reversal — Contact Simulation</title>
<style>
:root {
color-scheme: light;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; }
body { overflow: hidden; background: #dce5e7; color: #17252a; }
#stage { position: relative; width: 100%; height: 100%; }
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.panel {
position: absolute;
top: 16px;
left: 16px;
width: min(350px, calc(100vw - 32px));
padding: 15px;
border: 1px solid rgba(255,255,255,.75);
border-radius: 16px;
background: rgba(247,250,249,.91);
box-shadow: 0 12px 35px rgba(25,45,50,.14);
backdrop-filter: blur(8px);
}
h1 {
margin: 0 0 4px;
font-size: 17px;
line-height: 1.25;
letter-spacing: -.015em;
}
.sub {
margin: 0 0 12px;
color: #5b6c70;
font-size: 12px;
line-height: 1.4;
}
.controls { display: flex; gap: 8px; align-items: stretch; }
button {
appearance: none;
border: 0;
border-radius: 10px;
padding: 11px 14px;
font: 650 13px/1.15 system-ui, sans-serif;
cursor: pointer;
color: white;
background: #bd4938;
box-shadow: inset 0 -2px 0 rgba(0,0,0,.12);
}
button:hover { filter: brightness(1.06); }
button:active { transform: translateY(1px); }
button:focus-visible { outline: 3px solid #72a7b2; outline-offset: 2px; }
#toggle { flex: 1; }
#toggle.restore { background: #19756f; }
#reset {
color: #31454a;
background: #dce5e4;
padding-inline: 12px;
}
.meters {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 11px;
}
.meter {
padding: 8px 10px;
border-radius: 9px;
background: #e7edec;
}
.meter span {
display: block;
color: #6a797c;
font-size: 10px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.meter strong {
display: block;
margin-top: 2px;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
#status {
min-height: 32px;
margin: 10px 2px 0;
color: #43565b;
font-size: 12px;
line-height: 1.35;
}
.footnote {
position: absolute;
right: 14px;
bottom: 12px;
padding: 7px 10px;
border-radius: 9px;
color: #52666a;
background: rgba(247,250,249,.76);
font-size: 11px;
pointer-events: none;
}
@media (max-width: 600px) {
.panel { top: 9px; left: 9px; width: calc(100vw - 18px); padding: 12px; }
.sub, #status { display: none; }
.meters { margin-top: 8px; }
.footnote { display: none; }
}
</style>
</head>
<body>
<main id="stage">
<canvas id="cv" aria-label="Side-view simulation of eighteen people on a reversible escalator"></canvas>
<section class="panel" aria-label="Simulation controls">
<h1>Escalator reversal</h1>
<p class="sub">18 independent riders · fixed-step contact physics · no fall timers</p>
<div class="controls">
<button id="toggle" type="button" aria-pressed="false">Reverse at high speed</button>
<button id="reset" type="button" title="Reset the riders">Reset</button>
</div>
<div class="meters">
<div class="meter"><span>Belt</span><strong id="speed">+0.55 m/s</strong></div>
<div class="meter"><span>Riders</span><strong id="count">18 upright</strong></div>
</div>
<div id="status" aria-live="polite">Running normally. The button changes the drive, not the riders’ states.</div>
</section>
<div class="footnote">Watch the feet first, then the lower landing.</div>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("cv");
const ctx = canvas.getContext("2d");
const stage = document.getElementById("stage");
const toggleButton = document.getElementById("toggle");
const resetButton = document.getElementById("reset");
const speedLabel = document.getElementById("speed");
const countLabel = document.getElementById("count");
const statusLabel = document.getElementById("status");
const G = 9.81;
const RUN = 12.4;
const RISE = 5.2;
const THETA = Math.atan2(RISE, RUN);
const LENGTH = Math.hypot(RUN, RISE);
const C = Math.cos(THETA);
const S = Math.sin(THETA);
const NX = -S;
const NY = C;
const STEP_DEPTH = 0.29;
const DT = 1 / 120;
const TAU = Math.PI * 2;
const BELT_END = 0.85;
const HULL = [
{ q: -0.86, r: 0.110 },
{ q: -0.46, r: 0.145 },
{ q: -0.08, r: 0.180 },
{ q: 0.32, r: 0.190 },
{ q: 0.67, r: 0.150 }
];
const COATS = [
["#d5684d", "#934636"],
["#367e8d", "#275966"],
["#d5a33e", "#96712c"],
["#70699e", "#4d496f"],
["#518267", "#365b48"],
["#b65f7d", "#7e4057"],
["#5277a7", "#385476"],
["#b8794e", "#7f5336"]
];
const SKINS = ["#f0c6a2", "#dca27c", "#b97955", "#88583f", "#f2d2b7"];
let W = 1, H = 1, DPR = 1;
let view = { s: 50, ox: 0, base: 0 };
let people = [];
let mode = "up";
let beltV = 0.55;
let beltAcc = 0;
let beltPhase = 0;
let seed = 0x71a9c3;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a));
const X = x => x * view.s + view.ox;
const Y = y => view.base - y * view.s;
function random() {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 4294967296;
}
function surface(x) {
if (x < 0) return { y: 0, tx: 1, ty: 0, nx: 0, ny: 1 };
if (x > RUN) return { y: RISE, tx: 1, ty: 0, nx: 0, ny: 1 };
return { y: x * S / C, tx: C, ty: S, nx: NX, ny: NY };
}
function beltActive(x) {
return x >= -BELT_END && x <= RUN + BELT_END;
}
function beltVelocity(x) {
if (!beltActive(x)) return { x: 0, y: 0 };
const s = surface(x);
return { x: s.tx * beltV, y: s.ty * beltV };
}
function resize() {
const r = stage.getBoundingClientRect();
W = Math.max(1, r.width);
H = Math.max(1, r.height);
DPR = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
const minX = -4.1;
const maxX = RUN + 2.7;
const minY = -0.85;
const maxY = RISE + 2.25;
const scale = Math.min((W - 18) / (maxX - minX), (H - 18) / (maxY - minY));
const top = (H - (maxY - minY) * scale) / 2;
view.s = scale;
view.ox = (W - (maxX - minX) * scale) / 2 - minX * scale;
view.base = top + maxY * scale;
}
function makePerson(index, footX) {
const scale = 0.90 + random() * 0.18;
const mass = Math.pow(scale, 3) * (0.92 + random() * 0.16);
const coat = COATS[index % COATS.length];
const p = {
id: index,
scale,
mass,
lcom: 0.93 * scale,
invI: 1 / (0.34 * scale * scale * mass),
kp: 35 + random() * 6,
kd: 7.5 + random() * 2,
motor: 20 + random() * 4,
color: coat[0],
dark: coat[1],
skin: SKINS[Math.floor(random() * SKINS.length)],
hair: random() < 0.5 ? "#293338" : "#594437",
state: "standing",
x: 0, y: 0, vx: 0, vy: 0,
angle: 0, w: 0,
footX, footVX: 0, footVY: 0,
support: 0,
calm: 0,
walk: random() * TAU,
_nodes: []
};
resetRider(p, footX);
p.angle = (random() - 0.5) * 0.025;
return p;
}
function resetRider(p, footX) {
const s = surface(footX);
p.state = "standing";
p.footX = footX;
p.footVX = p.footVY = 0;
p.x = footX;
p.y = s.y + p.lcom;
p.vx = p.vy = 0;
p.angle = 0;
p.w = 0;
p.support = 0;
p.calm = 0;
}
function initialize() {
seed = 0x71a9c3;
people = [];
mode = "up";
beltV = 0.55;
beltAcc = 0;
beltPhase = 0;
for (let i = 0; i < 18; i++) {
const along = 0.50 + i * (LENGTH - 3.0) / 17;
people.push(makePerson(i, along * C));
}
updateUI();
}
function toRagdoll(p) {
if (p.state === "ragdoll") return;
p.state = "ragdoll";
p.angle = wrapAngle(p.angle);
p.calm = 0;
}
function moveFoot(p, dt, allowWalking) {
const before = surface(p.footX);
const active = beltActive(p.footX);
let vx = 0, vy = 0;
if (active) {
vx = before.tx * beltV;
vy = before.ty * beltV;
} else if (allowWalking && mode === "up") {
if (p.footX < -BELT_END) vx = 1.25;
else if (p.footX > RUN + BELT_END) vx = 1.40;
}
p.footVX = vx;
p.footVY = vy;
p.footX += vx * dt;
p.walk += Math.abs(vx) * dt * 7;
return {
surf: surface(p.footX),
ax: active ? before.tx * beltAcc : 0,
ay: active ? before.ty * beltAcc : 0
};
}
function updateStanding(p, dt) {
const moved = moveFoot(p, dt, true);
const eq = Math.atan2(moved.ax, G + moved.ay);
const effectiveG = Math.hypot(moved.ax, G + moved.ay);
const motor = clamp(-p.kp * p.angle - p.kd * p.w, -p.motor, p.motor);
p.w += ((effectiveG / p.lcom) * Math.sin(p.angle - eq) + motor) * dt;
p.w = clamp(p.w, -9, 9);
p.angle += p.w * dt;
p.x = p.footX + Math.sin(p.angle) * p.lcom;
p.y = moved.surf.y + Math.cos(p.angle) * p.lcom;
p.vx = p.footVX + p.lcom * p.w * Math.cos(p.angle);
p.vy = p.footVY - p.lcom * p.w * Math.sin(p.angle);
if (mode === "up" && p.footX > RUN + 3.1) {
resetRider(p, -4.55 - random() * 0.35);
return;
}
const capture = p.x + 0.075 * (p.vx - p.footVX) - p.footX;
const supportLimit = 0.23 * p.scale;
if ((Math.abs(capture) > supportLimit && Math.abs(p.angle) > 0.07) ||
Math.abs(p.angle) > 0.85) {
toRagdoll(p);
}
}
function updateRise(p, dt) {
const moved = moveFoot(p, dt, false);
const targetX = p.footX;
const targetY = moved.surf.y + p.lcom;
p.vx += ((targetX - p.x) * 34 - (p.vx - p.footVX) * 10) * dt;
p.vy += ((targetY - p.y) * 34 - (p.vy - p.footVY) * 10) * dt;
p.w += (wrapAngle(-p.angle) * 38 - p.w * 11) * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.angle = wrapAngle(p.angle + p.w * dt);
if (Math.abs(targetX - p.x) > 0.44 * p.scale || Math.abs(p.w) > 7) {
toRagdoll(p);
return;
}
const relativeSpeed = Math.hypot(p.vx - p.footVX, p.vy - p.footVY);
if (Math.hypot(targetX - p.x, targetY - p.y) < 0.065 &&
Math.abs(wrapAngle(p.angle)) < 0.075 &&
relativeSpeed < 0.38 && Math.abs(p.w) < 0.55) {
p.state = "standing";
p.angle = wrapAngle(p.angle);
p.w *= 0.2;
p.x = targetX;
p.y = targetY;
p.vx = p.footVX + p.lcom * p.w * Math.cos(p.angle);
p.vy = p.footVY - p.lcom * p.w * Math.sin(p.angle);
}
}
function integrateRagdoll(p, dt) {
p.vy -= G * dt;
const linearDamping = Math.exp(-0.10 * dt);
p.vx *= linearDamping;
p.vy *= linearDamping;
p.w *= Math.exp(-0.35 * dt);
p.x += p.vx * dt;
p.y += p.vy * dt;
p.angle = wrapAngle(p.angle + p.w * dt);
}
function nodesFor(p, x = p.x, y = p.y, angle = p.angle) {
const sn = Math.sin(angle);
const cs = Math.cos(angle);
const result = new Array(HULL.length);
for (let i = 0; i < HULL.length; i++) {
const h = HULL[i];
const q = h.q * p.scale;
result[i] = {
x: x + q * sn,
y: y + q * cs,
r: h.r * p.scale
};
}
return result;
}
function pointVelocity(p, rx, ry) {
return {
x: p.vx + p.w * ry,
y: p.vy - p.w * rx
};
}
function applyFreeImpulse(p, jx, jy, rx, ry) {
p.vx += jx / p.mass;
p.vy += jy / p.mass;
p.w -= (rx * jy - ry * jx) * p.invI;
}
function applyPairImpulse(p, jx, jy, rx, ry) {
if (p.state === "ragdoll") {
applyFreeImpulse(p, jx, jy, rx, ry);
} else {
// The feet stay anchored, but the balance controller receives the torque.
p.w -= (rx * jy - ry * jx) * p.invI * 0.32;
p.w = clamp(p.w, -9, 9);
}
}
function solveFloor(p, iteration) {
const nodes = nodesFor(p);
const correctionFactor = iteration === 0 ? 0.46 : 0.25;
for (const n of nodes) {
const s = surface(n.x);
const distance = (n.y - s.y) * s.ny;
const penetration = n.r - distance;
if (penetration <= 0) continue;
p.support++;
const rx = n.x - p.x;
const ry = n.y - p.y;
const correction = Math.min(penetration, 0.35) * correctionFactor;
p.x += s.nx * correction;
p.y += s.ny * correction;
const im = 1 / p.mass;
const ii = p.invI;
const rn = rx * s.ny - ry * s.nx;
const normalDenom = im + ii * rn * rn;
let v = pointVelocity(p, rx, ry);
const vn = v.x * s.nx + v.y * s.ny;
let normalImpulse = 0;
if (vn < 0) {
normalImpulse = -1.025 * vn / normalDenom;
applyFreeImpulse(p, s.nx * normalImpulse, s.ny * normalImpulse, rx, ry);
}
if (normalImpulse > 0) {
const movingSurface = beltActive(n.x);
const bv = movingSurface
? { x: s.tx * beltV, y: s.ty * beltV }
: { x: 0, y: 0 };
v = pointVelocity(p, rx, ry);
const rtx = rx * s.ty - ry * s.tx;
const tangentDenom = im + ii * rtx * rtx;
const vt = (v.x - bv.x) * s.tx + (v.y - bv.y) * s.ty;
let tangentImpulse = -vt / tangentDenom;
const friction = movingSurface ? 0.84 : 0.74;
tangentImpulse = clamp(
tangentImpulse,
-friction * normalImpulse,
friction * normalImpulse
);
applyFreeImpulse(
p,
s.tx * tangentImpulse,
s.ty * tangentImpulse,
rx, ry
);
}
}
}
function solvePairs() {
for (let i = 0; i < people.length; i++) {
const a = people[i];
for (let j = i + 1; j < people.length; j++) {
const b = people[j];
if (Math.abs(a.x - b.x) > 2.2 || Math.abs(a.y - b.y) > 2.2) continue;
const aDynamic = a.state === "ragdoll";
const bDynamic = b.state === "ragdoll";
const imA = aDynamic ? 1 / a.mass : 0;
const imB = bDynamic ? 1 / b.mass : 0;
const iiA = aDynamic ? a.invI : 0;
const iiB = bDynamic ? b.invI : 0;
for (const na of a._nodes) {
for (const nb of b._nodes) {
let dx = nb.x - na.x;
let dy = nb.y - na.y;
const rr = na.r + nb.r;
const d2 = dx * dx + dy * dy;
if (d2 >= rr * rr) continue;
let distance = Math.sqrt(d2);
if (distance < 1e-7) {
dx = 1;
dy = 0;
distance = 1;
}
const nx = dx / distance;
const ny = dy / distance;
const penetration = rr - Math.sqrt(d2);
if (aDynamic) a.support++;
if (bDynamic) b.support++;
const rax = na.x - a.x;
const ray = na.y - a.y;
const rbx = nb.x - b.x;
const rby = nb.y - b.y;
const va = pointVelocity(a, rax, ray);
const vb = pointVelocity(b, rbx, rby);
const rvx = vb.x - va.x;
const rvy = vb.y - va.y;
const vn = rvx * nx + rvy * ny;
const raCross = rax * ny - ray * nx;
const rbCross = rbx * ny - rby * nx;
const denom = imA + imB + iiA * raCross * raCross + iiB * rbCross * rbCross;
let normalImpulse = 0;
if (denom > 0 && vn < 0) {
normalImpulse = -1.035 * vn / denom;
applyPairImpulse(a, -nx * normalImpulse, -ny * normalImpulse, rax, ray);
applyPairImpulse(b, nx * normalImpulse, ny * normalImpulse, rbx, rby);
const tx = -ny;
const ty = nx;
const va2 = pointVelocity(a, rax, ray);
const vb2 = pointVelocity(b, rbx, rby);
const vt = (vb2.x - va2.x) * tx + (vb2.y - va2.y) * ty;
const raT = rax * ty - ray * tx;
const rbT = rbx * ty - rby * tx;
const tangentDenom =
imA + imB + iiA * raT * raT + iiB * rbT * rbT;
let tangentImpulse = -vt / tangentDenom;
tangentImpulse = clamp(
tangentImpulse,
-0.34 * normalImpulse,
0.34 * normalImpulse
);
applyPairImpulse(a, -tx * tangentImpulse, -ty * tangentImpulse, rax, ray);
applyPairImpulse(b, tx * tangentImpulse, ty * tangentImpulse, rbx, rby);
}
const inverseMassSum = imA + imB;
if (inverseMassSum > 0) {
const correction = Math.max(0, penetration - 0.002) * 0.38 / inverseMassSum;
if (aDynamic) {
a.x -= nx * correction * imA;
a.y -= ny * correction * imA;
}
if (bDynamic) {
b.x += nx * correction * imB;
b.y += ny * correction * imB;
}
}
}
}
}
}
}
function standingSite(p) {
const fx = p.x - Math.sin(p.angle) * p.lcom;
if (fx < -3.95 || fx > RUN + 2.45) return null;
if (beltActive(fx) && Math.abs(beltV) > 1.25) return null;
const fy = surface(fx).y;
const tx = fx;
const ty = fy + p.lcom;
if (Math.hypot(tx - p.x, ty - p.y) > 1.65 * p.scale) return null;
const uprightNodes = nodesFor(p, tx, ty, 0);
for (const other of people) {
if (other === p) continue;
if (Math.abs(other.x - fx) > 2.1 || Math.abs(other.y - ty) > 2.2) continue;
const otherNodes = nodesFor(other);
for (const n of otherNodes) {
if (n.y < fy + 0.38 &&
Math.hypot(n.x - fx, n.y - fy) < n.r + 0.16) {
return null;
}
}
for (const u of uprightNodes) {
for (const n of otherNodes) {
const clearance = (u.r + n.r) * 0.82;
if ((u.x - n.x) ** 2 + (u.y - n.y) ** 2 < clearance * clearance) {
return null;
}
}
}
}
return { x: tx, y: ty };
}
function updateRecovery(p, dt) {
const bv = beltVelocity(p.x);
const speed = Math.hypot(p.vx, p.vy);
const relativeSpeed = Math.hypot(p.vx - bv.x, p.vy - bv.y);
if (p.support > 0 && speed < 0.85 &&
relativeSpeed < 0.85 && Math.abs(p.w) < 1.1) {
p.calm += dt;
} else {
p.calm = 0;
}
if (p.calm > 0.28) {
const site = standingSite(p);
if (site) {
p.state = "rise";
p.footX = site.x;
p.footVX = p.footVY = 0;
p.vx *= 0.35;
p.vy *= 0.35;
p.w *= 0.25;
p.calm = 0;
}
}
}
function physics(dt) {
const target = mode === "down" ? -4.6 : 0.55;
const rate = mode === "down" ? 16.0 : 3.2;
const dv = clamp(target - beltV, -rate * dt, rate * dt);
beltV += dv;
beltAcc = dv / dt;
beltPhase += beltV * dt;
for (const p of people) p.support = 0;
for (const p of people) {
if (p.state === "standing") updateStanding(p, dt);
else if (p.state === "rise") updateRise(p, dt);
else integrateRagdoll(p, dt);
}
for (let iteration = 0; iteration < 3; iteration++) {
for (const p of people) {
if (p.state === "ragdoll") solveFloor(p, iteration);
}
for (const p of people) p._nodes = nodesFor(p);
solvePairs();
}
for (const p of people) {
if (p.state === "ragdoll") {
const speed = Math.hypot(p.vx, p.vy);
if (speed > 13) {
p.vx *= 13 / speed;
p.vy *= 13 / speed;
}
p.w = clamp(p.w, -18, 18);
p.angle = wrapAngle(p.angle);
updateRecovery(p, dt);
} else {
p.w = clamp(p.w, -9, 9);
}
}
}
function worldPath(points, close = false) {
ctx.beginPath();
ctx.moveTo(X(points[0][0]), Y(points[0][1]));
for (let i = 1; i < points.length; i++) {
ctx.lineTo(X(points[i][0]), Y(points[i][1]));
}
if (close) ctx.closePath();
}
function drawEscalator() {
const background = ctx.createLinearGradient(0, 0, 0, H);
background.addColorStop(0, "#edf3f2");
background.addColorStop(1, "#cbd9da");
ctx.fillStyle = background;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(85,110,114,.08)";
ctx.lineWidth = 1;
for (let x = 30; x < W; x += 105) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 70; y < H; y += 105) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Landings and under-step steelwork.
worldPath([
[-4.15, -0.10], [-0.85, -0.10], [0, 0],
[RUN, RISE], [RUN + 0.85, RISE], [RUN + 2.75, RISE],
[RUN + 2.75, RISE - 0.48], [RUN + 0.85, RISE - 0.48],
[RUN, RISE - 0.48], [0, -0.48], [-0.85, -0.48], [-4.15, -0.48]
], true);
ctx.fillStyle = "#53666b";
ctx.fill();
worldPath([[-4.1, -0.48], [-0.85, -0.48], [0, -0.48],
[RUN, RISE - 0.48], [RUN + 2.7, RISE - 0.48]]);
ctx.strokeStyle = "#34494f";
ctx.lineWidth = 4;
ctx.stroke();
ctx.strokeStyle = "rgba(220,233,232,.22)";
ctx.lineWidth = 1.2;
for (let x = 0.5; x < RUN; x += 1.15) {
const y = x * S / C;
ctx.beginPath();
ctx.moveTo(X(x), Y(y - 0.05));
ctx.lineTo(X(x + 0.58), Y(y - 0.44));
ctx.stroke();
}
// Glass balustrade, behind the riders.
worldPath([
[NX * 0.12, NY * 0.12],
[RUN + NX * 0.12, RISE + NY * 0.12],
[RUN + NX * 1.02, RISE + NY * 1.02],
[NX * 1.02, NY * 1.02]
], true);
ctx.fillStyle = "rgba(151,199,204,.19)";
ctx.fill();
ctx.strokeStyle = "rgba(81,128,135,.35)";
ctx.lineWidth = 1.3;
ctx.stroke();
worldPath([
[-0.82 + NX, NY],
[NX, NY],
[RUN + NX, RISE + NY],
[RUN + 0.88 + NX, RISE + NY]
]);
ctx.strokeStyle = "#263b40";
ctx.lineWidth = 7;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
worldPath([
[-0.82 + NX, NY + 0.025],
[NX, NY + 0.025],
[RUN + NX, RISE + NY + 0.025],
[RUN + 0.88 + NX, RISE + NY + 0.025]
]);
ctx.strokeStyle = "rgba(255,255,255,.28)";
ctx.lineWidth = 1.5;
ctx.stroke();
// Belt surface and moving tread markers.
worldPath([[-0.88, 0], [0, 0], [RUN, RISE], [RUN + 0.88, RISE]]);
ctx.strokeStyle = "#1f3035";
ctx.lineWidth = 5;
ctx.stroke();
const phase = ((beltPhase % STEP_DEPTH) + STEP_DEPTH) % STEP_DEPTH;
ctx.lineWidth = 1.15;
for (let along = phase - STEP_DEPTH; along < LENGTH + STEP_DEPTH; along += STEP_DEPTH) {
if (along < 0 || along > LENGTH) continue;
const x = along * C;
const y = along * S;
ctx.beginPath();
ctx.moveTo(X(x + NX * 0.015), Y(y + NY * 0.015));
ctx.lineTo(X(x - NX * 0.23), Y(y - NY * 0.23));
ctx.strokeStyle = "rgba(220,232,231,.55)";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(X(x - 0.10), Y(y + 0.012));
ctx.lineTo(X(x + 0.12), Y(y + 0.012));
ctx.strokeStyle = "rgba(255,255,255,.72)";
ctx.stroke();
}
ctx.font = "700 10px system-ui, sans-serif";
ctx.fillStyle = "rgba(40,62,67,.52)";
ctx.fillText("LOWER LANDING", X(-3.85), Y(-0.61));
ctx.fillText("UPPER LANDING", X(RUN + 1.15), Y(RISE - 0.61));
}
function localStroke(points, color, width) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
function drawPerson(p) {
const sx = X(p.x);
if (sx < -120 || sx > W + 120) return;
const down = p.state === "ragdoll";
const walking = p.state === "standing" &&
!beltActive(p.footX) && Math.abs(p.footVX) > 0.1;
const stride = walking ? Math.sin(p.walk) * 0.11 : 0;
const bend = down ? 0.12 + Math.min(0.15, Math.abs(p.w) * 0.018) : 0;
const flail = down ? Math.min(0.28, 0.08 + Math.abs(p.w) * 0.025) : 0;
const turn = p.w >= 0 ? 1 : -1;
ctx.save();
ctx.translate(sx, Y(p.y));
ctx.scale(view.s, -view.s);
ctx.rotate(-p.angle);
ctx.scale(p.scale, p.scale);
// Rear arm and leg.
localStroke(
[[0, 0.34], [-0.09 - flail, 0.07 + turn * flail * 0.4],
[-0.08 - flail * 1.25, -0.16 + flail]],
p.dark, 0.060
);
localStroke(
[[-0.025, -0.10], [-0.055 - bend - stride * 0.3, -0.49],
[-0.055 - stride, -0.88]],
"#35434b", 0.082
);
localStroke(
[[-0.10 - stride, -0.90], [-0.005 - stride, -0.90]],
"#202c31", 0.070
);
// Front leg.
localStroke(
[[0.025, -0.10], [0.065 + bend * 0.55 + stride * 0.3, -0.48],
[0.055 + stride, -0.88]],
"#465760", 0.086
);
localStroke(
[[0.00 + stride, -0.90], [0.13 + stride, -0.90]],
"#253238", 0.073
);
// Coat/torso.
localStroke([[0, -0.12], [0, 0.35]], p.dark, 0.315);
localStroke([[0.018, -0.10], [0.018, 0.34]], p.color, 0.255);
localStroke([[-0.11, -0.08], [0.12, -0.08]], p.dark, 0.035);
// Front arm; its flail is cosmetic, not part of the collision hull.
localStroke(
[[0.015, 0.34], [0.10 + flail, 0.08 - turn * flail * 0.35],
[0.08 + flail * 1.35, -0.15 + flail * 1.3]],
p.color, 0.067
);
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(0.08 + flail * 1.35, -0.15 + flail * 1.3, 0.047, 0, TAU);
ctx.fill();
// Head, hair, nose and eye.
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(0, 0.67, 0.145, 0, TAU);
ctx.fill();
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.moveTo(0.12, 0.70);
ctx.lineTo(0.175, 0.665);
ctx.lineTo(0.12, 0.64);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = p.hair;
ctx.lineWidth = 0.065;
ctx.beginPath();
ctx.arc(-0.006, 0.69, 0.135, 0.12, Math.PI * 1.18);
ctx.stroke();
ctx.fillStyle = "#263237";
ctx.beginPath();
ctx.arc(0.104, 0.705, 0.014, 0, TAU);
ctx.fill();
ctx.restore();
}
function draw() {
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.clearRect(0, 0, W, H);
drawEscalator();
// Soft contact shadows.
for (const p of people) {
const groundX = p.state === "ragdoll" ? p.x : p.footX;
const sy = surface(groundX).y;
const height = Math.max(0, p.y - sy);
const alpha = clamp(0.19 - height * 0.055, 0.035, 0.15);
ctx.fillStyle = `rgba(20,35,38,${alpha})`;
ctx.beginPath();
ctx.ellipse(
X(groundX), Y(sy) + 2,
0.28 * p.scale * view.s,
0.055 * view.s,
0, 0, TAU
);
ctx.fill();
}
for (const p of people) drawPerson(p);
}
function updateUI() {
const upright = people.filter(p => p.state === "standing").length;
const rising = people.filter(p => p.state === "rise").length;
const down = people.length - upright - rising;
const shownSpeed = Math.abs(beltV) < 0.005 ? 0 : beltV;
speedLabel.textContent = `${shownSpeed >= 0 ? "+" : ""}${shownSpeed.toFixed(2)} m/s`;
const parts = [];
if (upright) parts.push(`${upright} upright`);
if (rising) parts.push(`${rising} getting up`);
if (down) parts.push(`${down} down`);
countLabel.textContent = parts.join(" · ");
toggleButton.textContent = mode === "up"
? "Reverse at high speed"
: "Restore normal upward run";
toggleButton.classList.toggle("restore", mode === "down");
toggleButton.setAttribute("aria-pressed", mode === "down" ? "true" : "false");
if (mode === "down" && beltAcc < -0.2) {
statusLabel.textContent =
"The treads are accelerating out from under the riders’ centers of mass.";
} else if (mode === "down") {
statusLabel.textContent =
"High-speed reverse. Contacts, friction and available standing room determine what happens next.";
} else if (Math.abs(beltV - 0.55) > 0.03) {
statusLabel.textContent =
"Restoring the normal drive. Clear riders can rise; blocked riders must wait or be knocked down again.";
} else {
statusLabel.textContent =
"Running normally. The button changes the drive, not the riders’ states.";
}
}
toggleButton.addEventListener("click", () => {
mode = mode === "up" ? "down" : "up";
updateUI();
});
resetButton.addEventListener("click", initialize);
window.addEventListener("keydown", event => {
if (event.code === "Space" && document.activeElement.tagName !== "BUTTON") {
event.preventDefault();
mode = mode === "up" ? "down" : "up";
updateUI();
}
});
window.addEventListener("resize", resize);
resize();
initialize();
let previous = performance.now();
let accumulator = 0;
let lastUI = 0;
function frame(now) {
const elapsed = Math.min(0.04, Math.max(0, (now - previous) / 1000));
previous = now;
accumulator += elapsed;
let steps = 0;
while (accumulator >= DT && steps < 7) {
physics(DT);
accumulator -= DT;
steps++;
}
draw();
if (now - lastUI > 100) {
updateUI();
lastUI = now;
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Pressing Reverse changes only the belt’s target motion. The feet accelerate downhill, the riders’ capture points leave their support areas, and they become dynamic bodies. They commonly fall slightly up-slope first, because their feet have been pulled out from under them; step friction then tows them downhill. Subsequent impacts can knock anchored riders over, stop or rotate sliding bodies, and transmit momentum through a jam. A prone body can therefore obstruct the next arrival without either body being assigned a pile position.
Recovery is local rather than scheduled: a supported, comparatively calm body tests a prospective footprint and the whole upright silhouette for clearance. Someone safely off the belt may get up even while reverse is still running; someone on fast treads or underneath another body cannot. Restoring the upward drive usually lets the pile unwind from wherever space first becomes available.
Deliberate nudges and special cases:
The weak spots are visible in dense piles: bodies can twitch, overlap or occasionally squirt out under the small iterative solver. Feet sometimes skate or clip through a tread transition, and the spring-driven rise can look like a small hop. The result is often a loose, strung-out jam rather than one neatly dramatic heap.