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 custom 8-point Verlet ragdoll (head, chest, pelvis, two knees, two feet, and a free-swinging hand) bound by distance constraints and angular posture springs. While standing, a figure relies on active balance: muscles exert torque to align the chest and pelvis vertically over the midpoint of the feet, and if the torso tilts slightly, a stepping reflex lifts and repositions the trailing foot to catch the fall. A figure loses balance and goes limp when the center of mass moves too far past the feet, the torso tilts beyond 0.6 radians, a non-foot joint hits the stairs, or inter-person collision impulses spike above a threshold—all of which instantly happens when high-speed reversal yanks the steps out from under their high-friction shoes while upper-body inertia carries their torsos forward. Every body node has a physical radius and resolves pairwise elastic collisions with every node of every other person, transferring velocity and friction so that a fallen body forms a physical dam on the steps, tripping incoming riders and accumulating a tangled, multi-layered pileup at the bottom landing until individuals get pushed onto flat ground with low enough velocity and crowd pressure to plant their feet and stand back up.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Escalator Reversal Simulation</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
body {
background: #141619;
color: #e2e6ec;
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;
}
#ui {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 12px;
background: #1f2329;
padding: 10px 20px;
border-radius: 8px;
border: 1px solid #2e343d;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
button {
font-family: inherit;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.5px;
padding: 10px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
transition: transform 0.08s ease, background 0.15s ease;
}
button:active { transform: scale(0.96); }
#reverseBtn {
background: #d9383a;
color: #fff;
box-shadow: 0 0 16px rgba(217, 56, 58, 0.4);
}
#reverseBtn.reversing {
background: #2ea043;
box-shadow: 0 0 16px rgba(46, 160, 67, 0.4);
}
#resetBtn {
background: #323842;
color: #c9d1d9;
}
#resetBtn:hover { background: #3f4652; }
.stat {
font-size: 13px;
color: #8b949e;
min-width: 170px;
}
.stat strong { color: #e2e6ec; font-variant-numeric: tabular-nums; }
canvas {
background: #1a1d23;
border-radius: 8px;
border: 1px solid #2e343d;
box-shadow: 0 12px 36px rgba(0,0,0,0.6);
max-width: 98vw;
max-height: 82vh;
}
</style>
</head>
<body>
<div id="ui">
<button id="reverseBtn">THROW INTO HIGH-SPEED REVERSE</button>
<button id="resetBtn">RESET SCENE</button>
<div class="stat" id="statusText">Mode: <strong>Normal Ascent</strong></div>
<div class="stat" id="countText">Standing: <strong>18 / 18</strong></div>
</div>
<canvas id="sim" width="1080" height="640"></canvas>
<script>
(() => {
const canvas = document.getElementById('sim');
const ctx = canvas.getContext('2d');
const reverseBtn = document.getElementById('reverseBtn');
const resetBtn = document.getElementById('resetBtn');
const statusText = document.getElementById('statusText');
const countText = document.getElementById('countText');
// Escalator & World Geometry
const BOT_X = 290, BOT_Y = 540;
const TOP_X = 830, TOP_Y = 180;
const DX = TOP_X - BOT_X; // 540
const DY = TOP_Y - BOT_Y; // -360
const ESC_LEN = Math.hypot(DX, DY);
const NUM_STEPS = 18;
const STEP_W = DX / NUM_STEPS; // 30
const STEP_H = -DY / NUM_STEPS; // 20
const NORMAL_SPEED = 46; // px/sec ascending
const REVERSE_SPEED = -305; // px/sec violent reverse
const MOTOR_ACCEL = 1550; // px/sec^2
const GRAVITY = 960;
const SUBSTEPS = 6;
const SOLVER_ITERS = 5;
let targetSpeed = NORMAL_SPEED;
let currentSpeed = NORMAL_SPEED;
let stepPhase = 0; // [0, 1)
let people = [];
let nextId = 1;
// Node indices
const HEAD = 0, CHEST = 1, PELVIS = 2, KNEE_L = 3, KNEE_R = 4, FOOT_L = 5, FOOT_R = 6, HAND = 7;
const NODE_RADII = [9.5, 9.5, 9.5, 6, 6, 5.5, 5.5, 4.5];
const NODE_MASS = [1.2, 2.6, 2.8, 1.0, 1.0, 0.9, 0.9, 0.6];
const PALETTE = [
['#4f8fba', '#2b3a4a'], ['#d96b43', '#2e293a'], ['#6ab065', '#2a363b'],
['#c99e44', '#33302c'], ['#a864a8', '#282536'], ['#55a89e', '#243336'],
['#cf5567', '#2f2933'], ['#7d869c', '#232730'], ['#d48459', '#2b303a']
];
const SKIN_TONES = ['#f2d2b6', '#e0b18c', '#c68b63', '#8d5838', '#f6dec8'];
// Evaluate floor surface height and surface velocity at any X
function getFloor(x) {
if (x < BOT_X) {
// Bottom landing Comb plate & run-off
const beltZone = x > BOT_X - 24 ? (x - (BOT_X - 24)) / 24 : 0;
return {
y: BOT_Y,
vx: currentSpeed * (DX / ESC_LEN) * beltZone * 0.35,
vy: 0,
riserX: null,
onSteps: false
};
}
if (x >= TOP_X) {
const beltZone = x < TOP_X + 24 ? 1 - (x - TOP_X) / 24 : 0;
return {
y: TOP_Y,
vx: currentSpeed * (DX / ESC_LEN) * beltZone * 0.35,
vy: 0,
riserX: null,
onSteps: false
};
}
// Find which dynamic step tread contains X
const relX = x - BOT_X;
const k = Math.floor(relX / STEP_W - stepPhase);
const treadStartX = BOT_X + (k + stepPhase) * STEP_W;
const treadEndX = treadStartX + STEP_W;
let treadY = BOT_Y - (k + stepPhase) * STEP_H;
// Clamp tread height cleanly to landings near transition combs
treadY = Math.min(BOT_Y, Math.max(TOP_Y, treadY));
const svx = currentSpeed * (DX / ESC_LEN);
const svy = currentSpeed * (DY / ESC_LEN);
return {
y: treadY,
vx: svx,
vy: svy,
riserX: treadEndX,
riserTopY: Math.max(TOP_Y, treadY - STEP_H),
riserBotY: treadY,
onSteps: true
};
}
class Person {
constructor(x, y) {
this.id = nextId++;
const scale = 0.94 + Math.random() * 0.12;
this.scale = scale;
const colors = PALETTE[this.id % PALETTE.length];
this.shirt = colors[0];
this.pants = colors[1];
this.skin = SKIN_TONES[this.id % SKIN_TONES.length];
this.nodes = [];
const initPos = [
[x, y - 58 * scale], // HEAD
[x, y - 43 * scale], // CHEST
[x, y - 25 * scale], // PELVIS
[x - 3, y - 13 * scale], // KNEE_L
[x + 3, y - 13 * scale], // KNEE_R
[x - 5, y], // FOOT_L
[x + 5, y], // FOOT_R
[x + 7, y - 26 * scale] // HAND
];
const fl = getFloor(x);
for (let i = 0; i < 8; i++) {
const px = initPos[i][0];
const py = initPos[i][1];
this.nodes.push({
x: px, y: py,
ox: px - fl.vx * (1 / 60),
oy: py - fl.vy * (1 / 60),
r: NODE_RADII[i] * scale,
m: NODE_MASS[i],
grounded: i === FOOT_L || i === FOOT_R
});
}
this.sticks = [
// Structural bones [a, b, len, stiffness]
[HEAD, CHEST, 15 * scale, 0.95],
[CHEST, PELVIS, 18 * scale, 0.95],
[PELVIS, KNEE_L, 13.5 * scale, 0.95],
[PELVIS, KNEE_R, 13.5 * scale, 0.95],
[KNEE_L, FOOT_L, 13.5 * scale, 0.95],
[KNEE_R, FOOT_R, 13.5 * scale, 0.95],
[CHEST, HAND, 21 * scale, 0.9],
// Soft anti-collapse anatomical braces
[HEAD, PELVIS, 32.5 * scale, 0.22]
];
this.state = 'standing'; // 'standing' | 'ragdoll' | 'recovering'
this.impactStress = 0;
this.downTimer = 0;
this.recoverTimer = 0;
this.stepCooldown = 0;
this.steppingFoot = -1;
this.stepTargetX = 0;
this.stepProgress = 1;
this.walkDir = 0; // -1 walking off left, +1 walking off right
}
getCOM() {
let mx = 0, my = 0, total = 0;
for (let i = 0; i < 8; i++) {
const n = this.nodes[i];
mx += n.x * n.m;
my += n.y * n.m;
total += n.m;
}
return { x: mx / total, y: my / total };
}
getVelocity(idx, dt) {
const n = this.nodes[idx];
return { vx: (n.x - n.ox) / dt, vy: (n.y - n.oy) / dt };
}
updateBehavior(dt) {
const head = this.nodes[HEAD];
const chest = this.nodes[CHEST];
const pelvis = this.nodes[PELVIS];
const fl = this.nodes[FOOT_L];
const fr = this.nodes[FOOT_R];
const com = this.getCOM();
this.impactStress *= Math.pow(0.05, dt);
if (this.stepCooldown > 0) this.stepCooldown -= dt;
// Walk off landings so landings clear naturally during normal/post-recovery operation
if (pelvis.x > TOP_X + 6) this.walkDir = 1;
else if (pelvis.x < BOT_X - 35 && currentSpeed >= 0) this.walkDir = 1;
else if (pelvis.x < 95) this.walkDir = 1;
else this.walkDir = 0;
if (this.state === 'standing') {
const midFootX = (fl.x + fr.x) * 0.5;
const maxFootY = Math.max(fl.y, fr.y);
const torsoDX = chest.x - pelvis.x;
const torsoDY = pelvis.y - chest.y; // positive when upright
const tiltAngle = Math.atan2(torsoDX, torsoDY);
const eitherGrounded = fl.grounded || fr.grounded || (Math.abs(fl.y - getFloor(fl.x).y) < 8) || (Math.abs(fr.y - getFloor(fr.x).y) < 8);
const comOffset = com.x - midFootX;
// Check loss of balance triggers (pure physics state, zero scripting)
const tippedOver = Math.abs(tiltAngle) > 0.62 || torsoDY < 7;
const offBase = Math.abs(comOffset) > 23 * this.scale;
const collapsed = (maxFootY - head.y) < 33 * this.scale;
const hitHard = this.impactStress > 210;
if (!eitherGrounded || tippedOver || offBase || collapsed || hitHard) {
this.state = 'ragdoll';
this.downTimer = 0;
this.steppingFoot = -1;
return;
}
// Active standing balance muscles
const targetLean = this.walkDir * 3.5;
const errX = (midFootX + targetLean) - com.x;
// Apply horizontal ankle/hip balance torque (action on upper body, opposite reaction on feet)
const corr = Math.max(-240, Math.min(240, errX * 16)) * dt * dt;
chest.x += corr * 0.65;
pelvis.x += corr * 0.35;
fl.x -= corr * 0.5;
fr.x -= corr * 0.5;
// Upright spine alignment & knee extension force
const standHeight = 43 * this.scale;
const desiredChestY = maxFootY - standHeight;
if (chest.y > desiredChestY) {
const lift = Math.min(280 * dt * dt, (desiredChestY - chest.y) * 0.22);
chest.y -= lift * 0.65;
head.y -= lift * 0.85;
pelvis.y -= lift * 0.35;
fl.y += lift * 0.4;
fr.y += lift * 0.4;
}
// Keep knees slightly forward/straight so legs don't buckle backward
for (const [kIdx, fIdx] of [[KNEE_L, FOOT_L], [KNEE_R, FOOT_R]]) {
const knee = this.nodes[kIdx];
const foot = this.nodes[fIdx];
const midX = (pelvis.x + foot.x) * 0.5 + 1.5;
const midY = (pelvis.y + foot.y) * 0.5;
knee.x += (midX - knee.x) * 14 * dt;
knee.y += (midY - knee.y) * 10 * dt;
}
// Hand rests near escalator handrail height when riding
const hand = this.nodes[HAND];
if (pelvis.x >= BOT_X - 10 && pelvis.x <= TOP_X + 10) {
const railX = pelvis.x + 9 * this.scale;
const railY = getFloor(pelvis.x).y - 34 * this.scale;
hand.x += (railX - hand.x) * 10 * dt;
hand.y += (railY - hand.y) * 10 * dt;
}
// Stepping reflex when leaning or walking
if (this.steppingFoot === -1 && this.stepCooldown <= 0) {
const strideTrigger = this.walkDir !== 0 ? 4.5 : 8.5;
if (Math.abs(comOffset) > strideTrigger || Math.abs(fl.x - fr.x) > 18 || this.walkDir !== 0) {
const dir = this.walkDir !== 0 ? this.walkDir : Math.sign(comOffset);
// Pick trailing foot relative to desired step direction
this.steppingFoot = (dir > 0) ? (fl.x < fr.x ? FOOT_L : FOOT_R) : (fl.x > fr.x ? FOOT_L : FOOT_R);
const pVel = this.getVelocity(PELVIS, dt);
const flSurf = getFloor(pelvis.x);
const relVx = pVel.vx - flSurf.vx;
let rawTarget = com.x + dir * (9 * this.scale) + relVx * 0.14;
// Clamp step length to anatomical reach
rawTarget = Math.max(pelvis.x - 18 * this.scale, Math.min(pelvis.x + 18 * this.scale, rawTarget));
this.stepTargetX = rawTarget;
this.stepProgress = 0;
}
}
if (this.steppingFoot !== -1) {
this.stepProgress += dt * 6.5;
const fNode = this.nodes[this.steppingFoot];
const surf = getFloor(this.stepTargetX);
const arc = Math.sin(Math.min(1, this.stepProgress) * Math.PI) * 11;
fNode.x += (this.stepTargetX - fNode.x) * Math.min(1, dt * 22);
const targetY = Math.min(fNode.y, surf.y - arc);
fNode.y += (targetY - fNode.y) * Math.min(1, dt * 20);
if (this.stepProgress >= 1) {
this.steppingFoot = -1;
this.stepCooldown = 0.09;
}
} else {
// Keep modest natural stance separation
const sep = fr.x - fl.x;
if (Math.abs(sep) < 6) {
fl.x -= 8 * dt;
fr.x += 8 * dt;
}
}
} else if (this.state === 'ragdoll') {
this.downTimer += dt;
// Protective curl reflex slightly bends knees/arms during a tumble so limbs look organic
const kl = this.nodes[KNEE_L], kr = this.nodes[KNEE_R];
kl.y -= 15 * dt * dt;
kr.y -= 15 * dt * dt;
// Evaluate whether the person has room, low velocity, and footing to get back up
if (this.downTimer > 0.65) {
const pv = this.getVelocity(PELVIS, dt);
const surf = getFloor(pelvis.x);
const relSpeed = Math.hypot(pv.vx - surf.vx, pv.vy - surf.vy);
const nearGround = Math.abs(pelvis.y - surf.y) < 34 || Math.abs(chest.y - surf.y) < 34;
// Count weight of other bodies pressing down from above
let buriedScore = 0;
for (const other of people) {
if (other === this) continue;
const op = other.nodes[PELVIS];
const oc = other.nodes[CHEST];
if (Math.abs(op.x - pelvis.x) < 24 && op.y < pelvis.y && op.y > pelvis.y - 38) buriedScore++;
if (Math.abs(oc.x - chest.x) < 24 && oc.y < chest.y && oc.y > chest.y - 38) buriedScore++;
}
// Harder to stand up on fast-reversing stairs than on flat landings or normal stairs
const speedLimit = (surf.onSteps && Math.abs(currentSpeed) > 150) ? 38 : 78;
if (nearGround && relSpeed < speedLimit && buriedScore === 0 && this.impactStress < 75) {
this.state = 'recovering';
this.recoverTimer = 0;
}
}
} else if (this.state === 'recovering') {
this.recoverTimer += dt;
// Abort recovery if slammed into again
if (this.impactStress > 165) {
this.state = 'ragdoll';
this.downTimer = 0.1;
return;
}
// Check if someone fell directly on top of us mid-recovery
for (const other of people) {
if (other === this) continue;
const op = other.nodes[PELVIS];
if (Math.abs(op.x - chest.x) < 18 && op.y < chest.y - 4 && op.y > chest.y - 28) {
this.state = 'ragdoll';
this.downTimer = 0.2;
return;
}
}
// Pull feet underneath pelvis, push pelvis & chest upward against the floor
const baseSurf = getFloor(pelvis.x);
const goalFootL = pelvis.x - 6 * this.scale;
const goalFootR = pelvis.x + 6 * this.scale;
fl.x += (goalFootL - fl.x) * 8 * dt;
fr.x += (goalFootR - fr.x) * 8 * dt;
fl.y += (getFloor(fl.x).y - fl.y) * 9 * dt;
fr.y += (getFloor(fr.x).y - fr.y) * 9 * dt;
const standProgress = Math.min(1, this.recoverTimer / 0.65);
const footX = (fl.x + fr.x) * 0.5;
const footY = Math.max(fl.y, fr.y);
const targetPelvisY = footY - 25 * this.scale * standProgress;
const targetChestY = footY - 43 * this.scale * standProgress;
const targetHeadY = footY - 58 * this.scale * standProgress;
const pull = 12 * dt;
pelvis.x += (footX - pelvis.x) * pull;
pelvis.y += (targetPelvisY - pelvis.y) * pull;
chest.x += (footX - chest.x) * pull;
chest.y += (targetChestY - chest.y) * pull;
head.x += (footX - head.x) * pull;
head.y += (targetHeadY - head.y) * pull;
// Reaction force pushes feet down onto steps
fl.y += 90 * dt * dt;
fr.y += 90 * dt * dt;
if (this.recoverTimer >= 0.68) {
this.state = 'standing';
this.stepCooldown = 0.1;
}
}
}
}
function spawnInitialCrowd() {
people = [];
nextId = 1;
stepPhase = 0;
currentSpeed = NORMAL_SPEED;
targetSpeed = NORMAL_SPEED;
reverseBtn.classList.remove('reversing');
reverseBtn.textContent = 'THROW INTO HIGH-SPEED REVERSE';
// Place 18 figures cleanly on the steps
const count = 18;
for (let i = 0; i < count; i++) {
// Distribute across steps from bottom to near-top
const stepIdx = 0.6 + i * 0.94;
const px = BOT_X + stepIdx * STEP_W + 14;
const fl = getFloor(px);
people.push(new Person(px, fl.y - 5));
}
}
function stepPhysics(frameDt) {
const dt = frameDt / SUBSTEPS;
for (let sub = 0; sub < SUBSTEPS; sub++) {
// 1. Update escalator motor speed & step phase
if (currentSpeed < targetSpeed) {
currentSpeed = Math.min(targetSpeed, currentSpeed + MOTOR_ACCEL * dt);
} else if (currentSpeed > targetSpeed) {
currentSpeed = Math.max(targetSpeed, currentSpeed - MOTOR_ACCEL * dt);
}
const stepAdvanceRate = (currentSpeed * (DX / ESC_LEN)) / STEP_W;
stepPhase = (stepPhase + stepAdvanceRate * dt) % 1;
if (stepPhase < 0) stepPhase += 1;
// 2. Verlet integration & active human balance
for (let p = 0; p < people.length; p++) {
const person = people[p];
for (let i = 0; i < 8; i++) {
const n = person.nodes[i];
const vx = (n.x - n.ox) * 0.996;
const vy = (n.y - n.oy) * 0.996;
n.ox = n.x;
n.oy = n.y;
n.x += vx;
n.y += vy + GRAVITY * dt * dt;
n.grounded = false;
}
person.updateBehavior(dt);
}
// 3. Iterative constraint + collision + stair geometry solver
for (let iter = 0; iter < SOLVER_ITERS; iter++) {
// Internal skeleton constraints
for (let p = 0; p < people.length; p++) {
const person = people[p];
const nodes = person.nodes;
const sticks = person.sticks;
for (let s = 0; s < sticks.length; s++) {
const [ia, ib, restLen, stiff] = sticks[s];
// Soften anatomical head-pelvis brace when ragdolling
const k = (s === 7 && person.state === 'ragdoll') ? 0.04 : stiff;
const a = nodes[ia], b = nodes[ib];
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 0.0001;
const diff = (dist - restLen) / dist;
const invMa = 1 / a.m, invMb = 1 / b.m;
const wSum = invMa + invMb;
const corrX = dx * diff * k;
const corrY = dy * diff * k;
a.x += corrX * (invMa / wSum);
a.y += corrY * (invMa / wSum);
b.x -= corrX * (invMb / wSum);
b.y -= corrY * (invMb / wSum);
}
}
// Pairwise multi-body node collisions (run on first 2 iterations for speed + firm stacking)
if (iter < 2) {
const len = people.length;
for (let i = 0; i < len; i++) {
const pA = people[i];
const comA = pA.nodes[PELVIS];
for (let j = i + 1; j < len; j++) {
const pB = people[j];
const comB = pB.nodes[PELVIS];
// Broadphase bounding check
if (Math.abs(comA.x - comB.x) > 85 || Math.abs(comA.y - comB.y) > 85) continue;
for (let na = 0; na < 7; na++) { // collide head, chest, pelvis, knees, feet
const a = pA.nodes[na];
for (let nb = 0; nb < 7; nb++) {
const b = pB.nodes[nb];
// Allow feet of adjacent standing people to pass slightly without tripping during normal ride
if (na >= 5 && nb >= 5 && pA.state === 'standing' && pB.state === 'standing') continue;
const minDist = (a.r + b.r) * 0.86;
const dx = b.x - a.x;
const dy = b.y - a.y;
const d2 = dx * dx + dy * dy;
if (d2 < minDist * minDist && d2 > 0.00001) {
const d = Math.sqrt(d2);
const overlap = minDist - d;
const nx = dx / d;
const ny = dy / d;
const invMa = 1 / a.m, invMb = 1 / b.m;
const wSum = invMa + invMb;
const push = overlap * 0.62;
a.x -= nx * push * (invMa / wSum);
a.y -= ny * push * (invMa / wSum);
b.x += nx * push * (invMb / wSum);
b.y += ny * push * (invMb / wSum);
// Relative velocity impulse & friction between colliding bodies
const avx = a.x - a.ox, avy = a.y - a.oy;
const bvx = b.x - b.ox, bvy = b.y - b.oy;
const rvx = bvx - avx, rvy = bvy - avy;
const relNormal = rvx * nx + rvy * ny;
if (relNormal < 0) {
const impactSpeed = -relNormal / dt;
if (impactSpeed > 55) {
const jStress = impactSpeed * 0.36;
pA.impactStress += jStress * (b.m / a.m);
pB.impactStress += jStress * (a.m / b.m);
}
// Tangential body-on-body friction so piles catch & tangle
const tx = -ny, ty = nx;
const relTan = (rvx * tx + rvy * ty) * 0.18;
a.ox -= tx * relTan * 0.5;
a.oy -= ty * relTan * 0.5;
b.ox += tx * relTan * 0.5;
b.oy += ty * relTan * 0.5;
}
}
}
}
}
}
}
// Stair treads, vertical step risers, and world walls
for (let p = 0; p < people.length; p++) {
const person = people[p];
for (let i = 0; i < 8; i++) {
const n = person.nodes[i];
const isFoot = (i === FOOT_L || i === FOOT_R);
const effR = isFoot ? 2.5 : n.r * 0.75;
// Left / right boundary walls
if (n.x < 28 + effR) {
n.x = 28 + effR;
} else if (n.x > canvas.width - 28 - effR) {
n.x = canvas.width - 28 - effR;
}
const surf = getFloor(n.x);
// Tread horizontal contact
if (n.y + effR > surf.y) {
const pen = (n.y + effR) - surf.y;
n.y = surf.y - effR;
n.grounded = true;
// If a head, chest, or pelvis hits the stairs, lose balance immediately
if (i <= PELVIS && person.state === 'standing' && pen > 1.2) {
person.impactStress += 260;
}
// Surface friction: shoes grip the moving step hard; clothes/bodies slide & tumble
const grip = isFoot
? (person.state === 'standing' ? 0.92 : 0.55)
: 0.28;
const curVx = n.x - n.ox;
const curVy = n.y - n.oy;
const targetVx = surf.vx * dt;
const targetVy = surf.vy * dt;
const newVx = curVx + (targetVx - curVx) * grip;
// Dampen downward bounce into steel step
const newVy = Math.min(curVy, targetVy);
n.ox = n.x - newVx;
n.oy = n.y - newVy;
}
// Vertical step riser contact (keeps bodies & feet from clipping horizontally into the next step)
if (surf.onSteps && surf.riserX !== null) {
const distToRiser = surf.riserX - (n.x + effR);
if (distToRiser < 0 && n.y + effR > surf.riserTopY + 3.5) {
n.x = surf.riserX - effR;
const targetVx = surf.vx * dt;
const curVx = n.x - n.ox;
if (curVx > targetVx) {
const hitVel = (curVx - targetVx) / dt;
if (hitVel > 90 && !isFoot) person.impactStress += hitVel * 0.25;
n.ox = n.x - targetVx;
}
}
}
}
}
}
}
// Recycle riders who safely exit the top right landing back to the bottom left entrance
// (only when moving forward normally, so during reverse nobody teleports out of the pileup)
if (targetSpeed > 0 && currentSpeed > 0) {
for (let p = 0; p < people.length; p++) {
const person = people[p];
if (person.nodes[PELVIS].x > canvas.width - 55 && person.state === 'standing') {
// Check if bottom entrance has clear space
let clear = true;
for (let q = 0; q < people.length; q++) {
if (Math.abs(people[q].nodes[PELVIS].x - (BOT_X - 18)) < 28) {
clear = false;
break;
}
}
if (clear) {
const shiftX = (BOT_X - 18) - person.nodes[PELVIS].x;
const shiftY = BOT_Y - Math.max(person.nodes[FOOT_L].y, person.nodes[FOOT_R].y);
for (const n of person.nodes) {
n.x += shiftX; n.ox += shiftX;
n.y += shiftY; n.oy += shiftY;
}
}
}
}
}
}
function drawEscalator() {
ctx.save();
// Background architectural grid / Mall wall cues
ctx.strokeStyle = '#22262e';
ctx.lineWidth = 1;
for (let x = 60; x < canvas.width; x += 90) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(0, TOP_Y);
ctx.lineTo(canvas.width, TOP_Y);
ctx.stroke();
// Upper floor slab
ctx.fillStyle = '#252a33';
ctx.fillRect(TOP_X, TOP_Y, canvas.width - TOP_X, 38);
// Escalator heavy steel truss under-structure
ctx.fillStyle = '#1f232a';
ctx.strokeStyle = '#343b46';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, BOT_Y);
ctx.lineTo(BOT_X, BOT_Y);
ctx.lineTo(TOP_X, TOP_Y);
ctx.lineTo(canvas.width, TOP_Y);
ctx.lineTo(canvas.width, TOP_Y + 62);
ctx.lineTo(TOP_X + 26, TOP_Y + 62);
ctx.lineTo(BOT_X + 26, BOT_Y + 72);
ctx.lineTo(0, BOT_Y + 72);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Back handrail glass balustrade
ctx.fillStyle = 'rgba(130, 175, 210, 0.07)';
ctx.beginPath();
ctx.moveTo(BOT_X - 36, BOT_Y);
ctx.lineTo(BOT_X - 36, BOT_Y - 40);
ctx.lineTo(TOP_X + 36, TOP_Y - 40);
ctx.lineTo(TOP_X + 36, TOP_Y);
ctx.closePath();
ctx.fill();
// Moving steps (clipped cleanly between bottom and top comb plates)
ctx.save();
ctx.beginPath();
ctx.rect(BOT_X, TOP_Y - 5, DX, -DY + 10);
ctx.clip();
for (let k = -2; k <= NUM_STEPS + 2; k++) {
const sx = BOT_X + (k + stepPhase) * STEP_W;
const sy = Math.min(BOT_Y, Math.max(TOP_Y, BOT_Y - (k + stepPhase) * STEP_H));
// Step solid triangular/block body
ctx.fillStyle = '#3a414d';
ctx.fillRect(sx, sy, STEP_W + 1, STEP_H + 22);
// Vertical grooved riser face
ctx.strokeStyle = '#525b6b';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(sx + STEP_W, sy);
ctx.lineTo(sx + STEP_W, sy + STEP_H + 10);
ctx.stroke();
// Yellow safety demarcation edge on tread
ctx.fillStyle = '#d4a72c';
ctx.fillRect(sx, sy, STEP_W - 1, 2.5);
}
ctx.restore();
// Bottom & Top landing floors + yellow comb plates
ctx.fillStyle = '#2d333d';
ctx.fillRect(0, BOT_Y, BOT_X + 2, canvas.height - BOT_Y);
ctx.fillRect(TOP_X - 2, TOP_Y, canvas.width - TOP_X + 2, 26);
ctx.fillStyle = '#e0b028';
ctx.fillRect(BOT_X - 18, BOT_Y - 1, 22, 4);
ctx.fillRect(TOP_X - 4, TOP_Y - 1, 22, 4);
// Direction indicator arrow on truss side
const midTrussX = (BOT_X + TOP_X) * 0.5 + 28;
const midTrussY = (BOT_Y + TOP_Y) * 0.5 + 38;
ctx.save();
ctx.translate(midTrussX, midTrussY);
ctx.rotate(Math.atan2(DY, DX));
const rev = currentSpeed < -10;
ctx.fillStyle = rev ? '#ff4d4f' : '#3fb950';
const dirSign = rev ? -1 : 1;
for (let m = -1; m <= 1; m++) {
const ox = m * 24;
ctx.beginPath();
ctx.moveTo(ox + 8 * dirSign, 0);
ctx.lineTo(ox - 5 * dirSign, -6);
ctx.lineTo(ox - 5 * dirSign, 6);
ctx.closePath();
ctx.fill();
}
ctx.restore();
ctx.restore();
}
function drawPerson(p) {
const n = p.nodes;
const head = n[HEAD], chest = n[CHEST], pelvis = n[PELVIS];
const kl = n[KNEE_L], kr = n[KNEE_R], fl = n[FOOT_L], fr = n[FOOT_R], hand = n[HAND];
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Back leg
ctx.strokeStyle = p.pants;
ctx.lineWidth = 6.5 * p.scale;
ctx.beginPath();
ctx.moveTo(pelvis.x, pelvis.y);
ctx.lineTo(kl.x, kl.y);
ctx.lineTo(fl.x, fl.y);
ctx.stroke();
// Back shoe
ctx.strokeStyle = '#181a1f';
ctx.lineWidth = 4.5 * p.scale;
ctx.beginPath();
ctx.moveTo(fl.x - 2, fl.y);
ctx.lineTo(fl.x + 5, fl.y);
ctx.stroke();
// Front leg (slightly lighter shade for clear depth separation in a pile)
ctx.strokeStyle = p.pants;
ctx.lineWidth = 6.8 * p.scale;
ctx.beginPath();
ctx.moveTo(pelvis.x, pelvis.y);
ctx.lineTo(kr.x, kr.y);
ctx.lineTo(fr.x, fr.y);
ctx.stroke();
// Front shoe
ctx.strokeStyle = '#111317';
ctx.lineWidth = 4.5 * p.scale;
ctx.beginPath();
ctx.moveTo(fr.x - 2, fr.y);
ctx.lineTo(fr.x + 5, fr.y);
ctx.stroke();
// Torso
ctx.strokeStyle = p.shirt;
ctx.lineWidth = 10.5 * p.scale;
ctx.beginPath();
ctx.moveTo(pelvis.x, pelvis.y);
ctx.lineTo(chest.x, chest.y);
ctx.stroke();
// Neck & Head
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(head.x, head.y, 7.2 * p.scale, 0, Math.PI * 2);
ctx.fill();
// Simple hair cap oriented along spine angle
const neckAng = Math.atan2(head.y - chest.y, head.x - chest.x);
ctx.fillStyle = '#252120';
ctx.beginPath();
ctx.arc(head.x, head.y, 7.4 * p.scale, neckAng - 1.35, neckAng + 1.35);
ctx.fill();
// Arm with inferred elbow bend
const armDx = hand.x - chest.x;
const armDy = hand.y - chest.y;
const elbowX = chest.x + armDx * 0.5 - armDy * 0.18;
const elbowY = chest.y + armDy * 0.5 + Math.abs(armDx) * 0.16;
ctx.strokeStyle = p.shirt;
ctx.lineWidth = 5 * p.scale;
ctx.beginPath();
ctx.moveTo(chest.x, chest.y);
ctx.lineTo(elbowX, elbowY);
ctx.lineTo(hand.x, hand.y);
ctx.stroke();
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(hand.x, hand.y, 2.8 * p.scale, 0, Math.PI * 2);
ctx.fill();
}
function drawForegroundBalustrade() {
ctx.save();
// Translucent foreground glass panel
ctx.fillStyle = 'rgba(155, 195, 230, 0.10)';
ctx.strokeStyle = 'rgba(200, 225, 250, 0.22)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(BOT_X - 28, BOT_Y);
ctx.lineTo(BOT_X - 28, BOT_Y - 36);
ctx.lineTo(TOP_X + 28, TOP_Y - 36);
ctx.lineTo(TOP_X + 28, TOP_Y);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Moving black rubber handrail with subtle motion ticks
ctx.strokeStyle = '#111318';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(BOT_X - 32, BOT_Y - 36);
ctx.lineTo(BOT_X, BOT_Y - 36);
ctx.lineTo(TOP_X, TOP_Y - 36);
ctx.lineTo(TOP_X + 32, TOP_Y - 36);
ctx.stroke();
// Motion markers on handrail so velocity reversal is visually unmistakable
ctx.strokeStyle = '#485262';
ctx.lineWidth = 3;
for (let k = 0; k < NUM_STEPS; k += 2) {
const t = ((k + stepPhase) / NUM_STEPS) % 1;
const rx = BOT_X + t * DX;
const ry = (BOT_Y - 36) + t * DY;
ctx.beginPath();
ctx.moveTo(rx - 3, ry + 2);
ctx.lineTo(rx + 3, ry - 2);
ctx.stroke();
}
ctx.restore();
}
function updateHUD() {
let standing = 0;
for (let i = 0; i < people.length; i++) {
if (people[i].state === 'standing') standing++;
}
countText.innerHTML = `Standing: <strong>${standing} / ${people.length}</strong>`;
if (targetSpeed < 0) {
statusText.innerHTML = `Mode: <strong style="color:#ff6b6b">RUNAWAY REVERSE (${Math.round(Math.abs(currentSpeed))} px/s)</strong>`;
} else {
statusText.innerHTML = `Mode: <strong style="color:#56d364">Normal Ascent (${Math.round(currentSpeed)} px/s)</strong>`;
}
}
reverseBtn.addEventListener('click', () => {
if (targetSpeed > 0) {
targetSpeed = REVERSE_SPEED;
reverseBtn.classList.add('reversing');
reverseBtn.textContent = 'RESTORE NORMAL ASCENT';
} else {
targetSpeed = NORMAL_SPEED;
reverseBtn.classList.remove('reversing');
reverseBtn.textContent = 'THROW INTO HIGH-SPEED REVERSE';
}
});
resetBtn.addEventListener('click', () => {
spawnInitialCrowd();
});
// Main simulation loop with clamped fixed-feel delta
spawnInitialCrowd();
let lastTime = performance.now();
function frame(now) {
const rawDt = (now - lastTime) / 1000;
lastTime = now;
const clampedDt = Math.min(0.025, Math.max(0.004, rawDt));
stepPhysics(clampedDt);
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawEscalator();
// Sort draw order subtly by id so overlap flicker doesn't occur
for (let i = 0; i < people.length; i++) {
drawPerson(people[i]);
}
drawForegroundBalustrade();
updateHUD();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
When you press the reverse button, the escalator accelerates downward to 6.5x its normal ascending speed in about a fifth of a second; high-friction shoe contact yanks everyone's feet down-slope while upper-body inertia pitches their chests forward, causing almost everyone to face-plant onto the steps above them or domino into the person ahead, then slide down into a churning multi-body heap against the bottom comb plate. Two things are nudged beyond pure passive ragdolls: the landing comb plates have a tiny 24-pixel velocity transition zone so fallen bodies at the very bottom get nudged onto the flat landing floor instead of hovering forever on the last step's edge, and during normal forward ascent, riders who walk all the way off the top-right edge of the screen recycle back to the bottom-left entrance so you don't run out of passengers before pressing the button (recycling is disabled during reverse). What it does badly: because the simulation is strictly 2D in a single Z-plane, nobody can squeeze past a fallen body sideways—every collision is dead-on—so a dense pileup of 15+ ragdolls under heavy Verlet constraint pressure will occasionally jitter, fold limbs at awkward angles, or take several seconds of shoving after you restore normal ascent before the bottom-most person is unburied enough to stand up.