27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You write simulations where the interesting behavior is supposed to emerge rather than be animated by hand.
Build me a single HTML page: a side-view escalator carrying 15 to 20 people. Then the escalator reverses, at high speed.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: how a person is represented, what makes one lose balance, and what happens when two of them occupy the same place.
The single fenced html block.
Having written it, what does the page do when the escalator reverses? Name anything you special-cased or nudged. Say what it does badly — I am going to press the button and watch.
Each person is a 9-node, 22-constraint Verlet ragdoll (head, shoulders, hips, knees, feet, elbows, hands) where every node carries mass and radius. To stand, a figure relies on a balance controller that applies an uprighting torque to the torso and pulls the hips toward a target height above the feet, combined with friction that locks planted feet to the moving escalator steps beneath them. Balance is lost emergently when the horizontal distance between the center of mass and the support base exceeds a stability threshold, or when high-impulse forces—such as the sudden acceleration of the steps yanking the feet out from under the torso's inertia, or an impact from a neighbor—overwhelm the posture springs and trip the figure into a pure ragdoll state. When two figures occupy the same space, an $O(N^2)$ broad-phase and node-to-node narrow-phase collision solver pushes overlapping body nodes apart along their contact normals while transferring momentum and applying tangential friction, allowing fallen bodies to act as physical obstacles that trip oncoming riders and form multi-layered crush piles.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Escalator Reversal Simulation</title>
<style>
body {
margin: 0;
background: #141619;
color: #e8eaed;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
user-select: none;
}
#hud {
display: flex;
gap: 16px;
align-items: center;
background: #202328;
padding: 12px 24px;
border-radius: 10px;
margin-bottom: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
border: 1px solid #2f343d;
}
button {
background: #d9383a;
color: white;
border: none;
padding: 12px 24px;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.5px;
border-radius: 6px;
cursor: pointer;
transition: transform 0.08s ease, background 0.15s ease;
box-shadow: 0 4px 12px rgba(217, 56, 58, 0.35);
}
button:hover { background: #e84547; }
button:active { transform: scale(0.96); }
button.reversed {
background: #2b8a3e;
box-shadow: 0 4px 12px rgba(43, 138, 62, 0.35);
}
button.reversed:hover { background: #379c4b; }
.secondary-btn {
background: #363b44;
box-shadow: none;
font-weight: 600;
}
.secondary-btn:hover { background: #454b56; }
.stat-box {
font-size: 13px;
color: #9aa0a6;
display: flex;
flex-direction: column;
min-width: 120px;
}
.stat-box span {
color: #fff;
font-weight: 600;
font-size: 15px;
font-variant-numeric: tabular-nums;
}
canvas {
background: #1b1e23;
border-radius: 10px;
box-shadow: 0 12px 36px rgba(0,0,0,0.6);
border: 1px solid #2a2e37;
}
</style>
</head>
<body>
<div id="hud">
<button id="revBtn">THROW INTO REVERSE</button>
<button id="resetBtn" class="secondary-btn">Reset Crowd</button>
<div class="stat-box">Drive State <span id="stateLabel">FORWARD (NOMINAL)</span></div>
<div class="stat-box">Standing / Total <span id="countLabel">0 / 0</span></div>
</div>
<canvas id="sim" width="1040" height="620"></canvas>
<script>
const canvas = document.getElementById('sim');
const ctx = canvas.getContext('2d');
const revBtn = document.getElementById('revBtn');
const resetBtn = document.getElementById('resetBtn');
const stateLabel = document.getElementById('stateLabel');
const countLabel = document.getElementById('countLabel');
// Escalator Geometry
const BOT_LEFT = 40;
const BOT_X = 240;
const BOT_Y = 520;
const TOP_X = 800;
const TOP_Y = 140;
const TOP_RIGHT = 1000;
const DX = TOP_X - BOT_X;
const DY = TOP_Y - BOT_Y; // -380
const SLOPE_LEN = Math.hypot(DX, DY);
const NUM_STEPS = 22;
const STEP_W = DX / NUM_STEPS;
const STEP_H = -DY / NUM_STEPS;
const STEP_DIAG = SLOPE_LEN / NUM_STEPS;
// Physics constants
const SUBSTEPS = 6;
const GRAVITY = 0.016; // per substep^2
const NORMAL_SPEED = 0.22;
const REVERSE_SPEED = -1.35;
let targetSpeed = NORMAL_SPEED;
let currentSpeed = NORMAL_SPEED;
let stepOffset = 0;
let isReversed = false;
const PALETTE = [
'#ef476f', '#ffd166', '#06d6a0', '#118ab2',
'#9b5de5', '#f15bb5', '#00bbf9', '#fb5607',
'#8ac926', '#ff595e', '#4cc9f0', '#f72585'
];
class Node {
constructor(x, y, r, mass = 1) {
this.x = x;
this.y = y;
this.ox = x;
this.oy = y;
this.r = r;
this.mass = mass;
this.onGround = false;
}
}
class Stick {
constructor(a, b, len, stiffness = 1.0) {
this.a = a;
this.b = b;
this.len = len !== undefined ? len : Math.hypot(a.x - b.x, a.y - b.y);
this.stiffness = stiffness;
}
solve() {
const dx = this.b.x - this.a.x;
const dy = this.b.y - this.a.y;
const dist = Math.hypot(dx, dy) || 0.0001;
const diff = (this.len - dist) / dist * this.stiffness;
const totalMass = this.a.mass + this.b.mass;
const w1 = this.b.mass / totalMass;
const w2 = this.a.mass / totalMass;
this.a.x -= dx * diff * w1;
this.a.y -= dy * diff * w1;
this.b.x += dx * diff * w2;
this.b.y += dy * diff * w2;
}
}
class Person {
constructor(x, y, id) {
this.id = id;
this.color = PALETTE[id % PALETTE.length];
this.nodes = [];
this.sticks = [];
this.state = 'standing'; // 'standing', 'falling', 'recovering'
this.fallTimer = 0;
this.walkPhase = Math.random() * Math.PI * 2;
this.facing = 1;
// Proportions
const scale = 0.94 + (id % 5) * 0.03;
this.hLeg = 30 * scale;
this.hTorso = 26 * scale;
this.hHead = 13 * scale;
this.standHeight = (this.hLeg + this.hTorso);
// 0:Head, 1:Shoulder, 2:Hip, 3:LKnee, 4:RKnee, 5:LFoot, 6:RFoot, 7:Elbow, 8:Hand
const hipY = y - this.hLeg;
const shY = hipY - this.hTorso;
const headY = shY - this.hHead;
this.head = new Node(x, headY, 7.5 * scale, 1.2);
this.shoulder = new Node(x, shY, 6.5 * scale, 2.2);
this.hip = new Node(x, hipY, 7.5 * scale, 2.8);
this.lKnee = new Node(x - 3, y - this.hLeg * 0.5, 4.5, 1.0);
this.rKnee = new Node(x + 3, y - this.hLeg * 0.5, 4.5, 1.0);
this.lFoot = new Node(x - 5, y, 4.5, 1.5);
this.rFoot = new Node(x + 5, y, 4.5, 1.5);
this.elbow = new Node(x + 4, shY + 12 * scale, 4, 0.6);
this.hand = new Node(x + 10, hipY - 2, 4, 0.5);
this.nodes.push(
this.head, this.shoulder, this.hip,
this.lKnee, this.rKnee, this.lFoot, this.rFoot,
this.elbow, this.hand
);
const addS = (a, b, len, k) => this.sticks.push(new Stick(a, b, len, k));
// Core structural bones
addS(this.head, this.shoulder, this.hHead, 1.0);
addS(this.shoulder, this.hip, this.hTorso, 1.0);
addS(this.hip, this.lKnee, this.hLeg * 0.52, 1.0);
addS(this.hip, this.rKnee, this.hLeg * 0.52, 1.0);
addS(this.lKnee, this.lFoot, this.hLeg * 0.52, 1.0);
addS(this.rKnee, this.rFoot, this.hLeg * 0.52, 1.0);
addS(this.shoulder, this.elbow, 13 * scale, 0.9);
addS(this.elbow, this.hand, 13 * scale, 0.9);
// Joint limiters / support springs
addS(this.head, this.hip, this.hHead + this.hTorso, 0.35);
this.lLegSpring = new Stick(this.hip, this.lFoot, this.hLeg * 0.98, 0.2);
this.rLegSpring = new Stick(this.hip, this.rFoot, this.hLeg * 0.98, 0.2);
this.footSep = new Stick(this.lFoot, this.rFoot, 11, 0.15);
this.sticks.push(this.lLegSpring, this.rLegSpring, this.footSep);
}
getCOM() {
let mx = 0, my = 0, m = 0;
for (let i = 0; i < 7; i++) {
const n = this.nodes[i];
mx += n.x * n.mass;
my += n.y * n.mass;
m += n.mass;
}
return { x: mx / m, y: my / m };
}
updatePosture( crowdedAhead ) {
const com = this.getCOM();
const footMidX = (this.lFoot.x + this.rFoot.x) * 0.5;
const footMidY = Math.max(this.lFoot.y, this.rFoot.y);
const torsoDX = this.shoulder.x - this.hip.x;
const torsoDY = this.shoulder.y - this.hip.y;
const torsoAngle = Math.atan2(torsoDX, -torsoDY); // 0 = vertical
if (this.state === 'standing') {
this.lLegSpring.stiffness = 0.25;
this.rLegSpring.stiffness = 0.25;
this.footSep.stiffness = 0.18;
// Active balance torques & hip suspension
const targetHipY = footMidY - this.hLeg * 0.96;
if (this.lFoot.onGround || this.rFoot.onGround) {
this.hip.y += (targetHipY - this.hip.y) * 0.08;
this.shoulder.y -= 0.18;
this.head.y -= 0.12;
// Try to align torso over feet
const balanceErr = footMidX + 2 - this.hip.x;
this.hip.x += Math.max(-0.4, Math.min(0.4, balanceErr * 0.04));
this.shoulder.x += (this.hip.x + 2 - this.shoulder.x) * 0.06;
this.head.x += (this.shoulder.x + 1 - this.head.x) * 0.06;
// Keep knees from buckling backwards
this.lKnee.x += 0.08;
this.rKnee.x += 0.08;
}
// Hold handrail if on escalator
if (this.hip.x > BOT_X - 10 && this.hip.x < TOP_X + 10) {
const railY = getEscalatorBaseY(this.shoulder.x + 12) - 36;
this.hand.x += (this.shoulder.x + 12 - this.hand.x) * 0.08;
this.hand.y += (railY - this.hand.y) * 0.08;
}
// Walk on flat landings
const onFlat = (this.hip.x < BOT_X + 8 || this.hip.x > TOP_X - 14);
if (onFlat && !crowdedAhead) {
this.walkPhase += 0.045;
this.hip.x += 0.13;
this.shoulder.x += 0.13;
const stride = Math.sin(this.walkPhase) * 6;
const liftL = Math.max(0, Math.cos(this.walkPhase)) * 3.5;
const liftR = Math.max(0, -Math.cos(this.walkPhase)) * 3.5;
this.lFoot.x += (this.hip.x + stride - this.lFoot.x) * 0.12;
this.rFoot.x += (this.hip.x - stride - this.rFoot.x) * 0.12;
this.lFoot.y -= liftL * 0.25;
this.rFoot.y -= liftR * 0.25;
}
// Check for loss of balance (emergent)
const imbalance = Math.abs(com.x - footMidX);
consthipCollapsed = (footMidY - this.hip.y) < this.hLeg * 0.52;
const headLow = (footMidY - this.head.y) < this.standHeight * 0.65;
if (imbalance > 22 || Math.abs(torsoAngle) > 0.78 || consthipCollapsed || headLow) {
this.state = 'falling';
this.fallTimer = 65 + Math.floor(Math.random() * 50);
}
} else if (this.state === 'falling') {
// Ragdoll mode: loosen legs
this.lLegSpring.stiffness = 0.015;
this.rLegSpring.stiffness = 0.015;
this.footSep.stiffness = 0.02;
// Measure kinetic energy to see if settled
let speedSq = 0;
for (let n of this.nodes) {
const vx = n.x - n.ox, vy = n.y - n.oy;
speedSq += vx * vx + vy * vy;
}
if (speedSq < 0.9) {
this.fallTimer--;
} else {
this.fallTimer = Math.min(110, this.fallTimer + 1);
}
// Only start getting up when timer expires and not buried under another person
if (this.fallTimer <= 0 && !this.isPinned()) {
this.state = 'recovering';
this.recoverProgress = 0;
}
} else if (this.state === 'recovering') {
this.recoverProgress += 0.005;
const k = Math.min(1, this.recoverProgress);
this.lLegSpring.stiffness = 0.03 + k * 0.20;
this.rLegSpring.stiffness = 0.03 + k * 0.20;
this.footSep.stiffness = 0.12;
const groundY = Math.max(getSurfacePoint(this.lFoot.x).y, getSurfacePoint(this.rFoot.x).y);
// Pull feet underneath hips
this.lFoot.x += (this.hip.x - 5 - this.lFoot.x) * 0.05;
this.rFoot.x += (this.hip.x + 5 - this.rFoot.x) * 0.05;
this.lFoot.y += (groundY - this.lFoot.y) * 0.06;
this.rFoot.y += (groundY - this.rFoot.y) * 0.06;
// Push torso and head upward
const targetHipY = groundY - this.hLeg * 0.92;
const targetShY = targetHipY - this.hTorso;
const targetHeadY = targetShY - this.hHead;
this.hip.y += (targetHipY - this.hip.y) * 0.045 * (0.4 + k);
this.shoulder.y += (targetShY - this.shoulder.y) * 0.05 * (0.4 + k);
this.head.y += (targetHeadY - this.head.y) * 0.05 * (0.4 + k);
const midFootX = (this.lFoot.x + this.rFoot.x) * 0.5;
this.hip.x += (midFootX - this.hip.x) * 0.04;
this.shoulder.x += (this.hip.x - this.shoulder.x) * 0.05;
this.head.x += (this.shoulder.x - this.head.x) * 0.05;
// Knocked back down during recovery?
if (this.isPinned()) {
this.state = 'falling';
this.fallTimer = 50;
}
if (this.recoverProgress >= 1.0) {
const upright = Math.abs(this.shoulder.x - this.hip.x) < 14 && (groundY - this.hip.y) > this.hLeg * 0.7;
if (upright) {
this.state = 'standing';
} else {
this.state = 'falling';
this.fallTimer = 40;
}
}
}
}
isPinned() {
for (let other of people) {
if (other === this) continue;
for (let i = 0; i < 7; i++) {
const on = other.nodes[i];
// Check if another person's body mass is resting directly above our torso/head
if (Math.abs(on.x - this.hip.x) < 18 && on.y < this.hip.y - 4 && on.y > this.hip.y - 36) {
return true;
}
if (Math.abs(on.x - this.shoulder.x) < 18 && on.y < this.shoulder.y - 4 && on.y > this.shoulder.y - 36) {
return true;
}
}
}
return false;
}
}
// Escalator Surface Math
function getEscalatorBaseY(x) {
if (x <= BOT_X) return BOT_Y;
if (x >= TOP_X) return TOP_Y;
return BOT_Y + (x - BOT_X) * (DY / DX);
}
function getSurfacePoint(x) {
if (x <= BOT_X) {
return { y: BOT_Y, vx: 0, vy: 0, onStep: false };
}
if (x >= TOP_X) {
return { y: TOP_Y, vx: 0, vy: 0, onStep: false };
}
// Inside escalator incline
const relX = x - BOT_X;
const phase = (relX / STEP_W) - stepOffset;
const stepIdx = Math.floor(phase);
const frac = phase - stepIdx; // 0 = riser edge, 1 = back of tread
let stepY = BOT_Y - (stepIdx + stepOffset) * STEP_H;
stepY = Math.max(TOP_Y, Math.min(BOT_Y, stepY));
const vx = currentSpeed * (DX / SLOPE_LEN);
const vy = currentSpeed * (DY / SLOPE_LEN);
return { y: stepY, vx, vy, onStep: true, frac, stepIdx };
}
function resolveEnvironment(node, isFoot, personState) {
node.onGround = false;
// Left and Right Comb Plate Walls
if (node.x - node.r < BOT_LEFT) {
node.x = BOT_LEFT + node.r;
node.ox = node.x;
}
if (node.x + node.r > TOP_RIGHT) {
node.x = TOP_RIGHT - node.r;
node.ox = node.x;
}
const surf = getSurfacePoint(node.x);
// Horizontal collision against step risers (vertical step faces)
if (surf.onStep && surf.frac < 0.22) {
const nextStepY = Math.max(TOP_Y, surf.y - STEP_H);
const riserX = node.x - surf.frac * STEP_W;
if (node.y + node.r > nextStepY + 2 && node.y - node.r < surf.y) {
const penX = (riserX + node.r) - node.x;
if (penX > 0 && penX < 9) {
node.x -= penX * 0.6;
// Riser imparts horizontal step velocity
node.ox = node.x - surf.vx * 0.7;
}
}
}
// Vertical collision with tread / floor
if (node.y + node.r >= surf.y) {
node.y = surf.y - node.r;
node.onGround = true;
const vx = node.x - node.ox;
const vy = node.y - node.oy;
// Bounce / restitution
if (vy > 0) {
node.oy = node.y + vy * 0.05;
}
if (surf.onStep) {
// Friction couples node to moving escalator step
const grip = (isFoot && personState === 'standing') ? 0.85 : 0.42;
const newVx = vx * (1 - grip) + surf.vx * grip;
node.ox = node.x - newVx;
// Vertical step motion displacement
node.y += surf.vy * 0.35;
} else {
// Flat comb-plate landings have static floor friction, plus gentle comb clearing push
const floorFriction = (isFoot && personState === 'standing') ? 0.78 : 0.86;
node.ox = node.x - vx * floorFriction;
}
}
}
// Crowd Management
let people = [];
function spawnCrowd() {
people = [];
const total = 18;
for (let i = 0; i < total; i++) {
// Distribute along the escalator and bottom landing
const t = i / total;
const x = (BOT_X - 95) + t * (TOP_X - BOT_X + 115);
const y = getSurfacePoint(x).y;
people.push(new Person(x, y, i));
}
}
function solveCrowdCollisions() {
const n = people.length;
for (let i = 0; i < n; i++) {
const pA = people[i];
for (let j = i + 1; j < n; j++) {
const pB = people[j];
// Fast AABB / hip distance check
if (Math.abs(pA.hip.x - pB.hip.x) > 55 || Math.abs(pA.hip.y - pB.hip.y) > 75) {
continue;
}
// Collide primary body nodes (0..6: head, shoulder, hip, knees, feet)
for (let a = 0; a < 7; a++) {
const nA = pA.nodes[a];
for (let b = 0; b < 7; b++) {
const nB = pB.nodes[b];
// Skip foot-on-foot collisions so people can step past each other cleanly
if (a >= 5 && b >= 5) continue;
const dx = nB.x - nA.x;
const dy = nB.y - nA.y;
const minDist = (nA.r + nB.r) * 0.88;
const distSq = dx * dx + dy * dy;
if (distSq < minDist * minDist && distSq > 0.0001) {
const dist = Math.sqrt(distSq);
const overlap = (minDist - dist) * 0.55;
const nx = dx / dist;
const ny = dy / dist;
const totalMass = nA.mass + nB.mass;
const wA = nB.mass / totalMass;
const wB = nA.mass / totalMass;
// Positional separation
nA.x -= nx * overlap * wA;
nA.y -= ny * overlap * wA;
nB.x += nx * overlap * wB;
nB.y += ny * overlap * wB;
// Relative velocity damping & momentum transfer
const vxA = nA.x - nA.ox, vyA = nA.y - nA.oy;
const vxB = nB.x - nB.ox, vyB = nB.y - nB.oy;
const rvx = vxB - vxA;
const rvy = vyB - vyA;
const vn = rvx * nx + rvy * ny;
if (vn < 0) {
const imp = vn * 0.45;
nA.ox -= nx * imp * wA;
nA.oy -= ny * imp * wA;
nB.ox += nx * imp * wB;
nB.oy += ny * imp * wB;
// Tangential body-on-body friction (causes piling up instead of frictionless slipping)
const tx = -ny, ty = nx;
const vt = rvx * tx + rvy * ty;
const fric = vt * 0.18;
nA.ox -= tx * fric * wA;
nA.oy -= ty * fric * wA;
nB.ox += tx * fric * wB;
nB.oy += ty * fric * wB;
}
}
}
}
}
}
}
function stepPhysics() {
// High-torque motor velocity ramp: snappy enough to yank feet out from under riders
currentSpeed += (targetSpeed - currentSpeed) * 0.14;
for (let s = 0; s < SUBSTEPS; s++) {
stepOffset += (currentSpeed * (DX / SLOPE_LEN)) / STEP_W;
for (let i = 0; i < people.length; i++) {
const p = people[i];
// Check if blocked ahead for walking logic
let crowdedAhead = false;
for (let j = 0; j < people.length; j++) {
if (i === j) continue;
const other = people[j];
const dx = other.hip.x - p.hip.x;
if (dx > 0 && dx < 28 && Math.abs(other.hip.y - p.hip.y) < 35) {
crowdedAhead = true;
break;
}
}
p.updatePosture(crowdedAhead);
// Verlet integration
for (let n of p.nodes) {
const vx = (n.x - n.ox) * 0.992;
const vy = (n.y - n.oy) * 0.992;
n.ox = n.x;
n.oy = n.y;
n.x += vx;
n.y += vy + GRAVITY;
}
// Solve skeleton sticks
for (let k = 0; k < 3; k++) {
for (let st of p.sticks) st.solve();
}
}
// Solve person-to-person collisions
solveCrowdCollisions();
// Re-satisfy skeleton & environment boundaries
for (let p of people) {
for (let st of p.sticks) st.solve();
for (let idx = 0; idx < p.nodes.length; idx++) {
resolveEnvironment(p.nodes[idx], idx === 5 || idx === 6, p.state);
}
}
}
// Recycle riders who walk off the top right landing during normal forward operation
if (!isReversed) {
for (let p of people) {
if (p.hip.x > TOP_RIGHT - 32 && p.state === 'standing') {
// Check if bottom entrance has space
let clear = true;
for (let other of people) {
if (other !== p && other.hip.x < BOT_LEFT + 70) {
clear = false;
break;
}
}
if (clear) {
const shiftX = (BOT_LEFT + 35) - p.hip.x;
const shiftY = BOT_Y - getSurfacePoint(p.hip.x).y;
for (let n of p.nodes) {
n.x += shiftX;
n.ox += shiftX;
n.y += shiftY;
n.oy += shiftY;
}
}
}
}
}
}
// Rendering
function drawEscalator() {
ctx.save();
// Architectural Truss under-structure
ctx.fillStyle = '#252930';
ctx.strokeStyle = '#383e48';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(BOT_LEFT, BOT_Y);
ctx.lineTo(BOT_X, BOT_Y);
ctx.lineTo(TOP_X, TOP_Y);
ctx.lineTo(TOP_RIGHT, TOP_Y);
ctx.lineTo(TOP_RIGHT, TOP_Y + 65);
ctx.lineTo(TOP_X + 18, TOP_Y + 65);
ctx.lineTo(BOT_X + 18, BOT_Y + 65);
ctx.lineTo(BOT_LEFT, BOT_Y + 65);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Moving Steps
const fracOffset = ((stepOffset % 1) + 1) % 1;
ctx.fillStyle = '#434955';
ctx.strokeStyle = '#687182';
ctx.lineWidth = 1.5;
for (let i = -1; i <= NUM_STEPS + 1; i++) {
const s = i + fracOffset;
const x1 = BOT_X + s * STEP_W;
const x2 = x1 + STEP_W;
const y = BOT_Y - s * STEP_H;
const cx1 = Math.max(BOT_X, Math.min(TOP_X, x1));
const cx2 = Math.max(BOT_X, Math.min(TOP_X, x2));
if (cx2 <= cx1) continue;
const cy = Math.max(TOP_Y, Math.min(BOT_Y, y));
const nextY = Math.max(TOP_Y, Math.min(BOT_Y, y - STEP_H));
ctx.beginPath();
ctx.moveTo(cx1, cy);
ctx.lineTo(cx2, cy);
ctx.lineTo(cx2, cy + 12);
ctx.lineTo(cx1, nextY + 12);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Yellow safety step-edge strip
ctx.fillStyle = '#eab308';
ctx.fillRect(cx1, cy, 3, 4);
ctx.fillStyle = '#434955';
}
// Flat landing comb plates
ctx.fillStyle = '#5c6472';
ctx.fillRect(BOT_LEFT, BOT_Y - 2, BOT_X - BOT_LEFT + 6, 8);
ctx.fillRect(TOP_X - 6, TOP_Y - 2, TOP_RIGHT - TOP_X + 6, 8);
// Glass Balustrade & Handrail
ctx.fillStyle = 'rgba(130, 190, 230, 0.08)';
ctx.strokeStyle = 'rgba(160, 210, 245, 0.22)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(BOT_LEFT + 20, BOT_Y);
ctx.lineTo(BOT_X, BOT_Y);
ctx.lineTo(TOP_X, TOP_Y);
ctx.lineTo(TOP_RIGHT - 20, TOP_Y);
ctx.lineTo(TOP_RIGHT - 20, TOP_Y - 38);
ctx.lineTo(TOP_X, TOP_Y - 38);
ctx.lineTo(BOT_X, BOT_Y - 38);
ctx.lineTo(BOT_LEFT + 20, BOT_Y - 38);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Handrail belt
ctx.strokeStyle = isReversed ? '#e03131' : '#2b3038';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(BOT_LEFT + 16, BOT_Y - 38);
ctx.lineTo(BOT_X, BOT_Y - 38);
ctx.lineTo(TOP_X, TOP_Y - 38);
ctx.lineTo(TOP_RIGHT - 16, TOP_Y - 38);
ctx.stroke();
// Handrail motion markers
ctx.fillStyle = isReversed ? '#ffc9c9' : '#868e96';
for (let i = 0; i < NUM_STEPS; i += 2) {
const s = ((i + fracOffset * 2) % NUM_STEPS + NUM_STEPS) % NUM_STEPS;
const rx = BOT_X + s * STEP_W;
const ry = BOT_Y - s * STEP_H - 38;
ctx.beginPath();
ctx.arc(rx, ry, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawPerson(p) {
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Back leg
ctx.strokeStyle = shadeColor(p.color, -28);
ctx.lineWidth = 5.5;
ctx.beginPath();
ctx.moveTo(p.hip.x, p.hip.y);
ctx.lineTo(p.lKnee.x, p.lKnee.y);
ctx.lineTo(p.lFoot.x, p.lFoot.y);
ctx.stroke();
// Front leg
ctx.strokeStyle = p.color;
ctx.lineWidth = 5.5;
ctx.beginPath();
ctx.moveTo(p.hip.x, p.hip.y);
ctx.lineTo(p.rKnee.x, p.rKnee.y);
ctx.lineTo(p.rFoot.x, p.rFoot.y);
ctx.stroke();
// Torso
ctx.strokeStyle = p.color;
ctx.lineWidth = 9;
ctx.beginPath();
ctx.moveTo(p.hip.x, p.hip.y);
ctx.lineTo(p.shoulder.x, p.shoulder.y);
ctx.stroke();
// Head
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.head.x, p.head.y, p.head.r, 0, Math.PI * 2);
ctx.fill();
// Arm
ctx.strokeStyle = shadeColor(p.color, 20);
ctx.lineWidth = 4.5;
ctx.beginPath();
ctx.moveTo(p.shoulder.x, p.shoulder.y);
ctx.lineTo(p.elbow.x, p.elbow.y);
ctx.lineTo(p.hand.x, p.hand.y);
ctx.stroke();
ctx.restore();
}
function shadeColor(hex, percent) {
const num = parseInt(hex.slice(1), 16);
const amt = Math.round(2.55 * percent);
const R = Math.max(0, Math.min(255, (num >> 16) + amt));
const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amt));
const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
// Controls
revBtn.addEventListener('click', () => {
isReversed = !isReversed;
if (isReversed) {
targetSpeed = REVERSE_SPEED;
revBtn.textContent = 'RESTORE FORWARD DRIVE';
revBtn.classList.add('reversed');
stateLabel.textContent = 'REVERSE (RUNAWAY)';
stateLabel.style.color = '#ff6b6b';
} else {
targetSpeed = NORMAL_SPEED;
revBtn.textContent = 'THROW INTO REVERSE';
revBtn.classList.remove('reversed');
stateLabel.textContent = 'FORWARD (NOMINAL)';
stateLabel.style.color = '#ffffff';
}
});
resetBtn.addEventListener('click', () => {
isReversed = false;
targetSpeed = NORMAL_SPEED;
currentSpeed = NORMAL_SPEED;
revBtn.textContent = 'THROW INTO REVERSE';
revBtn.classList.remove('reversed');
stateLabel.textContent = 'FORWARD (NOMINAL)';
stateLabel.style.color = '#ffffff';
spawnCrowd();
});
// Main loop
spawnCrowd();
function frame() {
stepPhysics();
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawEscalator();
let standingCount = 0;
for (let p of people) {
if (p.state === 'standing') standingCount++;
drawPerson(p);
}
countLabel.textContent = `${standingCount} / ${people.length}`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
When you press the reverse button, targetSpeed flips from +0.22 to -1.35. Because the feet have high friction with the step treads while the upper body has inertia, the sudden downward acceleration yanks the feet down-slope out from under the hips and shoulders. Riders pitch forward (face-first toward the top of the escalator) as their centers of mass leave their support bases, causing them to transition from standing to falling. Once down, they slide and tumble down the jagged steps, sweeping out the legs of anyone who briefly kept their balance below them, and slam into a multi-layered pile at the bottom comb plate where incoming riders continue to cascade onto their backs.
What is special-cased or nudged:
solveCrowdCollisions(), node indices 5 and 6 (left and right feet) do not collide with other people's feet so that walking figures on the flat landings can stride past each other without their shoes snagging.isPinned() check inspects whether another person's body nodes are directly above their hip or shoulder; if so, the figure stays in the ragdoll state rather than firing their recovery springs and launching a pile of 4 people into the air.What it does badly: