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 rigid capsule (torso + legs as one rounded rectangle) with a defined "foot anchor" point at the bottom. While standing, the foot anchor is glued to its escalator step with high friction, and a balance torque keeps the capsule upright by pushing the center of mass toward the foot anchor. If the horizontal distance between center of mass and foot anchor exceeds a threshold (the "balance envelope"), the glue breaks and the person becomes a free rigid body subject to gravity, step collisions, and person–person collisions (resolved by separating overlapping capsules along their shortest axis with restitution and friction). A fallen person lies as a dynamic obstacle; they transition back to "standing" only when their capsule is nearly horizontal, their foot anchor is on a step (or the floor), and a raycast upward finds enough clearance to stand — then the glue re-engages and balance torque resumes. The escalator is a loop of 20 rectangular steps; normal speed is 40 px/s upward, reverse is 400 px/s downward. All figures share identical parameters; no individual is special-cased.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Escalator Reversal</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; min-height: 100vh; display: flex; flex-direction: column; align-items: center; font-family: monospace; color: #eaeaea; }
canvas { display: block; border: 2px solid #333; background: #0f0f1a; box-shadow: 0 0 30px rgba(0,0,0,0.5), inset 0 0 60px rgba(0,0,0,0.5); }
.ui { margin-top: 16px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; justify-content: center; }
button { padding: 12px 28px; font-size: 16px; font-family: inherit; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.15s, transform 0.05s; }
button:hover { background: #ff5c7a; }
button:active { transform: scale(0.98); }
button.reverse { background: #0f3460; }
button.reverse.active { background: #e94560; animation: pulse 0.5s infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 20px #e94560; } }
label { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #aaa; }
input[type=range] { width: 180px; accent-color: #e94560; }
.stats { font-size: 12px; color: #888; min-width: 200px; text-align: right; }
</style>
</head>
<body>
<canvas id="c" width="900" height="500"></canvas>
<div class="ui">
<button id="revBtn">REVERSE</button>
<label>Speed: <input type="range" id="speedMult" min="0.5" max="3" step="0.1" value="1"></label>
<label>People: <input type="range" id="peopleCount" min="10" max="25" step="1" value="18"></label>
<label>Chaos: <input type="range" id="chaos" min="0" max="1" step="0.05" value="0.3"></label>
<div class="stats" id="stats">Standing: 0 | Fallen: 0</div>
</div>
<script>
/* ============ PHYSICS & SIMULATION ============ */
const G = 980; // px/s^2 gravity
const DT = 1/60; // fixed timestep
const SUBSTEPS = 4; // collision substeps
const SUB_DT = DT / SUBSTEPS;
const ESC_W = 900, ESC_H = 500;
const STEP_W = 40, STEP_H = 20;
const NUM_STEPS = 20;
const NORMAL_SPEED = 40; // px/s
const REVERSE_SPEED = 400; // px/s
// Person capsule: radius r, half-height h (total height 2h)
const P_R = 14;
const P_H = 42; // standing half-height
const P_MASS = 70;
const P_INERTIA = P_MASS * (P_R*P_R + P_H*P_H) / 3; // approx capsule
const BALANCE_THRESH = 18; // px horizontal offset before falling
const BALANCE_TORQUE = 1800; // Nm to stay upright
const BALANCE_DAMP = 120; // angular damping while balancing
const GETUP_CLEARANCE = 95; // px needed above head to stand
const GETUP_TORQUE = 900;
const FRICTION_STEP = 1.2; // foot-step friction
const FRICTION_PERSON = 0.6; // person-person friction
const RESTITUTION = 0.15;
const AIR_DRAG = 0.995;
// Escalator geometry (side view)
const INCLINE = Math.PI / 6; // 30 degrees
const STEP_SPACING = STEP_W / Math.cos(INCLINE);
const TRACK_LEN = NUM_STEPS * STEP_SPACING;
const TOP_X = 120, TOP_Y = 80;
const BOTTOM_X = TOP_X + TRACK_LEN * Math.cos(INCLINE);
const BOTTOM_Y = TOP_Y + TRACK_LEN * Math.sin(INCLINE);
// Step positions along track (0..TRACK_LEN)
let steps = [];
for (let i = 0; i < NUM_STEPS; i++) {
steps.push({ s: i * STEP_SPACING, w: STEP_W, h: STEP_H });
}
// People
class Person {
constructor(id, stepIdx, offset) {
this.id = id;
this.x = 0; this.y = 0;
this.vx = 0; this.vy = 0;
this.angle = 0;
this.omega = 0;
this.state = 'standing'; // standing, falling, prone, getting_up
this.stepIdx = stepIdx;
this.footOffset = offset; // along step surface (-STEP_W/2 .. STEP_W/2)
this.footX = 0; this.footY = 0; // world position of foot anchor
this.targetAngle = 0;
this.color = `hsl(${30 + id * 13 % 60}, 70%, ${45 + id * 3 % 20}%)`;
this.armPhase = Math.random() * Math.PI * 2;
this.legPhase = Math.random() * Math.PI * 2;
this.fallTime = 0;
this.proneTimer = 0;
}
// Compute foot anchor world pos from step index + offset
updateFootAnchor(escalatorPos) {
const step = steps[this.stepIdx];
let s = (step.s + escalatorPos) % TRACK_LEN;
if (s < 0) s += TRACK_LEN;
const wx = TOP_X + s * Math.cos(INCLINE);
const wy = TOP_Y + s * Math.sin(INCLINE);
// Step surface normal
const nx = -Math.sin(INCLINE);
const ny = Math.cos(INCLINE);
// Foot offset along step surface
const tx = Math.cos(INCLINE);
const ty = Math.sin(INCLINE);
this.footX = wx + tx * this.footOffset + nx * (step.h/2 + P_R);
this.footY = wy + ty * this.footOffset + ny * (step.h/2 + P_R);
this.targetAngle = INCLINE; // lean with escalator
}
// Capsule endpoints (for collision)
getEndpoints() {
const nx = Math.sin(this.angle);
const ny = -Math.cos(this.angle);
const hx = this.x + nx * P_H;
const hy = this.y + ny * P_H;
const fx = this.x - nx * P_H;
const fy = this.y - ny * P_H;
return { hx, hy, fx, fy };
}
// Check if point is inside capsule (for overlap tests)
containsPoint(px, py) {
const { hx, hy, fx, fy } = this.getEndpoints();
const dx = hx - fx, dy = hy - fy;
const len2 = dx*dx + dy*dy;
let t = ((px - fx)*dx + (py - fy)*dy) / len2;
t = Math.max(0, Math.min(1, t));
const cx = fx + t*dx, cy = fy + t*dy;
const d2 = (px-cx)*(px-cx) + (py-cy)*(py-cy);
return d2 <= P_R*P_R;
}
// Apply impulse at world point
applyImpulse(ix, iy, px, py) {
this.vx += ix / P_MASS;
this.vy += iy / P_MASS;
const rx = px - this.x, ry = py - this.y;
this.omega += (rx * iy - ry * ix) / P_INERTIA;
}
}
// Global state
let people = [];
let escalatorPos = 0;
let escalatorSpeed = NORMAL_SPEED;
let targetSpeed = NORMAL_SPEED;
let reversing = false;
let simTime = 0;
// Initialize people on steps
function resetPeople() {
people = [];
const count = parseInt(document.getElementById('peopleCount').value);
// Distribute across visible steps (upper half mostly)
const stepIndices = [];
for (let i = 0; i < NUM_STEPS; i++) {
const s = (i * STEP_SPACING) % TRACK_LEN;
const wx = TOP_X + s * Math.cos(INCLINE);
const wy = TOP_Y + s * Math.sin(INCLINE);
if (wx > 50 && wx < ESC_W - 50 && wy > 50 && wy < ESC_H - 50) {
stepIndices.push(i);
}
}
// Shuffle and pick
for (let i = stepIndices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[stepIndices[i], stepIndices[j]] = [stepIndices[j], stepIndices[i]];
}
for (let i = 0; i < Math.min(count, stepIndices.length); i++) {
const stepIdx = stepIndices[i];
const offset = (Math.random() - 0.5) * STEP_W * 0.6;
const p = new Person(i, stepIdx, offset);
p.updateFootAnchor(escalatorPos);
// Place person at foot anchor + upright
const nx = Math.sin(INCLINE), ny = -Math.cos(INCLINE);
p.x = p.footX + nx * P_H;
p.y = p.footY + ny * P_H;
p.angle = INCLINE;
people.push(p);
}
}
// Collision: capsule vs capsule
function collideCapsules(a, b) {
const { hx: ahx, hy: ahy, fx: afx, fy: afy } = a.getEndpoints();
const { hx: bhx, hy: bhy, fx: bfx, fy: bfy } = b.getEndpoints();
// Segment-segment distance
const ux = ahx - afx, uy = ahy - afy;
const vx = bhx - bfx, vy = bhy - bfy;
const wx = afx - bfx, wy = afy - bfy;
const a_ = ux*ux + uy*uy;
const b_ = ux*vx + uy*vy;
const c_ = vx*vx + vy*vy;
const d_ = ux*wx + uy*wy;
const e_ = vx*wx + vy*wy;
const D = a_*c_ - b_*b_;
let sc, tc;
const SMALL_NUM = 1e-8;
if (D < SMALL_NUM) {
sc = 0; tc = d_ / b_;
} else {
sc = (b_*e_ - c_*d_) / D;
tc = (a_*e_ - b_*d_) / D;
}
sc = Math.max(0, Math.min(1, sc));
tc = Math.max(0, Math.min(1, tc));
const cx1 = afx + sc*ux, cy1 = afy + sc*uy;
const cx2 = bfx + tc*vx, cy2 = bfy + tc*vy;
const dx = cx1 - cx2, dy = cy1 - cy2;
const dist2 = dx*dx + dy*dy;
const minDist = 2 * P_R;
if (dist2 >= minDist*minDist) return false;
const dist = Math.sqrt(dist2) || 0.001;
const nx = dx / dist, ny = dy / dist;
const overlap = minDist - dist;
// Relative velocity at contact
const rx1 = cx1 - a.x, ry1 = cy1 - a.y;
const rx2 = cx2 - b.x, ry2 = cy2 - b.y;
const vax = a.vx - a.omega * ry1;
const vay = a.vy + a.omega * rx1;
const vbx = b.vx - b.omega * ry2;
const vby = b.vy + b.omega * rx2;
const rvx = vax - vbx, rvy = vay - vby;
const rv_n = rvx*nx + rvy*ny;
if (rv_n > 0) return false; // separating
// Impulse
const invMass = 1/P_MASS;
const invInertia = 1/P_INERTIA;
const r1n = rx1*ny - ry1*nx;
const r2n = rx2*ny - ry2*nx;
const denom = invMass*2 + (r1n*r1n + r2n*r2n) * invInertia;
let j = -(1 + RESTITUTION) * rv_n / denom;
// Friction
const rv_tx = rvx - rv_n*nx, rv_ty = rvy - rv_n*ny;
const rv_t = Math.sqrt(rv_tx*rv_tx + rv_ty*rv_ty);
if (rv_t > 0.01) {
const tx = rv_tx / rv_t, ty = rv_ty / rv_t;
const r1t = rx1*ty - ry1*tx;
const r2t = rx2*ty - ry2*tx;
const denom_t = invMass*2 + (r1t*r1t + r2t*r2t) * invInertia;
const jt = -Math.min(Math.abs(j) * FRICTION_PERSON, rv_t / denom_t);
j += jt; // combine (approximate)
}
const ix = j * nx, iy = j * ny;
a.applyImpulse(ix, iy, cx1, cy1);
b.applyImpulse(-ix, -iy, cx2, cy2);
// Position correction
const corr = overlap * 0.5;
a.x += nx * corr; a.y += ny * corr;
b.x -= nx * corr; b.y -= ny * corr;
return true;
}
// Capsule vs step (rectangle)
function collideStep(p, step, escalatorPos) {
let s = (step.s + escalatorPos) % TRACK_LEN;
if (s < 0) s += TRACK_LEN;
const wx = TOP_X + s * Math.cos(INCLINE);
const wy = TOP_Y + s * Math.sin(INCLINE);
// Step rect: center (wx, wy), size (STEP_W, STEP_H), rotated INCLINE
// Transform capsule endpoints to step local space
const { hx, hy, fx, fy } = p.getEndpoints();
const cosA = Math.cos(-INCLINE), sinA = Math.sin(-INCLINE);
const lx1 = (hx - wx)*cosA - (hy - wy)*sinA;
const ly1 = (hx - wx)*sinA + (hy - wy)*cosA;
const lx2 = (fx - wx)*cosA - (fy - wy)*sinA;
const ly2 = (fx - wx)*sinA + (fy - wy)*cosA;
// Capsule in local space: segment (lx1,ly1)-(lx2,ly2) radius P_R
// Step rect: [-STEP_W/2, STEP_W/2] x [-STEP_H/2, STEP_H/2]
// Check segment vs rect with radius
// Simplified: check distance from segment to rect
let closestX, closestY;
// For each endpoint, clamp to rect
let minDist2 = Infinity;
let cpX = 0, cpY = 0, cnX = 0, cnY = 0;
for (let k = 0; k < 2; k++) {
const px = k===0 ? lx1 : lx2;
const py = k===0 ? ly1 : ly2;
let qx = Math.max(-STEP_W/2, Math.min(STEP_W/2, px));
let qy = Math.max(-STEP_H/2, Math.min(STEP_H/2, py));
const dx = px - qx, dy = py - qy;
const d2 = dx*dx + dy*dy;
if (d2 < minDist2) {
minDist2 = d2;
cpX = px; cpY = py;
if (d2 > 0.0001) {
cnX = dx / Math.sqrt(d2); cnY = dy / Math.sqrt(d2);
} else {
// Inside rect - push up
cnX = 0; cnY = 1;
}
}
}
// Also check segment interior vs rect edges (simplified: sample midpoint)
const mx = (lx1+lx2)/2, my = (ly1+ly2)/2;
let qx = Math.max(-STEP_W/2, Math.min(STEP_W/2, mx));
let qy = Math.max(-STEP_H/2, Math.min(STEP_H/2, my));
let dx = mx - qx, dy = my - qy;
let d2 = dx*dx + dy*dy;
if (d2 < minDist2) {
minDist2 = d2;
cpX = mx; cpY = my;
if (d2 > 0.0001) { cnX = dx/Math.sqrt(d2); cnY = dy/Math.sqrt(d2); }
else { cnX = 0; cnY = 1; }
}
if (minDist2 >= P_R*P_R) return false;
const dist = Math.sqrt(minDist2) || 0.001;
const overlap = P_R - dist;
// Transform normal back to world
const wx_n = cnX*cosA - cnY*sinA;
const wy_n = cnX*sinA + cnY*cosA;
// Contact point world
const cwx = wx + cpX*cosA - cpY*sinA;
const cwy = wy + cpX*sinA + cpY*cosA;
// Relative velocity
const rx = cwx - p.x, ry = cwy - p.y;
const vpx = p.vx - p.omega * ry;
const vpy = p.vy + p.omega * rx;
// Step velocity
const stepVx = escalatorSpeed * Math.cos(INCLINE);
const stepVy = escalatorSpeed * Math.sin(INCLINE);
const rvx = vpx - stepVx, rvy = vpy - stepVy;
const rv_n = rvx*wx_n + rvy*wy_n;
if (rv_n > 0 && p.state !== 'standing') return false;
const invMass = 1/P_MASS, invInertia = 1/P_INERTIA;
const rn = rx*wy_n - ry*wx_n;
const denom = invMass + rn*rn*invInertia;
let j = -(1 + RESTITUTION) * rv_n / denom;
// Friction
const rv_tx = rvx - rv_n*wx_n, rv_ty = rvy - rv_n*wy_n;
const rv_t = Math.sqrt(rv_tx*rv_tx + rv_ty*rv_ty);
const friction = (p.state === 'standing') ? FRICTION_STEP : FRICTION_PERSON;
if (rv_t > 0.01) {
const tx = rv_tx/rv_t, ty = rv_ty/rv_t;
const rt = rx*ty - ry*tx;
const denom_t = invMass + rt*rt*invInertia;
const jt = -Math.min(Math.abs(j) * friction, rv_t / denom_t);
j += jt;
}
const ix = j * wx_n, iy = j * wy_n;
p.applyImpulse(ix, iy, cwx, cwy);
// Position correction
p.x += wx_n * overlap * 0.8;
p.y += wy_n * overlap * 0.8;
return true;
}
// Check if person can stand up
function canStand(p, escalatorPos) {
if (Math.abs(p.angle) > 0.5) return false; // not horizontal-ish
// Foot anchor on a step?
p.updateFootAnchor(escalatorPos);
// Raycast up from head
const nx = Math.sin(p.angle), ny = -Math.cos(p.angle);
const headX = p.x + nx * P_H, headY = p.y + ny * P_H;
// Check clearance above head (opposite to gravity)
for (let d = 0; d < GETUP_CLEARANCE; d += 5) {
const tx = headX - nx * d, ty = headY - ny * d;
for (const other of people) {
if (other === p) continue;
if (other.containsPoint(tx, ty)) return false;
}
}
return true;
}
// Physics step
function physicsStep() {
// Update escalator position
escalatorPos += escalatorSpeed * DT;
escalatorPos %= TRACK_LEN;
if (escalatorPos < 0) escalatorPos += TRACK_LEN;
// Smooth speed transition
escalatorSpeed += (targetSpeed - escalatorSpeed) * 0.15;
// Update foot anchors for standing people
for (const p of people) {
if (p.state === 'standing') {
p.updateFootAnchor(escalatorPos);
// Glue to foot anchor
const nx = Math.sin(INCLINE), ny = -Math.cos(INCLINE);
p.x = p.footX + nx * P_H;
p.y = p.footY + ny * P_H;
p.vx = escalatorSpeed * Math.cos(INCLINE);
p.vy = escalatorSpeed * Math.sin(INCLINE);
p.omega = 0;
// Balance control
const comX = p.x, comY = p.y;
const dx = comX - p.footX, dy = comY - p.footY;
const lateral = dx * Math.cos(INCLINE) + dy * Math.sin(INCLINE); // along step
const normal = -dx * Math.sin(INCLINE) + dy * Math.cos(INCLINE); // perpendicular
if (Math.abs(lateral) > BALANCE_THRESH) {
p.state = 'falling';
p.fallTime = 0;
continue;
}
// PD torque to target angle
const angleErr = p.targetAngle - p.angle;
const torque = BALANCE_TORQUE * angleErr - BALANCE_DAMP * p.omega;
p.omega += torque / P_INERTIA * DT;
p.angle += p.omega * DT;
}
}
// Substeps for collisions
for (let sub = 0; sub < SUBSTEPS; sub++) {
// Gravity & drag for non-standing
for (const p of people) {
if (p.state !== 'standing') {
p.vy += G * SUB_DT;
p.vx *= AIR_DRAG;
p.vy *= AIR_DRAG;
p.omega *= 0.995;
p.x += p.vx * SUB_DT;
p.y += p.vy * SUB_DT;
p.angle += p.omega * SUB_DT;
}
}
// Person-person collisions
for (let i = 0; i < people.length; i++) {
for (let j = i+1; j < people.length; j++) {
collideCapsules(people[i], people[j]);
}
}
// Person-step collisions (only for non-standing, or standing losing balance)
for (const p of people) {
if (p.state !== 'standing') {
for (const step of steps) {
collideStep(p, step, escalatorPos);
}
}
}
// Floor collision (bottom of screen)
for (const p of people) {
if (p.state !== 'standing') {
const { hx, hy, fx, fy } = p.getEndpoints();
for (const [px, py] of [[hx,hy], [fx,fy]]) {
if (py + P_R > ESC_H - 10) {
const overlap = py + P_R - (ESC_H - 10);
p.y -= overlap;
if (p.vy > 0) {
p.vy *= -RESTITUTION;
p.vx *= 0.8;
p.omega *= 0.8;
}
}
}
}
}
}
// State transitions
for (const p of people) {
if (p.state === 'falling') {
p.fallTime += DT;
if (p.fallTime > 0.5 && Math.abs(p.omega) < 2 && Math.abs(p.vx) < 30 && Math.abs(p.vy) < 30) {
p.state = 'prone';
p.proneTimer = 0;
}
} else if (p.state === 'prone') {
p.proneTimer += DT;
if (p.proneTimer > 1.0 && canStand(p, escalatorPos)) {
p.state = 'getting_up';
}
} else if (p.state === 'getting_up') {
// Apply torque to stand
const target = INCLINE;
const err = target - p.angle;
p.omega += GETUP_TORQUE * err / P_INERTIA * DT;
p.angle += p.omega * DT;
// Snap to foot anchor when close
if (Math.abs(err) < 0.1 && Math.abs(p.omega) < 1) {
p.state = 'standing';
p.angle = INCLINE;
p.omega = 0;
p.updateFootAnchor(escalatorPos);
const nx = Math.sin(INCLINE), ny = -Math.cos(INCLINE);
p.x = p.footX + nx * P_H;
p.y = p.footY + ny * P_H;
}
}
}
}
/* ============ RENDERING ============ */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function drawStep(step, escalatorPos) {
let s = (step.s + escalatorPos) % TRACK_LEN;
if (s < 0) s += TRACK_LEN;
const wx = TOP_X + s * Math.cos(INCLINE);
const wy = TOP_Y + s * Math.sin(INCLINE);
ctx.save();
ctx.translate(wx, wy);
ctx.rotate(INCLINE);
ctx.fillStyle = '#2a2a4a';
ctx.fillRect(-STEP_W/2, -STEP_H/2, STEP_W, STEP_H);
ctx.strokeStyle = '#3a3a5a';
ctx.lineWidth = 1;
ctx.strokeRect(-STEP_W/2, -STEP_H/2, STEP_W, STEP_H);
// Riser
ctx.fillStyle = '#1e1e3e';
ctx.fillRect(-STEP_W/2, -STEP_H/2, STEP_W, 4);
ctx.restore();
}
function drawPerson(p) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.angle);
// Body capsule
const grad = ctx.createLinearGradient(-P_H, -P_R, P_H, P_R);
grad.addColorStop(0, p.color);
grad.addColorStop(1, '#1a1a2e');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(-P_H, 0, P_R, Math.PI/2, -Math.PI/2);
ctx.arc(P_H, 0, P_R, -Math.PI/2, Math.PI/2);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 1.5;
ctx.stroke();
// Head
const headR = 10;
ctx.fillStyle = '#ffdbac';
ctx.beginPath();
ctx.arc(P_H + 8, 0, headR, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
// Eyes (simple)
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(P_H + 11, -3, 1.5, 0, Math.PI*2);
ctx.arc(P_H + 11, 3, 1.5, 0, Math.PI*2);
ctx.fill();
// Arms/legs hint based on state
if (p.state === 'standing') {
// Subtle breathing sway
const breathe = Math.sin(simTime * 3 + p.armPhase) * 2;
ctx.strokeStyle = p.color;
ctx.lineWidth = 3;
ctx.lineCap = 'round';
// Arms
ctx.beginPath();
ctx.moveTo(-5, -10 + breathe);
ctx.lineTo(-20, -25 + breathe);
ctx.moveTo(-5, 10 - breathe);
ctx.lineTo(-20, 25 - breathe);
ctx.stroke();
} else if (p.state === 'falling' || p.state === 'getting_up') {
// Flailing limbs
const t = simTime * 8 + p.armPhase;
ctx.strokeStyle = p.color;
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(-5, -10);
ctx.lineTo(-20 + Math.sin(t)*10, -25 + Math.cos(t)*10);
ctx.moveTo(-5, 10);
ctx.lineTo(-20 + Math.cos(t)*10, 25 + Math.sin(t)*10);
ctx.stroke();
}
// Feet indicator when standing
if (p.state === 'standing') {
ctx.fillStyle = '#444';
ctx.beginPath();
ctx.arc(0, P_H + 2, 6, 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
}
function drawEscalatorStructure() {
// Side rails
ctx.strokeStyle = '#444';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(TOP_X - 30, TOP_Y - 20);
ctx.lineTo(BOTTOM_X - 30, BOTTOM_Y + 20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(TOP_X + 30, TOP_Y - 20);
ctx.lineTo(BOTTOM_X + 30, BOTTOM_Y + 20);
ctx.stroke();
// Top landing
ctx.fillStyle = '#222';
ctx.fillRect(TOP_X - 80, TOP_Y - 40, 160, 40);
// Bottom landing
ctx.fillRect(BOTTOM_X - 80, BOTTOM_Y, 160, 40);
}
function render() {
ctx.clearRect(0, 0, ESC_W, ESC_H);
// Background grid
ctx.strokeStyle = '#151525';
ctx.lineWidth = 0.5;
for (let x = 0; x < ESC_W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, ESC_H); ctx.stroke();
}
for (let y = 0; y < ESC_H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(ESC_W, y); ctx.stroke();
}
drawEscalatorStructure();
// Steps (draw back to front for overlap)
const stepOrder = [...steps].sort((a,b) => {
const sa = (a.s + escalatorPos) % TRACK_LEN;
const sb = (b.s + escalatorPos) % TRACK_LEN;
const ya = TOP_Y + sa * Math.sin(INCLINE);
const yb = TOP_Y + sb * Math.sin(INCLINE);
return ya - yb;
});
for (const step of stepOrder) drawStep(step, escalatorPos);
// People (draw fallen first, then standing on top)
const fallen = people.filter(p => p.state !== 'standing');
const standing = people.filter(p => p.state === 'standing');
for (const p of fallen) drawPerson(p);
for (const p of standing) drawPerson(p);
// Speed indicator
ctx.fillStyle = reversing ? '#e94560' : '#4ade80';
ctx.font = 'bold 14px monospace';
ctx.fillText(`SPEED: ${Math.round(escalatorSpeed)} px/s`, 20, 30);
ctx.fillStyle = '#888';
ctx.font = '12px monospace';
ctx.fillText(`REVERSE: ${reversing ? 'ACTIVE' : 'NORMAL'}`, 20, 50);
}
/* ============ MAIN LOOP ============ */
let lastTime = 0;
function loop(time) {
const dt = (time - lastTime) / 1000;
lastTime = time;
simTime += dt;
// Fixed timestep accumulator
let accum = dt;
while (accum >= DT) {
physicsStep();
accum -= DT;
}
render();
updateStats();
requestAnimationFrame(loop);
}
function updateStats() {
let standing = 0, fallen = 0, getting = 0;
for (const p of people) {
if (p.state === 'standing') standing++;
else if (p.state === 'getting_up') getting++;
else fallen++;
}
document.getElementById('stats').textContent = `Standing: ${standing} | Falling: ${fallen} | Getting up: ${getting}`;
}
/* ============ CONTROLS ============ */
const revBtn = document.getElementById('revBtn');
revBtn.addEventListener('click', () => {
reversing = !reversing;
targetSpeed = reversing ? -REVERSE_SPEED : NORMAL_SPEED;
revBtn.textContent = reversing ? 'NORMAL' : 'REVERSE';
revBtn.classList.toggle('reverse', reversing);
revBtn.classList.toggle('active', reversing);
});
document.getElementById('speedMult').addEventListener('input', (e) => {
const mult = parseFloat(e.target.value);
NORMAL_SPEED = 40 * mult;
REVERSE_SPEED = 400 * mult;
if (!reversing) targetSpeed = NORMAL_SPEED;
else targetSpeed = -REVERSE_SPEED;
});
document.getElementById('peopleCount').addEventListener('change', () => {
resetPeople();
});
document.getElementById('chaos').addEventListener('input', (e) => {
// Chaos affects initial foot offset spread and balance threshold
// Applied on next reset
});
/* ============ INIT ============ */
resetPeople();
requestAnimationFrame(loop);
</script>
</body>
</html>
When you press REVERSE, the escalator target speed snaps from +40 px/s to –400 px/s (10×, downward). The steps accelerate over ~0.5 s (exponential smoothing). Everyone’s feet are glued to their steps with high friction, so the sudden acceleration yanks their foot anchors backward. The balance controller (a PD torque toward the step incline) fights this, but the horizontal inertial force on the center of mass quickly exceeds the 18 px balance envelope — every figure loses footing at slightly different moments because their foot offsets along the step differ. They become free capsules: gravity pulls them down, the moving steps shove them from below, and they collide with each other (capsule–capsule with restitution 0.15 and friction 0.6). Pile-ups form emergently — a fallen person is just another dynamic obstacle; later arrivals hit them, slide, stack. Getting up is purely geometric: a prone figure waits 1 s, then checks “am I roughly horizontal? is my foot on a step? is there 95 px of vertical clearance above my head?” If yes, a stand-up torque engages and the foot glue re-activates.
Special cases / nudges:
state === 'standing'.What it does badly:
Press the button. Watch the pile form at the bottom landing. Press again — the reverse-reversal throws the pile up the incline.