16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 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 build physics toys, and you have held an umbrella in a gale.
Build me a single HTML page: a person holding an umbrella in wind that keeps getting stronger.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: what the canopy is made of in your code, what inversion is as a state change, and what decides when it happens.
The single fenced html block.
Where it looks right and where it looks wrong. Everyone who has held an umbrella knows what one does in wind, so say what yours gets wrong before I find it.
The canopy is a fan of ribs on a rigid shaft with a polyline of cloth panels between the tips; each panel takes a quasi-steady thin-shell pressure from the relative wind, and those pressures are summed for force, torque about the grip, and a crown-sag coordinate. Sag is a damped spring scaled so that one unit is the catch travel — that is the flex you see, and the rib color is that same downwind load. Inversion is not the sag itself: it is a boolean catch that flips only after sag has stayed past one, meaning the downwind load has held above the catch force long enough that a single gust spike does not quite do it. On that flip the spring’s rest shape jumps from dome to inside-out, drag rises with how far the crown has gone through, and the catch stays open until the wind drops low enough for the person to haul it back, or you reset it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Umbrella in a Gale</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #14181e; }
canvas { display: block; width: 100%; height: 100%; touch-action: none; }
#hud {
position: fixed; left: 14px; top: 14px; z-index: 2;
width: min(280px, calc(100vw - 28px));
max-height: calc(100% - 28px); overflow: auto;
color: #e7eef4;
font: 13px/1.35 ui-sans-serif, system-ui, sans-serif;
background: rgba(10, 14, 20, 0.55);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 12px 14px 10px;
box-sizing: border-box;
}
.kicker { letter-spacing: 0.16em; text-transform: uppercase; font-size: 11px; opacity: 0.7; }
#spd { font-variant-numeric: tabular-nums; font-size: 34px; font-weight: 650; letter-spacing: -0.03em; margin: 2px 0 0; }
.unit { font-size: 14px; font-weight: 500; opacity: 0.75; margin-left: 2px; }
#extra, #mode { opacity: 0.78; font-size: 12px; }
#state { margin: 6px 0 2px; min-height: 1.3em; }
input[type=range] { width: 100%; margin: 8px 0 4px; accent-color: #e2c9a0; }
.bar { height: 7px; border-radius: 4px; background: rgba(255,255,255,0.12); overflow: hidden; margin: 4px 0 8px; }
#load { height: 100%; width: 0%; background: #c8b48a; border-radius: 4px; }
.row { display: flex; gap: 8px; margin: 8px 0 4px; }
button {
font: inherit; color: #e8eef2; cursor: pointer;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.16);
border-radius: 7px; padding: 5px 8px;
}
button:hover { background: rgba(255,255,255,0.16); }
.hint { margin: 6px 0 0; opacity: 0.62; font-size: 11px; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div class="kicker">Wind</div>
<div id="spd"><span id="spdNum">0.0</span><span class="unit">m/s</span></div>
<div id="extra">0 km/h · calm</div>
<input id="wind" type="range" min="0" max="40" step="0.1" value="0.8" aria-label="Mean wind speed">
<div class="bar" title="Frame load, full at the catch"><div id="load"></div></div>
<div id="state">canopy quiet · grip firm</div>
<div id="mode">rising on its own</div>
<div class="row">
<button type="button" id="pause">Pause rise</button>
<button type="button" id="reset">Reset umbrella</button>
</div>
<p class="hint">Arrows change the wind, including back down. Space pauses the rise. R puts the umbrella back in their hands.</p>
</div>
<script>
'use strict';
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const spdNum = document.getElementById('spdNum');
const extraEl = document.getElementById('extra');
const stateEl = document.getElementById('state');
const modeEl = document.getElementById('mode');
const loadEl = document.getElementById('load');
const slider = document.getElementById('wind');
const pauseBtn = document.getElementById('pause');
const resetBtn = document.getElementById('reset');
const SHAFT_L = 0.80;
const COM_LY = 0.62;
const MASS = 0.42;
const I_UM = 0.16;
const TAU_MAX = 78;
const KP = 190;
const KD = 22;
const F_CATCH = 100;
const AREA_K = 0.28;
const G = 9.81;
const K_E = 70;
const D_E = 18;
let dpr = 1, W = 800, H = 600, S = 100, groundY = 480;
let t = 0, base = 0.8, wind = 0.8, shown = 0.8, auto = true, sliding = false, gustT = 0;
let theta = -0.12, omega = 0, targetTheta = -0.1;
let e = 0, eVel = 0, shape = 1, inverted = false, overT = 0;
let held = true, grip = 1, badT = 0, stumble = 0;
let handleX = -0.3, handleY = 1.15;
let comX = 0, comY = 1.4, comVx = 0, comVy = 0;
let lean = 0, leanVel = 0;
let Fdrive = 0, handLoad = 0, lastTau = 0, tauVis = 0;
let frameGeo = null;
let crown = {x: 0, y: 2};
const keys = Object.create(null);
const scarf = [];
const coat = [];
const rain = [];
const drips = [];
const splashes = [];
const sparks = [];
const leaves = [];
const grass = [];
const clouds = [
{x: -2.2, y: 4.4, w: 1.8, h: 0.38, k: 0.35},
{x: 1.4, y: 4.9, w: 2.4, h: 0.42, k: 0.22},
{x: 4.2, y: 4.1, w: 1.5, h: 0.3, k: 0.4},
{x: -5, y: 5.1, w: 2.1, h: 0.36, k: 0.18}
];
for (let i = 0; i < 64; i++) grass.push({x: -7 + i * 0.24, h: 0.07 + (i * 17 % 5) * 0.016, ph: i * 0.7});
let actx = null, windAudio = null;
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function mixHex(a, b, u) {
u = clamp(u, 0, 1);
const pa = parseInt(a.slice(1), 16);
const pb = parseInt(b.slice(1), 16);
const ar = (pa >> 16) & 255, ag = (pa >> 8) & 255, ab = pa & 255;
const br = (pb >> 16) & 255, bg = (pb >> 8) & 255, bb = pb & 255;
return 'rgb(' + ((ar + (br - ar) * u) | 0) + ',' + ((ag + (bg - ag) * u) | 0) + ',' + ((ab + (bb - ab) * u) | 0) + ')';
}
function P(x, y) { return [W * 0.58 + x * S, groundY - y * S]; }
function lw(px) { return Math.max(1, px * S / 110); }
function viewLeft() { return (0 - W * 0.58) / S; }
function viewRight() { return (W - W * 0.58) / S; }
function localToWorld(lx, ly, th, hx, hy) {
const c = Math.cos(th), s = Math.sin(th);
return {x: hx + lx * c + ly * s, y: hy - lx * s + ly * c};
}
function velFromOmega(lx, ly, th, om) {
const c = Math.cos(th), s = Math.sin(th);
return {x: om * (ly * c - lx * s), y: -om * (lx * c + ly * s)};
}
function tauAt(lx, ly, th, Fx, Fy) {
const c = Math.cos(th), s = Math.sin(th);
return Fx * (ly * c - lx * s) + Fy * (-(lx * c + ly * s));
}
function handleFromCom() {
const off = localToWorld(0, COM_LY, theta, 0, 0);
return {x: comX - off.x, y: comY - off.y};
}
function distToSeg(px, py, ax, ay, bx, by) {
const dx = bx - ax, dy = by - ay;
const l2 = dx * dx + dy * dy || 1e-8;
let u = ((px - ax) * dx + (py - ay) * dy) / l2;
u = clamp(u, 0, 1);
const qx = ax + dx * u, qy = ay + dy * u;
return {d: Math.hypot(px - qx, py - qy), x: qx, y: qy};
}
function scaleName(v) {
const limits = [0.5, 2, 3.5, 5.5, 8, 11, 14, 17, 21, 25, 29, 33];
const names = ['calm', 'light air', 'light breeze', 'gentle breeze', 'moderate breeze', 'fresh breeze', 'strong breeze', 'near gale', 'gale', 'strong gale', 'storm', 'violent storm', 'hurricane'];
let i = 0;
while (i < limits.length && v >= limits[i]) i++;
return names[i];
}
function pokeAudio() {
try {
if (!actx) {
actx = new (window.AudioContext || window.webkitAudioContext)();
const n = actx.sampleRate * 2;
const buf = actx.createBuffer(1, n, actx.sampleRate);
const data = buf.getChannelData(0);
let last = 0;
for (let i = 0; i < n; i++) {
const white = Math.random() * 2 - 1;
last = (last + 0.02 * white) / 1.02;
data[i] = last * 3.2;
}
const src = actx.createBufferSource();
src.buffer = buf;
src.loop = true;
const filter = actx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 400;
const gain = actx.createGain();
gain.gain.value = 0;
src.connect(filter);
filter.connect(gain);
gain.connect(actx.destination);
src.start();
windAudio = {filter: filter, gain: gain};
} else if (actx.state === 'suspended') {
actx.resume();
}
} catch (err) {}
}
function popSound(freq) {
if (!actx) return;
try {
const o = actx.createOscillator();
const g = actx.createGain();
o.type = 'triangle';
o.frequency.setValueAtTime(freq, actx.currentTime);
o.frequency.exponentialRampToValueAtTime(Math.max(40, freq * 0.45), actx.currentTime + 0.16);
g.gain.setValueAtTime(0.07, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.18);
o.connect(g);
g.connect(actx.destination);
o.start();
o.stop(actx.currentTime + 0.2);
} catch (err) {}
}
function burst(x, y, color, n, speed) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const s = speed * (0.35 + Math.random());
sparks.push({x: x, y: y, vx: Math.cos(a) * s, vy: Math.sin(a) * s + 0.4, life: 0.35 + Math.random() * 0.35, color: color});
}
}
function computePose() {
const stance = 0.16 + Math.min(0.18, base * 0.006 + Math.abs(lean) * 0.12);
const crouch = Math.min(0.16, base * 0.0032 + (inverted ? 0.045 : 0) + e * 0.035 + stumble * 0.07);
const frontFoot = {x: -stance, y: 0};
const backFoot = {x: stance * 0.8, y: 0};
const mid = (frontFoot.x + backFoot.x) / 2;
const hip = {x: mid - Math.sin(lean) * 0.16, y: 0.84 - crouch};
const shoulder = {
x: hip.x - Math.sin(lean) * 0.50,
y: hip.y + Math.cos(lean) * 0.46
};
const neck = {
x: shoulder.x - Math.sin(lean) * 0.08,
y: shoulder.y + Math.cos(lean) * 0.08
};
const head = {
x: neck.x - Math.sin(lean) * 0.18,
y: neck.y + Math.cos(lean) * 0.20
};
const tuck = Math.min(1, base / 24);
const hand = {
x: shoulder.x - (0.34 - tuck * 0.12) - lean * 0.06,
y: shoulder.y - 0.04 - tuck * 0.14
};
const frontKnee = {
x: (hip.x + frontFoot.x) / 2 - 0.06 - lean * 0.05,
y: Math.max(0.18, (hip.y + frontFoot.y) / 2 - 0.02)
};
const backKnee = {
x: (hip.x + backFoot.x) / 2 + 0.045,
y: Math.max(0.2, (hip.y + backFoot.y) / 2)
};
return {hip: hip, shoulder: shoulder, neck: neck, head: head, hand: hand, frontFoot: frontFoot, backFoot: backFoot, frontKnee: frontKnee, backKnee: backKnee, crouch: crouch};
}
function buildCanopy() {
const N = 12;
const R = 0.56;
const spread = 1.12;
const pts = [];
for (let i = 0; i < N; i++) {
const u = i / (N - 1);
const ang = (u - 0.5) * 2 * spread;
const bow = Math.sin(Math.PI * u);
const lx = Math.sin(ang) * R;
const ly = SHAFT_L + Math.cos(ang) * (R * 0.16) + bow * shape * (R * 0.50);
const w = localToWorld(lx, ly, theta, handleX, handleY);
w.x += bow * e * 0.15;
let pvx, pvy;
if (held) {
const v = velFromOmega(lx, ly, theta, omega);
pvx = v.x;
pvy = v.y;
} else {
const v = velFromOmega(lx, ly - COM_LY, theta, omega);
pvx = comVx + v.x;
pvy = comVy + v.y;
}
pts.push({u: u, bow: bow, lx: lx, ly: ly, x: w.x, y: w.y, vx: pvx, vy: pvy});
}
return pts;
}
function aero(pts) {
let Fx = 0, Fy = 0, tau = 0;
const cdMul = 1 + 0.6 * Math.max(0, -shape);
for (let i = 0; i < pts.length - 1; i++) {
const a = pts[i], b = pts[i + 1];
const tx = b.x - a.x, ty = b.y - a.y;
const len = Math.hypot(tx, ty) || 1e-6;
let nx = -ty / len, ny = tx / len;
const rvx = wind - (a.vx + b.vx) / 2;
const rvy = -(a.vy + b.vy) / 2;
if (nx * rvx + ny * rvy < 0) { nx = -nx; ny = -ny; }
const vn = rvx * nx + rvy * ny;
const mag = AREA_K * cdMul * len * vn * vn;
const fpx = nx * mag, fpy = ny * mag;
Fx += fpx;
Fy += fpy;
const mlx = (a.lx + b.lx) / 2;
const mly = (a.ly + b.ly) / 2;
if (held) tau += tauAt(mlx, mly, theta, fpx, fpy);
else tau += tauAt(mlx, mly - COM_LY, theta, fpx, fpy);
}
return {Fx: Fx, Fy: Fy, tau: tau};
}
function release() {
if (!held) return;
const off = localToWorld(0, COM_LY, theta, handleX, handleY);
comX = off.x;
comY = off.y;
const v = velFromOmega(0, COM_LY, theta, omega);
comVx = v.x + Math.min(wind, 14) * 0.22;
comVy = v.y + 1.3;
held = false;
leanVel -= 3.1;
stumble = 1;
popSound(86);
burst(handleX, handleY, '#d6453d', 16, 2.8);
for (let i = 0; i < 8; i++) {
leaves.push({
x: handleX, y: handleY,
vx: wind * 0.4 + (Math.random() - 0.3) * 3,
vy: 1 + Math.random() * 2,
a: Math.random() * 6, spin: (Math.random() - 0.5) * 8,
w: 0.04 + Math.random() * 0.04
});
}
}
function resetUmbrella() {
held = true;
inverted = false;
e = 0; eVel = 0; shape = 1; overT = 0;
theta = -0.2; omega = 0;
grip = 1; badT = 0;
comVx = 0; comVy = 0;
}
function step(dt) {
t += dt;
if (auto && !sliding) base += 0.45 * dt;
if (keys.ArrowLeft || keys.ArrowDown || keys.a || keys.A) base -= 7 * dt;
if (keys.ArrowRight || keys.ArrowUp || keys.d || keys.D) base += 7 * dt;
base = clamp(base, 0, 40);
gustT += dt;
const gustAmp = 0.2 + base * 0.055;
const gust = (Math.sin(gustT * 1.7) * 0.55 + Math.sin(gustT * 3.05) * 0.28 + Math.sin(gustT * 0.43) * 0.42) * gustAmp;
wind = Math.max(0, base + gust);
shown += (wind - shown) * Math.min(1, dt * 6);
const pose = computePose();
const startedHeld = held;
if (startedHeld) {
handleX = pose.hand.x + (1 - grip) * 0.006 * Math.sin(t * 34);
handleY = pose.hand.y;
} else {
const h = handleFromCom();
handleX = h.x;
handleY = h.y;
}
const pts = buildCanopy();
const forces = aero(pts);
Fdrive = Math.max(0, forces.Fx);
lastTau = forces.tau;
tauVis += (forces.tau - tauVis) * Math.min(1, dt * 8);
targetTheta = inverted ? 0.52 : -0.42 * Math.tanh(base / 15);
if (startedHeld) {
const tG = tauAt(0, COM_LY, theta, 0, -MASS * G);
const tauA = forces.tau + tG;
const err = targetTheta - theta;
let tCmd = KP * err - KD * omega;
const tLim = TAU_MAX * grip;
tCmd = clamp(tCmd, -tLim, tLim);
omega += (tauA + tCmd) / I_UM * dt;
omega *= Math.exp(-dt * 0.7);
theta += omega * dt;
handLoad = Math.hypot(forces.Fx, forces.Fy - MASS * G);
const angErr = Math.abs(theta - targetTheta);
const bad = angErr > 1.08 || handLoad > 250 || (inverted && grip < 0.2);
if (bad) badT += dt;
else badT = Math.max(0, badT - dt * 0.4);
if (badT > 0.16) release();
} else {
comVx += forces.Fx / MASS * dt;
comVy += (forces.Fy / MASS - G) * dt;
comX += comVx * dt;
comY += comVy * dt;
omega += forces.tau / I_UM * dt;
omega *= Math.exp(-dt * (0.6 + wind * 0.03));
theta += omega * dt;
const h = handleFromCom();
handleX = h.x;
handleY = h.y;
}
const tugging = inverted && held && base < 3.2;
const restE = (inverted && !tugging) ? 1.28 : 0;
eVel += ((restE - e) * K_E - D_E * eVel + (Fdrive / F_CATCH) * K_E) * dt;
e += eVel * dt;
e = clamp(e, -0.05, 1.85);
if (!inverted && e > 1) overT += dt;
else if (!inverted) overT = 0;
if (!inverted && overT > 0.08) {
inverted = true;
overT = 0;
popSound(168);
const mid = pts[pts.length >> 1];
burst(mid.x, mid.y, '#f3e6d4', 12, 1.8);
}
if (inverted && held && base < 3.2 && e < 0.22) {
inverted = false;
grip = Math.min(1, grip + 0.45);
}
const shapeTarget = inverted ? -1 : (1 - Math.min(e, 1.05) * 0.92);
const chase = ((inverted && shape > -0.15) || (!inverted && shape < 0.15)) ? 14 : 5;
shape += (shapeTarget - shape) * Math.min(1, dt * chase);
shape = clamp(shape, -1.25, 1.25);
if (held && inverted && base > 8) grip = Math.max(0.18, grip - dt * 0.24);
else if (held && !inverted) grip = Math.min(1, grip + dt * 0.18);
if (!held) {
const h = handleFromCom();
handleX = h.x;
handleY = h.y;
frameGeo = buildCanopy();
let minY = handleY, minX = handleX;
for (let i = 0; i < frameGeo.length; i++) {
if (frameGeo[i].y < minY) { minY = frameGeo[i].y; minX = frameGeo[i].x; }
}
if (minY < 0) {
comY -= minY;
if (comVy < 0) comVy = -comVy * 0.22;
comVx *= 0.86;
omega *= 0.9;
omega += (minX - comX) * 0.8 * dt;
const h2 = handleFromCom();
handleX = h2.x;
handleY = h2.y;
frameGeo = buildCanopy();
}
} else {
frameGeo = buildCanopy();
}
crown = frameGeo[frameGeo.length >> 1];
const push = held ? forces.Fx : 0;
const targetLean = held ? clamp(push / 220, -0.04, 0.56) : stumble * -0.08;
leanVel += ((targetLean - lean) * 14 - leanVel * 6.2) * dt;
lean += leanVel * dt;
stumble = Math.max(0, stumble - dt * 0.45);
const neck = pose.neck;
if (scarf.length === 0) {
for (let i = 0; i < 7; i++) scarf.push({x: neck.x, y: neck.y, vx: 0, vy: 0});
for (let i = 0; i < 4; i++) coat.push({x: pose.hip.x, y: pose.hip.y, vx: 0, vy: 0});
}
scarf[0].x = neck.x; scarf[0].y = neck.y;
for (let i = 1; i < scarf.length; i++) {
const p = scarf[i - 1];
const restX = p.x + 0.02 + wind * 0.012 * i;
const restY = p.y - 0.055;
scarf[i].vx += (restX - scarf[i].x) * 14 * dt;
scarf[i].vy += (restY - scarf[i].y) * 14 * dt;
scarf[i].vx += wind * 0.35 * dt;
scarf[i].vx *= Math.exp(-dt * 3.2);
scarf[i].vy *= Math.exp(-dt * 3.2);
scarf[i].x += scarf[i].vx * dt;
scarf[i].y += scarf[i].vy * dt;
}
coat[0].x = pose.hip.x + 0.1;
coat[0].y = pose.hip.y - 0.02;
for (let i = 1; i < coat.length; i++) {
const p = coat[i - 1];
const restX = p.x + 0.03 + wind * 0.01;
const restY = p.y - 0.07;
coat[i].vx += (restX - coat[i].x) * 10 * dt + wind * 0.2 * dt;
coat[i].vy += (restY - coat[i].y) * 10 * dt;
coat[i].vx *= Math.exp(-dt * 4);
coat[i].vy *= Math.exp(-dt * 4);
coat[i].x += coat[i].vx * dt;
coat[i].y += coat[i].vy * dt;
}
const left = viewLeft() - 1.2;
const right = viewRight() + 1.2;
if (rain.length === 0) {
for (let i = 0; i < 190; i++) {
rain.push({x: left + Math.random() * (right - left), y: Math.random() * 5.2, depth: Math.random()});
}
}
for (let i = 0; i < rain.length; i++) {
const d = rain[i];
const vx = wind * (0.82 + d.depth * 0.22);
const vy = -(6.2 + d.depth * 7.2);
d.x += vx * dt;
d.y += vy * dt;
let hit = false;
if (d.depth > 0.42 && frameGeo) {
for (let s = 0; s < frameGeo.length - 1; s++) {
const a = frameGeo[s], b = frameGeo[s + 1];
const h = distToSeg(d.x, d.y, a.x, a.y, b.x, b.y);
if (h.d < 0.034) {
hit = true;
if (drips.length < 36) {
drips.push({x: h.x, y: h.y, vx: wind * 0.12 + (b.x - a.x) * 1.4, vy: 0.15, life: 0.5});
}
break;
}
}
}
if (hit || d.x > right || d.y > 5.6) {
d.x = left + Math.random() * (right - left) * 0.35;
d.y = 3.2 + Math.random() * 2.2;
} else if (d.y < 0) {
if (splashes.length < 40 && Math.random() < 0.35) {
splashes.push({x: d.x, y: 0, vx: (Math.random() - 0.4) * 1.4, vy: 0.5 + Math.random() * 0.9, life: 0.22});
}
d.x = left + Math.random() * (right - left);
d.y = 3.4 + Math.random() * 2;
}
}
for (let i = drips.length - 1; i >= 0; i--) {
const d = drips[i];
d.vy -= G * dt;
d.vx += (wind * 0.3 - d.vx) * dt;
d.x += d.vx * dt;
d.y += d.vy * dt;
d.life -= dt;
if (d.y < 0) {
if (splashes.length < 40) splashes.push({x: d.x, y: 0, vx: d.vx * 0.2, vy: 0.4, life: 0.18});
drips.splice(i, 1);
} else if (d.life <= 0) drips.splice(i, 1);
}
for (let i = splashes.length - 1; i >= 0; i--) {
const s = splashes[i];
s.vy -= G * dt;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.life -= dt;
if (s.life <= 0 || s.y < 0) splashes.splice(i, 1);
}
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i];
s.vy -= G * dt;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.life -= dt;
if (s.life <= 0 || s.y < 0) sparks.splice(i, 1);
}
if (wind > 7 && leaves.length < 22 && Math.random() < dt * wind * 0.12) {
leaves.push({
x: left - 0.2, y: 0.15 + Math.random() * 2.2,
vx: wind * 0.55, vy: (Math.random() - 0.2) * 1.5,
a: Math.random() * 6, spin: (Math.random() - 0.5) * 7,
w: 0.035 + Math.random() * 0.04
});
}
for (let i = leaves.length - 1; i >= 0; i--) {
const L = leaves[i];
L.vx += (wind * 0.85 - L.vx) * 1.4 * dt;
L.vy += -G * 0.25 * dt;
L.x += L.vx * dt;
L.y += L.vy * dt;
L.a += L.spin * dt;
if (L.y < 0.02) { L.y = 0.02; L.vy *= -0.2; L.vx *= 0.9; }
if (L.x > right + 0.5) leaves.splice(i, 1);
}
for (let i = 0; i < clouds.length; i++) {
clouds[i].x += wind * clouds[i].k * 0.12 * dt;
if (clouds[i].x > viewRight() + 3) clouds[i].x = viewLeft() - 3;
}
if (windAudio && actx) {
const g = Math.min(0.11, base * 0.0036);
windAudio.gain.gain.setTargetAtTime(g, actx.currentTime, 0.12);
windAudio.filter.frequency.setTargetAtTime(260 + base * 42, actx.currentTime, 0.12);
}
if (!Number.isFinite(theta) || !Number.isFinite(e) || !Number.isFinite(comX)) resetUmbrella();
}
function drawSky() {
const storm = clamp(base / 32, 0, 1);
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, mixHex('#243044', '#12161c', storm));
g.addColorStop(0.55, mixHex('#8ea0b4', '#46525f', storm));
g.addColorStop(1, mixHex('#d5d0c6', '#6e6a64', storm * 0.85));
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(220,225,230,0.08)';
for (let i = 0; i < clouds.length; i++) {
const c = clouds[i];
const p = P(c.x, c.y);
ctx.beginPath();
ctx.ellipse(p[0], p[1], c.w * S, c.h * S, 0, 0, 6.3);
ctx.fill();
}
const streaks = 36;
ctx.strokeStyle = 'rgba(255,255,255,' + (0.04 + Math.min(0.12, wind * 0.004)) + ')';
ctx.lineWidth = 1;
for (let i = 0; i < streaks; i++) {
const y = (i * 47 + t * 20) % groundY;
const x = ((i * 97 + t * wind * 40) % (W + 80)) - 40;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + 10 + wind * 1.1, y + 1);
ctx.stroke();
}
}
function drawGround() {
ctx.fillStyle = '#3c434a';
ctx.fillRect(0, groundY, W, H - groundY);
ctx.fillStyle = '#4a545c';
ctx.fillRect(0, groundY, W, 3);
ctx.fillStyle = 'rgba(170,185,195,0.08)';
const puddles = [[-1.1, 0.34], [0.55, 0.22], [1.5, 0.4], [-0.2, 0.16]];
for (let i = 0; i < puddles.length; i++) {
const p = P(puddles[i][0], 0);
ctx.beginPath();
ctx.ellipse(p[0], p[1] + 2, puddles[i][1] * S, 5, 0, 0, 6.3);
ctx.fill();
}
ctx.strokeStyle = 'rgba(190,200,170,0.45)';
ctx.lineWidth = lw(1.1);
for (let i = 0; i < grass.length; i++) {
const b = grass[i];
const bend = wind * 0.014 * (0.7 + 0.3 * Math.sin(t * 3 + b.ph));
const a = P(b.x, 0);
const c = P(b.x + bend, b.h);
ctx.beginPath();
ctx.moveTo(a[0], a[1]);
ctx.lineTo(c[0], c[1]);
ctx.stroke();
}
}
function drawTree() {
const bend = wind * 0.018;
const base = P(-2.85, 0);
const top = P(-2.85 + bend, 1.7);
const mid = P(-2.85 + bend * 0.35, 0.8);
ctx.strokeStyle = '#3a332c';
ctx.lineWidth = lw(5);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(base[0], base[1]);
ctx.quadraticCurveTo(mid[0], mid[1], top[0], top[1]);
ctx.stroke();
ctx.fillStyle = 'rgba(70,90,72,0.85)';
ctx.beginPath();
ctx.ellipse(top[0] + bend * S * 0.2, top[1] + 4, 28 * S / 110, 22 * S / 110, bend * 0.3, 0, 6.3);
ctx.fill();
}
function drawLamp() {
const bend = wind * 0.01;
const base = P(2.25, 0);
const top = P(2.25 + bend, 2.15);
const mid = P(2.25 + bend * 0.45, 1.05);
ctx.strokeStyle = '#2c333b';
ctx.lineWidth = lw(3.5);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(base[0], base[1]);
ctx.quadraticCurveTo(mid[0], mid[1], top[0], top[1]);
ctx.stroke();
ctx.fillStyle = '#3d4650';
ctx.fillRect(top[0] - 7, top[1] - 3, 16, 6);
ctx.fillStyle = '#c4553a';
ctx.beginPath();
ctx.moveTo(top[0] + 8, top[1]);
ctx.lineTo(top[0] + 14 + wind * 1.3, top[1] + 3);
ctx.lineTo(top[0] + 10 + wind * 0.7, top[1] + 8);
ctx.closePath();
ctx.fill();
}
function drawRain(near) {
for (let i = 0; i < rain.length; i++) {
const d = rain[i];
const isNear = d.depth > 0.55;
if (isNear !== near) continue;
const vx = wind * (0.82 + d.depth * 0.22);
const vy = -(6.2 + d.depth * 7.2);
const a = P(d.x, d.y);
const b = P(d.x - vx * 0.04, d.y - vy * 0.04);
ctx.strokeStyle = near ? 'rgba(210,220,230,0.55)' : 'rgba(180,190,200,0.22)';
ctx.lineWidth = near ? 1.3 : 1;
ctx.beginPath();
ctx.moveTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
ctx.stroke();
}
}
function drawLeaves() {
for (let i = 0; i < leaves.length; i++) {
const L = leaves[i];
const p = P(L.x, L.y);
ctx.save();
ctx.translate(p[0], p[1]);
ctx.rotate(-L.a);
ctx.fillStyle = i % 2 ? '#7d8a58' : '#8a6a42';
ctx.fillRect(-L.w * S / 2, -2, L.w * S, 3.5);
ctx.restore();
}
}
function drawUmbrella() {
if (!frameGeo || frameGeo.length < 2) return;
const hub = localToWorld(0, SHAFT_L, theta, handleX, handleY);
const mid = localToWorld(0, SHAFT_L * 0.5, theta, handleX, handleY);
mid.x += tauVis * 0.0022;
const h0 = P(handleX, handleY);
const hm = P(mid.x, mid.y);
const hh = P(hub.x, hub.y);
ctx.lineCap = 'round';
ctx.strokeStyle = '#1b1e22';
ctx.lineWidth = lw(3.2);
ctx.beginPath();
ctx.moveTo(h0[0], h0[1]);
ctx.quadraticCurveTo(hm[0], hm[1], hh[0], hh[1]);
ctx.stroke();
ctx.beginPath();
let started = false;
for (let a = 0; a <= Math.PI; a += 0.4) {
const lx = -0.105 * Math.sin(a);
const ly = -0.105 + 0.105 * Math.cos(a);
const w = localToWorld(lx, ly, theta, handleX, handleY);
const p = P(w.x, w.y);
if (!started) { ctx.moveTo(p[0], p[1]); started = true; }
else ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
const cloth = shape < 0 ? '#f3e6d4' : mixHex('#e15a4c', '#8e2c28', clamp(Fdrive / F_CATCH, 0, 1));
ctx.fillStyle = cloth;
ctx.beginPath();
const p0 = P(frameGeo[0].x, frameGeo[0].y);
ctx.moveTo(p0[0], p0[1]);
for (let i = 1; i < frameGeo.length; i++) {
const p = P(frameGeo[i].x, frameGeo[i].y);
ctx.lineTo(p[0], p[1]);
}
ctx.closePath();
ctx.fill();
ctx.strokeStyle = shape < 0 ? '#c8b49a' : '#6e221c';
ctx.lineWidth = lw(2.2);
ctx.beginPath();
ctx.moveTo(p0[0], p0[1]);
for (let i = 1; i < frameGeo.length; i++) {
const p = P(frameGeo[i].x, frameGeo[i].y);
ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
const loadT = clamp(Fdrive / F_CATCH, 0, 1.3);
ctx.strokeStyle = mixHex('#3a4048', '#ff5a2a', Math.min(1, loadT));
ctx.lineWidth = lw(1.35);
for (let i = 0; i < frameGeo.length; i++) {
const p = frameGeo[i];
const tip = P(p.x, p.y);
const cpx = (hub.x + p.x) / 2 + p.bow * loadT * 0.07;
const cpy = (hub.y + p.y) / 2;
const c = P(cpx, cpy);
ctx.beginPath();
ctx.moveTo(hh[0], hh[1]);
ctx.quadraticCurveTo(c[0], c[1], tip[0], tip[1]);
ctx.stroke();
}
ctx.fillStyle = inverted ? '#e23b2f' : mixHex('#c8ccd0', '#e23b2f', clamp(e, 0, 1));
ctx.beginPath();
ctx.arc(hh[0], hh[1], lw(3.2), 0, 6.3);
ctx.fill();
}
function drawPerson(pose) {
const skin = '#f0cbb8';
const coatCol = '#2c3d58';
const leg = '#232833';
function seg(a, b, width, color) {
const pa = P(a.x, a.y), pb = P(b.x, b.y);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(pa[0], pa[1]);
ctx.lineTo(pb[0], pb[1]);
ctx.stroke();
}
const sh = P(0.05, 0);
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.beginPath();
ctx.ellipse(sh[0], sh[1], 0.32 * S, 0.07 * S, 0, 0, 6.3);
ctx.fill();
seg(pose.hip, pose.backKnee, lw(7), leg);
seg(pose.backKnee, pose.backFoot, lw(6), leg);
seg(pose.hip, pose.frontKnee, lw(7), leg);
seg(pose.frontKnee, pose.frontFoot, lw(6), leg);
function boot(f) {
const p = P(f.x, 0);
ctx.fillStyle = '#2a211c';
ctx.beginPath();
ctx.ellipse(p[0] - 4, p[1] - 2, 0.09 * S, 0.035 * S, 0, 0, 6.3);
ctx.fill();
}
boot(pose.frontFoot);
boot(pose.backFoot);
const shL = {x: pose.shoulder.x - 0.09, y: pose.shoulder.y + 0.02};
const shR = {x: pose.shoulder.x + 0.11, y: pose.shoulder.y - 0.01};
const hemL = {x: pose.hip.x - 0.13, y: pose.hip.y + 0.02};
const hemR = {x: pose.hip.x + 0.16 + Math.min(wind, 25) * 0.006, y: pose.hip.y - 0.06};
ctx.fillStyle = coatCol;
ctx.beginPath();
const a = P(shL.x, shL.y), b = P(shR.x, shR.y), c = P(hemR.x, hemR.y), d = P(hemL.x, hemL.y);
ctx.moveTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
ctx.lineTo(c[0], c[1]);
ctx.lineTo(d[0], d[1]);
ctx.closePath();
ctx.fill();
if (coat.length > 1) {
ctx.strokeStyle = '#24344c';
ctx.lineWidth = lw(5);
ctx.beginPath();
const p0 = P(coat[0].x, coat[0].y);
ctx.moveTo(p0[0], p0[1]);
for (let i = 1; i < coat.length; i++) {
const p = P(coat[i].x, coat[i].y);
ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
}
const head = P(pose.head.x, pose.head.y);
const hr = 0.115 * S;
ctx.fillStyle = '#241c18';
ctx.beginPath();
ctx.arc(head[0] + wind * 0.15, head[1] - hr * 0.15, hr * 1.05, Math.PI * 1.05, Math.PI * 1.95);
ctx.fill();
ctx.fillStyle = skin;
ctx.beginPath();
ctx.arc(head[0], head[1], hr, 0, 6.3);
ctx.fill();
const squint = clamp(base / 20, 0, 1);
ctx.strokeStyle = '#2a211c';
ctx.lineWidth = lw(1.3);
ctx.beginPath();
ctx.moveTo(head[0] - hr * 0.45, head[1] - hr * 0.1);
ctx.lineTo(head[0] - hr * 0.12, head[1] - hr * 0.1 + squint * 1.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(head[0] - hr * 0.55, head[1] - hr * 0.38);
ctx.lineTo(head[0] - hr * 0.08, head[1] - hr * 0.28);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(head[0] - hr * 0.42, head[1] + hr * 0.28);
ctx.lineTo(head[0] - hr * 0.12, head[1] + hr * 0.34 + squint);
ctx.stroke();
if (scarf.length > 1) {
ctx.strokeStyle = '#e6d3b0';
ctx.lineWidth = lw(4.5);
ctx.lineCap = 'round';
ctx.beginPath();
const s0 = P(scarf[0].x, scarf[0].y);
ctx.moveTo(s0[0], s0[1]);
for (let i = 1; i < scarf.length; i++) {
const p = P(scarf[i].x, scarf[i].y);
ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
}
if (held) {
const two = base > 6 || e > 0.35 || inverted;
const jitter = (1 - grip) * 0.01 * Math.sin(t * 40);
const h1 = {x: handleX, y: handleY + jitter};
seg(pose.shoulder, h1, lw(5.5), coatCol);
const hp = P(h1.x, h1.y);
ctx.fillStyle = '#1e2430';
ctx.beginPath();
ctx.arc(hp[0], hp[1], lw(4.2), 0, 6.3);
ctx.fill();
if (two) {
const h2w = localToWorld(0.02, 0.14, theta, handleX, handleY + jitter);
seg({x: pose.shoulder.x + 0.04, y: pose.shoulder.y - 0.06}, h2w, lw(5), coatCol);
const hp2 = P(h2w.x, h2w.y);
ctx.beginPath();
ctx.arc(hp2[0], hp2[1], lw(3.6), 0, 6.3);
ctx.fill();
}
} else {
const hang = {x: pose.shoulder.x + 0.06, y: pose.shoulder.y - 0.42};
seg(pose.shoulder, hang, lw(5.5), coatCol);
const o = P(hang.x, hang.y);
ctx.fillStyle = skin;
ctx.beginPath();
ctx.arc(o[0], o[1], lw(3.2), 0, 6.3);
ctx.fill();
}
}
function drawBits() {
ctx.strokeStyle = 'rgba(200,210,220,0.7)';
ctx.lineWidth = 1;
for (let i = 0; i < drips.length; i++) {
const d = drips[i];
const p = P(d.x, d.y);
ctx.beginPath();
ctx.moveTo(p[0], p[1]);
ctx.lineTo(p[0] - d.vx * 1.5, p[1] + d.vy * 1.5);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(210,220,230,0.45)';
for (let i = 0; i < splashes.length; i++) {
const s = splashes[i];
const p = P(s.x, Math.max(0, s.y));
ctx.beginPath();
ctx.moveTo(p[0] - 3, p[1]);
ctx.lineTo(p[0] + 3, p[1]);
ctx.stroke();
}
for (let i = 0; i < sparks.length; i++) {
const s = sparks[i];
const p = P(s.x, s.y);
ctx.fillStyle = s.color;
ctx.globalAlpha = clamp(s.life * 2, 0, 1);
ctx.fillRect(p[0], p[1], 2.2, 2.2);
ctx.globalAlpha = 1;
}
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
drawSky();
ctx.save();
const sh = Math.min(2.2, base * 0.05);
ctx.translate(Math.sin(t * 17) * sh, Math.cos(t * 13) * sh * 0.45);
drawTree();
drawLamp();
drawGround();
drawRain(false);
drawLeaves();
const pose = computePose();
drawPerson(pose);
drawUmbrella();
if (held) {
// hands already drawn inside drawPerson after the coat; umbrella may cover them
// redraw grip so the hold stays readable when the canopy swings back
const hp = P(handleX, handleY);
ctx.fillStyle = '#1e2430';
ctx.beginPath();
ctx.arc(hp[0], hp[1], lw(4.2), 0, 6.3);
ctx.fill();
}
drawRain(true);
drawBits();
ctx.restore();
const vg = ctx.createRadialGradient(W * 0.5, H * 0.45, H * 0.2, W * 0.5, H * 0.5, H * 0.78);
vg.addColorStop(0, 'rgba(0,0,0,0)');
vg.addColorStop(1, 'rgba(0,0,0,0.28)');
ctx.fillStyle = vg;
ctx.fillRect(0, 0, W, H);
}
function updateHud() {
spdNum.textContent = shown.toFixed(1);
extraEl.textContent = Math.round(shown * 3.6) + ' km/h · ' + scaleName(shown);
if (!sliding) slider.value = String(Math.round(base * 10) / 10);
const load = clamp(Fdrive / F_CATCH, 0, 1);
loadEl.style.width = (load * 100) + '%';
loadEl.style.background = inverted ? '#e23b2f' : mixHex('#c8b48a', '#e23b2f', load);
let can = 'canopy quiet';
if (!held) can = inverted ? 'inside out and gone' : 'gone with the wind';
else if (inverted) can = 'INSIDE OUT';
else if (e > 0.82) can = 'canopy on the catch';
else if (e > 0.28) can = 'canopy flexing';
let gr = 'grip firm';
if (!held) gr = 'hands empty';
else if (Math.abs(theta - targetTheta) > 0.55) gr = 'wrenched sideways';
else if (grip < 0.45) gr = 'grip failing';
else if (grip < 0.8 || e > 0.45) gr = 'grip working';
stateEl.textContent = can + ' · ' + gr;
modeEl.textContent = auto ? 'rising on its own' : 'rise paused';
pauseBtn.textContent = auto ? 'Pause rise' : 'Resume rise';
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.floor(W * dpr);
canvas.height = Math.floor(H * dpr);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
S = Math.min(Math.max(W / 10, 78), H / 6.2);
groundY = H * 0.80;
}
window.addEventListener('resize', resize);
window.addEventListener('keydown', function (e) {
keys[e.key] = true;
if (e.key === ' ' || e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') e.preventDefault();
if (e.repeat) return;
pokeAudio();
if (e.key === ' ') auto = !auto;
if (e.key === 'r' || e.key === 'R') resetUmbrella();
});
window.addEventListener('keyup', function (e) { keys[e.key] = false; });
window.addEventListener('blur', function () { for (const k in keys) keys[k] = false; });
slider.addEventListener('input', function () { base = +slider.value; pokeAudio(); });
slider.addEventListener('pointerdown', function () { sliding = true; pokeAudio(); });
window.addEventListener('pointerup', function () { sliding = false; });
pauseBtn.addEventListener('click', function () { auto = !auto; pokeAudio(); });
resetBtn.addEventListener('click', function () { resetUmbrella(); pokeAudio(); });
resize();
let last = 0, acc = 0;
function frame(now) {
if (!last) last = now;
let dt = (now - last) / 1000;
last = now;
if (dt > 0.1) dt = 0.1;
acc += dt;
const h = 1 / 60;
let n = 0;
while (acc >= h && n < 4) {
step(h);
acc -= h;
n++;
}
draw();
updateHud();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
It reads right in the sequence you already know: the cloth sags and the ribs heat up before a distinct snap, not a slow morph; once it is inside out it catches more air, cocks sideways in the hands, and then leaves; the person leans, widens their stance, tucks and lowers the umbrella, and puts a second hand on it; rain slants with the wind and sheds off the cloth; the number on screen is the wind that is actually hitting, gusts included, and you can drive that mean up or down while it otherwise keeps rising.
What it gets wrong: