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 small 2D rigid body with mass, rotational inertia, two feet, and circular torso/head collision proxies. Floor contacts use spring-damper normal forces and capped friction against the local belt velocity; the resulting tangential force acts below the center of mass, while finite ankle torque tries to remain vertical, so a sudden belt reversal produces real slipping and tipping rather than a triggered fall. Overlapping riders receive positional separation and collision impulses, making fallen bodies solid obstacles. Recovery is also physical: a generic stand-up motor can activate only when the rider is supported, moving slowly, and has clearance around a prospective standing pose.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Escalator Reversal — Emergent Contact Simulation</title>
<style>
:root {
color-scheme: dark;
--bg: #071019;
--panel: #0d1a27;
--line: #263848;
--ink: #e9f1f7;
--muted: #8ea4b5;
--cyan: #58d8e8;
--amber: #f2bb52;
--red: #ff6577;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at 50% 0%, #142638 0, transparent 48rem),
var(--bg);
color: var(--ink);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
width: min(1400px, calc(100% - 28px));
margin: 18px auto 30px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 22px;
margin-bottom: 12px;
}
.eyebrow {
color: var(--cyan);
font: 700 0.72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .15em;
text-transform: uppercase;
}
h1 {
margin: 5px 0 0;
font-size: clamp(1.35rem, 3vw, 2.25rem);
line-height: 1;
letter-spacing: -.035em;
}
.actions {
display: flex;
align-items: center;
gap: 9px;
flex: 0 0 auto;
}
button {
appearance: none;
border: 1px solid #3a5368;
border-radius: 9px;
padding: 11px 15px;
background: #142638;
color: var(--ink);
font: 800 0.76rem/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .045em;
cursor: pointer;
transition: transform .12s ease, background .12s ease, border-color .12s ease;
}
button:hover { transform: translateY(-1px); border-color: var(--cyan); }
button:active { transform: translateY(1px); }
button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
#reverse {
min-width: 190px;
background: var(--amber);
border-color: var(--amber);
color: #211607;
}
#reverse.active {
background: var(--red);
border-color: var(--red);
color: #21070b;
}
#reset {
background: transparent;
color: var(--muted);
}
.readout {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 18px;
min-height: 43px;
padding: 9px 13px;
margin-bottom: 10px;
border: 1px solid var(--line);
border-radius: 10px 10px 0 0;
background: rgba(13, 26, 39, .9);
color: var(--muted);
font: 700 .72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .04em;
}
.readout strong {
color: var(--ink);
font-weight: 800;
}
#direction {
color: var(--cyan);
min-width: 84px;
display: inline-block;
}
body.is-reversed #direction { color: var(--red); }
canvas {
display: block;
width: 100%;
aspect-ratio: 16 / 8.4;
border: 1px solid var(--line);
border-top: 0;
border-radius: 0 0 12px 12px;
background: #09131e;
box-shadow: 0 20px 60px rgba(0, 0, 0, .35);
}
.caption {
display: flex;
justify-content: space-between;
gap: 18px;
margin: 10px 2px 0;
color: var(--muted);
font-size: .78rem;
line-height: 1.45;
}
.caption span:last-child {
color: #b7c7d3;
text-align: right;
}
@media (max-width: 700px) {
header { align-items: flex-start; flex-direction: column; }
.actions { width: 100%; }
#reverse { flex: 1; }
.caption { flex-direction: column; }
.caption span:last-child { text-align: left; }
}
</style>
</head>
<body>
<main>
<header>
<div>
<div class="eyebrow">Emergent contact simulation</div>
<h1>Escalator Reversal</h1>
</div>
<div class="actions">
<button id="reverse" type="button" aria-pressed="false">THROW INTO REVERSE</button>
<button id="reset" type="button">RESET</button>
</div>
</header>
<div class="readout" aria-label="Simulation status">
<span>BELT <strong id="direction">FORWARD</strong></span>
<span>SPEED <strong id="speed">0.20 m/s</strong></span>
<span>UPRIGHT <strong id="upright">18</strong></span>
<span>GETTING UP <strong id="rising">0</strong></span>
<span>DOWN <strong id="down">0</strong></span>
<span id="note">Riders are being carried uphill.</span>
</div>
<canvas id="scene" width="1200" height="630" aria-label="Side-view simulation of an escalator carrying eighteen people">
This simulation requires a browser with HTML canvas support.
</canvas>
<div class="caption">
<span>Space also toggles reverse. Reset restores the same seeded starting arrangement.</span>
<span>Orange is a weak ankle; cyan is stronger. Colors are not tied to outcomes.</span>
</div>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const reverseButton = document.getElementById("reverse");
const resetButton = document.getElementById("reset");
const directionEl = document.getElementById("direction");
const speedEl = document.getElementById("speed");
const uprightEl = document.getElementById("upright");
const risingEl = document.getElementById("rising");
const downEl = document.getElementById("down");
const noteEl = document.getElementById("note");
const PI = Math.PI;
const TAU = PI * 2;
const G = 9.81;
const CONTACT_TOL = 0.025;
const NORMAL_SPEED = 0.20;
const REVERSE_SPEED = 4.80;
const FIXED_DT = 1 / 120;
const WORLD_LEFT = -4.2;
const WORLD_RIGHT = 20.2;
const WORLD_TOP = 8.2;
const WORLD_BOTTOM = -1.1;
const WALL_LEFT = WORLD_LEFT + 0.18;
const WALL_RIGHT = WORLD_RIGHT - 0.18;
const ANGLE = 18 * PI / 180;
const U = { x: Math.cos(ANGLE), y: Math.sin(ANGLE) };
const N = { x: -Math.sin(ANGLE), y: Math.cos(ANGLE) };
const BELT_START = { x: 0, y: 0 };
const BELT_END = { x: 16.4, y: 16.4 * Math.tan(ANGLE) };
const BELT_LENGTH = Math.hypot(BELT_END.x - BELT_START.x, BELT_END.y - BELT_START.y);
const X_AXIS = { x: 1, y: 0 };
const SEED = 0x51c0ffee;
let random = mulberry32(SEED);
let agents = [];
let reversed = false;
let beltSpeed = NORMAL_SPEED;
let beltTravel = 0;
let cssWidth = 1200;
let cssHeight = 630;
let dpr = 1;
let view = { scale: 50, ox: 0, oy: 500 };
const palette = [
["#5bd6e6", "#efc39d"], ["#f0a95b", "#d79b79"],
["#ef6b7b", "#f0c6a4"], ["#92d36d", "#c88f68"],
["#b08cff", "#e5ae8b"], ["#ffd166", "#9a684b"],
["#6ea8ff", "#f2c1a1"], ["#ff8fc7", "#d89a72"],
["#74d9b8", "#b97e5c"], ["#e2e8f0", "#8e5e45"],
["#ff9f55", "#f2c7a5"], ["#7ee787", "#d2a078"],
["#d7a0ff", "#9b6a4d"], ["#64d9e8", "#e0aa82"],
["#f07178", "#f3c5a0"], ["#c2d35b", "#80583f"],
["#efb4dc", "#ba805e"], ["#76a7ff", "#e8b48b"]
];
function mulberry32(a) {
return function() {
let t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function rand(min = 0, max = 1) {
return min + (max - min) * random();
}
function clamp(v, lo, hi) {
return v < lo ? lo : v > hi ? hi : v;
}
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function cross(a, b) {
return a.x * b.y - a.y * b.x;
}
function rotate(x, y, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return { x: x * c - y * s, y: x * s + y * c };
}
function wrapAngle(a) {
a %= TAU;
if (a > PI) a -= TAU;
if (a < -PI) a += TAU;
return a;
}
function approachAngle(current, target, amount) {
return current + wrapAngle(target - current) * amount;
}
function mapX(x) {
return view.ox + (x - WORLD_LEFT) * view.scale;
}
function mapY(y) {
return view.oy - y * view.scale;
}
function resize() {
const rect = canvas.getBoundingClientRect();
cssWidth = Math.max(320, rect.width);
cssHeight = Math.max(280, rect.height);
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
const horizontalScale = cssWidth / (WORLD_RIGHT - WORLD_LEFT);
const verticalScale = cssHeight / (WORLD_TOP - WORLD_BOTTOM);
view.scale = Math.min(horizontalScale, verticalScale);
view.ox = (cssWidth - (WORLD_RIGHT - WORLD_LEFT) * view.scale) / 2;
view.oy = cssHeight * 0.82;
}
if ("ResizeObserver" in window) {
new ResizeObserver(resize).observe(canvas);
} else {
window.addEventListener("resize", resize);
}
function footLocalWith(stanceTilt, standHeight, stance, side) {
return {
x: side * stance * Math.cos(stanceTilt),
y: -standHeight + side * stance * Math.sin(stanceTilt)
};
}
function footLocal(a, side) {
return footLocalWith(a.stanceTilt, a.standHeight, a.stance, side);
}
function makeAgent(index) {
const h = rand(0.96, 1.08);
const standHeight = 0.70 * h;
const mass = rand(60, 84);
const inertia = mass * 0.34 * h * h;
const s = 2.2 + index * ((15.2 - 2.2) / 17) + rand(-0.035, 0.035);
const footMid = {
x: BELT_START.x + U.x * s,
y: BELT_START.y + U.y * s
};
const colors = palette[index % palette.length];
return {
x: footMid.x,
y: footMid.y + standHeight,
vx: U.x * NORMAL_SPEED,
vy: U.y * NORMAL_SPEED,
angle: rand(-0.012, 0.012),
omega: 0,
mass,
invMass: 1 / mass,
inertia,
invInertia: 1 / inertia,
h,
standHeight,
stance: rand(0.09, 0.115),
stanceTilt: ANGLE,
shoeMu: rand(0.38, 0.70),
clothMu: rand(0.20, 0.32),
ankleMax: rand(55, 115),
reaction: rand(0.025, 0.075),
phase: rand(0, TAU),
state: "standing",
downAge: 0,
recoverAge: 0,
recoverProgress: 0,
recoveryTarget: null,
senseAngle: 0,
grounded: true,
bodyContact: false,
footContact: false,
contactCount: 0,
pendingHit: 0,
jacket: colors[0],
skin: colors[1],
trousers: index % 3 === 0 ? "#263544" : "#344a5c"
};
}
function resetSimulation() {
random = mulberry32(SEED);
agents = [];
for (let i = 0; i < 18; i++) agents.push(makeAgent(i));
reversed = false;
beltSpeed = NORMAL_SPEED;
beltTravel = 0;
updateButton();
}
function supportKind(a) {
const rel = { x: a.x - BELT_START.x, y: a.y - BELT_START.y };
const s = dot(rel, U);
const d = dot(rel, N);
if (s >= -0.18 && s <= BELT_LENGTH + 0.18 && d >= -0.45 && d <= 1.25) {
return "belt";
}
if (a.x <= 0.35 && a.y <= 1.45) return "lower";
if (a.x >= BELT_END.x - 0.35 && a.y <= BELT_END.y + 1.45) return "upper";
return "belt";
}
function standingTarget(a) {
const kind = supportKind(a);
if (kind === "lower") {
return {
kind,
x: clamp(a.x, WALL_LEFT + 0.22, -0.05),
y: a.standHeight,
tilt: 0
};
}
if (kind === "upper") {
return {
kind,
x: clamp(a.x, BELT_END.x + 0.05, WALL_RIGHT - 0.22),
y: BELT_END.y + a.standHeight,
tilt: 0
};
}
const rel = { x: a.x - BELT_START.x, y: a.y - BELT_START.y };
const s = clamp(dot(rel, U), 0, BELT_LENGTH);
const footMid = {
x: BELT_START.x + U.x * s,
y: BELT_START.y + U.y * s
};
return {
kind,
x: footMid.x,
y: footMid.y + a.standHeight,
tilt: ANGLE
};
}
function proxyList(a) {
const torso = rotate(0, 0, a.angle);
const head = rotate(0, 0.58 * a.h, a.angle);
return [
{ x: a.x + torso.x, y: a.y + torso.y, r: 0.23 * a.h },
{ x: a.x + head.x, y: a.y + head.y, r: 0.16 * a.h }
];
}
function hasRoomToStand(a, target) {
const targetTorso = { x: target.x, y: target.y, r: 0.23 * a.h };
const targetHead = { x: target.x, y: target.y + 0.58 * a.h, r: 0.16 * a.h };
const targetProxies = [targetTorso, targetHead];
const left = footLocalWith(target.tilt, a.standHeight, a.stance, -1);
const right = footLocalWith(target.tilt, a.standHeight, a.stance, 1);
const targetFeet = [
{ x: target.x + left.x, y: target.y + left.y, r: 0.12 * a.h },
{ x: target.x + right.x, y: target.y + right.y, r: 0.12 * a.h }
];
for (const b of agents) {
if (b === a) continue;
const otherProxies = proxyList(b);
for (const op of otherProxies) {
for (const tp of targetProxies) {
const d = Math.hypot(op.x - tp.x, op.y - tp.y);
if (d < op.r + tp.r + 0.07) return false;
}
for (const tf of targetFeet) {
const d = Math.hypot(op.x - tf.x, op.y - tf.y);
if (d < op.r + tf.r + 0.025) return false;
}
if (Math.abs(op.x - target.x) < 0.18 * a.h && op.y < target.y + 0.20 * a.h) {
return false;
}
}
const dx = b.x - a.x;
const dv = b.vx - a.vx;
if (Math.abs(dx) < 0.80 && dx * dv < 0 && Math.abs(dv) > 0.65) {
return false;
}
}
return true;
}
function findSurface(center, radius) {
let best = null;
function offer(id, signed, normal, tangent, surfaceSpeed, contactR) {
if (signed > CONTACT_TOL) return;
const penetration = CONTACT_TOL - signed;
if (!best || penetration > best.penetration) {
best = {
id,
signed,
penetration,
normal,
tangent,
surfaceSpeed,
r: contactR
};
}
}
const rel = { x: center.x - BELT_START.x, y: center.y - BELT_START.y };
const s = dot(rel, U);
if (s >= -radius * 0.25 && s <= BELT_LENGTH + radius * 0.25) {
const signed = dot(rel, N) - radius;
const contact = {
x: center.x - N.x * radius,
y: center.y - N.y * radius
};
offer(
"belt",
signed,
N,
U,
beltSpeed,
{ x: contact.x - center.x + (center.x - center.x), y: 0 }
);
best.r = { x: contact.x - (center.x - center.x), y: 0 };
// The caller's agent position is added by findSurface's wrapper below.
best._worldContact = contact;
}
if (center.x >= WORLD_LEFT && center.x <= 0) {
const signed = center.y - radius;
const contact = { x: center.x, y: center.y - radius };
offer("lower", signed, { x: 0, y: 1 }, X_AXIS, 0, { x: 0, y: 0 });
best._worldContact = contact;
}
if (center.x >= BELT_END.x && center.x <= WORLD_RIGHT) {
const signed = center.y - BELT_END.y - radius;
const contact = { x: center.x, y: center.y - radius };
offer("upper", signed, { x: 0, y: 1 }, X_AXIS, 0, { x: 0, y: 0 });
best._worldContact = contact;
}
return best;
}
function surfaceForAgent(a, center, radius) {
const surface = findSurface(center, radius);
if (!surface) return null;
surface.r = {
x: surface._worldContact.x - a.x,
y: surface._worldContact.y - a.y
};
delete surface._worldContact;
return surface;
}
function pointVelocity(a, r) {
return {
x: a.vx - a.omega * r.y,
y: a.vy + a.omega * r.x
};
}
function applyImpulse(a, jx, jy, r) {
a.vx += jx * a.invMass;
a.vy += jy * a.invMass;
a.omega += cross(r, { x: jx, y: jy }) * a.invInertia;
}
function applyGroundContacts(a, dt) {
const groups = new Map();
for (const side of [-1, 1]) {
const local = footLocal(a, side);
const world = rotate(local.x, local.y, a.angle);
const center = { x: a.x + world.x, y: a.y + world.y };
const surface = surfaceForAgent(a, center, 0);
if (!surface) continue;
if (!groups.has(surface.id)) groups.set(surface.id, []);
groups.get(surface.id).push(surface);
}
for (const contacts of groups.values()) {
let avgR = { x: 0, y: 0 };
let normalBase = 0;
let normalDamping = 0;
for (const c of contacts) {
avgR.x += c.r.x / contacts.length;
avgR.y += c.r.y / contacts.length;
const vp = pointVelocity(a, c.r);
normalBase += 22000 * c.penetration;
normalDamping += 260 * dot(vp, c.normal);
}
const surface = contacts[0];
let normalForce = clamp(normalBase - normalDamping, 0, a.mass * G * 5);
if (normalForce <= 0) continue;
applyImpulse(
a,
surface.normal.x * normalForce * dt,
surface.normal.y * normalForce * dt,
avgR
);
const vp = pointVelocity(a, avgR);
const relativeTangent = dot(vp, surface.tangent) - surface.surfaceSpeed;
const lever = cross(avgR, surface.tangent);
const denominator = a.invMass + lever * lever * a.invInertia;
const maxImpulse = a.shoeMu * normalForce * dt;
const impulse = clamp(
-relativeTangent / denominator,
-maxImpulse,
maxImpulse
);
applyImpulse(
a,
surface.tangent.x * impulse,
surface.tangent.y * impulse,
avgR
);
a.grounded = true;
a.footContact = true;
a.contactCount += contacts.length;
}
for (const proxy of proxyList(a)) {
const surface = surfaceForAgent(a, proxy, proxy.r);
if (!surface) continue;
const vp = pointVelocity(a, surface.r);
const vn = dot(vp, surface.normal);
let normalForce = clamp(
22000 * surface.penetration - 190 * vn,
0,
a.mass * G * 2.5
);
if (normalForce <= 0) continue;
applyImpulse(
a,
surface.normal.x * normalForce * dt,
surface.normal.y * normalForce * dt,
surface.r
);
const updatedVelocity = pointVelocity(a, surface.r);
const relativeTangent = dot(updatedVelocity, surface.tangent) - surface.surfaceSpeed;
const lever = cross(surface.r, surface.tangent);
const denominator = a.invMass + lever * lever * a.invInertia;
const maxImpulse = a.clothMu * normalForce * dt;
const impulse = clamp(
-relativeTangent / denominator,
-maxImpulse,
maxImpulse
);
applyImpulse(
a,
surface.tangent.x * impulse,
surface.tangent.y * impulse,
surface.r
);
a.grounded = true;
a.bodyContact = true;
a.contactCount++;
}
}
function applyStandingControl(a, dt) {
a.senseAngle = approachAngle(
a.senseAngle,
a.angle,
1 - Math.exp(-dt / a.reaction)
);
let minX = Infinity;
let maxX = -Infinity;
let supported = false;
for (const side of [-1, 1]) {
const local = footLocal(a, side);
const world = rotate(local.x, local.y, a.angle);
const center = { x: a.x + world.x, y: a.y + world.y };
if (surfaceForAgent(a, center, 0)) {
supported = true;
minX = Math.min(minX, center.x);
maxX = Math.max(maxX, center.x);
}
}
if (!supported || a.x < minX - 0.025 || a.x > maxX + 0.025) return;
const error = wrapAngle(a.senseAngle);
const desiredTorque = -330 * error - 90 * a.omega;
const torque = clamp(
desiredTorque,
-a.ankleMax,
a.ankleMax
);
a.omega += torque * a.invInertia * dt;
}
function applyRecoveryMotor(a, dt) {
const target = a.recoveryTarget;
if (!target) return;
const error = wrapAngle(a.angle);
const desiredTorque = -285 * error - 82 * a.omega;
const torque = clamp(
desiredTorque,
-a.ankleMax * 1.5,
a.ankleMax * 1.5
);
a.omega += torque * a.invInertia * dt;
const desiredVx = clamp((target.x - a.x) * 2.5, -0.9, 0.9);
const horizontalAcceleration = clamp(
desiredVx * 10 - a.vx * 7,
-6,
6
);
applyImpulse(
a,
a.mass * horizontalAcceleration * dt,
0,
{ x: 0, y: 0 }
);
const desiredVy = clamp((target.y - a.y) * 3, -1.2, 1.6);
const liftAcceleration = clamp(
G + desiredVy * 10 - a.vy * 9,
0,
18
);
applyImpulse(
a,
0,
a.mass * liftAcceleration * dt,
{ x: 0, y: 0 }
);
}
function resolveCollisions() {
for (let iteration = 0; iteration < 2; iteration++) {
for (let i = 0; i < agents.length; i++) {
const a = agents[i];
for (let j = i + 1; j < agents.length; j++) {
const b = agents[j];
const ap = proxyList(a);
const bp = proxyList(b);
for (const pa of ap) {
for (const pb of bp) {
let dx = pb.x - pa.x;
let dy = pb.y - pa.y;
let distance = Math.hypot(dx, dy);
const minimum = pa.r + pb.r;
if (distance >= minimum) continue;
if (distance < 0.00001) {
dx = 1;
dy = 0;
distance = 0.00001;
}
const nx = dx / distance;
const ny = dy / distance;
const overlap = minimum - distance;
const totalInverseMass = a.invMass + b.invMass;
const correction = Math.max(0, overlap - 0.001) * 0.56 / totalInverseMass;
a.x -= nx * correction * a.invMass;
a.y -= ny * correction * a.invMass;
b.x += nx * correction * b.invMass;
b.y += ny * correction * b.invMass;
if (iteration !== 0) continue;
const ra = { x: pa.x - a.x, y: pa.y - a.y };
const rb = { x: pb.x - b.x, y: pb.y - b.y };
const va = pointVelocity(a, ra);
const vb = pointVelocity(b, rb);
const rvx = vb.x - va.x;
const rvy = vb.y - va.y;
const vn = rvx * nx + rvy * ny;
if (vn < 0) {
const ca = cross(ra, { x: nx, y: ny });
const cb = cross(rb, { x: nx, y: ny });
const denominator =
a.invMass + b.invMass +
ca * ca * a.invInertia +
cb * cb * b.invInertia;
const impulse = -(1.02 * vn) / denominator;
applyImpulse(a, -nx * impulse, -ny * impulse, ra);
applyImpulse(b, nx * impulse, ny * impulse, rb);
const tx = -ny;
const ty = nx;
const tangentVelocity = rvx * tx + rvy * ty;
const ta = cross(ra, { x: tx, y: ty });
const tb = cross(rb, { x: tx, y: ty });
const tangentDenominator =
a.invMass + b.invMass +
ta * ta * a.invInertia +
tb * tb * b.invInertia;
let tangentImpulse = -tangentVelocity / tangentDenominator;
tangentImpulse = clamp(
tangentImpulse,
-0.34 * impulse,
0.34 * impulse
);
applyImpulse(a, -tx * tangentImpulse, -ty * tangentImpulse, ra);
applyImpulse(b, tx * tangentImpulse, ty * tangentImpulse, rb);
a.pendingHit += impulse;
b.pendingHit += impulse;
}
}
}
}
}
}
}
function physicsStep(dt) {
const targetSpeed = reversed ? -REVERSE_SPEED : NORMAL_SPEED;
const maximumAcceleration = reversed ? 45 : 28;
beltSpeed += clamp(
targetSpeed - beltSpeed,
-maximumAcceleration * dt,
maximumAcceleration * dt
);
beltTravel += beltSpeed * dt;
for (const a of agents) {
const previousGrounded = a.grounded;
const previousBodyContact = a.bodyContact;
const previousHit = a.pendingHit;
a.pendingHit = 0;
if (a.state === "recovering" && previousHit > a.mass * 1.30) {
a.state = "down";
a.downAge = 0;
a.recoveryTarget = null;
a.senseAngle = a.angle;
}
if (a.state === "standing") {
if (Math.abs(wrapAngle(a.angle)) > 0.72 || previousBodyContact) {
a.state = "down";
a.downAge = 0;
a.senseAngle = a.angle;
} else if (previousGrounded) {
const desiredTilt = supportKind(a) === "belt" ? ANGLE : 0;
a.stanceTilt = approachAngle(
a.stanceTilt,
desiredTilt,
1 - Math.exp(-dt * 4)
);
}
} else if (a.state === "down") {
a.downAge += dt;
const slowEnough = Math.hypot(a.vx, a.vy) < 0.70 && Math.abs(a.omega) < 1.4;
if (
previousGrounded &&
a.downAge > 0.22 + a.reaction &&
slowEnough
) {
const target = standingTarget(a);
if (hasRoomToStand(a, target)) {
a.state = "recovering";
a.recoverAge = 0;
a.recoveryTarget = target;
a.recoveryStartY = a.y;
a.stanceTilt = target.tilt;
}
}
} else if (a.state === "recovering") {
if (
!a.recoveryTarget ||
!hasRoomToStand(a, a.recoveryTarget) ||
(!previousGrounded && a.recoverAge > 0.15)
) {
a.state = "down";
a.downAge = 0;
a.recoveryTarget = null;
a.senseAngle = a.angle;
}
}
a.grounded = false;
a.bodyContact = false;
a.footContact = false;
a.contactCount = 0;
a.vy -= G * dt;
if (a.state === "standing") {
applyStandingControl(a, dt);
} else if (a.state === "recovering") {
a.recoverAge += dt;
applyRecoveryMotor(a, dt);
}
applyGroundContacts(a, dt);
a.x += a.vx * dt;
a.y += a.vy * dt;
a.angle += a.omega * dt;
a.vx = clamp(a.vx, -12, 12);
a.vy = clamp(a.vy, -15, 15);
a.omega = clamp(a.omega, -14, 14);
const angularDamping = a.state === "down" && a.grounded ? 1.0 : 0.035;
a.omega *= Math.exp(-angularDamping * dt);
a.phase += dt * (0.8 + Math.hypot(a.vx, a.vy) * 0.12);
if (a.state === "recovering" && a.recoveryTarget) {
const vertical = clamp(
(a.y - a.recoveryStartY) /
Math.max(0.01, a.recoveryTarget.y - a.recoveryStartY),
0,
1
);
const angular = clamp(1 - Math.abs(wrapAngle(a.angle)) / 1.6, 0, 1);
a.recoverProgress = clamp(vertical * 0.72 + angular * 0.28, 0, 1);
}
}
resolveCollisions();
for (const a of agents) {
if (a.x < WALL_LEFT) {
a.x = WALL_LEFT;
if (a.vx < 0) a.vx *= -0.12;
a.omega *= 0.82;
}
if (a.x > WALL_RIGHT) {
a.x = WALL_RIGHT;
if (a.vx > 0) a.vx *= -0.12;
a.omega *= 0.82;
}
}
}
function drawPath(points, fill, stroke, lineWidth = 1) {
ctx.beginPath();
ctx.moveTo(mapX(points[0].x), mapY(points[0].y));
for (let i = 1; i < points.length; i++) {
ctx.lineTo(mapX(points[i].x), mapY(points[i].y));
}
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
function drawBelt() {
const lowerOffset = { x: -N.x * 0.24, y: -N.y * 0.24 };
drawPath([
BELT_START,
BELT_END,
{ x: BELT_END.x + lowerOffset.x, y: BELT_END.y + lowerOffset.y },
{ x: BELT_START.x + lowerOffset.x, y: BELT_START.y + lowerOffset.y }
], "#101c27", "#30495b", 1.5);
const lowerLanding = [
{ x: WORLD_LEFT, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: -0.24 },
{ x: WORLD_LEFT, y: -0.24 }
];
const upperLanding = [
{ x: BELT_END.x, y: BELT_END.y },
{ x: WORLD_RIGHT, y: BELT_END.y },
{ x: WORLD_RIGHT, y: BELT_END.y - 0.24 },
{ x: BELT_END.x, y: BELT_END.y - 0.24 }
];
drawPath(lowerLanding, "#182633", "#526676", 1.5);
drawPath(upperLanding, "#182633", "#526676", 1.5);
const trussA = { x: BELT_START.x + lowerOffset.x, y: BELT_START.y + lowerOffset.y };
const trussB = { x: BELT_END.x + lowerOffset.x, y: BELT_END.y + lowerOffset.y };
ctx.strokeStyle = "#263c4d";
ctx.lineWidth = Math.max(1, view.scale * 0.035);
ctx.beginPath();
ctx.moveTo(mapX(trussA.x), mapY(trussA.y));
ctx.lineTo(mapX(trussB.x), mapY(trussB.y));
ctx.stroke();
for (let s = 0; s < BELT_LENGTH; s += 1.5) {
const p1 = { x: BELT_START.x + U.x * s, y: BELT_START.y + U.y * s };
const p2 = {
x: BELT_START.x + U.x * Math.min(BELT_LENGTH, s + 1.5),
y: BELT_START.y + U.y * Math.min(BELT_LENGTH, s + 1.5)
};
ctx.beginPath();
ctx.moveTo(mapX(p1.x + lowerOffset.x), mapY(p1.y + lowerOffset.y));
ctx.lineTo(mapX(p2.x), mapY(p2.y));
ctx.stroke();
}
const spacing = 0.37;
const kStart = Math.floor((-spacing - beltTravel) / spacing);
const kEnd = Math.ceil((BELT_LENGTH - beltTravel) / spacing);
for (let k = kStart; k <= kEnd; k++) {
const s = beltTravel + k * spacing;
if (s < -spacing || s > BELT_LENGTH + spacing) continue;
const center = {
x: BELT_START.x + U.x * s,
y: BELT_START.y + U.y * s
};
const width = spacing * view.scale * 0.89;
const depth = view.scale * 0.18;
ctx.save();
ctx.translate(mapX(center.x), mapY(center.y));
ctx.rotate(-ANGLE);
ctx.fillStyle = k % 2 === 0 ? "#263b49" : "#203340";
ctx.fillRect(-width / 2, 0, width, depth);
ctx.fillStyle = "#567080";
ctx.fillRect(-width / 2, 0, Math.max(1, view.scale * 0.022), depth);
ctx.fillStyle = "#78909d";
ctx.fillRect(-width / 2, 0, width, Math.max(1, view.scale * 0.012));
ctx.restore();
}
ctx.strokeStyle = "#8ba1ad";
ctx.lineWidth = Math.max(1.5, view.scale * 0.025);
ctx.beginPath();
ctx.moveTo(mapX(BELT_START.x), mapY(BELT_START.y));
ctx.lineTo(mapX(BELT_END.x), mapY(BELT_END.y));
ctx.stroke();
const railOffset = 1.12;
const railA = {
x: BELT_START.x + N.x * railOffset,
y: BELT_START.y + N.y * railOffset
};
const railB = {
x: BELT_END.x + N.x * railOffset,
y: BELT_END.y + N.y * railOffset
};
ctx.strokeStyle = "rgba(121, 150, 167, .22)";
ctx.lineWidth = view.scale * 0.15;
ctx.beginPath();
ctx.moveTo(mapX(railA.x), mapY(railA.y));
ctx.lineTo(mapX(railB.x), mapY(railB.y));
ctx.stroke();
ctx.strokeStyle = "rgba(178, 204, 216, .48)";
ctx.lineWidth = Math.max(2, view.scale * 0.055);
ctx.setLineDash([view.scale * 0.34, view.scale * 0.18]);
ctx.lineDashOffset = -beltTravel * view.scale;
ctx.beginPath();
ctx.moveTo(mapX(railA.x), mapY(railA.y));
ctx.lineTo(mapX(railB.x), mapY(railB.y));
ctx.stroke();
ctx.setLineDash([]);
for (const p of [BELT_START, BELT_END]) {
ctx.beginPath();
ctx.arc(mapX(p.x), mapY(p.y), view.scale * 0.22, 0, TAU);
ctx.fillStyle = "#0a131c";
ctx.fill();
ctx.strokeStyle = "#607988";
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.strokeStyle = "#516b7b";
ctx.lineWidth = Math.max(1.5, view.scale * 0.03);
ctx.beginPath();
ctx.moveTo(mapX(WALL_LEFT), mapY(0));
ctx.lineTo(mapX(WALL_LEFT), mapY(1.4));
ctx.moveTo(mapX(WALL_RIGHT), mapY(BELT_END.y));
ctx.lineTo(mapX(WALL_RIGHT), mapY(BELT_END.y + 1.4));
ctx.stroke();
ctx.fillStyle = "#708796";
ctx.font = `600 ${Math.max(9, view.scale * 0.18)}px ui-monospace, monospace`;
ctx.textAlign = "center";
ctx.fillText("LOWER LANDING", mapX(-2), mapY(-0.42));
ctx.fillText("UPPER LANDING", mapX(18.2), mapY(BELT_END.y - 0.42));
}
function drawShadow(a) {
const kind = supportKind(a);
let x = a.x;
let y = 0;
let angle = 0;
if (kind === "belt") {
const rel = { x: a.x - BELT_START.x, y: a.y - BELT_START.y };
const s = clamp(dot(rel, U), 0, BELT_LENGTH);
x = BELT_START.x + U.x * s;
y = BELT_START.y + U.y * s;
angle = -ANGLE;
} else if (kind === "upper") {
y = BELT_END.y;
}
const distance = Math.max(0, y - a.y);
const alpha = clamp(0.24 - distance * 0.07, 0.04, 0.24);
ctx.save();
ctx.translate(mapX(x), mapY(y) + 2);
ctx.rotate(angle);
ctx.scale(1, 0.28);
ctx.beginPath();
ctx.arc(0, 0, view.scale * (0.25 + a.h * 0.08), 0, TAU);
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
ctx.fill();
ctx.restore();
}
function drawPerson(a) {
drawShadow(a);
const speed = Math.hypot(a.vx, a.vy);
if (speed > 0.9) {
ctx.strokeStyle = "rgba(174, 215, 228, .18)";
ctx.lineWidth = Math.max(1, view.scale * 0.025);
ctx.beginPath();
ctx.moveTo(mapX(a.x - a.vx * 0.14), mapY(a.y - a.vy * 0.14));
ctx.lineTo(mapX(a.x), mapY(a.y));
ctx.stroke();
}
const S = view.scale * a.h;
ctx.save();
ctx.translate(mapX(a.x), mapY(a.y));
ctx.rotate(-a.angle);
ctx.scale(S, S);
const isDown = a.state === "down";
const isRising = a.state === "recovering";
const sway = Math.sin(a.phase) * 0.018;
let footL;
let footR;
if (isDown) {
footL = { x: -0.31, y: -0.55 };
footR = { x: 0.30, y: -0.50 };
} else {
footL = footLocal(a, -1);
footR = footLocal(a, 1);
}
const hip = { x: 0, y: -0.10 };
const neck = { x: 0, y: 0.34 };
const shoulderL = { x: -0.16, y: 0.27 };
const shoulderR = { x: 0.16, y: 0.27 };
const kneeL = {
x: hip.x * 0.25 + footL.x * 0.48,
y: hip.y * 0.48 + footL.y * 0.52
};
const kneeR = {
x: hip.x * 0.25 + footR.x * 0.48,
y: hip.y * 0.48 + footR.y * 0.52
};
let elbowL = { x: -0.19, y: 0.08 };
let handL = { x: -0.20 + sway, y: -0.12 };
let elbowR = { x: 0.19, y: 0.08 };
let handR = { x: 0.20 - sway, y: -0.12 };
if (isDown) {
elbowL = { x: -0.27, y: 0.10 };
handL = { x: -0.44, y: -0.12 };
elbowR = { x: 0.28, y: 0.04 };
handR = { x: 0.43, y: -0.20 };
} else if (isRising) {
const q = a.recoverProgress;
elbowL = { x: -0.24, y: 0.03 };
handL = { x: -0.25 - 0.08 * q, y: 0.02 - 0.40 * (1 - q) };
elbowR = { x: 0.24, y: 0.03 };
handR = { x: 0.25 + 0.08 * q, y: 0.02 - 0.40 * (1 - q) };
}
ctx.lineCap = "round";
ctx.lineJoin = "round";
function limb(points, color, width) {
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
}
limb([shoulderL, elbowL, handL], "#253746", 0.070);
limb([hip, kneeL, footL], a.trousers, 0.082);
limb([hip, kneeR, footR], a.trousers, 0.082);
limb([hip, neck], a.jacket, 0.175);
limb([neck, shoulderR, elbowR, handR], a.jacket, 0.072);
ctx.strokeStyle = "#182531";
ctx.lineWidth = 0.085;
ctx.beginPath();
ctx.moveTo(footL.x - 0.04, footL.y);
ctx.lineTo(footL.x + 0.07, footL.y);
ctx.moveTo(footR.x - 0.04, footR.y);
ctx.lineTo(footR.x + 0.07, footR.y);
ctx.stroke();
ctx.fillStyle = a.skin;
ctx.beginPath();
ctx.arc(0, 0.58, 0.16, 0, TAU);
ctx.fill();
ctx.fillStyle = "rgba(24, 30, 35, .76)";
ctx.beginPath();
ctx.arc(0, 0.59, 0.163, PI * 1.02, PI * 2.02);
ctx.lineTo(0.11, 0.56);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#1b2228";
ctx.beginPath();
ctx.arc(0.055, 0.585, 0.017, 0, TAU);
ctx.fill();
ctx.fillStyle = a.skin;
ctx.beginPath();
ctx.moveTo(0.145, 0.575);
ctx.lineTo(0.195, 0.545);
ctx.lineTo(0.145, 0.525);
ctx.closePath();
ctx.fill();
if (isRising) {
ctx.strokeStyle = "rgba(255,255,255,.32)";
ctx.lineWidth = 0.025;
ctx.beginPath();
ctx.arc(0, 0.10, 0.26, -0.7, 0.7);
ctx.stroke();
}
ctx.restore();
}
function drawScene() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssWidth, cssHeight);
const background = ctx.createLinearGradient(0, 0, 0, cssHeight);
background.addColorStop(0, "#0b1825");
background.addColorStop(0.72, "#0a141e");
background.addColorStop(1, "#050b11");
ctx.fillStyle = background;
ctx.fillRect(0, 0, cssWidth, cssHeight);
ctx.strokeStyle = "rgba(121, 157, 180, .055)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = Math.ceil(WORLD_LEFT); x <= WORLD_RIGHT; x++) {
ctx.moveTo(mapX(x), 0);
ctx.lineTo(mapX(x), cssHeight);
}
for (let y = Math.ceil(WORLD_BOTTOM); y <= WORLD_TOP; y++) {
ctx.moveTo(0, mapY(y));
ctx.lineTo(cssWidth, mapY(y));
}
ctx.stroke();
drawBelt();
const sorted = agents.slice().sort((a, b) => {
if (a.state === "down" && b.state !== "down") return -1;
if (b.state === "down" && a.state !== "down") return 1;
return a.y - b.y;
});
for (const a of sorted) drawPerson(a);
const vignette = ctx.createRadialGradient(
cssWidth * 0.5, cssHeight * 0.45, cssHeight * 0.15,
cssWidth * 0.5, cssHeight * 0.45, cssWidth * 0.72
);
vignette.addColorStop(0, "rgba(0,0,0,0)");
vignette.addColorStop(1, "rgba(0,0,0,.38)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, cssWidth, cssHeight);
}
function updateMetrics() {
const upright = agents.filter(a => a.state === "standing").length;
const rising = agents.filter(a => a.state === "recovering").length;
const down = agents.length - upright - rising;
directionEl.textContent = reversed ? "REVERSE" : "FORWARD";
speedEl.textContent = `${Math.abs(beltSpeed).toFixed(2)} m/s`;
uprightEl.textContent = String(upright);
risingEl.textContent = String(rising);
downEl.textContent = String(down);
noteEl.textContent = reversed
? "The belt is driving riders downhill."
: "Riders are being carried uphill.";
}
function updateButton() {
reverseButton.textContent = reversed
? "PUT IT BACK — FORWARD"
: "THROW INTO REVERSE";
reverseButton.classList.toggle("active", reversed);
reverseButton.setAttribute("aria-pressed", String(reversed));
document.body.classList.toggle("is-reversed", reversed);
}
reverseButton.addEventListener("click", () => {
reversed = !reversed;
updateButton();
});
resetButton.addEventListener("click", resetSimulation);
window.addEventListener("keydown", event => {
if (event.code === "Space" && event.target === document.body) {
event.preventDefault();
reverseButton.click();
}
});
resetSimulation();
resize();
let previousTime = performance.now();
let accumulator = 0;
function frame(now) {
const elapsed = Math.min(0.05, (now - previousTime) / 1000);
previousTime = now;
accumulator += elapsed;
while (accumulator >= FIXED_DT) {
physicsStep(FIXED_DT);
accumulator -= FIXED_DT;
}
drawScene();
updateMetrics();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Pressing the button changes only the escalator’s commanded belt velocity. It does not alter any rider’s state. The belt accelerates to roughly 4.8 m/s downhill in about a tenth of a second; because the riders initially have uphill momentum, their feet slip against the new surface. Friction then acts below their centers of mass, the incline adds a downhill force, and the finite ankle torque is overcome. The riders lose balance at slightly different times because of seeded mass, height, stance, shoe friction, ankle strength, and response lag. Bodies collide, slide, spin, and collect at the lower landing, where fallen riders obstruct those arriving behind them.
There are no rider-specific fall scripts. The main modeling assists are the generic finite-torque balance controller and a generic recovery motor. The recovery motor cannot begin unless a rider is supported, moving slowly, and has clearance for a proposed standing pose; a hard collision can cancel it. The rider art uses only torso/head collision circles, and the belt is physically a smooth incline even though its steps are drawn separately.
It does badly in a few visible ways: limbs are mostly decorative rather than independently articulated, so there is no realistic stepping response; a recovering rider can look as though a motor is lifting them; and the coarse collision shapes can produce minor visual overlap. The crowd also tends to funnel into a heap at the lower landing rather than forming evenly distributed piles. Pressing the button again restores the original slow forward motion, but the rapid deceleration can throw the accumulated crowd again.