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 the same kind of active rigid capsule: a center of mass, height, radius, orientation, linear and angular velocity, foot friction, and a limited balance controller that tries to keep the body upright. The moving escalator applies friction at the contact point; a rapid reversal pulls the feet out from under the center of mass, and a person falls whenever that torque exceeds their ability to balance. Capsule-to-capsule contacts are resolved with separation, collision, and friction impulses, so overlapping people push, rotate, slide over, or become obstacles to one another rather than passing through.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Emergent Escalator Reversal</title>
<style>
:root {
color-scheme: dark;
--ink: #edf4f8;
--muted: #9eb0bb;
--panel: #121a20;
--edge: #31414b;
--accent: #ffb33b;
--danger: #ff5f57;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at 50% 20%, #233039 0, #11181d 55%, #090d10 100%);
color: var(--ink);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: 18px;
}
main {
width: min(1120px, 100%);
background: rgba(12, 18, 22, .92);
border: 1px solid var(--edge);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 24px 80px rgba(0,0,0,.45);
}
header {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border-bottom: 1px solid var(--edge);
background: linear-gradient(#182229, #11191e);
}
.title {
min-width: 170px;
}
h1 {
font-size: 17px;
line-height: 1.1;
margin: 0 0 4px;
letter-spacing: .025em;
}
.subtitle {
color: var(--muted);
font-size: 12px;
}
.controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
justify-content: flex-end;
}
button {
border: 1px solid #556773;
border-radius: 10px;
color: white;
background: #24323b;
padding: 9px 13px;
font: inherit;
font-weight: 750;
letter-spacing: .02em;
cursor: pointer;
box-shadow: inset 0 1px rgba(255,255,255,.08);
}
button:hover { background: #2c3c46; }
button:active { transform: translateY(1px); }
#toggle {
min-width: 205px;
border-color: #9f6620;
background: linear-gradient(#b76a18, #8d4613);
}
#toggle.reversed {
border-color: #8b3430;
background: linear-gradient(#a93d37, #742824);
}
.readout {
min-width: 155px;
color: var(--muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
text-align: right;
}
.canvas-wrap {
position: relative;
background: #c7d4d8;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1000 / 620;
touch-action: manipulation;
}
.hint {
position: absolute;
left: 14px;
bottom: 12px;
padding: 6px 9px;
border-radius: 8px;
color: rgba(255,255,255,.76);
background: rgba(10,15,18,.66);
font-size: 11px;
pointer-events: none;
backdrop-filter: blur(3px);
}
@media (max-width: 700px) {
header { align-items: flex-start; }
.title { display: none; }
.controls { margin-left: 0; width: 100%; }
#toggle { flex: 1; min-width: 180px; }
.readout { min-width: 120px; }
}
</style>
</head>
<body>
<main>
<header>
<div class="title">
<h1>Escalator dynamics</h1>
<div class="subtitle">18 identical-rule active bodies</div>
</div>
<div class="controls">
<button id="toggle" type="button">REVERSE — HIGH SPEED</button>
<button id="reset" type="button">Reset</button>
<div class="readout" id="readout">Ascending normally</div>
</div>
</header>
<div class="canvas-wrap">
<canvas id="scene" width="1000" height="620" aria-label="Side-view physics simulation of people riding an escalator"></canvas>
<div class="hint">Press reverse again to restore upward travel.</div>
</div>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const toggle = document.getElementById("toggle");
const resetButton = document.getElementById("reset");
const readout = document.getElementById("readout");
const W = canvas.width;
const H = canvas.height;
// Escalator-local coordinates:
// s = distance uphill, q = height normal to the escalator.
const angle = 23 * Math.PI / 180;
const T = { x: Math.cos(angle), y: Math.sin(angle) };
const N = { x: -Math.sin(angle), y: Math.cos(angle) };
const base = { x: 82, y: 91 };
const beltLength = 850;
const lowerStop = 29;
const upperStop = beltLength - 25;
const G = 520;
const gravityS = -G * Math.sin(angle);
const gravityQ = -G * Math.cos(angle);
const fixedDt = 1 / 120;
const solverIterations = 5;
const colors = [
"#e95d5d", "#4da3e6", "#e6a84d", "#65b87a", "#b47bd3", "#e879b0",
"#55b8ad", "#db744f", "#758ce3", "#9eba52", "#ce6bc2", "#4fb2d0",
"#e38f50", "#72a8d8", "#d36666", "#79b86b", "#bd82e0", "#dfa94f"
];
let people = [];
let reversed = false;
let beltSpeed = 16;
let beltTarget = 16;
let beltAcceleration = 0;
let beltOffset = 0;
let accumulator = 0;
let previousTime = performance.now();
let fps = 60;
let seed = 1;
function random() {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 4294967296;
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function wrapAngle(a) {
while (a > Math.PI) a -= Math.PI * 2;
while (a < -Math.PI) a += Math.PI * 2;
return a;
}
function localToCanvas(s, q) {
const x = base.x + T.x * s + N.x * q;
const yWorld = base.y + T.y * s + N.y * q;
return { x, y: H - yWorld };
}
function axisOf(p) {
return { s: Math.sin(p.theta), q: Math.cos(p.theta) };
}
function endpoints(p) {
const a = axisOf(p);
const h = p.length * 0.5;
return {
a: { s: p.s - a.s * h, q: p.q - a.q * h },
b: { s: p.s + a.s * h, q: p.q + a.q * h }
};
}
function reset() {
seed = 481516;
reversed = false;
beltSpeed = 16;
beltTarget = 16;
beltAcceleration = 0;
beltOffset = 0;
accumulator = 0;
people = [];
for (let i = 0; i < 18; i++) {
const length = 34 + random() * 5;
const radius = 5.7 + random() * 1.2;
const mass = 0.88 + random() * 0.25;
const theta = (random() - 0.5) * 0.025;
const inertia = mass * (length * length / 12 + radius * radius);
people.push({
id: i,
s: 68 + i * 39,
q: length * 0.5 + radius,
vs: beltSpeed,
vq: 0,
theta,
omega: 0,
length,
radius,
mass,
invMass: 1 / mass,
inertia,
invInertia: 1 / inertia,
friction: 0.56 + random() * 0.16,
balance: 0.9 + random() * 0.18,
recoveryClock: 0,
color: colors[i % colors.length],
down: false
});
}
toggle.textContent = "REVERSE — HIGH SPEED";
toggle.classList.remove("reversed");
updateReadout();
}
function toggleDirection() {
reversed = !reversed;
beltTarget = reversed ? -145 : 16;
toggle.textContent = reversed ? "RESTORE UPWARD TRAVEL" : "REVERSE — HIGH SPEED";
toggle.classList.toggle("reversed", reversed);
}
toggle.addEventListener("click", toggleDirection);
resetButton.addEventListener("click", reset);
function bodyVelocityAt(p, r) {
return {
s: p.vs + p.omega * r.q,
q: p.vq - p.omega * r.s
};
}
function addImpulse(p, js, jq, r) {
p.vs += js * p.invMass;
p.vq += jq * p.invMass;
p.omega += (r.q * js - r.s * jq) * p.invInertia;
}
function lowerSupport(p) {
const ep = endpoints(p);
let c;
if (Math.abs(ep.a.q - ep.b.q) < 0.25) {
c = { s: p.s, q: p.q };
} else {
c = ep.a.q < ep.b.q ? ep.a : ep.b;
}
return {
minQ: Math.min(ep.a.q, ep.b.q) - p.radius,
point: { s: c.s, q: c.q - p.radius }
};
}
function sideExtent(p, left) {
const ep = endpoints(p);
if (left) return Math.min(ep.a.s, ep.b.s) - p.radius;
return Math.max(ep.a.s, ep.b.s) + p.radius;
}
function hasRecoveryRoom(p) {
for (const other of people) {
if (other === p) continue;
const ds = other.s - p.s;
const dq = other.q - p.q;
const reach = (p.length + other.length) * 0.61;
if (ds * ds + dq * dq < reach * reach && Math.abs(dq) < 42) {
return false;
}
}
return true;
}
function applyActiveControl(p, dt) {
const support = lowerSupport(p);
const grounded = support.minQ < 2.4;
const lean = Math.abs(wrapAngle(p.theta));
const relativeSpeed = Math.abs(p.vs - beltSpeed);
let torque = 0;
if (grounded && lean < 0.72) {
// Limited ankle/hip balance. It is intentionally weaker than reversal torque.
torque = (-p.theta * 1450 - p.omega * 165) * p.balance;
torque = clamp(torque, -1050, 1050);
p.recoveryClock = 0;
} else if (
grounded &&
relativeSpeed < 72 &&
Math.abs(p.omega) < 3.1 &&
hasRecoveryRoom(p)
) {
p.recoveryClock += dt;
if (p.recoveryClock > 0.34) {
// A bounded righting effort. Ground correction supplies the changing pivot.
torque = clamp(
(-wrapAngle(p.theta) * 850 - p.omega * 190) * p.balance,
-1350,
1350
);
p.vq += 22 * dt;
}
} else {
p.recoveryClock = Math.max(0, p.recoveryClock - dt * 2);
}
p.omega += torque * p.invInertia * dt;
}
function integrateForces(p, dt) {
p.vs += gravityS * dt;
p.vq += gravityQ * dt;
p.vs *= Math.pow(0.999, dt * 120);
p.vq *= Math.pow(0.9994, dt * 120);
p.omega *= Math.pow(0.997, dt * 120);
applyActiveControl(p, dt);
p.s += p.vs * dt;
p.q += p.vq * dt;
p.theta = wrapAngle(p.theta + p.omega * dt);
}
function solveGround(p, dt) {
const support = lowerSupport(p);
if (support.minQ > 0.7) return;
const penetration = Math.max(0, -support.minQ);
if (penetration > 0) p.q += penetration * 0.76 + 0.002;
const r = {
s: support.point.s - p.s,
q: -p.q
};
let v = bodyVelocityAt(p, r);
const normalDenom = p.invMass + r.s * r.s * p.invInertia;
let jn = 0;
if (v.q < 0) {
jn = -(1.025 * v.q) / normalDenom;
addImpulse(p, 0, jn, r);
}
v = bodyVelocityAt(p, r);
const tangentDenom = p.invMass + r.q * r.q * p.invInertia;
let jt = -(v.s - beltSpeed) / tangentDenom;
const supportImpulse =
Math.max(0, jn) +
p.mass * Math.abs(gravityQ) * dt / solverIterations;
const limit = p.friction * supportImpulse;
jt = clamp(jt, -limit, limit);
addImpulse(p, jt, 0, r);
}
function solveWall(p, left) {
const extent = sideExtent(p, left);
const boundary = left ? lowerStop : upperStop;
const penetration = left ? boundary - extent : extent - boundary;
if (penetration <= 0) return;
p.s += left ? penetration * 0.8 : -penetration * 0.8;
const ep = endpoints(p);
let c;
if (left) c = ep.a.s < ep.b.s ? ep.a : ep.b;
else c = ep.a.s > ep.b.s ? ep.a : ep.b;
const r = { s: c.s - p.s, q: c.q - p.q };
const normalS = left ? 1 : -1;
const v = bodyVelocityAt(p, r);
const vn = v.s * normalS;
if (vn < 0) {
const rotational = r.q * normalS;
const denom = p.invMass + rotational * rotational * p.invInertia;
const j = -(1.04 * vn) / denom;
addImpulse(p, normalS * j, 0, r);
const v2 = bodyVelocityAt(p, r);
const jq = clamp(
-v2.q / (p.invMass + r.s * r.s * p.invInertia),
-j * 0.28,
j * 0.28
);
addImpulse(p, 0, jq, r);
}
}
function closestSegments(p1, q1, p2, q2) {
const d1 = { s: q1.s - p1.s, q: q1.q - p1.q };
const d2 = { s: q2.s - p2.s, q: q2.q - p2.q };
const r = { s: p1.s - p2.s, q: p1.q - p2.q };
const a = d1.s * d1.s + d1.q * d1.q;
const e = d2.s * d2.s + d2.q * d2.q;
const f = d2.s * r.s + d2.q * r.q;
let s, t;
if (a < 1e-9 && e < 1e-9) {
s = t = 0;
} else if (a < 1e-9) {
s = 0;
t = clamp(f / e, 0, 1);
} else {
const c = d1.s * r.s + d1.q * r.q;
if (e < 1e-9) {
t = 0;
s = clamp(-c / a, 0, 1);
} else {
const b = d1.s * d2.s + d1.q * d2.q;
const denom = a * e - b * b;
s = denom !== 0 ? clamp((b * f - c * e) / denom, 0, 1) : 0;
t = (b * s + f) / e;
if (t < 0) {
t = 0;
s = clamp(-c / a, 0, 1);
} else if (t > 1) {
t = 1;
s = clamp((b - c) / a, 0, 1);
}
}
}
return {
a: { s: p1.s + d1.s * s, q: p1.q + d1.q * s },
b: { s: p2.s + d2.s * t, q: p2.q + d2.q * t }
};
}
function solvePair(a, b) {
const ea = endpoints(a);
const eb = endpoints(b);
const closest = closestSegments(ea.a, ea.b, eb.a, eb.b);
let ds = closest.b.s - closest.a.s;
let dq = closest.b.q - closest.a.q;
let distance = Math.hypot(ds, dq);
const target = a.radius + b.radius;
if (distance >= target) return;
if (distance < 1e-6) {
ds = b.s >= a.s ? 1 : -1;
dq = 0.02;
distance = Math.hypot(ds, dq);
}
const ns = ds / distance;
const nq = dq / distance;
const penetration = target - distance;
const totalInv = a.invMass + b.invMass;
const correction = Math.max(0, penetration - 0.04) * 0.72 / totalInv;
a.s -= ns * correction * a.invMass;
a.q -= nq * correction * a.invMass;
b.s += ns * correction * b.invMass;
b.q += nq * correction * b.invMass;
const contact = {
s: closest.a.s + ns * a.radius,
q: closest.a.q + nq * a.radius
};
const ra = { s: contact.s - a.s, q: contact.q - a.q };
const rb = { s: contact.s - b.s, q: contact.q - b.q };
const va = bodyVelocityAt(a, ra);
const vb = bodyVelocityAt(b, rb);
const rvs = vb.s - va.s;
const rvq = vb.q - va.q;
const vn = rvs * ns + rvq * nq;
const ga = ra.q * ns - ra.s * nq;
const gb = rb.q * ns - rb.s * nq;
const denom =
a.invMass + b.invMass +
ga * ga * a.invInertia +
gb * gb * b.invInertia;
let normalImpulse = 0;
if (vn < 0) {
normalImpulse = -(1.04 * vn) / denom;
addImpulse(a, -ns * normalImpulse, -nq * normalImpulse, ra);
addImpulse(b, ns * normalImpulse, nq * normalImpulse, rb);
}
const va2 = bodyVelocityAt(a, ra);
const vb2 = bodyVelocityAt(b, rb);
const ts = -nq;
const tq = ns;
const vt = (vb2.s - va2.s) * ts + (vb2.q - va2.q) * tq;
const gta = ra.q * ts - ra.s * tq;
const gtb = rb.q * ts - rb.s * tq;
const tangentDenom =
a.invMass + b.invMass +
gta * gta * a.invInertia +
gtb * gtb * b.invInertia;
let frictionImpulse = -vt / tangentDenom;
const frictionLimit = Math.max(0.2, normalImpulse * 0.34);
frictionImpulse = clamp(frictionImpulse, -frictionLimit, frictionLimit);
addImpulse(a, -ts * frictionImpulse, -tq * frictionImpulse, ra);
addImpulse(b, ts * frictionImpulse, tq * frictionImpulse, rb);
}
function step(dt) {
const before = beltSpeed;
const maxChange = 900 * dt;
beltSpeed += clamp(beltTarget - beltSpeed, -maxChange, maxChange);
beltAcceleration = (beltSpeed - before) / dt;
beltOffset += beltSpeed * dt;
for (const p of people) integrateForces(p, dt);
for (let iteration = 0; iteration < solverIterations; iteration++) {
for (const p of people) {
solveGround(p, dt);
solveWall(p, true);
solveWall(p, false);
}
for (let i = 0; i < people.length; i++) {
for (let j = i + 1; j < people.length; j++) {
solvePair(people[i], people[j]);
}
}
}
for (const p of people) {
p.down = Math.abs(wrapAngle(p.theta)) > 0.72;
if (!Number.isFinite(p.s + p.q + p.theta)) reset();
}
}
function pathEscalatorBody() {
const a = localToCanvas(-38, -28);
const b = localToCanvas(beltLength + 38, -28);
const c = localToCanvas(beltLength + 38, 7);
const d = localToCanvas(-38, 7);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.lineTo(c.x, c.y);
ctx.lineTo(d.x, d.y);
ctx.closePath();
}
function drawBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#dbe5e7");
grad.addColorStop(0.63, "#b9c8cc");
grad.addColorStop(1, "#8c9da3");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "rgba(255,255,255,.24)";
for (let x = 40; x < W; x += 90) {
ctx.fillRect(x, 55, 1, 360);
}
ctx.fillStyle = "#708087";
ctx.fillRect(0, 530, W, 90);
ctx.fillStyle = "#82939a";
ctx.fillRect(0, 526, W, 5);
// Landings.
const lower = localToCanvas(-80, 0);
const lowerEnd = localToCanvas(45, 0);
const upper = localToCanvas(beltLength - 20, 0);
ctx.strokeStyle = "#35444b";
ctx.lineWidth = 15;
ctx.lineCap = "butt";
ctx.beginPath();
ctx.moveTo(0, lower.y);
ctx.lineTo(lowerEnd.x, lowerEnd.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(upper.x, upper.y);
ctx.lineTo(W, upper.y);
ctx.stroke();
// Truss and side panel.
pathEscalatorBody();
ctx.fillStyle = "#37474f";
ctx.fill();
ctx.strokeStyle = "#1c282e";
ctx.lineWidth = 3;
ctx.stroke();
const railA = localToCanvas(-25, 14);
const railB = localToCanvas(beltLength + 25, 14);
ctx.strokeStyle = "#65777f";
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(railA.x, railA.y);
ctx.lineTo(railB.x, railB.y);
ctx.stroke();
// Moving cleats.
ctx.save();
pathEscalatorBody();
ctx.clip();
const spacing = 24;
let start = ((beltOffset % spacing) + spacing) % spacing - spacing;
for (let s = start; s < beltLength + spacing; s += spacing) {
const p1 = localToCanvas(s, -2);
const p2 = localToCanvas(s, -25);
ctx.strokeStyle = "#1d292f";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
const tread = localToCanvas(s + 17, -2);
ctx.strokeStyle = "#74848a";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(tread.x, tread.y);
ctx.stroke();
}
ctx.restore();
// Mechanical end-stops that also serve as simulation boundaries.
for (const s of [lowerStop, upperStop]) {
const a = localToCanvas(s, -8);
const b = localToCanvas(s, 16);
ctx.strokeStyle = "#202c31";
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
ctx.strokeStyle = "#f0a52b";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
function lineLocal(a, b) {
const p1 = localToCanvas(a.s, a.q);
const p2 = localToCanvas(b.s, b.q);
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
function drawPerson(p) {
const a = axisOf(p);
const side = { s: a.q, q: -a.s };
const h = p.length * 0.5;
const lower = { s: p.s - a.s * h, q: p.q - a.q * h };
const upper = { s: p.s + a.s * h, q: p.q + a.q * h };
const point = (along, across = 0) => ({
s: lower.s + a.s * p.length * along + side.s * across,
q: lower.q + a.q * p.length * along + side.q * across
});
const hip = point(0.31);
const chest = point(0.63);
const shoulderL = point(0.70, -2.4);
const shoulderR = point(0.70, 2.4);
const neck = point(0.82);
const head = point(0.94);
const motion = clamp((p.vs - beltSpeed) / 45, -1, 1);
const swing = Math.sin(p.s * 0.09 + p.id * 1.7) * 2.2 + motion * 2;
const footL = point(0.01, -3.5);
const footR = point(0.01, 3.5);
const kneeL = point(0.16, -2.6 - swing * 0.25);
const kneeR = point(0.16, 2.6 + swing * 0.25);
const handL = point(0.43, -7 - swing);
const handR = point(0.43, 7 + swing);
ctx.lineCap = "round";
ctx.lineJoin = "round";
// Shadow/contact marker.
const support = lowerSupport(p);
if (support.minQ < 4) {
const shadow = localToCanvas(support.point.s, 0);
ctx.fillStyle = "rgba(20,30,34,.20)";
ctx.beginPath();
ctx.ellipse(shadow.x, shadow.y + 2, 9, 3, -angle, 0, Math.PI * 2);
ctx.fill();
}
// Limbs.
ctx.strokeStyle = "#20272b";
ctx.lineWidth = 4.3;
lineLocal(hip, kneeL);
lineLocal(kneeL, footL);
lineLocal(hip, kneeR);
lineLocal(kneeR, footR);
lineLocal(shoulderL, handL);
lineLocal(shoulderR, handR);
// Torso.
ctx.strokeStyle = p.color;
ctx.lineWidth = 8;
lineLocal(hip, chest);
ctx.lineWidth = 5;
lineLocal(chest, neck);
// Shoes.
ctx.strokeStyle = "#151b1e";
ctx.lineWidth = 4;
lineLocal(footL, {
s: footL.s + T.x * 0 + side.s * 3.5,
q: footL.q + side.q * 3.5
});
lineLocal(footR, {
s: footR.s + side.s * 3.5,
q: footR.q + side.q * 3.5
});
// Head.
const hc = localToCanvas(head.s, head.q);
ctx.fillStyle = "#f0c7a1";
ctx.strokeStyle = "#283035";
ctx.lineWidth = 2.1;
ctx.beginPath();
ctx.arc(hc.x, hc.y, p.radius * 0.82, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Small orientation mark makes tumbling readable.
const face = localToCanvas(
head.s + side.s * p.radius * 0.45,
head.q + side.q * p.radius * 0.45
);
ctx.fillStyle = "#31373a";
ctx.beginPath();
ctx.arc(face.x, face.y, 1.2, 0, Math.PI * 2);
ctx.fill();
}
function drawOverlay() {
const down = people.reduce((n, p) => n + (p.down ? 1 : 0), 0);
const speed = Math.abs(beltSpeed).toFixed(0);
const label = beltSpeed >= 0 ? "UP" : "DOWN";
ctx.fillStyle = "rgba(16,24,28,.78)";
ctx.fillRect(18, 18, 196, 58);
ctx.fillStyle = beltSpeed >= 0 ? "#4bca83" : "#ff655e";
ctx.font = "800 15px system-ui, sans-serif";
ctx.fillText(`BELT ${label} ${speed} px/s`, 30, 42);
ctx.fillStyle = "#d6e0e4";
ctx.font = "12px system-ui, sans-serif";
ctx.fillText(`${down} down · ${people.length - down} upright`, 30, 62);
if (Math.abs(beltAcceleration) > 300) {
ctx.fillStyle = "rgba(194,49,43,.88)";
ctx.fillRect(815, 18, 167, 34);
ctx.fillStyle = "white";
ctx.font = "800 12px system-ui, sans-serif";
ctx.fillText("RAPID DIRECTION CHANGE", 827, 40);
}
}
function draw() {
drawBackground();
// Drawing in increasing height approximates useful side-view occlusion.
const ordered = [...people].sort((a, b) => b.q - a.q);
for (const p of ordered) drawPerson(p);
drawOverlay();
}
function updateReadout() {
const down = people.reduce((n, p) => n + (p.down ? 1 : 0), 0);
const mode = beltSpeed < -5
? "Descending fast"
: Math.abs(beltAcceleration) > 200
? "Direction changing"
: "Ascending normally";
readout.textContent = `${mode} · ${down} down`;
}
function frame(now) {
const elapsed = Math.min(0.05, (now - previousTime) / 1000);
previousTime = now;
accumulator += elapsed;
fps += ((1 / Math.max(elapsed, 0.001)) - fps) * 0.03;
while (accumulator >= fixedDt) {
step(fixedDt);
accumulator -= fixedDt;
}
updateReadout();
draw();
requestAnimationFrame(frame);
}
reset();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The escalator begins moving uphill slowly with 18 figures balanced on its steps. Pressing the control drives the belt toward a much faster downhill speed with a large but finite acceleration. Friction first drags each figure’s feet downhill while its center of mass continues uphill, producing rotation; the resulting falls, collisions, sliding, and bottom pile are solved from the current contacts rather than scheduled. Pressing again rapidly restores upward travel, which can loosen the pile and may knock recovering figures over again.
No individual figure or fall is special-cased. Everyone uses the same controller and collision rules, though seeded variations in height, mass, friction, balance strength, and initial lean prevent perfectly synchronized motion. The explicit nudges are limited balance torque, a delayed righting torque plus slight upward recovery assist when a fallen figure has space and traction, and physical end-stops at both ends so bodies remain in the demonstration. It does badly at detailed human behavior: bodies are single rigid capsules under articulated drawings, cannot step or grab a handrail, can occasionally lever themselves upright in an awkward way, and dense piles may jitter or remain wedged.