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 not cloth simulated per-thread but a one-dimensional arc of eleven points strung across seven ribs, all generated each frame from three state variables: shaft angle θ, a lateral flex/skew, and a curvature whose sign is the dome — positive bulging upwind-normal, negative when flipped. Inversion is a bistable snap-through, not a gradual bend: curvature is driven by a stiff, under-damped spring toward one of two stable targets (+1 normal, −1 inverted), and the target flips when the aerodynamic load — roughly wind speed squared, amplified when the tilted canopy catches wind underneath and accumulated over a fraction of a second so a single gust frame can't do it — exceeds the frame's threshold around 60 km/h; once inverted the drag and grip torque jump, strain accumulates faster, and past a second, higher load threshold the grip torque saturates and the umbrella detaches into a free tumbling body, while dropping the wind below ~40 km/h lets you pop the canopy back to its normal well.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Umbrella in a Gale</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Georgia, 'Times New Roman', serif;
background: #1a2028; color: #e8e4da;
min-height: 100vh; display: flex; flex-direction: column; align-items: center;
padding: 18px 12px 28px;
}
h1 { font-size: 26px; font-weight: normal; letter-spacing: .04em; margin-bottom: 2px; }
.sub { font-style: italic; color: #9fb0bd; font-size: 14px; margin-bottom: 14px; }
#wrap { width: min(960px, 96vw); }
canvas {
width: 100%; display: block; border-radius: 10px;
background: #6f8798; box-shadow: 0 12px 40px rgba(0,0,0,.55);
}
#hud {
display: flex; gap: 18px; align-items: stretch; flex-wrap: wrap;
margin-top: 12px;
}
#windBox {
background: #232d38; border: 1px solid #3a4756; border-radius: 8px;
padding: 8px 18px 10px; min-width: 190px; text-align: center;
}
#windVal { font-size: 46px; line-height: 1; color: #ffd97a; font-variant-numeric: tabular-nums; }
#windVal small { font-size: 16px; color: #b9c4cf; }
#beaufort { font-style: italic; color: #9fb0bd; font-size: 13px; margin-top: 3px; }
#statusBox {
flex: 1; min-width: 220px; background: #232d38; border: 1px solid #3a4756;
border-radius: 8px; padding: 10px 16px;
}
#status { font-size: 19px; color: #f2ede1; }
.barLabel { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: #8fa1b1; margin-top: 8px; }
.bar { height: 8px; background: #141a21; border-radius: 4px; overflow: hidden; margin-top: 3px; }
.bar > div { height: 100%; width: 0%; border-radius: 4px; transition: width .08s linear; }
#loadFill { background: linear-gradient(90deg,#7fb069,#ffd97a,#e4572e); }
#strainFill { background: linear-gradient(90deg,#5aa9e6,#e4572e,#c1121f); }
#controls {
margin-top: 14px; background: #232d38; border: 1px solid #3a4756; border-radius: 8px;
padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 14px 22px; align-items: center;
}
#controls label { font-size: 13px; color: #b9c4cf; display: flex; align-items: center; gap: 7px; }
input[type=range] { width: min(320px, 60vw); accent-color: #e4572e; cursor: pointer; }
button {
font-family: inherit; font-size: 14px; background: #33465c; color: #f2ede1;
border: 1px solid #54708c; border-radius: 6px; padding: 6px 14px; cursor: pointer;
}
button:hover { background: #41597a; }
button:active { transform: translateY(1px); }
.keys { width: 100%; font-size: 12.5px; color: #8fa1b1; font-style: italic; }
kbd {
font-family: inherit; background: #141a21; border: 1px solid #3a4756;
border-bottom-width: 2px; border-radius: 4px; padding: 0 5px; font-style: normal; color: #e8e4da;
}
</style>
</head>
<body>
<h1>The Umbrella Problem</h1>
<div class="sub">wind rises. hold on. it will not be enough.</div>
<div id="wrap">
<canvas id="cv" width="960" height="540"></canvas>
<div id="hud">
<div id="windBox">
<div id="windVal">0 <small>km/h</small></div>
<div id="beaufort">calm</div>
</div>
<div id="statusBox">
<div id="status">Holding comfortably.</div>
<div class="barLabel">Canopy load (inversion near full)</div>
<div class="bar"><div id="loadFill"></div></div>
<div class="barLabel">Grip strain (umbrella lost at full)</div>
<div class="bar"><div id="strainFill"></div></div>
</div>
</div>
<div id="controls">
<label>Wind
<input type="range" id="windSlider" min="0" max="120" step="0.5" value="8">
<span id="sliderVal">8 km/h</span>
</label>
<label><input type="checkbox" id="autoChk" checked> Let it keep rising</label>
<button id="fixBtn">Pop canopy back (F)</button>
<button id="resetBtn">New umbrella (R)</button>
<div class="keys">
<kbd>←</kbd>/<kbd>→</kbd> or <kbd>A</kbd>/<kbd>D</kbd> drive the wind down/up ·
<kbd>F</kbd> fight the canopy back (only works in weak wind) · <kbd>R</kbd> replace a lost umbrella ·
dragging the slider hands the wind to you and stops the automatic rise.
</div>
</div>
</div>
<script>
(function(){
"use strict";
var cv = document.getElementById('cv'), ctx = cv.getContext('2d');
var W = 960, H = 540, GROUND = 472;
var DPR = Math.min(window.devicePixelRatio || 1, 2);
cv.width = W * DPR; cv.height = H * DPR; ctx.scale(DPR, DPR);
var slider = document.getElementById('windSlider'),
sliderVal = document.getElementById('sliderVal'),
autoChk = document.getElementById('autoChk'),
windVal = document.getElementById('windVal'),
beaufortEl = document.getElementById('beaufort'),
statusEl = document.getElementById('status'),
loadFill = document.getElementById('loadFill'),
strainFill = document.getElementById('strainFill');
/* ---------- state ---------- */
var baseWind = 8; // km/h, the slider / auto-rise value
var effWind = 8; // km/h, with gusts
var time = 0;
var theta = -0.12, omega = 0; // shaft angle from vertical (+ = blown downwind/right)
var flex = 0, flexV = 0; // canopy skew
var invTarget = 0, invProg = 0, invVel = 0; // 0 normal, 1 inverted (spring-driven)
var inverted = false;
var invTimer = 0;
var strain = 0;
var detached = false;
var free = { x:0, y:0, vx:0, vy:0, a:0, va:0 };
var leanSm = 0, crouchSm = 0, lastHand = {x:430,y:300}, lastTop = {x:430,y:160};
var shake = 0;
/* ---------- particles ---------- */
var rain = [], leaves = [], clouds = [], i;
for (i=0;i<280;i++) rain.push({x:Math.random()*W, y:Math.random()*H, o:Math.random()*10});
for (i=0;i<26;i++) leaves.push({x:Math.random()*W, y:GROUND-Math.random()*260, s:Math.random()*6.28, v:0.5+Math.random()});
for (i=0;i<7;i++) clouds.push({x:Math.random()*W, y:20+Math.random()*110, s:0.6+Math.random()*0.9});
function clamp(v,a,b){ return v<a?a:(v>b?b:v); }
function lerp(a,b,t){ return a+(b-a)*t; }
function gustAt(t){
return Math.sin(t*0.9)*0.5 + Math.sin(t*2.37+1.7)*0.3 + Math.sin(t*5.1+0.4)*0.2;
}
function beaufort(kmh){
if (kmh<2) return "calm";
if (kmh<12) return "light breeze";
if (kmh<29) return "fresh breeze — manageable";
if (kmh<49) return "strong wind — two hands";
if (kmh<61) return "near gale — canopy thinking about it";
if (kmh<74) return "gale — canopy has opinions";
if (kmh<88) return "strong gale — grip failing";
if (kmh<102) return "storm — let go or lose it";
return "violent storm — it's gone, accept it";
}
/* ---------- actions ---------- */
function resetUmbrella(){
detached = false; inverted = false; invTarget = 0; invProg = 0; invVel = 0;
strain = 0; invTimer = 0; theta = -0.15; omega = 0; flex = 0; flexV = 0;
}
function attemptFix(){
if (detached || !inverted) return;
if (effWind < 42){
inverted = false; invTarget = 0; invVel = -6; shake = 4;
} else {
shake = 7; // it just slaps back
invVel += 3;
}
}
document.getElementById('resetBtn').addEventListener('click', resetUmbrella);
document.getElementById('fixBtn').addEventListener('click', attemptFix);
slider.addEventListener('input', function(){
baseWind = parseFloat(slider.value);
autoChk.checked = false;
});
window.addEventListener('keydown', function(e){
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D'){ baseWind = clamp(baseWind+2,0,120); autoChk.checked=false; e.preventDefault(); }
else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A'){ baseWind = clamp(baseWind-2,0,120); autoChk.checked=false; e.preventDefault(); }
else if (e.key === 'f' || e.key === 'F'){ attemptFix(); }
else if (e.key === 'r' || e.key === 'R'){ resetUmbrella(); }
});
/* ---------- person pose ---------- */
function personPose(wf){
var baseX = 432;
var lean = -0.04 - wf*0.30; // lean into the wind (left)
var crouch = wf*wf*20;
var stance = 20 + wf*38;
var footL = {x: baseX - stance*0.72, y: GROUND};
var footR = {x: baseX + stance, y: GROUND}; // braced downwind foot
var hip = { x: baseX + Math.sin(lean)*30 - wf*10, y: GROUND - 96 + crouch };
var shoulder = { x: hip.x + Math.sin(lean)*62, y: hip.y - 58 + crouch*0.3 };
var head = { x: shoulder.x + Math.sin(lean)*16 - 2, y: shoulder.y - 26 };
var hand = { x: shoulder.x - 30 - wf*8, y: shoulder.y + 30 - wf*10 };
return {baseX:baseX, lean:lean, crouch:crouch, footL:footL, footR:footR, hip:hip, shoulder:shoulder, head:head, hand:hand};
}
/* ---------- umbrella geometry ---------- */
function canopyPoints(Hx, Hy, th, curv, flx, wf, tNow){
var ux = Math.sin(th), uy = -Math.cos(th); // shaft up
var vx = Math.cos(th), vy = Math.sin(th); // across canopy
var Tx = Hx + ux*148, Ty = Hy + uy*148; // ferrule / top
var R = 98 * (1 - 0.14*invProg);
var dome = 40 * curv;
var pts = [], k, tt, px, py, flut;
for (k=0;k<=10;k++){
tt = -1 + k*0.2;
flut = Math.sin(tNow*26 + tt*4.2) * wf * 5 * Math.abs(tt);
px = Tx + vx*(tt*R) + ux*(dome*(1-tt*tt)) + flx*34*(1-tt*tt) + flut*vx;
py = Ty + vy*(tt*R) + uy*(dome*(1-tt*tt)) + flut*vy + Math.abs(tt)*6;
pts.push({x:px,y:py});
}
return {pts:pts, top:{x:Tx,y:Ty}, ux:ux, uy:uy, vx:vx, vy:vy};
}
function drawUmbrella(Hx, Hy, th, curv, flx, wf, tNow){
var g = canopyPoints(Hx, Hy, th, curv, flx, wf, tNow);
var ux=g.ux, uy=g.uy;
// shaft
ctx.strokeStyle = '#2b2b30'; ctx.lineWidth = 4; ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(Hx - ux*34, Hy - uy*34);
ctx.lineTo(g.top.x + ux*10, g.top.y + uy*10);
ctx.stroke();
// handle hook
ctx.lineWidth = 3.5; ctx.strokeStyle = '#5a3a22';
ctx.beginPath();
ctx.moveTo(Hx - ux*34, Hy - uy*34);
ctx.quadraticCurveTo(Hx - ux*46 - g.vx*4, Hy - uy*46 - g.vy*4, Hx - ux*40 - g.vx*13, Hy - uy*40 - g.vy*13);
ctx.stroke();
// canopy fabric
ctx.beginPath();
ctx.moveTo(g.pts[0].x, g.pts[0].y);
for (var k=1;k<g.pts.length;k++) ctx.lineTo(g.pts[k].x, g.pts[k].y);
// underside return edge
for (var k2=g.pts.length-1;k2>=0;k2--){
ctx.lineTo(g.pts[k2].x - ux*7, g.pts[k2].y - uy*7);
}
ctx.closePath();
ctx.fillStyle = inverted || invProg>0.5 ? '#8e2f38' : '#c0392b';
ctx.fill();
ctx.strokeStyle = '#7c241c'; ctx.lineWidth = 1.5; ctx.stroke();
// ribs
ctx.strokeStyle = 'rgba(40,10,10,.55)'; ctx.lineWidth = 1.4;
for (var r=0;r<g.pts.length;r+= (r===0||r===g.pts.length-1)?1:2){
ctx.beginPath();
ctx.moveTo(g.top.x, g.top.y);
ctx.lineTo(g.pts[r].x, g.pts[r].y);
ctx.stroke();
}
// ferrule tip
ctx.fillStyle = '#ddd6c4';
ctx.beginPath(); ctx.arc(g.top.x + ux*12, g.top.y + uy*12, 3, 0, 6.29); ctx.fill();
lastTop = g.top;
return g;
}
/* ---------- drawing the person ---------- */
function drawPerson(p, wf, tNow){
var s = p.shoulder, h = p.hip;
ctx.lineCap = 'round';
// legs
ctx.strokeStyle = '#26303c'; ctx.lineWidth = 9;
var kneeL = {x:(p.footL.x+h.x)/2 - 6, y:(p.footL.y+h.y)/2};
var kneeR = {x:(p.footR.x+h.x)/2 + 8 + wf*4, y:(p.footR.y+h.y)/2 + 2};
ctx.beginPath(); ctx.moveTo(p.footL.x,p.footL.y-4); ctx.lineTo(kneeL.x,kneeL.y); ctx.lineTo(h.x,h.y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(p.footR.x,p.footR.y-4); ctx.lineTo(kneeR.x,kneeR.y); ctx.lineTo(h.x,h.y); ctx.stroke();
// boots
ctx.strokeStyle = '#17130f'; ctx.lineWidth = 6;
ctx.beginPath(); ctx.moveTo(p.footL.x-9,GROUND-2); ctx.lineTo(p.footL.x+5,GROUND-2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(p.footR.x-3,GROUND-2); ctx.lineTo(p.footR.x+12,GROUND-2); ctx.stroke();
// coat (flutters downwind)
var flap = Math.sin(tNow*11)*wf*10 + wf*22;
ctx.fillStyle = '#31556e';
ctx.beginPath();
ctx.moveTo(s.x-11, s.y);
ctx.lineTo(s.x+12, s.y+4);
ctx.lineTo(h.x+13+flap*0.4, h.y+16+Math.sin(tNow*13)*wf*4);
ctx.lineTo(h.x+2+flap, h.y+26);
ctx.lineTo(h.x-13, h.y+14);
ctx.closePath(); ctx.fill();
// torso
ctx.strokeStyle = '#31556e'; ctx.lineWidth = 15;
ctx.beginPath(); ctx.moveTo(h.x,h.y); ctx.lineTo(s.x,s.y); ctx.stroke();
// scarf streaming downwind
ctx.strokeStyle = '#d9a441'; ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(s.x, s.y-6);
ctx.quadraticCurveTo(s.x+22+wf*30, s.y-10+Math.sin(tNow*9)*6*wf,
s.x+40+wf*55, s.y-4+Math.sin(tNow*12+1)*9*(0.3+wf));
ctx.stroke();
// head + hat
ctx.fillStyle = '#d9a37e';
ctx.beginPath(); ctx.arc(p.head.x, p.head.y, 12, 0, 6.29); ctx.fill();
ctx.strokeStyle = '#24303c'; ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(p.head.x-13, p.head.y-7); ctx.lineTo(p.head.x+11, p.head.y-9); ctx.stroke();
if (!detached){
// main arm: shoulder -> elbow -> hand on shaft
var elbow = {x:(s.x+lastHand.x)/2 + 6, y:(s.y+lastHand.y)/2 + 16};
ctx.strokeStyle = '#2a4a63'; ctx.lineWidth = 8;
ctx.beginPath(); ctx.moveTo(s.x,s.y+4); ctx.lineTo(elbow.x,elbow.y); ctx.lineTo(lastHand.x,lastHand.y); ctx.stroke();
ctx.fillStyle = '#d9a37e';
ctx.beginPath(); ctx.arc(lastHand.x,lastHand.y,5,0,6.29); ctx.fill();
// second hand joins the shaft when it's serious
if (effWind > 38){
var ux = Math.sin(theta), uy = -Math.cos(theta);
var h2 = {x: lastHand.x - ux*36, y: lastHand.y - uy*36};
var el2 = {x:(s.x+h2.x)/2 - 4, y:(s.y+h2.y)/2 + 22};
ctx.strokeStyle = '#2a4a63';
ctx.beginPath(); ctx.moveTo(s.x,s.y+6); ctx.lineTo(el2.x,el2.y); ctx.lineTo(h2.x,h2.y); ctx.stroke();
ctx.fillStyle = '#d9a37e';
ctx.beginPath(); ctx.arc(h2.x,h2.y,5,0,6.29); ctx.fill();
}
} else {
// empty arms, reaching after it for a moment, then just watching
ctx.strokeStyle = '#2a4a63'; ctx.lineWidth = 8;
var reach = clamp(1.4 - (time - (drawPerson.tGone||time)), 0, 1);
ctx.beginPath(); ctx.moveTo(s.x,s.y+4);
ctx.lineTo(s.x+26*reach+6, s.y+16-10*reach); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s.x,s.y+6);
ctx.lineTo(s.x+10, s.y+34); ctx.stroke();
}
}
/* ---------- main loop ---------- */
var lastTs = 0;
function frame(ts){
var dt = Math.min((ts-lastTs)/1000 || 0.016, 0.033);
lastTs = ts; time += dt;
/* wind */
if (autoChk.checked){
baseWind = clamp(baseWind + dt*1.5, 0, 118);
slider.value = baseWind;
}
var gustAmp = 3 + baseWind*0.16;
effWind = clamp(baseWind + gustAt(time)*gustAmp, 0, 135);
var wf = clamp(effWind/100, 0, 1.35);
sliderVal.textContent = Math.round(baseWind) + ' km/h';
/* person pose (smoothed) */
leanSm += (wf - leanSm) * Math.min(1, dt*3);
var pose = personPose(leanSm);
lastHand = pose.hand;
if (shake > 0) { lastHand = {x:pose.hand.x + (Math.random()-0.5)*shake, y:pose.hand.y + (Math.random()-0.5)*shake}; shake *= Math.pow(0.02, dt); if (shake<0.2) shake=0; }
/* umbrella held physics */
var load = 0, catchF = 1;
if (!detached){
var thetaDes = -0.14 - wf*0.5; // person tilts it into the wind
catchF = clamp(1.05 - theta*0.35, 0.55, 1.5); // tilted-back canopy catches more
load = Math.pow(effWind/64, 2) * catchF * (inverted ? 1.35 : 1);
var windTorque = Math.pow(effWind,2) * 0.00042 * (inverted ? 1.5 : 1);
var gripTorque = clamp(-(theta-thetaDes)*3.2, -1.15, 1.15); // grip saturates
omega += (gripTorque + windTorque - omega*1.6) * dt * 3.2;
theta += omega * dt * 3.2;
theta = clamp(theta, -1.15, 1.45);
// flex spring
var flexTarget = clamp(load*0.4, 0, 1.1) * (inverted?-0.5:1) + Math.sin(time*23)*wf*0.06;
flexV += ((flexTarget-flex)*90 - flexV*11) * dt;
flex += flexV * dt;
// inversion: load must persist briefly
if (!inverted){
if (load > 1.0) invTimer += dt; else invTimer = Math.max(0, invTimer - dt*2);
if (invTimer > 0.28){ inverted = true; invTarget = 1; invVel = 7; invTimer = 0; shake = 6; }
}
// strain -> losing it
var strainThresh = inverted ? 70 : 88;
if (effWind > strainThresh) strain += dt * (effWind-strainThresh)/34;
else strain = Math.max(0, strain - dt*0.22);
if (strain >= 1){
detached = true; drawPerson.tGone = time;
free.x = lastHand.x; free.y = lastHand.y;
free.vx = 60 + effWind*2.2; free.vy = -70;
free.a = theta; free.va = 3 + Math.random()*3;
}
} else {
/* free umbrella: drag toward wind speed, gravity, tumble, ground bounce */
var windPx = effWind * 3.4;
free.vx += (windPx - free.vx) * dt * 1.4;
free.vy += (520 - Math.abs(Math.sin(free.a))*160) * dt;
free.vy *= (1 - dt*0.25);
free.x += free.vx*dt; free.y += free.vy*dt;
free.a += free.va*dt; free.va *= (1 - dt*0.4);
free.va += Math.sin(time*7)*dt*3;
if (free.y > GROUND-14){
free.y = GROUND-14;
free.vy = -Math.abs(free.vy)*0.35;
free.vx *= 0.82; free.va *= 0.5;
if (Math.abs(free.vy) < 30) free.vy = 0;
}
if (free.x > W+140){ free.x = W+140; free.vx = 0; free.va = 0; }
load = 0;
}
/* inversion snap spring (under-damped -> overshoot) */
invVel += ((invTarget-invProg)*110 - invVel*9) * dt;
invProg += invVel * dt;
var curv = 1 - 2*invProg;
/* ---------- draw ---------- */
var dark = clamp(wf,0,1);
var sky = ctx.createLinearGradient(0,0,0,GROUND);
sky.addColorStop(0, 'rgb(' + Math.round(lerp(96,52,dark)) + ',' + Math.round(lerp(124,66,dark)) + ',' + Math.round(lerp(146,84,dark)) + ')');
sky.addColorStop(1, 'rgb(' + Math.round(lerp(168,104,dark)) + ',' + Math.round(lerp(182,122,dark)) + ',' + Math.round(lerp(188,132,dark)) + ')');
ctx.fillStyle = sky; ctx.fillRect(0,0,W,H);
// clouds
ctx.fillStyle = 'rgba(230,235,238,.5)';
for (i=0;i<clouds.length;i++){
var c = clouds[i];
c.x += (6 + effWind*2.6) * c.s * dt;
if (c.x > W+90) { c.x = -90; c.y = 20+Math.random()*110; }
ctx.beginPath();
ctx.ellipse(c.x, c.y, 52*c.s, 15*c.s, 0, 0, 6.29);
ctx.ellipse(c.x+30*c.s, c.y+4, 36*c.s, 12*c.s, 0, 0, 6.29);
ctx.fill();
}
// distant houses
ctx.fillStyle = 'rgba(40,52,64,.55)';
ctx.fillRect(60, GROUND-64, 74, 64); ctx.fillRect(700, GROUND-52, 90, 52); ctx.fillRect(806, GROUND-40, 60, 40);
ctx.beginPath(); ctx.moveTo(52,GROUND-64); ctx.lineTo(97,GROUND-92); ctx.lineTo(142,GROUND-64); ctx.fill();
// bending tree
var bend = wf*46;
ctx.strokeStyle = '#3a2e22'; ctx.lineWidth = 8; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(872, GROUND);
ctx.quadraticCurveTo(872+bend*0.3, GROUND-60, 872+bend, GROUND-108); ctx.stroke();
ctx.fillStyle = 'rgba(52,84,54,.85)';
ctx.beginPath(); ctx.ellipse(872+bend+14, GROUND-122, 44+Math.sin(time*8)*wf*4, 26, 0, 0, 6.29); ctx.fill();
ctx.beginPath(); ctx.ellipse(872+bend-16, GROUND-104, 32, 20, 0, 0, 6.29); ctx.fill();
// ground + puddles
ctx.fillStyle = '#4c5548'; ctx.fillRect(0,GROUND,W,H-GROUND);
ctx.fillStyle = '#5d6a58'; ctx.fillRect(0,GROUND,W,5);
ctx.fillStyle = 'rgba(150,175,195,.5)';
ctx.beginPath(); ctx.ellipse(240,GROUND+26,64,7,0,0,6.29); ctx.fill();
ctx.beginPath(); ctx.ellipse(620,GROUND+40,84,8,0,0,6.29); ctx.fill();
ctx.beginPath(); ctx.ellipse(480,GROUND+18,36,5,0,0,6.29); ctx.fill();
// leaves / debris
if (effWind > 22){
ctx.fillStyle = '#7a6a35';
for (i=0;i<leaves.length;i++){
var lf = leaves[i];
lf.x += (effWind*3.2*lf.v) * dt; lf.y += Math.sin(time*6+lf.s)*40*dt - wf*14*dt;
if (lf.x > W+10){ lf.x = -10; lf.y = GROUND - Math.random()*240; }
if (lf.y > GROUND-3) lf.y = GROUND-3;
ctx.save(); ctx.translate(lf.x, lf.y); ctx.rotate(lf.s + time*4);
ctx.fillRect(-4,-2,8,4); ctx.restore();
}
}
// person + umbrella
drawPerson(pose, leanSm, time);
if (!detached){
drawUmbrella(lastHand.x, lastHand.y, theta, curv, flex, wf, time);
} else {
drawUmbrella(free.x, free.y, free.a, curv, flex*0.4, wf, time);
}
// rain (direction & speed follow wind)
var rvx = effWind*3.3, rvy = 430 + effWind*1.1;
var rl = 0.028 + wf*0.012;
ctx.strokeStyle = 'rgba(200,220,240,.55)'; ctx.lineWidth = 1.3;
ctx.beginPath();
for (i=0;i<rain.length;i++){
var rp = rain[i];
rp.x += (rvx + rp.o*4) * dt; rp.y += rvy * dt;
if (rp.y > GROUND+4){ rp.y = -12; rp.x = Math.random()*W - rvx*0.3; }
if (rp.x > W+12){ rp.x -= (W+24); rp.y = Math.random()*H*0.5; }
if (rp.x < -12){ rp.x += (W+24); }
ctx.moveTo(rp.x, rp.y);
ctx.lineTo(rp.x - rvx*rl, rp.y - rvy*rl);
}
ctx.stroke();
// splash ticks
ctx.strokeStyle = 'rgba(200,220,240,.35)';
ctx.beginPath();
for (i=0;i<14;i++){
var sx = (i*173 + Math.floor(time*9)*61) % W, sy = GROUND + 8 + (i*37)%44;
ctx.moveTo(sx-3, sy); ctx.lineTo(sx, sy-4); ctx.lineTo(sx+3, sy);
}
ctx.stroke();
/* ---------- HUD ---------- */
windVal.innerHTML = Math.round(effWind) + ' <small>km/h (' + Math.round(effWind*0.621) + ' mph)</small>';
beaufortEl.textContent = beaufort(effWind) + (autoChk.checked ? ' · still rising…' : '');
loadFill.style.width = clamp(load/1.15*100,0,100) + '%';
strainFill.style.width = clamp(strain*100,0,100) + '%';
if (detached) statusEl.textContent = 'Gone. It belongs to the wind now. Press R for another.';
else if (inverted && effWind > 70) statusEl.textContent = 'INSIDE OUT — and the grip is going…';
else if (inverted) statusEl.textContent = 'INSIDE OUT. Drop the wind, then press F to pop it back.';
else if (effWind > 82) statusEl.textContent = 'Fingers are losing this argument…';
else if (effWind > 58) statusEl.textContent = 'Frame is loaded. One good gust flips it.';
else if (effWind > 38) statusEl.textContent = 'Two hands. Lean in. Brace the back foot.';
else if (effWind > 15) statusEl.textContent = 'Leaning in, coping fine.';
else statusEl.textContent = 'Holding comfortably. For now.';
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it looks right: the sequence of failure is the real one — first the lean, widened stance and braced back foot, then the second hand coming up the shaft, then the canopy skewing and fluttering at its tips, then the sudden snap inside-out rather than a slow bend, and only after that the grip going, because an inverted canopy is a sail. Rain slants and lengthens with the wind, the scarf and coat stream downwind, the tree bends, and gusts make the whole thing hunt and shudder instead of sitting at a steady angle. Dropping the wind and pressing F pops the canopy back with a satisfying overshoot wobble, and a lost umbrella tumbles, bounces, and skids to a stop against the right edge rather than vanishing.
Where it looks wrong, before you find it: the canopy is a single arc, so it flexes like a bent ruler — real fabric bellies between ribs, wrinkles, and one panel usually collapses first, and real inversion starts at one rib flipping, not the whole dome snapping symmetrically. The wind torque is a tuned quadratic, not airflow, so the umbrella never does the real trick of suddenly spilling wind and going briefly calm at extreme angles, and it can't be caught underneath by an upward gust from a doorway or a passing lorry, which is how most real inversions happen. The person's feet are nailed down: no stumbling step, no being dragged a half-pace, no free arm windmilling, and the grip is one saturating torque value rather than fingers actually peeling off. The detached umbrella tumbles plausibly but has no real lift, so it never does the eerie thing a real one does — catching the wind and sailing twenty metres at head height. Rain has no variation in drop size, splashes are decorative ticks, and there's no sound, which matters more than I'd like: half of holding an umbrella in a gale is hearing the frame creak before it goes.