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
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 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 chain of ~13 point masses connected by stiff springs, anchored at the shaft top and at each rib tip, with a bending stiffness term that resists curvature change — that's what makes it hold a dome shape instead of flopping like a flag. Wind applies a pressure force normal to each canopy segment, scaled by the segment's projected area and by a gust factor. Inversion is a state change: when the net torque about the shaft axis (computed from the canopy's center of pressure relative to the hand) exceeds a threshold for a sustained time, the canopy's rest curvature flips sign — the springs' target angles invert, so the dome actively drives itself inside-out, and from then on the wind pushes it further out (a genuinely unstable equilibrium, like the real thing). Losing the umbrella is a second state change: grip force is finite, and when the canopy's drag plus the person's lean compensation exceeds what friction at the hand can hold, the whole umbrella rigid-body flies off on its own trajectory while the person staggers and recovers. Rain is a particle system whose velocity is wind velocity plus gravity; the person is a simple articulated figure — feet planted, legs braced, torso leaning into the wind, arms pulling the shaft toward the body — with lean angle driven by a PD controller trying to keep the net moment at the feet near zero.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gale</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; background: #0d1117; }
body { font-family: "Courier New", monospace; color: #c9d1d9; }
#stage { position: fixed; inset: 0; display: block; }
#hud {
position: fixed; top: 14px; left: 14px; z-index: 10;
background: rgba(13,17,23,.82); border: 1px solid #30363d;
border-radius: 8px; padding: 14px 18px; min-width: 240px;
backdrop-filter: blur(4px);
}
#hud h1 { font-size: 13px; letter-spacing: 3px; color: #8b949e; font-weight: normal; margin-bottom: 8px; }
#windread { font-size: 34px; color: #f0f6fc; }
#windread small { font-size: 14px; color: #8b949e; }
#beaufort { font-size: 12px; color: #58a6ff; margin-top: 2px; letter-spacing: 1px; }
#controls { margin-top: 12px; }
#controls label { font-size: 10px; color: #8b949e; letter-spacing: 1px; display: block; margin-bottom: 4px; }
input[type=range] { width: 100%; accent-color: #58a6ff; }
#hint { margin-top: 10px; font-size: 10px; color: #6e7681; line-height: 1.5; }
#hint b { color: #8b949e; }
#status {
position: fixed; top: 14px; right: 14px; z-index: 10;
background: rgba(13,17,23,.82); border: 1px solid #30363d;
border-radius: 8px; padding: 10px 16px; font-size: 12px;
letter-spacing: 1px; color: #8b949e; text-align: right;
}
#status .val { color: #f0f6fc; font-size: 15px; }
#lost {
position: fixed; inset: 0; z-index: 20; display: none;
align-items: center; justify-content: center;
background: rgba(13,17,23,.55);
}
#lost .box {
background: #161b22; border: 1px solid #f85149; border-radius: 12px;
padding: 28px 40px; text-align: center;
}
#lost h2 { color: #f85149; font-size: 22px; letter-spacing: 4px; margin-bottom: 8px; }
#lost p { color: #8b949e; font-size: 13px; margin-bottom: 16px; }
#lost button {
background: #238636; color: #fff; border: none; border-radius: 6px;
padding: 10px 24px; font-family: inherit; font-size: 13px; cursor: pointer;
letter-spacing: 1px;
}
#lost button:hover { background: #2ea043; }
</style>
</head>
<body>
<canvas id="stage"></canvas>
<div id="hud">
<h1>WIND</h1>
<div id="windread">0<small> km/h</small></div>
<div id="beaufort">CALM</div>
<div id="controls">
<label>DRIVE IT YOURSELF — SLIDER, OR ← → KEYS, SPACE TO GUST</label>
<input type="range" id="windslider" min="0" max="140" value="0">
</div>
<div id="hint"><b>←/→</b> nudge wind · <b>↑</b> hold to surge · <b>space</b> sudden gust · <b>R</b> reset</div>
</div>
<div id="status">
<div>CANOPY: <span class="val" id="stCanopy">INTACT</span></div>
<div>GRIP: <span class="val" id="stGrip">FIRM</span></div>
<div>LEAN: <span class="val" id="stLean">0°</span></div>
</div>
<div id="lost">
<div class="box">
<h2>IT'S GONE</h2>
<p>The gust took it. The person is still standing, watching it go.</p>
<button id="again">HOLD ANOTHER</button>
</div>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('windslider');
const windRead = document.getElementById('windread');
const beaufortEl = document.getElementById('beaufort');
const stCanopy = document.getElementById('stCanopy');
const stGrip = document.getElementById('stGrip');
const stLean = document.getElementById('stLean');
const lostEl = document.getElementById('lost');
let W = 0, H = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = W * dpr; canvas.height = H * dpr;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
groundY = H * 0.86;
}
let groundY = 0;
window.addEventListener('resize', resize);
resize();
// ---------- Wind model ----------
// windTarget: user-driven base. wind: actual, follows target with lag + gusts.
const wind = {
target: 0, // km/h, user controlled
value: 0, // km/h, actual (lagged)
gust: 0, // additive gust energy
surge: 0, // while up-arrow held
};
const keys = {};
window.addEventListener('keydown', e => {
keys[e.key] = true;
if (e.key === ' ') { e.preventDefault(); wind.gust = Math.max(wind.gust, 55 + Math.random()*25); }
if (e.key === 'r' || e.key === 'R') reset();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
slider.addEventListener('input', () => { wind.target = +slider.value; });
function beaufortName(v) {
if (v < 2) return "CALM";
if (v < 12) return "LIGHT AIR";
if (v < 20) return "LIGHT BREEZE";
if (v < 29) return "GENTLE BREEZE";
if (v < 39) return "MODERATE BREEZE";
if (v < 50) return "FRESH BREEZE";
if (v < 62) return "STRONG BREEZE";
if (v < 75) return "NEAR GALE";
if (v < 89) return "GALE";
if (v < 103) return "STRONG GALE";
if (v < 118) return "STORM";
return "VIOLENT STORM";
}
// ---------- Rain ----------
const rain = [];
const RAIN_N = 260;
function seedRain() {
rain.length = 0;
for (let i = 0; i < RAIN_N; i++) {
rain.push({
x: Math.random() * (W + 400) - 200,
y: Math.random() * H,
len: 8 + Math.random() * 16,
sp: 0.5 + Math.random() * 0.5
});
}
}
// ---------- Umbrella canopy: chain of point masses ----------
// Points p[0..N-1], p[0] anchored at shaft top. Springs between neighbors
// plus angular (bending) springs that hold the dome curvature.
const N = 13;
const SEG = 26; // rest segment length (px)
const person = {
x: 0, // feet x (fixed to ground)
h: 0, // scale
lean: 0, // torso lean into wind (rad, + = leaning right into wind from left)
lost: false,
lostT: 0,
umbrella: null, // rigid body when flown away
gripSlip: 0
};
function makeCanopy(shaftTop) {
const pts = [];
// rest shape: dome. angle from vertical, spread over ~100 degrees.
const spread = 1.75; // radians total
for (let i = 0; i < N; i++) {
const t = i / (N - 1);
const a = -spread/2 + t * spread; // -left .. +right relative to straight-up
pts.push({
x: shaftTop.x + Math.sin(a) * SEG * i * 0.92,
y: shaftTop.y - Math.cos(a) * SEG * i * 0.92,
vx: 0, vy: 0,
restA: a, // rest angle of this rib from vertical
px: 0, py: 0
});
}
return {
pts,
inverted: false,
invertCharge: 0, // accumulates torque-about-shaft when loaded
invertT: 0,
shaftTop, // {x,y} — follows the hand
angle: 0, // shaft tilt from vertical (rad, + tilts right)
vel: { x: 0, y: 0 }, // shaft top velocity (for dynamics feel)
drag: 0
};
}
// ---------- Person ----------
function makePerson() {
person.x = W * 0.42;
person.h = Math.min(H * 0.34, 240);
person.lean = 0;
person.lost = false;
person.umbrella = null;
person.gripSlip = 0;
const hand = handPos();
person.canopy = makeCanopy({ x: hand.x, y: hand.y - person.h * 0.62 });
}
// geometry helpers (side view, wind blows left -> right, person faces left into it)
function hipPos() {
// lean rotates the whole body about the feet; feet planted.
const L = person.lean; // + leans right (downwind) — bracing
return {
x: person.x + Math.sin(L) * person.h * 0.45,
y: groundY - Math.cos(L) * person.h * 0.45
};
}
function shoulderPos() {
const L = person.lean;
return {
x: person.x + Math.sin(L) * person.h * 0.88,
y: groundY - Math.cos(L) * person.h * 0.88
};
}
function handPos() {
// arms reach up-forward (to the left, into the wind) to hold the shaft
const s = shoulderPos();
const L = person.lean;
return {
x: s.x - Math.cos(L) * person.h * 0.30 + Math.sin(L) * person.h * 0.05,
y: s.y - Math.cos(L) * person.h * 0.42 - Math.sin(L) * person.h * 0.10
};
}
function footL() { const L = person.lean; return { x: person.x - Math.cos(L)*person.h*0.10, y: groundY }; }
function footR() { const L = person.lean; return { x: person.x + Math.cos(L)*person.h*0.14 + Math.sin(L)*person.h*0.04, y: groundY }; }
let canopy = null;
// ---------- Physics step ----------
let lastT = performance.now();
function frame(now) {
const dt = Math.min((now - lastT) / 1000, 0.033);
lastT = now;
stepWind(dt);
if (!person.lost) stepPerson(dt);
if (canopy) stepCanopy(dt);
if (person.umbrella) stepFledUmbrella(dt);
stepRain(dt);
draw();
requestAnimationFrame(frame);
}
function stepWind(dt) {
// user input
if (keys['ArrowLeft']) wind.target = Math.max(0, wind.target - 90 * dt);
if (keys['ArrowRight']) wind.target = Math.min(140, wind.target + 90 * dt);
if (keys['ArrowUp']) wind.surge = Math.min(1, wind.surge + 2.5 * dt);
else wind.surge = Math.max(0, wind.surge - 1.2 * dt);
slider.value = Math.round(wind.target);
const goal = wind.target + wind.surge * 45;
// wind lags toward goal; gusts add a decaying spike
wind.value += (goal - wind.value) * Math.min(1, dt * 1.4);
wind.gust *= Math.exp(-dt * 1.6);
const actual = wind.value + wind.gust + Math.sin(now_s() * 0.9) * wind.value * 0.06 + Math.sin(now_s() * 2.3) * wind.value * 0.04;
wind.actual = actual;
windRead.innerHTML = Math.max(0, Math.round(actual)) + '<small> km/h</small>';
beaufortEl.textContent = beaufortName(actual);
}
function now_s() { return performance.now() / 1000; }
// wind force scale: km/h -> px/s^2-ish pressure on canopy
function windForce(v) { return v * v * 0.0016; }
function stepPerson(dt) {
// The person is a controller: lean into the wind to keep the moment at
// the feet near zero. Max lean ~ 38 degrees. Grip force limited.
const v = Math.max(0, wind.actual);
const f = windForce(v);
const c = canopy;
// canopy drag acts at canopy center, roughly at hand + up
const hand = handPos();
const cx = c ? c.pts.reduce((s,p)=>s+p.x,0)/N : hand.x;
const cy = c ? c.pts.reduce((s,p)=>s+p.y,0)/N : hand.y - 60;
const lever = Math.max(0.2, (hand.y - cy)); // vertical lever of drag
const moment = f * lever * 0.05; // torque about feet from wind
// desired lean: lean downwind (right) to counter? Actually you lean INTO
// the wind (left) and pull the umbrella toward you. Model: torso leans
// left (negative) proportional to load, feet brace apart.
const targetLean = -Math.min(0.62, moment * 0.9) - (c && c.inverted ? 0.15 : 0);
person.lean += (targetLean - person.lean) * Math.min(1, dt * 4);
// feet: shift slightly, always on ground
// grip: required grip force grows with drag and inversion
const gripNeed = f * 0.9 + (c && c.inverted ? 26 : 0) + Math.abs(person.lean) * 10;
const gripMax = 55 + v * 0.25; // adrenaline: you grip harder in stronger wind
person.gripSlip = gripNeed / gripMax;
stGrip.textContent = person.gripSlip > 0.92 ? "SLIPPING" : person.gripSlip > 0.6 ? "STRAINING" : "FIRM";
stGrip.style.color = person.gripSlip > 0.92 ? "#f85149" : person.gripSlip > 0.6 ? "#d29922" : "#c9d1d9";
stLean.textContent = Math.round(-person.lean * 180 / Math.PI) + "°";
if (person.gripSlip > 1.15 && v > 30) loseUmbrella();
}
function loseUmbrella() {
// umbrella becomes a free rigid-ish body: shaft + canopy blob
const hand = handPos();
person.umbrella = {
x: hand.x, y: hand.y - 70,
vx: 40 + wind.actual * 6 + Math.random() * 60,
vy: -30 - Math.random() * 40,
rot: -0.4 + Math.random() * 0.3,
vr: (Math.random() - 0.3) * 2,
canopy: canopy,
t: 0
};
canopy = null;
person.lost = true;
lostEl.style.display = 'flex';
stCanopy.textContent = "GONE";
stCanopy.style.color = "#f85149";
}
function stepFledUmbrella(dt) {
const u = person.umbrella;
u.t += dt;
const f = windForce(Math.max(0, wind.actual));
u.vx += (f * 3 - u.vx * 0.4) * dt * 2;
u.vy += (260 - u.vy * 0.3) * dt; // falls, drag-limited
u.vr += (wind.actual * 0.004 - u.vr * 0.5) * dt * 3;
u.x += u.vx * dt; u.y += u.vy * dt; u.rot += u.vr * dt;
if (u.y > groundY - 20) { u.vy *= -0.2; u.vx *= 0.9; u.vr *= 0.8; u.y = groundY - 20; }
if (u.x > W + 200) { /* drifted off */ }
}
function stepCanopy(dt) {
const c = canopy;
const hand = handPos();
const v = Math.max(0, wind.actual);
const f = windForce(v);
// shaft tilt: wind pushes canopy, person pulls shaft; shaft tilts downwind
const shaftTarget = Math.min(0.55, f * 0.012) * (c.inverted ? 1.4 : 1);
c.angle += (shaftTarget - c.angle) * Math.min(1, dt * 5);
// anchor point 0 at hand, offset along shaft
const top = {
x: hand.x + Math.sin(c.angle) * 8,
y: hand.y - Math.cos(c.angle) * 8
};
c.shaftTop.x = top.x; c.shaftTop.y = top.y;
const pts = c.pts;
// angular spring targets: dome curvature, flips sign on inversion
const sgn = c.inverted ? -1 : 1;
// inversion dynamics: torque of wind about the shaft axis
// center of pressure vs shaft top
let cx = 0, cy = 0;
for (const p of pts) { cx += p.x; cy += p.y; }
cx /= N; cy /= N;
const torque = (cx - top.x) * f * 0.05 + (c.inverted ? 0 : 0);
// when loaded hard and canopy is on the "wrong" side, charge inversion
const load = f * 0.05;
if (!c.inverted && load > 2.2 && (cx - top.x) > 4) {
c.invertCharge += dt;
} else if (c.inverted) {
c.invertCharge = 0;
} else {
c.invertCharge *= Math.exp(-dt * 2);
}
// hysteresis: invert past threshold, stay inverted (real umbrellas don't flip back easily)
if (!c.inverted && c.invertCharge > 0.55 && v > 45) {
c.inverted = true;
c.invertT = 0;
stCanopy.textContent = "INVERTED";
stCanopy.style.color = "#d29922";
}
if (c.inverted) c.invertT += dt;
// integrate chain
const damp = 0.86;
for (let i = 0; i < N; i++) {
const p = pts[i];
if (i === 0) { p.x = top.x; p.y = top.y; p.vx = 0; p.vy = 0; continue; }
const q = pts[i - 1];
// spring to neighbor
let fx = (q.x - p.x), fy = (q.y - p.y);
const d = Math.hypot(fx, fy) || 0.001;
const stretch = (d - SEG) / d;
fx *= stretch * 900; fy *= stretch * 900;
// bending spring: hold rest angle between segments
if (i >= 2) {
const r = pts[i - 2];
const a1 = Math.atan2(p.y - q.y, p.x - q.x);
const a2 = Math.atan2(q.y - r.y, q.x - r.x);
let da = a1 - a2;
while (da > Math.PI) da -= 2 * Math.PI;
while (da < -Math.PI) da += 2 * Math.PI;
// rest curvature: dome — target da depends on position
const t = i / (N - 1);
const restDa = sgn * (-0.16 + 0.05 * Math.sin(t * Math.PI)); // dome bends one way
const bend = (da - restDa) * 260;
fx += Math.cos(a1 + Math.PI/2) * bend * 0.0;
// apply as torque on the pair: push p perpendicular to segment
const perp = { x: -Math.sin(a1), y: Math.cos(a1) };
p.vx += perp.x * bend * dt * 0.02;
p.vy += perp.y * bend * dt * 0.02;
q.vx -= perp.x * bend * dt * 0.02;
q.vy -= perp.y * bend * dt * 0.02;
}
// wind force: normal to segment, stronger on windward side
const nx = -(q.y - p.y) / d, ny = (q.x - p.x) / d;
let wf = f * 26 * (0.4 + 0.6 * Math.max(0, nx)); // push right-ish
if (c.inverted) wf *= 1.5; // inverted canopy catches wind catastrophically
// also global drag
fx += wf * nx * 0.0 + wf * 0.35;
fy += wf * Math.abs(ny) * 0.15;
// gravity
fy += 900;
p.vx = (p.vx + fx * dt) * damp;
p.vy = (p.vy + fy * dt) * damp;
p.x += p.vx * dt; p.y += p.vy * dt;
}
// re-anchor
pts[0].x = top.x; pts[0].y = top.y;
// keep tips from crossing the shaft (canopy can't pass through)
for (let i = 1; i < N; i++) {
const p = pts[i];
const rel = p.x - top.x;
// allow both sides (dome spans both), but limit extreme stretch
}
c.drag = f;
}
function stepRain(dt) {
const v = Math.max(0, wind.actual);
const windVX = v * 14; // px/s horizontal
for (const r of rain) {
r.x += windVX * r.sp * dt;
r.y += (620 + 160 * r.sp) * dt;
if (r.y > H + 30) { r.y = -30; r.x = Math.random() * (W + 400) - 200; }
if (r.x > W + 220) r.x = -220;
if (r.x < -260 && windVX < 0) r.x = W + 200;
}
}
// ---------- Drawing ----------
function draw() {
// sky gradient
const g = ctx.createLinearGradient(0, 0, 0, H);
const windy = Math.min(1, Math.max(0, wind.actual) / 90);
g.addColorStop(0, lerpColor('#0d1117', '#1c2a3a', windy));
g.addColorStop(1, lerpColor('#0d1117', '#232d3a', windy * 0.6));
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// wind streaks
ctx.strokeStyle = `rgba(140,180,220,${0.05 + windy * 0.16})`;
ctx.lineWidth = 1.5;
const streakN = 40;
for (let i = 0; i < streakN; i++) {
const sy = (i * 97.3) % H;
const sp = 60 + ((i * 53) % 200) + wind.actual * 6;
const len = 30 + (i % 5) * 25;
const x0 = ((i * 211 + now_s() * sp) % (W + 400)) - 200;
ctx.beginPath();
ctx.moveTo(x0, sy);
ctx.lineTo(x0 + len, sy + len * 0.12);
ctx.stroke();
}
// ground
ctx.fillStyle = '#161b22';
ctx.fillRect(0, groundY, W, H - groundY);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(W, groundY); ctx.stroke();
// ground streaks (rain splash lines + wind)
ctx.strokeStyle = 'rgba(120,150,180,0.25)';
for (let i = 0; i < 30; i++) {
const gx = (i * 173) % W;
const len = 20 + (i % 4) * 18;
ctx.beginPath();
ctx.moveTo(gx, groundY + 4 + (i % 7));
ctx.lineTo(gx + len + wind.actual * 0.8, groundY + 4 + (i % 7) + 2);
ctx.stroke();
}
// rain (behind person)
ctx.strokeStyle = 'rgba(160,190,220,0.35)';
ctx.lineWidth = 1.2;
const windVX = Math.max(0, wind.actual) * 14;
for (const r of rain) {
const slant = windVX * 0.35 * r.sp;
ctx.beginPath();
ctx.moveTo(r.x, r.y);
ctx.lineTo(r.x - slant, r.y + r.len);
ctx.stroke();
}
if (!person.lost) drawPerson();
if (canopy) drawUmbrella(canopy, false);
if (person.umbrella) drawUmbrella(person.umbrella.canopy, true, person.umbrella);
// rain in front (light pass)
ctx.strokeStyle = 'rgba(170,200,230,0.18)';
for (const r of rain) {
if (r.sp < 0.7) continue;
const slant = windVX * 0.35 * r.sp;
ctx.beginPath();
ctx.moveTo(r.x + 7, r.y + 3);
ctx.lineTo(r.x + 7 - slant, r.y + 3 + r.len);
ctx.stroke();
}
}
function drawPerson() {
const hip = hipPos(), sh = shoulderPos(), hand = handPos();
const fl = footL(), fr = footR();
const L = person.lean;
const strain = Math.min(1, person.gripSlip);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// back leg (braced, back)
ctx.strokeStyle = '#2b3a4a';
ctx.lineWidth = person.h * 0.075;
ctx.beginPath();
ctx.moveTo(hip.x, hip.y);
ctx.lineTo(fl.x + Math.sin(L)*person.h*0.02, fl.y - 2);
ctx.stroke();
// front leg (planted, slightly bent feel)
ctx.strokeStyle = '#33465a';
ctx.beginPath();
ctx.moveTo(hip.x, hip.y);
ctx.lineTo(fr.x, fr.y - 2);
ctx.stroke();
// feet
ctx.strokeStyle = '#1a222c';
ctx.lineWidth = person.h * 0.045;
ctx.beginPath();
ctx.moveTo(fl.x - 14, fl.y); ctx.lineTo(fl.x + 12, fl.y);
ctx.moveTo(fr.x - 10, fr.y); ctx.lineTo(fr.x + 16, fr.y);
ctx.stroke();
// torso
ctx.strokeStyle = strain > 0.85 ? '#4a3b45' : '#3b4a5c';
ctx.lineWidth = person.h * 0.11;
ctx.beginPath();
ctx.moveTo(hip.x, hip.y);
ctx.lineTo(sh.x, sh.y);
ctx.stroke();
// head
const headR = person.h * 0.085;
const headC = {
x: sh.x - Math.cos(L) * headR * 1.1,
y: sh.y - Math.cos(L) * headR * 1.5 - Math.sin(L) * headR * 0.4
};
ctx.fillStyle = '#c9a184';
ctx.beginPath(); ctx.arc(headC.x, headC.y, headR, 0, Math.PI * 2); ctx.fill();
// hair, blown
ctx.fillStyle = '#2a211b';
ctx.beginPath();
ctx.arc(headC.x - 2, headC.y - headR * 0.35, headR * 0.85, Math.PI * 0.9, Math.PI * 1.9);
ctx.fill();
// windblown hair streak
ctx.strokeStyle = '#2a211b';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(headC.x + headR * 0.4, headC.y - headR * 0.5);
ctx.lineTo(headC.x + headR * 0.4 + 26 + wind.actual * 0.5, headC.y - headR * 0.3);
ctx.stroke();
// arms: two arms reaching to the hand point
ctx.strokeStyle = '#33465a';
ctx.lineWidth = person.h * 0.05;
// far arm (darker)
ctx.strokeStyle = '#2b3a4a';
ctx.beginPath();
ctx.moveTo(sh.x + 4, sh.y + 4);
ctx.lineTo(hand.x + 8, hand.y + 6);
ctx.stroke();
// near arm
ctx.strokeStyle = '#3d5166';
ctx.beginPath();
ctx.moveTo(sh.x - 2, sh.y);
ctx.lineTo(hand.x, hand.y);
ctx.stroke();
// hands gripping
ctx.fillStyle = '#c9a184';
ctx.beginPath(); ctx.arc(hand.x, hand.y, person.h * 0.035, 0, Math.PI*2); ctx.fill();
// scarf / coat tail blowing
ctx.fillStyle = '#b54a3f';
ctx.beginPath();
ctx.moveTo(sh.x + 6, sh.y + 10);
const blow = 30 + wind.actual * 1.6;
ctx.lineTo(sh.x + 6 + blow, sh.y + 16 + blow * 0.25);
ctx.lineTo(sh.x + 4 + blow * 0.8, sh.y + 26 + blow * 0.15);
ctx.lineTo(sh.x + 2, sh.y + 22);
ctx.closePath();
ctx.fill();
}
function drawUmbrella(c, fled, body) {
const pts = c.pts;
let ox = 0, oy = 0, rot = 0;
if (fled && body) {
ox = body.x; oy = body.y; rot = body.rot;
}
ctx.save();
if (fled && body) {
ctx.translate(ox, oy);
ctx.rotate(rot);
}
// canopy: filled shape through the chain, closed along the chord
const grad = ctx.createLinearGradient(0, -140, 0, 20);
grad.addColorStop(0, c.inverted ? '#7a4a8f' : '#c94f3d');
grad.addColorStop(1, c.inverted ? '#4a2a5f' : '#8f2f22');
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < N; i++) {
// smooth curve
const p = pts[i], q = pts[i-1];
const mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2;
ctx.quadraticCurveTo(q.x, q.y, mx, my);
}
ctx.lineTo(pts[N-1].x, pts[N-1].y);
// close back along the underside (slightly inside)
for (let i = N - 1; i >= 0; i--) {
const p = pts[i];
const lift = c.inverted ? -10 : 10;
ctx.lineTo(p.x * 0.98 + pts[0].x * 0.02, p.y + lift * (i / (N-1)));
}
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
ctx.lineWidth = 1.5;
ctx.stroke();
// ribs
ctx.strokeStyle = 'rgba(30,20,20,0.55)';
ctx.lineWidth = 1.2;
for (let i = 1; i < N; i += 2) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
ctx.lineTo(pts[i].x, pts[i].y);
ctx.stroke();
}
// shaft
const top = pts[0];
const shaftLen = person.h * 0.62;
const ang = c.angle + (fled ? 0 : 0);
const bx = top.x - Math.sin(ang) * shaftLen;
const by = top.y + Math.cos(ang) * shaftLen;
ctx.strokeStyle = '#1c1c22';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(top.x, top.y);
ctx.lineTo(bx, by);
ctx.stroke();
// handle hook
ctx.beginPath();
ctx.arc(bx + Math.sin(ang) * 6, by - Math.cos(ang) * 2, 7, 0, Math.PI * 1.4);
ctx.stroke();
// tip
ctx.fillStyle = '#1c1c22';
ctx.beginPath();
ctx.arc(top.x + Math.sin(ang) * 3, top.y - Math.cos(ang) * 3, 3, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function lerpColor(a, b, t) {
const pa = parseInt(a.slice(1), 16), pb = parseInt(b.slice(1), 16);
const r = Math.round(((pa>>16)&255) + (((pb>>16)&255) - ((pa>>16)&255)) * t);
const g = Math.round(((pa>>8)&255) + (((pb>>8)&255) - ((pa>>8)&255)) * t);
const bl = Math.round((pa&255) + ((pb&255) - (pa&255)) * t);
return `rgb(${r},${g},${bl})`;
}
function reset() {
wind.target = 0; wind.value = 0; wind.gust = 0; wind.surge = 0;
slider.value = 0;
seedRain();
makePerson();
canopy = makeCanopy({ x: handPos().x, y: handPos().y - person.h * 0.62 });
person.lost = false;
person.umbrella = null;
lostEl.style.display = 'none';
stCanopy.textContent = "INTACT";
stCanopy.style.color = "#c9d1d9";
stGrip.textContent = "FIRM";
stGrip.style.color = "#c9d1d9";
}
document.getElementById('again').addEventListener('click', reset);
seedRain();
makePerson();
canopy = makeCanopy({ x: handPos().x, y: handPos().y - person.h * 0.62 });
requestAnimationFrame(t => { lastT = t; requestAnimationFrame(frame); });
})();
</script>
</body>
</html>
Where it looks right: The rain slanting harder as you push the slider, the person's lean growing and the scarf streaming out, the canopy visibly loading up and the shaft tilting downwind before the whole thing inverts in one committed flip — inversion is a one-way state, like a real umbrella, and once it's inverted the wind catches the concave side and it really does get worse. Past the grip limit the umbrella detaches and tumbles off downwind while the person stays planted. The Beaufort readout and km/h counter track a wind that lags your slider input and has gust noise on top, so it never feels like a number you set — it feels like weather you're holding a line against.
Where it looks wrong: The canopy is a single chain of points, so it's really a two-dimensional cross-section of an umbrella — there's no rib splay, no scalloped edge between ribs, and the fabric has no billowing between attachment points; it reads as a flexible blade more than a dome of fabric. The inversion trigger is a torque-about-the-shaft threshold with a timer, not an actual buckling instability, so the flip is a state change that animates rather than a snap you feel coming — there's no visible "frame loading up" moment where the ribs visibly strain before it goes. The person is a stick figure with a coat; the compensation is a single lean angle driven by a PD-ish controller, so there's no weight shift, no white-knuckle arm tremor, no moment where they let go with one hand to re-grip. The fled umbrella keeps its canopy shape rigidly attached instead of tumbling and folding. And the rain doesn't interact with the canopy at all — no runoff streaming off the edge, no droplets exploding off the fabric in a gust, which is one of the most visible things a real umbrella does in weather.