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 seven moving masses—head, chest, hips, two knees, and two feet—joined by compliant distance constraints. Gravity, limited-traction contacts with the moving tread deck, muscular stepping forces, and person-to-person contact impulses determine their motion; losing balance means the body can no longer keep its feet and torso under the available support. Overlapping masses are pushed apart and lose normal velocity, while friction resists sliding. A geometric down/up flag only changes the strength of the common balance reflex: nobody is assigned a fall, and getting up requires enough free space, planted feet, low angular motion, and a sustained stable posture.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'">
<title>Up / Down — Escalator Reversal Study</title>
<style>
:root {
color-scheme: dark;
--bg: #10191f;
--panel: #18242c;
--line: #31414a;
--text: #edf1ed;
--muted: #9aabae;
--accent: #d9f071;
--orange: #f2a568;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(ellipse at 15% 0%, #24353b 0%, transparent 55%),
var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
.shell {
width: min(1450px, calc(100% - 48px));
margin: 0 auto;
padding: 30px 0 24px;
}
.masthead {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
margin-bottom: 25px;
}
.identity {
display: flex;
gap: 17px;
align-items: center;
}
.mark {
width: 49px;
height: 49px;
display: grid;
place-items: center;
border: 1px solid #5a6c68;
color: var(--accent);
font-size: 29px;
line-height: 1;
transform: rotate(-4deg);
}
.eyebrow, .panel-label {
margin: 0 0 7px;
color: var(--muted);
font-size: 10px;
font-weight: 750;
letter-spacing: .18em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(24px, 3vw, 37px);
font-weight: 650;
letter-spacing: -.045em;
line-height: 1;
}
h1 span { color: var(--muted); font-weight: 350; }
.rider-count {
display: flex;
align-items: center;
gap: 11px;
font-size: 12px;
color: var(--muted);
}
.rider-count strong {
font-size: 29px;
line-height: 1;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.experiment {
display: grid;
grid-template-columns: minmax(0, 1fr) 265px;
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 24px 65px #0005;
}
.viewport {
min-width: 0;
background: #d8ddd6;
}
.viewport-top {
height: 43px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #b8c1ba;
color: #485a5b;
font-size: 10px;
font-weight: 750;
letter-spacing: .13em;
text-transform: uppercase;
}
.live {
display: flex;
align-items: center;
gap: 7px;
}
.live::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: #527963;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1200 / 560;
outline: none;
}
canvas:focus-visible { box-shadow: inset 0 0 0 3px #7395a0; }
.viewport-bottom {
display: flex;
flex-wrap: wrap;
gap: 18px;
justify-content: space-between;
padding: 12px 19px;
border-top: 1px solid #bcc5be;
background: #e6e8e0;
color: #5b6968;
font-size: 10px;
letter-spacing: .025em;
}
.key {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 13px;
}
.key i {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
}
.key .green { background: #547b65; }
.key .amber { background: #c97e44; }
.controls {
display: flex;
flex-direction: column;
padding: 25px 23px 21px;
background: var(--panel);
border-left: 1px solid var(--line);
}
.direction {
margin: 0 0 5px;
font-size: 26px;
font-weight: 550;
letter-spacing: -.04em;
}
.direction-note {
min-height: 38px;
margin: 0 0 24px;
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.speed-reading {
display: flex;
align-items: baseline;
gap: 8px;
margin-top: 8px;
}
.speed-reading strong {
font-size: 54px;
line-height: 1;
letter-spacing: -.065em;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.speed-reading span {
color: var(--muted);
font-size: 12px;
}
.meter {
position: relative;
height: 7px;
margin: 17px 0 7px;
border-radius: 4px;
background: #35434a;
}
.meter::after {
content: "";
position: absolute;
left: 50%;
top: -4px;
width: 1px;
height: 15px;
background: #76858a;
}
.speed-fill {
position: absolute;
inset: 0;
background: var(--accent);
transform: scaleX(.43);
transform-origin: left;
border-radius: inherit;
}
.speed-fill.negative {
background: var(--orange);
transform-origin: right;
}
.meter-labels {
display: flex;
justify-content: space-between;
color: #809398;
font-size: 9px;
letter-spacing: .12em;
text-transform: uppercase;
}
.postures {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 25px 0 23px;
}
.posture {
padding: 12px 11px;
background: #111c23;
border: 1px solid #31414a;
border-radius: 6px;
}
.posture span {
display: block;
margin-bottom: 4px;
color: var(--muted);
font-size: 9px;
letter-spacing: .1em;
text-transform: uppercase;
}
.posture strong {
font-size: 24px;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.posture.down strong { color: var(--orange); }
.primary, .secondary {
width: 100%;
min-height: 48px;
border-radius: 6px;
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 750;
}
.primary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 13px;
border: 1px solid #d9f071;
background: var(--accent);
color: #1d2a23;
box-shadow: 0 4px 0 #090f13;
}
.primary:hover { background: #e5fa8c; }
.primary:active { transform: translateY(2px); box-shadow: 0 2px 0 #090f13; }
.primary:focus-visible, .secondary:focus-visible { outline: 2px solid white; outline-offset: 3px; }
.primary .arrow {
display: inline-block;
margin-right: 7px;
font-size: 19px;
line-height: 1;
}
.primary.reversed .arrow { transform: rotate(180deg); }
kbd {
padding: 3px 6px;
border: 1px solid #526048;
border-radius: 3px;
font-family: inherit;
font-size: 9px;
letter-spacing: .04em;
}
.secondary {
margin-top: 13px;
border: 1px solid #45565b;
background: transparent;
color: #c3cecc;
}
.secondary:hover { background: #223139; }
.micro {
margin: auto 0 0;
padding-top: 25px;
color: #8fa1a3;
font-size: 10px;
line-height: 1.7;
}
.micro strong { color: #c6d2cd; font-weight: 600; }
.footer {
display: flex;
justify-content: space-between;
gap: 20px;
margin-top: 15px;
color: #7e949a;
font-size: 10px;
line-height: 1.6;
}
.footer span:last-child { white-space: nowrap; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 960px) {
.shell { width: min(100% - 28px, 760px); padding-top: 20px; }
.experiment { grid-template-columns: 1fr; }
.controls { border-left: 0; border-top: 1px solid var(--line); }
.direction-note { min-height: 0; }
.postures { margin-bottom: 18px; }
.micro { padding-top: 18px; }
}
@media (max-width: 480px) {
.shell { width: calc(100% - 18px); }
.masthead { margin: 0 3px 18px; }
.rider-count { gap: 6px; }
.rider-count > span { display: none; }
.mark { width: 39px; height: 39px; font-size: 24px; }
.identity { gap: 10px; }
.eyebrow { font-size: 8px; }
.viewport-top { padding: 0 11px; font-size: 8px; }
.viewport-top > span:last-child { display: none; }
.footer { flex-direction: column; gap: 3px; }
.controls { padding: 21px 19px; }
}
</style>
</head>
<body>
<main class="shell">
<header class="masthead">
<div class="identity">
<div class="mark" aria-hidden="true">↕</div>
<div>
<p class="eyebrow">Crowd dynamics / experiment 01</p>
<h1>UP <span>/ DOWN</span></h1>
</div>
</div>
<div class="rider-count">
<strong>18</strong>
<span>riders<br>one shared world</span>
</div>
</header>
<section class="experiment" aria-label="Interactive escalator simulation">
<div class="viewport">
<div class="viewport-top">
<span class="live">Live side elevation</span>
<span>12.0 m deck · 24 treads · 180 Hz</span>
</div>
<canvas id="scene" width="2400" height="1120" tabindex="0"
role="img" aria-label="Eighteen articulated people riding a side-view escalator"
aria-describedby="sceneDescription">
Your browser needs canvas support to display the simulation.
</canvas>
<p id="sceneDescription" class="sr-only">
A side-view moving escalator carries eighteen articulated people.
Press the reverse button to make the deck travel in the opposite
direction, or press it again to restore its forward direction.
</p>
<div class="viewport-bottom">
<div>
<span class="key"><i class="green"></i>upright</span>
<span class="key"><i class="amber"></i>down / recovering</span>
</div>
<div>Sealed machine ends · friction and contact, not animation</div>
</div>
</div>
<aside class="controls" aria-label="Simulation controls">
<p class="panel-label">Drive input</p>
<h2 class="direction" id="directionTitle">Normal ascent</h2>
<p class="direction-note" id="directionNote">
The drive is carrying the deck upward.
</p>
<div aria-label="Actual deck speed">
<p class="panel-label">Deck velocity</p>
<div class="speed-reading">
<strong id="speedValue">1.8</strong>
<span>m/s</span>
</div>
<div class="meter" aria-hidden="true">
<div class="speed-fill" id="speedFill"></div>
</div>
<div class="meter-labels" aria-hidden="true">
<span>4.2 m/s down</span>
<span>up 1.8</span>
</div>
</div>
<div class="postures">
<div class="posture">
<span>Upright</span>
<strong id="upCount">18</strong>
</div>
<div class="posture down">
<span>Down</span>
<strong id="downCount">0</strong>
</div>
</div>
<button class="primary" id="reverse" aria-keyshortcuts="Space">
<span><span class="arrow" aria-hidden="true">↔</span><span id="buttonLabel">Throw into reverse</span></span>
<kbd>SPACE</kbd>
</button>
<button class="secondary" id="reset" aria-keyshortcuts="R">Reset the ride · R</button>
<p class="micro">
<strong>No fall schedule.</strong><br>
Seven masses per person. Shared balance reflexes, individual
dimensions. Contacts push back; traction is finite.
</p>
<div id="announcement" class="sr-only" aria-live="polite"></div>
</aside>
</section>
<footer class="footer">
<span>SPACE reverses the drive · R restores the initial, settled ride</span>
<span id="simTime">SIM 00:00.0</span>
</footer>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
ctx.scale(2, 2);
const $ = id => document.getElementById(id);
const reverseButton = $("reverse");
const resetButton = $("reset");
// World coordinates are metres; screen Y is inverted only when drawing.
const W = 1200, H = 560;
const LEFT = 145, GROUND = 389, SCALE = 84;
const TREAD = 42, TREAD_COUNT = 24;
const RIGHT = LEFT + TREAD * TREAD_COUNT;
const GRAVITY = 9.81;
const DT = 1 / 180;
const SOLVER_PASSES = 4;
const NORMAL_SPEED = 1.8;
const REVERSE_SPEED = 4.2;
const DRIVE_ACCELERATION = 7.5;
const RIDERS = 18;
const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
const wrap = (v, length) => ((v % length) + length) % length;
const lerp = (a, b, t) => a + (b - a) * t;
function seededRandom(seed) {
return function() {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const shirts = ["#437983", "#b56f51", "#687d50", "#8d6b87", "#c2a258", "#577087"];
const trousers = ["#2e414c", "#3c4547", "#394654", "#514c48", "#384e49"];
const skins = ["#d7a780", "#b97c59", "#e0b995", "#996448", "#c08b69"];
const people = [];
const belt = { speed: NORMAL_SPEED, target: NORMAL_SPEED, travel: 0 };
let simTime = 0;
let accumulator = 0;
let lastFrame = performance.now();
let lastUI = 0;
// Reused contact records: 18 choose 2 pairs times solver passes is below
// this capacity. There is no per-frame physics-object allocation.
const contactPool = Array.from({ length: 4096 }, () => ({
a: null, b: null, nx: 0, ny: 0
}));
let contactCount = 0;
function makeBeam(a, b, strength) {
const pa = people[people.length - 1].parts;
return {
a, b, strength,
rest: Math.hypot(pa[a].x - pa[b].x, pa[a].y - pa[b].y)
};
}
function makePerson(index) {
const rand = seededRandom(9041 + index * 7919);
const size = 0.96 + rand() * 0.08;
const x = LEFT + (index + 0.5) * (RIGHT - LEFT) / RIDERS;
const massScale = size * size;
const definition = [
// x offset, height, mass, radius, ground friction
[0, 0.46, 0.36, 0.075, 0.48],
[0, 0.94, 0.30, 0.100, 0.28],
[-0.092, 0.24, 0.09, 0.046, 0.53],
[ 0.092, 0.24, 0.09, 0.046, 0.53],
[-0.115, 0.05, 0.10, 0.052, 0.92],
[ 0.115, 0.05, 0.10, 0.052, 0.92],
[0, 0.70, 0.24, 0.074, 0.36]
];
const parts = definition.map((d, i) => {
const mass = d[2] * massScale;
return {
x: x + d[0] * size,
y: GROUND + d[1] * size,
vx: NORMAL_SPEED,
vy: 0,
ox: 0, oy: 0,
r: d[3] * size,
mass,
invMass: 1 / mass,
friction: d[4] * (0.97 + rand() * 0.06),
grounded: false,
wallHit: false
};
});
const p = {
parts,
size,
strength: 0.90 + rand() * 0.20,
down: false,
lastAngle: 0,
steadyTime: 0,
probeIn: rand() * 0.09,
room: true,
spot: x,
motor: 1,
angle: 0,
comX: x,
spotOffset: 0,
footGoals: [parts[4].x, parts[5].x],
shirt: shirts[Math.floor(rand() * shirts.length)],
pants: trousers[Math.floor(rand() * trousers.length)],
skin: skins[Math.floor(rand() * skins.length)],
hair: ["#303332", "#4b3b31", "#70614b"][Math.floor(rand() * 3)]
};
// The temporary final person lets makeBeam read the just-built skeleton.
people.push(p);
p.beams = [
makeBeam(0, 1, 0.96),
makeBeam(0, 6, 0.96),
makeBeam(1, 6, 0.96),
makeBeam(0, 2, 0.90),
makeBeam(0, 3, 0.90),
makeBeam(2, 4, 0.92),
makeBeam(3, 5, 0.92),
makeBeam(0, 4, 0.66),
makeBeam(0, 5, 0.66),
makeBeam(1, 2, 0.59),
makeBeam(1, 3, 0.59)
];
return p;
}
function freeStandingColumn(x, owner) {
const halfWidth = 0.17;
const top = 1.07 * owner.size + 0.12;
const xmin = x - halfWidth;
const xmax = x + halfWidth;
for (const other of people) {
if (other === owner) continue;
for (const q of other.parts) {
if (q.y + q.r < 0.09 || q.y - q.r > top) continue;
if (q.x + q.r + 0.17 >= xmin && q.x - q.r - 0.17 <= xmax) {
return false;
}
}
}
return true;
}
// Search for a usable stance, not a pose. It never moves a person there.
function findStanceSpace(p) {
const offsets = [0, -0.27, 0.27, -0.47, 0.47];
for (const offset of offsets) {
const x = clamp(
p.comX + offset,
LEFT + 0.24,
RIGHT - 0.24
);
if (freeStandingColumn(x, p)) {
p.spot = x;
p.spotOffset = x - p.comX;
return true;
}
}
p.spotOffset = 0;
return false;
}
function updateMotor(p, dt) {
const [hip, head, chest, , , footL, footR] = p.parts;
p.comX =
hip.x * 0.38 + head.x * 0.28 + chest.x * 0.14
+ (footL.x + footR.x) * 0.10;
p.angle = Math.atan2(head.x - hip.x, head.y - hip.y);
p.probeIn -= dt;
if (p.probeIn <= 0) {
p.probeIn = 0.09;
p.room = findStanceSpace(p);
}
const planted = Number(footL.grounded) + Number(footR.grounded);
// Every person receives the same policy. A blocked person may still
// make a weak effort, but cannot be declared upright until recovered.
if (p.room) {
p.motor = p.down ? 0.72 : (planted ? 1 : 0.25);
} else {
p.motor = p.down ? 0.12 : 0.20;
}
const center = p.room ? p.spot : p.comX;
const stanceError = p.comX - (footL.x + footR.x) * 0.5;
const offset = clamp(stanceError * 1.2 + p.angle * 0.08, -0.13, 0.13);
// Targets move at a finite stepping rate. Feet never get teleported
// into their targets; the muscles can only pull against traction.
for (let leg = 0; leg < 2; leg++) {
const sign = leg === 0 ? -1 : 1;
const q = p.parts[4 + leg];
const desired = center + offset + sign * 0.115 * p.size;
const maxGoalStep = 0.80 * p.size * dt;
p.footGoals[leg] += clamp(
desired - p.footGoals[leg],
-maxGoalStep,
maxGoalStep
);
const force = clamp(
110 * (p.footGoals[leg] - q.x) - 4.5 * q.vx,
-24 * p.strength * p.motor,
24 * p.strength * p.motor
);
q.vx += force * q.invMass * dt;
}
// A common torso/hip reflex: the head is pulled toward a vertical
// position above the pelvis, with equal and opposite forces.
const targetHeadX = hip.x;
const targetHeadY = hip.y + 0.48 * p.size;
const maxCoreForce = 21 * p.strength * p.motor;
const fx = clamp(
80 * (targetHeadX - head.x) - 3.8 * head.vx,
-maxCoreForce, maxCoreForce
);
const fy = clamp(
95 * (targetHeadY - head.y) - 4.2 * head.vy,
-maxCoreForce, maxCoreForce
);
head.vx += fx * head.invMass * dt;
head.vy += fy * head.invMass * dt;
hip.vx -= fx * hip.invMass * dt;
hip.vy -= fy * hip.invMass * dt;
}
function updateDrive(dt) {
const change = clamp(
belt.target - belt.speed,
-DRIVE_ACCELERATION * dt,
DRIVE_ACCELERATION * dt
);
belt.speed += change;
belt.travel += belt.speed * dt;
}
function integrate(p, dt) {
const damping = Math.exp(-0.045 * dt);
for (const q of p.parts) {
q.grounded = false;
q.wallHit = false;
q.vy -= GRAVITY * dt;
q.vx *= damping;
q.vy *= damping;
q.ox = q.x;
q.oy = q.y;
q.x += q.vx * dt;
q.y += q.vy * dt;
}
}
function solveBeam(p, beam) {
const a = p.parts[beam.a], b = p.parts[beam.b];
let dx = b.x - a.x, dy = b.y - a.y;
const distance = Math.hypot(dx, dy);
if (distance < 1e-8) return;
const w = a.invMass + b.invMass;
const correction = (distance - beam.rest) / distance * beam.strength;
const cx = dx * correction, cy = dy * correction;
a.x += cx * a.invMass / w;
a.y += cy * a.invMass / w;
b.x -= cx * b.invMass / w;
b.y -= cy * b.invMass / w;
}
function solvePeople() {
for (let pass = 0; pass < SOLVER_PASSES; pass++) {
for (const p of people) {
for (const beam of p.beams) solveBeam(p, beam);
}
for (let i = 0; i < people.length; i++) {
const a = people[i].parts;
for (let j = i + 1; j < people.length; j++) {
const b = people[j].parts;
for (let n = 0; n < a.length; n++) {
const A = a[n];
for (let m = 0; m < b.length; m++) {
const B = b[m];
let dx = B.x - A.x, dy = B.y - A.y;
let distance = Math.hypot(dx, dy);
const minimum = A.r + B.r;
if (distance >= minimum) continue;
if (distance < 1e-7) {
dx = n === m ? 1 : 0;
dy = n === m ? 0 : 1;
distance = 1;
}
const nx = dx / distance, ny = dy / distance;
const overlap = minimum - distance;
const w = A.invMass + B.invMass;
const correction = overlap * 0.86 / w;
A.x -= nx * correction * A.invMass;
A.y -= ny * correction * A.invMass;
B.x += nx * correction * B.invMass;
B.y += ny * correction * B.invMass;
// Record each touching pair once, after the positional solve.
if (contactCount < contactPool.length) {
const c = contactPool[contactCount++];
c.a = A;
c.b = B;
c.nx = nx;
c.ny = ny;
}
}
}
}
}
projectSurfaces();
}
}
function projectSurfaces() {
for (const p of people) {
for (const q of p.parts) {
const floor = GROUND + q.r;
if (q.y < floor) {
q.y = floor;
q.grounded = true;
}
// A bounded machine enclosure prevents bodies leaving the model.
if (q.x < LEFT + q.r) {
q.x = LEFT + q.r;
q.wallHit = true;
} else if (q.x > RIGHT - q.r) {
q.x = RIGHT - q.r;
q.wallHit = true;
}
const ceiling = GROUND + 1.72;
if (q.y > ceiling) {
q.y = ceiling;
q.wallHit = true;
}
}
}
}
function finishVelocities(dt) {
// Position-based contact corrections have now established velocities.
for (const p of people) {
for (const q of p.parts) {
q.vx = (q.x - q.ox) / dt;
q.vy = (q.y - q.oy) / dt;
}
}
// Inelastic body contacts, followed by Coulomb-limited tangential
// impulses. A contact is not a spring pushing two people apart forever.
for (let i = 0; i < contactCount; i++) {
const c = contactPool[i];
const A = c.a, B = c.b;
let vx = B.vx - A.vx, vy = B.vy - A.vy;
const vn = vx * c.nx + vy * c.ny;
if (vn >= 0) continue;
const impulse = -vn / (A.invMass + B.invMass);
A.vx -= impulse * A.invMass * c.nx;
A.vy -= impulse * A.invMass * c.ny;
B.vx += impulse * B.invMass * c.nx;
B.vy += impulse * B.invMass * c.ny;
const tvx = B.vx - A.vx, tvy = B.vy - A.vy;
const tangentLength = Math.hypot(tvx, tvy);
if (tangentLength > 1e-7) {
const tx = tvx / tangentLength, ty = tvy / tangentLength;
const friction = 0.43 * impulse;
const jt = clamp(
-(tx * (B.vx - A.vx) + ty * (B.vy - A.vy))
/ (A.invMass + B.invMass),
-friction, friction
);
A.vx -= jt * A.invMass * tx;
A.vy -= jt * A.invMass * ty;
B.vx += jt * B.invMass * tx;
B.vy += jt * B.invMass * ty;
}
}
// Each contact uses its own load approximation for normal friction.
// The moving floor's velocity, rather than a special fall effect,
// causes the feet to overrun the bodies.
for (const p of people) {
for (const q of p.parts) {
if (q.grounded) {
if (q.vy < 0) q.vy = 0;
const relative = q.vx - belt.speed;
const dv = q.friction * GRAVITY * dt;
q.vx -= clamp(relative, -dv, dv);
}
if (q.wallHit) {
if (q.x <= LEFT + q.r + 0.001 && q.vx < 0) q.vx *= -0.10;
if (q.x >= RIGHT - q.r - 0.001 && q.vx > 0) q.vx *= -0.10;
if (q.y >= GROUND + 1.72 - 0.001 && q.vy > 0) q.vy *= -0.1;
}
}
}
}
function updatePosture(p, dt) {
const [hip, head, , , , footL, footR] = p.parts;
const angle = Math.atan2(head.x - hip.x, head.y - hip.y);
const angularSpeed = Math.abs(angle - p.lastAngle) / dt;
p.lastAngle = angle;
// Geometric classification, never a fall trigger. These flags only
// determine how much of the common balance reflex is available.
if (hip.y < GROUND + 0.28 ||
(Math.abs(angle) > 1.02 && hip.y < GROUND + 0.43)) {
p.down = true;
}
const planted = Number(footL.grounded) + Number(footR.grounded);
const low = Math.min(footL.x, footR.x);
const high = Math.max(footL.x, footR.x);
const speed = Math.hypot(hip.vx, hip.vy);
const stable =
p.room &&
Math.abs(p.spot - p.comX) < 0.34 &&
planted === 2 &&
high - low < 0.55 * p.size &&
p.comX > low + 0.015 &&
p.comX < high - 0.015 &&
Math.abs(angle) < 0.30 &&
angularSpeed < 0.85 &&
speed < 0.75;
p.steadyTime = stable ? p.steadyTime + dt : 0;
// A recovery must remain stable, rather than being awarded on a timer.
if (p.down && p.steadyTime > 0.32) {
p.down = false;
}
}
function physicsStep(dt) {
updateDrive(dt);
for (const p of people) updateMotor(p, dt);
for (const p of people) integrate(p, dt);
contactCount = 0;
solvePeople();
finishVelocities(dt);
for (const p of people) updatePosture(p, dt);
simTime += dt;
}
function resetSimulation() {
people.length = 0;
for (let i = 0; i < RIDERS; i++) makePerson(i);
belt.speed = belt.target = NORMAL_SPEED;
belt.travel = 0;
simTime = 0;
accumulator = 0;
contactCount = 0;
// Let the initial masses settle before the visible ride begins.
for (let i = 0; i < 108; i++) physicsStep(DT);
for (const p of people) {
p.steadyTime = 1;
p.lastAngle = p.angle;
}
simTime = 0;
belt.travel = 0;
lastFrame = performance.now();
updateUI();
draw();
}
function toggleDirection() {
belt.target = belt.target < 0 ? NORMAL_SPEED : -REVERSE_SPEED;
reverseButton.classList.toggle("reversed", belt.target < 0);
$("buttonLabel").textContent =
belt.target < 0 ? "Put back in forward" : "Throw into reverse";
$("announcement").textContent =
belt.target < 0
? "High-speed reverse selected. The drive will decelerate and reverse."
: "Forward ascent selected. The drive is returning to its forward direction.";
updateUI();
}
function updateUI() {
let down = 0;
for (const p of people) if (p.down) down++;
$("downCount").textContent = down;
$("upCount").textContent = people.length - down;
$("speedValue").textContent = Math.abs(belt.speed).toFixed(1);
const ratio = clamp(belt.speed / REVERSE_SPEED, -1, 1);
$("speedFill").style.transform = `scaleX(${Math.abs(ratio)})`;
$("speedFill").classList.toggle("negative", ratio < 0);
if (Math.abs(belt.speed - belt.target) > 0.06) {
$("directionTitle").textContent = "Reversing hard";
$("directionNote").textContent = "The drive is changing direction. Watch the feet.";
} else if (belt.target < 0) {
$("directionTitle").textContent = "High-speed descent";
$("directionNote").textContent = "The tread deck is carrying everybody downward.";
} else {
$("directionTitle").textContent = "Normal ascent";
$("directionNote").textContent = "The drive is carrying the deck upward.";
}
const minutes = Math.floor(simTime / 60);
const seconds = (simTime % 60).toFixed(1).padStart(4, "0");
$("simTime").textContent = `SIM ${String(minutes).padStart(2, "0")}:${seconds}`;
}
function roundedRect(x, y, w, h, r, fill, stroke) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); }
}
function line(x1, y1, x2, y2, color, width) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
}
function point(q) {
return { x: q.x * SCALE, y: GROUND - q.y * SCALE };
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, 0, H);
gradient.addColorStop(0, "#e1e6df");
gradient.addColorStop(1, "#cdd5ce");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
// Very light drafting grid.
ctx.strokeStyle = "#7e95951b";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x < W; x += 30) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
for (let y = 0; y < H; y += 30) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
ctx.stroke();
ctx.fillStyle = "#485e61";
ctx.font = "700 10px system-ui, sans-serif";
ctx.letterSpacing = "2px";
ctx.fillText("SIDE ELEVATION / A", 28, 31);
ctx.fillStyle = "#718481";
ctx.font = "9px system-ui, sans-serif";
ctx.fillText("RIGID-LINK ARTWORK · CONTINUOUS MOVING SUPPORT SURFACE", 28, 49);
ctx.textAlign = "right";
ctx.fillStyle = "#485e61";
ctx.font = "700 10px system-ui, sans-serif";
ctx.fillText("CROWD RESPONSE", W - 28, 31);
ctx.fillStyle = "#718481";
ctx.font = "9px system-ui, sans-serif";
ctx.fillText("7 MASSES / PERSON", W - 28, 49);
ctx.textAlign = "left";
ctx.letterSpacing = "0px";
}
function drawEscalator() {
const span = RIGHT - LEFT;
const travel = wrap(belt.travel, span);
const sign = belt.speed >= 0 ? -1 : 1;
// Direction markings in the space above the moving figures.
ctx.save();
ctx.globalAlpha = 0.20;
ctx.strokeStyle = "#48696a";
ctx.lineWidth = 2;
for (let x = LEFT + 42; x < RIGHT - 25; x += 94) {
const sway = wrap(x + belt.travel * 0.2, span);
if (sway < 18 || sway > span - 18) continue;
ctx.beginPath();
ctx.moveTo(LEFT + sway - 7 * sign, 254 - 7 * sign);
ctx.lineTo(LEFT + sway, 254);
ctx.lineTo(LEFT + sway - 7 * sign, 254 + 7 * sign);
ctx.stroke();
}
ctx.restore();
// Return run and the dark bottom of the machine.
roundedRect(LEFT - 17, GROUND + 79, span + 34, 37, 9, "#34454c");
for (let x = LEFT - 10; x < RIGHT + 20; x += 22) {
line(x, GROUND + 84, x + 12, GROUND + 111, "#4a5b60", 1);
}
roundedRect(LEFT - 21, GROUND + 107, span + 42, 13, 4, "#23373e");
line(LEFT - 12, GROUND + 111, RIGHT + 12, GROUND + 111, "#5c7070", 2);
// Treads: their actual visual phase comes only from belt.travel.
ctx.save();
ctx.beginPath();
ctx.rect(LEFT, GROUND, span, 22);
ctx.clip();
ctx.fillStyle = "#687b7d";
ctx.fillRect(LEFT, GROUND, span, 22);
for (let i = 0; i < TREAD_COUNT; i++) {
const x = LEFT + wrap(i * TREAD + travel, span);
ctx.fillStyle = i % 2 ? "#7e8f8e" : "#718584";
ctx.fillRect(x, GROUND, TREAD - 1, 18);
line(x, GROUND, x, GROUND + 19, "#425a60", 1.2);
line(x + 2, GROUND + 1, x + TREAD - 3, GROUND + 1, "#b0b8ac", 2);
for (let y = GROUND + 6; y < GROUND + 17; y += 4) {
line(x + 4, y, x + TREAD - 5, y, "#617577", 0.7);
}
}
ctx.restore();
// Glass balustrade and handrail. Moving seams reveal belt direction.
ctx.globalAlpha = 0.34;
ctx.fillStyle = "#bdd0c9";
ctx.fillRect(LEFT, GROUND - 161, span, 160);
ctx.globalAlpha = 1;
line(LEFT, GROUND - 161, RIGHT, GROUND - 161, "#8fa6a1", 1);
line(LEFT, GROUND - 2, RIGHT, GROUND - 2, "#344e55", 3);
for (let i = 0; i < 13; i++) {
const x = LEFT + wrap(i * span / 13 + travel * 0.12, span);
line(x, GROUND - 151, x, GROUND - 14, "#6d92923c", 1);
}
roundedRect(LEFT - 9, GROUND - 171, span + 18, 14, 7, "#304c53", "#203b41");
line(LEFT - 3, GROUND - 166, RIGHT + 3, GROUND - 166, "#69837f", 1.4);
// Sealed end housings are also the simulation's containment boundaries.
roundedRect(LEFT - 17, GROUND - 176, 17, 282, 3, "#36525a", "#29444b");
roundedRect(RIGHT, GROUND - 176, 17, 282, 3, "#36525a", "#29444b");
line(LEFT - 10, GROUND - 165, LEFT - 10, GROUND + 101, "#74908c", 1);
line(RIGHT + 7, GROUND - 165, RIGHT + 7, GROUND + 101, "#74908c", 1);
ctx.fillStyle = "#425d60";
ctx.font = "700 8px system-ui, sans-serif";
ctx.letterSpacing = "1.6px";
ctx.fillText("TREAD DECK", LEFT + 7, GROUND + 36);
ctx.fillStyle = "#8ba09a";
ctx.fillText("RETURN RUN", RIGHT - 79, GROUND + 97);
ctx.letterSpacing = "0px";
}
function drawArm(shoulder, handX, handY, p) {
const shoulderX = shoulder.x + p.parts[6].vx * 0.009;
const elbowX = (shoulderX + handX) * 0.5 + 0.035 * SCALE;
const elbowY = (shoulder.y + handY) * 0.5 - 0.018 * SCALE;
line(shoulderX, shoulder.y, elbowX, elbowY, p.shirt, 3.3);
line(elbowX, elbowY, handX, handY, p.shirt, 2.8);
ctx.fillStyle = p.skin;
ctx.beginPath();
ctx.arc(handX, handY, 2.4, 0, Math.PI * 2);
ctx.fill();
}
function drawPerson(p) {
const hip = point(p.parts[0]);
const head = point(p.parts[1]);
const chest = point(p.parts[6]);
const kneeL = point(p.parts[2]);
const kneeR = point(p.parts[3]);
const footL = point(p.parts[4]);
const footR = point(p.parts[5]);
const reach = clamp(-p.parts[6].vx * 0.011, -6, 6);
const up = !p.down;
// Small ground shadow; the body itself may be some distance above it.
ctx.fillStyle = "#263e421b";
ctx.beginPath();
ctx.ellipse(p.comX * SCALE, GROUND + 3, 20, 3.4, 0, 0, Math.PI * 2);
ctx.fill();
// The seven simulated masses form the real skeleton.
line(hip.x, hip.y, chest.x, chest.y, p.shirt, 8);
line(chest.x, chest.y, head.x, head.y, p.skin, 5);
line(hip.x, hip.y, kneeL.x, kneeL.y, p.pants, 4);
line(hip.x, hip.y, kneeR.x, kneeR.y, p.pants, 4);
line(kneeL.x, kneeL.y, footL.x, footL.y, p.pants, 3.5);
line(kneeR.x, kneeR.y, footR.x, footR.y, p.pants, 3.5);
line(footL.x - 3, footL.y + 1, footL.x + 4, footL.y + 1, "#273b40", 3);
line(footR.x - 3, footR.y + 1, footR.x + 4, footR.y + 1, "#273b40", 3);
drawArm(chest, chest.x - 18, chest.y - 17 + reach, p);
drawArm(chest, chest.x + 19, chest.y - 13 - reach, p);
ctx.fillStyle = p.skin;
ctx.strokeStyle = "#42504f";
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.arc(head.x, head.y, p.parts[1].r * SCALE, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// A small fixed-on-head hair cap, turned with the torso.
const dx = head.x - chest.x, dy = head.y - chest.y;
const length = Math.max(1, Math.hypot(dx, dy));
const ux = dx / length, uy = dy / length;
const hx = head.x - ux * 5, hy = head.y - uy * 5;
line(hx - uy * 6, hy + ux * 6, hx + uy * 6, hy - ux * 6, p.hair, 3.3);
ctx.fillStyle = up ? "#3d755e" : "#c97a42";
ctx.strokeStyle = "#edf0e5";
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.arc(head.x, head.y - 18, 3.1, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
function draw() {
drawBackground();
drawEscalator();
for (const p of people) drawPerson(p);
ctx.fillStyle = "#5e7374";
ctx.font = "8px system-ui, sans-serif";
ctx.fillText("← MACHINE ENDS", LEFT + 7, 541);
ctx.textAlign = "right";
ctx.fillText("MACHINE ENDS →", RIGHT - 7, 541);
ctx.textAlign = "left";
}
function frame(now) {
const elapsed = Math.min((now - lastFrame) / 1000, 0.06);
lastFrame = now;
accumulator += elapsed;
let steps = 0;
while (accumulator >= DT && steps < 16) {
physicsStep(DT);
accumulator -= DT;
steps++;
}
draw();
if (now - lastUI > 100) {
updateUI();
lastUI = now;
}
requestAnimationFrame(frame);
}
reverseButton.addEventListener("click", toggleDirection);
resetButton.addEventListener("click", resetSimulation);
document.addEventListener("keydown", event => {
const interactive = event.target.closest &&
event.target.closest("button, input, textarea, select, [contenteditable]");
if (interactive) return;
if (event.code === "Space") {
event.preventDefault();
if (!event.repeat) toggleDirection();
} else if (event.code === "KeyR" && !event.repeat) {
event.preventDefault();
resetSimulation();
}
});
// No hidden catch-up when a tab has been in the background.
document.addEventListener("visibilitychange", () => {
lastFrame = performance.now();
accumulator = 0;
});
resetSimulation();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
On the first reversal, the treads decelerate at 7.5 m/s² and head downward at 4.2 m/s. The entire row is carried briefly, then the deck runs out underneath it. Feet slip and spread, torsos tip at different rates, and the group compresses toward the selected end. The first few arrivals provide the obstruction for the next ones, producing a traveling crush and a heap rather than a synchronized fall. The people at the outside of the heap usually begin recovering first; their attempts to stand or crawl into free space can knock others back down. Pressing the button again accelerates the deck back toward its normal upward speed, so recovery can get interrupted by a new wave.
The down flag is a geometric controller input, not a fall event: low hips or a strongly tilted torso reduce the available balance reflex. Standing requires free vertical space, two planted feet, a low torso angle, and a continuously stable posture. The same thresholds and policies apply to everyone. Seeded variations in body size, mass, friction, and strength come from fixed ranges; I did not select particular people to fall or rescue. There are no per-person timelines, pose clips, or scripted collisions.
The approximations worth watching are: