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 (rounded rectangle) with a center of mass offset slightly above the geometric center, two foot contact points at the bottom corners, and a state machine (STANDING, FALLING, SLIDING, GETTING_UP). While STANDING, the feet are pinned to the moving step by infinite friction — the escalator drags them by the soles. Balance is lost when the horizontal acceleration of the support (escalator step) creates a torque about the center of mass that exceeds what the foot-base width can resist; formally, when |a_support| * h_com > g * (foot_width/2), the person transitions to FALLING and the foot pins release. In FALLING/SLIDING the body is a free rigid body subject to gravity, step collision (half-plane constraint), and person-person collision resolved by a stiff repulsive spring-damper along the overlap normal with Coulomb friction. A person on the ground (SLIDING with low vertical velocity and contact) transitions to GETTING_UP when the local pressure above them drops (no overlapping torsos) and they have a clear foot-plant zone; GETTING_UP applies a strong restoring torque toward upright and re-pins feet when the angle nears zero and vertical velocity is near zero. All figures run identical rules; the only special-casing is the initial placement onto steps and the escalator's prescribed velocity profile.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Escalator Reverse</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #111; color: #eee; font-family: system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; min-height: 100vh; overflow: hidden; }
canvas { display: block; background: #0a0a0a; box-shadow: 0 0 40px rgba(0,0,0,0.8), inset 0 0 60px rgba(0,0,0,0.5); }
.ui { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); display: flex; gap: 16px; z-index: 10; pointer-events: auto; }
button { padding: 14px 32px; font-size: 16px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; transition: transform 0.05s, box-shadow 0.1s; pointer-events: auto; }
button:active { transform: scale(0.97); }
#revBtn { background: linear-gradient(180deg, #c00, #800); color: #fff; box-shadow: 0 4px 20px rgba(200,0,0,0.4); }
#revBtn.active { background: linear-gradient(180deg, #0a0, #060); box-shadow: 0 4px 20px rgba(0,200,0,0.4); }
#resetBtn { background: #333; color: #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
.info { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); text-align: center; font-size: 13px; color: #888; pointer-events: none; }
.stats { position: fixed; top: 16px; right: 24px; font: 12px monospace; color: #666; pointer-events: none; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Click <strong>REVERSE</strong> to flip direction at high speed. Click again to restore.</div>
<div class="stats" id="stats"></div>
<div class="ui">
<button id="revBtn">REVERSE</button>
<button id="resetBtn">RESET</button>
</div>
<script>
/* ============ CONFIG ============ */
const CFG = {
// World
G: 980, // px/s^2 gravity
WIDTH: 1000,
HEIGHT: 700,
// Escalator
STEP_H: 28, // step rise
STEP_D: 36, // step depth (tread)
NUM_STEPS: 28, // visible steps
NORMAL_SPEED: 80, // px/s
REVERSE_SPEED: 380, // px/s (high speed reverse)
ACCEL_TIME: 0.18, // s to flip direction
INCLINE: Math.atan2(28, 36), // ~38 deg
// Person
PERSON_COUNT: 18,
BODY_H: 110, // capsule height
BODY_W: 36, // capsule width
COM_OFFSET_Y: -18, // COM above geometric center (negative = higher)
FOOT_W: 36, // distance between feet
MASS: 65, // kg (arbitrary units)
INERTIA: 0, // computed from mass & dimensions
// Balance
BALANCE_TOL: 1.05, // multiplier on critical accel
// Collision
SPRING_K: 18000, // N/m person-person
DAMP_B: 450, // Ns/m person-person
FRICTION_MU: 0.45, // person-person friction
STEP_SPRING_K: 25000, // person-step
STEP_DAMP_B: 600,
STEP_FRICTION_MU: 0.6,
GROUND_FRICTION_MU: 0.7,
// Getting up
GETUP_TORQUE: 2800, // Nm
GETUP_ANGLE_THRESH: 0.15, // rad
GETUP_SPEED_THRESH: 15, // px/s
CLEARANCE_CHECK_RADIUS: 55,
// Visual
COLORS: [
'#e8d5b7','#d4a574','#c9845a','#b8734a','#a06040',
'#8b5538','#7a4a3c','#6b4234','#5c3a2e','#4d3228',
'#e0c8a0','#d0b890','#c0a880','#b09870','#a08860',
'#907850','#806840','#705830','#604828','#503820'
]
};
CFG.INERTIA = CFG.MASS * (CFG.BODY_H*CFG.BODY_H + CFG.BODY_W*CFG.BODY_W) / 12;
/* ============ MATH UTIL ============ */
const Vec = {
add: (a,b)=>({x:a.x+b.x, y:a.y+b.y}),
sub: (a,b)=>({x:a.x-b.x, y:a.y-b.y}),
mul: (a,s)=>({x:a.x*s, y:a.y*s}),
dot: (a,b)=>a.x*b.x+a.y*b.y,
len: a=>Math.hypot(a.x,a.y),
norm: a=>{const l=Math.hypot(a.x,a.y); return l?{x:a.x/l,y:a.y/l}:{x:0,y:0};},
perp: a=>({x:-a.y, y:a.x}),
rot: (a,ang)=>{const c=Math.cos(ang), s=Math.sin(ang); return {x:a.x*c-a.y*s, y:a.x*s+a.y*c};},
lerp: (a,b,t)=>({x:a.x+(b.x-a.x)*t, y:a.y+(b.y-a.y)*t})
};
/* ============ ESCALATOR GEOMETRY ============ */
class Escalator {
constructor() {
this.angle = CFG.INCLINE;
this.sinA = Math.sin(this.angle);
this.cosA = Math.cos(this.angle);
this.stepH = CFG.STEP_H;
this.stepD = CFG.STEP_D;
this.stepLen = Math.hypot(this.stepD, this.stepH);
this.numSteps = CFG.NUM_STEPS;
this.targetSpeed = CFG.NORMAL_SPEED;
this.currentSpeed = CFG.NORMAL_SPEED;
this.accelStart = 0;
this.accelDur = CFG.ACCEL_TIME;
this.reversing = false;
// Anchor: top of escalator at canvas center-ish
this.topX = CFG.WIDTH * 0.5;
this.topY = CFG.HEIGHT * 0.22;
this.length = this.numSteps * this.stepLen;
}
setReverse(on) {
if (on === this.reversing) return;
this.reversing = on;
this.targetSpeed = on ? -CFG.REVERSE_SPEED : CFG.NORMAL_SPEED;
this.accelStart = performance.now() / 1000;
}
update(dt, now) {
if (this.currentSpeed !== this.targetSpeed) {
const t = Math.min(1, (now - this.accelStart) / this.accelDur);
const eased = t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2;
this.currentSpeed = this.reversing ?
CFG.NORMAL_SPEED + ( -CFG.REVERSE_SPEED - CFG.NORMAL_SPEED ) * eased :
-CFG.REVERSE_SPEED + ( CFG.NORMAL_SPEED + CFG.REVERSE_SPEED ) * eased;
}
}
// Get step index and local coordinate for a world point projected onto escalator line
worldToStep(x, y) {
const dx = x - this.topX;
const dy = y - this.topY;
const along = dx * this.cosA + dy * this.sinA;
const perp = -dx * this.sinA + dy * this.cosA;
const stepIdx = Math.floor(along / this.stepLen);
const localAlong = along - stepIdx * this.stepLen;
return { stepIdx, localAlong, perp, along };
}
// Get step surface normal (upward) and tangent (upward along escalator)
get surfaceNormal() { return { x: -this.sinA, y: this.cosA }; }
get surfaceTangent() { return { x: this.cosA, y: this.sinA }; }
// Get world position of a point on step (localAlong in [0, stepLen], perp offset)
stepToWorld(stepIdx, localAlong, perpOffset=0) {
const along = stepIdx * this.stepLen + localAlong;
return {
x: this.topX + along * this.cosA - perpOffset * this.sinA,
y: this.topY + along * this.sinA + perpOffset * this.cosA
};
}
// Velocity of step surface at current speed
get surfaceVelocity() {
return Vec.mul(this.surfaceTangent, this.currentSpeed);
}
// Check if step index is visible
isVisible(stepIdx) {
return stepIdx >= -1 && stepIdx < this.numSteps + 1;
}
// Draw
draw(ctx) {
ctx.save();
ctx.translate(this.topX, this.topY);
ctx.rotate(this.angle);
// Steps
for (let i = -1; i <= this.numSteps; i++) {
const y = i * this.stepLen;
// Riser
ctx.fillStyle = i % 2 === 0 ? '#2a2a2a' : '#222';
ctx.fillRect(0, y, this.stepD, this.stepH);
// Tread
ctx.fillStyle = i % 2 === 0 ? '#333' : '#2a2a2a';
ctx.fillRect(0, y + this.stepH, this.stepD, 4);
// Yellow edge
ctx.fillStyle = '#cc8800';
ctx.fillRect(0, y + this.stepH, this.stepD, 3);
}
// Side skirts
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(-12, -this.stepLen, 12, this.length + 2*this.stepLen);
ctx.fillRect(this.stepD, -this.stepLen, 12, this.length + 2*this.stepLen);
// Handrail suggestion
ctx.strokeStyle = '#444';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-20, -this.stepLen);
ctx.lineTo(-20, this.length + this.stepLen);
ctx.stroke();
ctx.restore();
}
}
/* ============ PERSON ============ */
const State = { STANDING: 0, FALLING: 1, SLIDING: 2, GETTING_UP: 3 };
class Person {
constructor(id, escalator, startStep, startLocalAlong) {
this.id = id;
this.esc = escalator;
this.color = CFG.COLORS[id % CFG.COLORS.length];
// State
this.state = State.STANDING;
this.stuckFoot = null; // {stepIdx, localAlong} when standing
// Physics
this.pos = {x:0, y:0};
this.vel = {x:0, y:0};
this.angle = 0;
this.angVel = 0;
// Contacts
this.contacts = []; // {normal, point, depth, isStep, stepIdx, otherId}
// Init on step
this.placeOnStep(startStep, startLocalAlong);
}
placeOnStep(stepIdx, localAlong) {
const footOffset = CFG.FOOT_W / 2;
// Place feet on tread (localAlong near stepD)
const footAlong = Math.min(localAlong, CFG.STEP_D - 2);
const world = this.esc.stepToWorld(stepIdx, footAlong, 2); // 2px above tread
this.stuckFoot = { stepIdx, localAlong: footAlong };
this.pos = world;
this.vel = this.esc.surfaceVelocity;
this.angle = 0;
this.angVel = 0;
this.state = State.STANDING;
}
// Get COM position
get com() {
const offset = Vec.rot({x:0, y:CFG.COM_OFFSET_Y}, this.angle);
return Vec.add(this.pos, offset);
}
// Get foot positions (world)
getFeet() {
const halfW = CFG.FOOT_W / 2;
const leftLocal = Vec.rot({x:-halfW, y:CFG.BODY_H/2}, this.angle);
const rightLocal = Vec.rot({x:halfW, y:CFG.BODY_H/2}, this.angle);
return [
Vec.add(this.pos, leftLocal),
Vec.add(this.pos, rightLocal)
];
}
// Get capsule endpoints for collision
getCapsule() {
const halfH = CFG.BODY_H / 2;
const top = Vec.rot({x:0, y:-halfH}, this.angle);
const bot = Vec.rot({x:0, y:halfH}, this.angle);
return {
a: Vec.add(this.pos, top),
b: Vec.add(this.pos, bot),
r: CFG.BODY_W / 2
};
}
// Balance check: returns true if should fall
checkBalance(accel) {
// Critical acceleration: a_crit = g * (foot_width/2) / h_com
const hCom = Math.abs(CFG.COM_OFFSET_Y) + CFG.BODY_H/2; // approx COM height from feet
const aCrit = CFG.G * (CFG.FOOT_W/2) / hCom * CFG.BALANCE_TOL;
const aTangent = Vec.dot(accel, this.esc.surfaceTangent);
return Math.abs(aTangent) > aCrit;
}
// Main update
update(dt, others, escalator) {
this.contacts = [];
const accel = {x:0, y:CFG.G}; // gravity
// State machine
switch (this.state) {
case State.STANDING:
this.updateStanding(dt, escalator);
break;
case State.FALLING:
case State.SLIDING:
this.updateFalling(dt, escalator, others);
break;
case State.GETTING_UP:
this.updateGettingUp(dt, escalator, others);
break;
}
// Integrate
this.vel = Vec.add(this.vel, Vec.mul(accel, dt));
this.pos = Vec.add(this.pos, Vec.mul(this.vel, dt));
this.angle += this.angVel * dt;
this.angVel *= 0.995; // slight air damping
// Clamp angle
if (this.angle > Math.PI) this.angle -= 2*Math.PI;
if (this.angle < -Math.PI) this.angle += 2*Math.PI;
}
updateStanding(dt, esc) {
// Feet pinned to step
if (this.stuckFoot) {
const world = esc.stepToWorld(this.stuckFoot.stepIdx, this.stuckFoot.localAlong, 2);
this.pos = world;
this.vel = esc.surfaceVelocity;
this.angle = 0;
this.angVel = 0;
// Check balance loss due to escalator acceleration
const escAccel = Vec.mul(esc.surfaceTangent, (esc.currentSpeed - esc.targetSpeed) / esc.accelDur); // approximate
// Better: compute actual acceleration from speed change
// We'll pass escalator acceleration from main loop
if (this.checkBalance(esc.currentAccel || {x:0,y:0})) {
this.state = State.FALLING;
this.stuckFoot = null;
// Initial angular velocity from torque
const hCom = Math.abs(CFG.COM_OFFSET_Y) + CFG.BODY_H/2;
const torque = CFG.MASS * esc.currentAccelTangent * hCom;
this.angVel = torque / CFG.INERTIA * 0.5; // kick
}
} else {
this.state = State.FALLING;
}
}
updateFalling(dt, esc, others) {
// Collide with steps
this.collideSteps(esc);
// Collide with others
this.collideOthers(others);
// Determine if sliding on ground
let onGround = false;
let groundNormal = {x:0,y:0};
for (const c of this.contacts) {
if (c.isStep && c.normal.y > 0.5) { onGround = true; groundNormal = c.normal; }
}
if (onGround && Vec.len(this.vel) < 30 && Math.abs(this.angVel) < 2) {
this.state = State.SLIDING;
}
// Try to get up
if (onGround && this.canGetUp(others)) {
this.state = State.GETTING_UP;
}
}
updateGettingUp(dt, esc, others) {
this.collideSteps(esc);
this.collideOthers(others);
// Apply restoring torque
const targetAngle = 0;
const angleDiff = targetAngle - this.angle;
// Normalize
let diff = angleDiff;
while (diff > Math.PI) diff -= 2*Math.PI;
while (diff < -Math.PI) diff += 2*Math.PI;
const torque = CFG.GETUP_TORQUE * diff;
this.angVel += torque / CFG.INERTIA * dt;
// Damping
this.angVel *= 0.92;
// Check if upright enough
if (Math.abs(diff) < CFG.GETUP_ANGLE_THRESH && Math.abs(this.angVel) < CFG.GETUP_SPEED_THRESH) {
// Find a foot plant spot
const feet = this.getFeet();
let planted = false;
for (const foot of feet) {
const ws = esc.worldToStep(foot.x, foot.y);
if (ws.perp > -5 && ws.perp < 10 && ws.localAlong > 2 && ws.localAlong < CFG.STEP_D - 2 && esc.isVisible(ws.stepIdx)) {
// Check no one else's foot there
let occupied = false;
for (const o of others) {
if (o === this) continue;
if (o.stuckFoot && o.stuckFoot.stepIdx === ws.stepIdx && Math.abs(o.stuckFoot.localAlong - ws.localAlong) < 10) {
occupied = true; break;
}
}
if (!occupied) {
this.stuckFoot = { stepIdx: ws.stepIdx, localAlong: ws.localAlong };
this.state = State.STANDING;
planted = true;
break;
}
}
}
if (!planted) {
// Stay in GETTING_UP, keep trying
}
}
}
canGetUp(others) {
// Check clearance above: no other torsos overlapping vertically
const cap = this.getCapsule();
for (const o of others) {
if (o === this) continue;
const ocap = o.getCapsule();
// Capsule-capsule distance
const dist = this.capsuleDist(cap, ocap);
if (dist < CFG.BODY_W * 0.8) return false;
}
return true;
}
capsuleDist(a, b) {
// Distance between two line segments (capsule centerlines)
const u = Vec.sub(a.b, a.a);
const v = Vec.sub(b.b, b.a);
const w = Vec.sub(a.a, b.a);
const uu = Vec.dot(u,u);
const vv = Vec.dot(v,v);
const uv = Vec.dot(u,v);
const uw = Vec.dot(u,w);
const vw = Vec.dot(v,w);
const denom = uu*vv - uv*uv;
let s, t;
if (denom < 1e-6) {
s = 0; t = vw/vv;
} else {
s = (uv*vw - vv*uw) / denom;
t = (uu*vw - uv*uw) / denom;
}
s = Math.max(0, Math.min(1, s));
t = Math.max(0, Math.min(1, t));
const pa = Vec.add(a.a, Vec.mul(u, s));
const pb = Vec.add(b.a, Vec.mul(v, t));
return Vec.len(Vec.sub(pa, pb)) - a.r - b.r;
}
collideSteps(esc) {
const cap = this.getCapsule();
const feet = this.getFeet();
// Check each visible step
for (let si = -1; si <= esc.numSteps; si++) {
if (!esc.isVisible(si)) continue;
// Step surface: line from (0, si*stepLen + stepH) to (stepD, si*stepLen + stepH) in local coords
// In world: two endpoints
const p1 = esc.stepToWorld(si, 0, CFG.STEP_H);
const p2 = esc.stepToWorld(si, CFG.STEP_D, CFG.STEP_H);
const stepVec = Vec.sub(p2, p1);
const stepLen = Vec.len(stepVec);
const stepNorm = esc.surfaceNormal;
// Capsule vs segment
// Check capsule endpoints and mid against step line
const checkPoints = [cap.a, cap.b, Vec.lerp(cap.a, cap.b, 0.5), feet[0], feet[1]];
for (const pt of checkPoints) {
// Project onto step line
const toPt = Vec.sub(pt, p1);
const along = Vec.dot(toPt, Vec.norm(stepVec));
if (along < -cap.r || along > stepLen + cap.r) continue;
const perp = Vec.dot(toPt, stepNorm);
if (perp < cap.r && perp > -20) { // penetration
const depth = cap.r - perp;
this.contacts.push({
normal: stepNorm,
point: pt,
depth: depth,
isStep: true,
stepIdx: si
});
// Resolve position
this.pos = Vec.add(this.pos, Vec.mul(stepNorm, depth * 0.5));
// Resolve velocity
const vn = Vec.dot(this.vel, stepNorm);
if (vn < 0) {
this.vel = Vec.sub(this.vel, Vec.mul(stepNorm, vn * 1.2));
// Friction
const vt = Vec.sub(this.vel, Vec.mul(stepNorm, vn));
const vtLen = Vec.len(vt);
if (vtLen > 0.1) {
const frictionImpulse = Math.min(vtLen, Math.abs(vn) * CFG.STEP_FRICTION_MU);
this.vel = Vec.sub(this.vel, Vec.mul(Vec.norm(vt), frictionImpulse));
}
}
}
}
// Riser collision (vertical face at x=stepD in local)
const riserP1 = esc.stepToWorld(si, CFG.STEP_D, 0);
const riserP2 = esc.stepToWorld(si, CFG.STEP_D, CFG.STEP_H);
const riserNorm = {x: -esc.cosA, y: -esc.sinA}; // pointing down-escalator
for (const pt of [cap.a, cap.b, feet[0], feet[1]]) {
const toPt = Vec.sub(pt, riserP1);
const along = Vec.dot(toPt, esc.surfaceTangent); // along riser height
if (along < -cap.r || along > CFG.STEP_H + cap.r) continue;
const perp = Vec.dot(toPt, riserNorm);
if (perp < cap.r && perp > -5) {
const depth = cap.r - perp;
this.contacts.push({ normal: riserNorm, point: pt, depth, isStep: true, stepIdx: si });
this.pos = Vec.add(this.pos, Vec.mul(riserNorm, depth * 0.5));
const vn = Vec.dot(this.vel, riserNorm);
if (vn < 0) {
this.vel = Vec.sub(this.vel, Vec.mul(riserNorm, vn * 1.2));
}
}
}
}
}
collideOthers(others) {
const cap = this.getCapsule();
for (const o of others) {
if (o === this) continue;
const ocap = o.getCapsule();
const dist = this.capsuleDist(cap, ocap);
if (dist < 0) {
// Find closest points
const u = Vec.sub(cap.b, cap.a);
const v = Vec.sub(ocap.b, ocap.a);
const w = Vec.sub(cap.a, ocap.a);
const uu = Vec.dot(u,u), vv = Vec.dot(v,v), uv = Vec.dot(u,v);
const uw = Vec.dot(u,w), vw = Vec.dot(v,w);
const denom = uu*vv - uv*uv;
let s, t;
if (denom < 1e-6) { s = 0; t = vw/vv; }
else { s = (uv*vw - vv*uw)/denom; t = (uu*vw - uv*uw)/denom; }
s = Math.max(0, Math.min(1, s));
t = Math.max(0, Math.min(1, t));
const pa = Vec.add(cap.a, Vec.mul(u, s));
const pb = Vec.add(ocap.a, Vec.mul(v, t));
const n = Vec.norm(Vec.sub(pa, pb));
const overlap = -dist;
this.contacts.push({ normal: n, point: pa, depth: overlap, isStep: false, otherId: o.id });
// Impulse resolution (simplified)
const relVel = Vec.sub(this.vel, o.vel);
const vn = Vec.dot(relVel, n);
if (vn < 0) {
const restitution = 0.15;
const impulse = -(1+restitution)*vn / (1/CFG.MASS + 1/CFG.MASS);
const impVec = Vec.mul(n, impulse);
this.vel = Vec.add(this.vel, Vec.mul(impVec, 1/CFG.MASS));
o.vel = Vec.sub(o.vel, Vec.mul(impVec, 1/CFG.MASS));
// Friction
const vt = Vec.sub(relVel, Vec.mul(n, vn));
const vtLen = Vec.len(vt);
if (vtLen > 0.1) {
const fImp = Math.min(vtLen, Math.abs(impulse) * CFG.FRICTION_MU);
const fVec = Vec.mul(Vec.norm(vt), fImp);
this.vel = Vec.sub(this.vel, Vec.mul(fVec, 1/CFG.MASS));
o.vel = Vec.add(o.vel, Vec.mul(fVec, 1/CFG.MASS));
}
}
// Position correction
const corr = Vec.mul(n, overlap * 0.5);
this.pos = Vec.add(this.pos, corr);
o.pos = Vec.sub(o.pos, corr);
}
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.pos.x, this.pos.y);
ctx.rotate(this.angle);
// Body capsule
const h = CFG.BODY_H, w = CFG.BODY_W;
const r = w/2;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.roundRect(-r, -h/2, w, h, r);
ctx.fill();
// Head
ctx.beginPath();
ctx.arc(0, -h/2 - r*0.7, r*0.9, 0, Math.PI*2);
ctx.fill();
// Face suggestion
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(-r*0.25, -h/2 - r*0.85, 2, 0, Math.PI*2);
ctx.arc(r*0.25, -h/2 - r*0.85, 2, 0, Math.PI*2);
ctx.fill();
// Arms (simple lines)
ctx.strokeStyle = this.color;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
const armSwing = Math.sin(this.angle * 2) * 0.3;
ctx.beginPath();
ctx.moveTo(-r, -h/2 + 15);
ctx.lineTo(-r - 20, -h/2 + 35 + armSwing*10);
ctx.moveTo(r, -h/2 + 15);
ctx.lineTo(r + 20, -h/2 + 35 - armSwing*10);
ctx.stroke();
// Legs when standing
if (this.state === State.STANDING) {
ctx.strokeStyle = '#333';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(-CFG.FOOT_W/2, h/2);
ctx.lineTo(-CFG.FOOT_W/2, h/2 + 8);
ctx.moveTo(CFG.FOOT_W/2, h/2);
ctx.lineTo(CFG.FOOT_W/2, h/2 + 8);
ctx.stroke();
}
ctx.restore();
// Debug: state label
// ctx.fillStyle = '#fff'; ctx.font = '10px monospace'; ctx.fillText(State[this.state], this.pos.x, this.pos.y - 80);
}
}
/* ============ MAIN ============ */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = Math.min(CFG.WIDTH, window.innerWidth - 20);
H = Math.min(CFG.HEIGHT, window.innerHeight - 100);
canvas.width = W;
canvas.height = H;
CFG.WIDTH = W; CFG.HEIGHT = H;
}
window.addEventListener('resize', resize);
resize();
const esc = new Escalator();
const people = [];
let lastTime = performance.now() / 1000;
let paused = false;
// Initialize people on steps
function initPeople() {
people.length = 0;
const stepsPerPerson = Math.max(1, Math.floor(esc.numSteps / CFG.PERSON_COUNT));
for (let i = 0; i < CFG.PERSON_COUNT; i++) {
const stepIdx = i * stepsPerPerson + 1;
const localAlong = CFG.STEP_D * 0.6 + (Math.random()-0.5)*8;
people.push(new Person(i, esc, stepIdx, localAlong));
}
}
initPeople();
// UI
const revBtn = document.getElementById('revBtn');
const resetBtn = document.getElementById('resetBtn');
revBtn.addEventListener('click', () => {
esc.setReverse(!esc.reversing);
revBtn.classList.toggle('active', esc.reversing);
revBtn.textContent = esc.reversing ? 'NORMAL' : 'REVERSE';
});
resetBtn.addEventListener('click', () => {
esc.setReverse(false);
revBtn.classList.remove('active');
revBtn.textContent = 'REVERSE';
initPeople();
});
// Main loop
function frame(nowMs) {
const now = nowMs / 1000;
const dt = Math.min(1/30, now - lastTime);
lastTime = now;
// Compute escalator acceleration
const prevSpeed = esc.currentSpeed;
esc.update(dt, now);
esc.currentAccel = (esc.currentSpeed - prevSpeed) / dt;
esc.currentAccelTangent = esc.currentAccel; // scalar along tangent
esc.currentAccelVec = Vec.mul(esc.surfaceTangent, esc.currentAccel);
// Update people
for (const p of people) {
p.update(dt, people, esc);
}
// Draw
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, W, H);
// Escalator
esc.draw(ctx);
// People (draw back to front by Y)
people.sort((a,b) => a.pos.y - b.pos.y);
for (const p of people) p.draw(ctx);
// Stats
const states = { standing:0, falling:0, sliding:0, gettingup:0 };
for (const p of people) {
if (p.state === State.STANDING) states.standing++;
else if (p.state === State.FALLING) states.falling++;
else if (p.state === State.SLIDING) states.sliding++;
else states.gettingup++;
}
document.getElementById('stats').textContent =
`Standing: ${states.standing} Falling: ${states.falling} Sliding: ${states.sliding} GettingUp: ${states.gettingup}\n` +
`EscSpeed: ${esc.currentSpeed.toFixed(0)} px/s Accel: ${esc.currentAccel.toFixed(0)} px/s²`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
When you press REVERSE, the escalator's target speed flips from +80 px/s to -380 px/s over ~180 ms (a ~2.5 G lateral jerk). Because every figure's feet are pinned to the treads while STANDING, their soles accelerate with the steps while their center of mass lags — the balance check (|a_tangent| * h_com > g * foot_half_width) trips for everyone almost simultaneously, releasing the foot pins and dropping them into FALLING with a small initial angular velocity proportional to the torque.
From there it's pure simulation:
Special cases / nudges I admit:
What it does badly:
Press REVERSE, watch the wave of falls propagate upward, see the knot form at the bottom, then hit NORMAL and watch them slowly, clumsily stand up again — each on its own terms.