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 parametric fabric strip of eighteen nodes across its diameter, supported by five ribs drawn from a central hub, with each node's height set by a dome profile, windward lift, leeward press and rim flutter, and the shaft and ribs treated as bendable beams whose deflection grows with load; inversion is a bistable snap-through state change in which a single inversion parameter interpolates the profile from a dome (centre high, rim low) to a bowl (centre low, rim high), flipping the ribs and panel shading with it, and it is latched rather than elastic, so once inverted it stays inverted until the load falls well below the trigger; the trigger is the integrated aerodynamic load, proportional to wind speed squared times exposure and tilt, plus gust and flutter peaks, exceeding a rib-buckling threshold for a short sustained interval, after which the parameter snaps to one in a fraction of a second, the canopy then catches wind from below, strain and grip loss rise sharply, and recovery requires wind dropping to roughly a third of the trigger or a manual reset.
<!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>
*{box-sizing:border-box}
html,body{margin:0;height:100%;overflow:hidden;background:#0b1220;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#fff}
canvas{display:block}
#hud{position:absolute;top:14px;left:14px;background:rgba(8,14,24,.55);border:1px solid rgba(255,255,255,.15);border-radius:12px;padding:12px 16px;min-width:240px}
.windBig{font-size:44px;font-weight:800;line-height:1}
.windBig small{font-size:16px;font-weight:600;opacity:.8}
#beaufort{font-size:14px;opacity:.9;margin-top:2px}
#status{font-size:13px;font-weight:700;margin:8px 0;color:#ffd166}
.barRow{display:flex;align-items:center;gap:8px;font-size:11px;margin-top:6px}
.barRow span{width:96px;opacity:.85}
.bar{flex:1;height:8px;background:rgba(255,255,255,.18);border-radius:99px;overflow:hidden}
.bar i{display:block;height:100%;width:0%;background:#ff6b4a}
#gripBar{background:#4ade80}
#controls{position:absolute;left:50%;transform:translateX(-50%);bottom:14px;display:flex;align-items:center;gap:12px;background:rgba(8,14,24,.62);border:1px solid rgba(255,255,255,.15);border-radius:12px;padding:10px 14px;flex-wrap:wrap;justify-content:center;max-width:94vw;font-size:13px}
#controls input[type=range]{width:220px}
button{background:#1f4d5a;color:#fff;border:1px solid rgba(255,255,255,.25);border-radius:8px;padding:7px 10px;cursor:pointer;font-weight:600}
button:hover{background:#2a6577}
#hint{position:absolute;bottom:76px;left:50%;transform:translateX(-50%);font-size:12px;opacity:.8;background:rgba(0,0,0,.32);padding:4px 10px;border-radius:99px;white-space:nowrap}
#title{position:absolute;top:14px;right:14px;font-size:13px;opacity:.85;background:rgba(8,14,24,.45);padding:8px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.12)}
@media(max-width:640px){.windBig{font-size:32px}#controls input[type=range]{width:140px}#hint{display:none}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div class="windBig"><span id="windVal">3.0</span><small> m/s</small></div>
<div id="beaufort">Light breeze</div>
<div id="status">Holding</div>
<div class="barRow"><span>Canopy strain</span><div class="bar"><i id="strainBar"></i></div></div>
<div class="barRow"><span>Grip</span><div class="bar"><i id="gripBar"></i></div></div>
</div>
<div id="title">Umbrella in a Gale</div>
<div id="hint">Up/Down wind · Space gust · R reset · drag slider to drive wind down</div>
<div id="controls">
<label for="windSlider">Wind</label>
<input id="windSlider" type="range" min="0" max="30" step="0.1" value="3">
<span id="windSliderVal">3.0 m/s</span>
<label><input id="autoChk" type="checkbox" checked> Auto-rising storm</label>
<button id="gustBtn">Gust (Space)</button>
<button id="resetBtn">Reset umbrella (R)</button>
</div>
<script>
'use strict';
const canvas=document.getElementById('c'),ctx=canvas.getContext('2d');
const slider=document.getElementById('windSlider'),sliderVal=document.getElementById('windSliderVal');
const windVal=document.getElementById('windVal'),beaufortEl=document.getElementById('beaufort'),statusEl=document.getElementById('status');
const strainBar=document.getElementById('strainBar'),gripBar=document.getElementById('gripBar');
let W=0,H=0,groundY=0,S=1,personX=0;
function resize(){const d=Math.min(window.devicePixelRatio||1,2);W=window.innerWidth;H=window.innerHeight;canvas.width=W*d;canvas.height=H*d;canvas.style.width=W+'px';canvas.style.height=H+'px';ctx.setTransform(d,0,0,d,0,0);groundY=H*0.84;S=Math.max(0.65,Math.min(1.25,Math.min(W,H)/720));personX=W*0.36;}
window.addEventListener('resize',resize);resize();
function clamp(v,a,b){return v<a?a:v>b?b:v}
function lerp(a,b,t){return a+(b-a)*t}
function mix(c1,c2,t){return[lerp(c1[0],c2[0],t),lerp(c1[1],c2[1],t),lerp(c1[2],c2[2],t)]}
function rgb(c,a){return 'rgba('+(c[0]|0)+','+(c[1]|0)+','+(c[2]|0)+','+(a===undefined?1:a)+')'}
let time=0,windTarget=3,wind=3,gust=0,auto=true,autoHold=0,flash=0;
let pose={hipsX:0,hipsY:0,shoulderX:0,shoulderY:0,headX:0,headY:0,handX:0,handY:0,elbowX:0,elbowY:0,footFrontX:0,footBackX:0,footY:0,kneeFrontX:0,kneeFrontY:0,kneeBackX:0,kneeBackY:0,freeHandX:0,freeHandY:0,freeElbowX:0,freeElbowY:0,lean:0,crouch:0,brace:0};
const umb={held:true,x:0,y:0,angle:-0.15,angVel:0,vx:0,vy:0,spin:0,inv:0,inverted:false,invTimer:0,grip:1,strain:0,lostTimer:0};
const drops=[];for(let i=0;i<340;i++)drops.push({x:Math.random()*W,y:Math.random()*H,s:0.7+Math.random()*0.6});
const clouds=[];for(let i=0;i<9;i++)clouds.push({x:Math.random()*W,y:20+Math.random()*H*0.35,s:0.6+Math.random()*1.1});
let ripples=[];
function releaseUmbrella(){umb.held=false;umb.vx=wind*10+120;umb.vy=-180-umb.strain*60;umb.spin=(Math.random()*4+2)*(Math.random()<0.5?-1:1);umb.lostTimer=0;}
function resetUmbrella(){umb.held=true;umb.inverted=false;umb.inv=0;umb.invTimer=0;umb.grip=1;umb.angle=-0.15;umb.angVel=0;umb.vx=umb.vy=umb.spin=0;umb.lostTimer=0;}
function computePose(){
let lean=clamp(-0.04-wind*0.009-gust*0.006,-0.38,0.08);
if(umb.inverted&&umb.held)lean+=0.06;
let crouch=clamp((wind-9)/18,0,1)*20*S+(umb.inverted?6*S:0);
let shuffle=Math.sin(time*2.2)*clamp((wind-12)/10,0,1)*4*S;
let hipsX=personX+wind*0.5*S+shuffle*0.3;
let hipsY=groundY-92*S+crouch;
let torsoLen=82*S,tx=Math.sin(lean),ty=-Math.cos(lean);
let shoulderX=hipsX+tx*torsoLen,shoulderY=hipsY+ty*torsoLen;
let headX=shoulderX+tx*20*S-4*S,headY=shoulderY+ty*20*S-12*S;
let jitter=(Math.sin(time*37)+Math.sin(time*23)*0.6)*clamp(umb.strain-0.6,0,1)*2.5*S;
let handX=shoulderX-14*S+jitter,handY=shoulderY-58*S+crouch*0.4+jitter*0.5;
if(!umb.held){handX=shoulderX-12*S;handY=shoulderY+38*S;}
let footFrontX=personX-36*S+shuffle,footBackX=personX+38*S+shuffle*0.6,footY=groundY;
let kneeFrontX=(hipsX+footFrontX)/2-14*S,kneeFrontY=(hipsY+footY)/2+4*S;
let kneeBackX=(hipsX+footBackX)/2-6*S,kneeBackY=(hipsY+footY)/2+6*S;
let brace=clamp((wind-11)/12,0,1);
let elbowX=(shoulderX+handX)/2+12*S,elbowY=(shoulderY+handY)/2+20*S;
elbowX=lerp(elbowX,(shoulderX+handX)/2+4*S,brace*0.6);elbowY=lerp(elbowY,(shoulderY+handY)/2+8*S,brace*0.6);
let freeHandX=shoulderX-18*S+wind*1.1*S+Math.sin(time*3)*brace*6*S,freeHandY=shoulderY+52*S-brace*8*S;
let freeElbowX=(shoulderX+freeHandX)/2-10*S,freeElbowY=(shoulderY+freeHandY)/2+10*S;
pose={lean,crouch,hipsX,hipsY,shoulderX,shoulderY,headX,headY,handX,handY,elbowX,elbowY,footFrontX,footBackX,footY,kneeFrontX,kneeFrontY,kneeBackX,kneeBackY,freeHandX,freeHandY,freeElbowX,freeElbowY,brace};
}
function update(dt){
time+=dt;
if(auto&&autoHold>0)autoHold-=dt;
if(auto&&autoHold<=0){windTarget=Math.min(30,windTarget+dt*0.22);slider.value=windTarget;}
gust*=Math.exp(-dt*0.7);if(gust<0.01)gust=0;
let turb=Math.sin(time*1.7)*0.6+Math.sin(time*3.9+1.3)*0.4;
let targetActual=windTarget+gust+turb*(0.4+windTarget*0.03);
wind+=(targetActual-wind)*Math.min(1,dt*2.5);wind=Math.max(0,wind);
if(wind>20&&Math.random()<dt*0.06)flash=1;flash=Math.max(0,flash-dt*2.5);
let tiltFactor=1+Math.abs(umb.angle)*0.6;
umb.strain=(wind/17)*(wind/17)*tiltFactor*(umb.inverted?0.85:1)+Math.abs(gust)*0.02;
if(umb.held){
if(!umb.inverted){
if(umb.strain>1.0&&wind>13){umb.invTimer+=dt;if(umb.invTimer>0.25)umb.inverted=true;}
else umb.invTimer=Math.max(0,umb.invTimer-dt*2);
}else if(umb.strain<0.30&&wind<9){umb.inverted=false;}
let invTarget=umb.inverted?1:0,rate=umb.inverted?6:3;
umb.inv+=clamp(invTarget-umb.inv,-rate*dt,rate*dt);
if(umb.strain>0.85)umb.grip-=dt*(umb.strain-0.85)*0.55*(umb.inverted?1.7:1);else umb.grip+=dt*0.12;
if(Math.abs(umb.angle)>0.85)umb.grip-=dt*0.4;
umb.grip=clamp(umb.grip,0,1);
if(umb.grip<=0)releaseUmbrella();
}else{
umb.inv+=((umb.inverted?1:0)-umb.inv)*dt*2;
}
if(umb.held){
let desired=-0.12-wind*0.004;
let windTorque=wind*wind*0.0038*(umb.inverted?1.25:1);
let spring=-(umb.angle-desired)*10-umb.angVel*2.8;
let flutterTorque=Math.sin(time*11+Math.sin(time*5))*wind*0.012;
umb.angVel+=(spring+windTorque+flutterTorque)*dt;
umb.angle=clamp(umb.angle+umb.angVel*dt,-0.9,1.35);
}else{
let windPx=wind*13;
umb.vx+=(windPx-umb.vx)*dt*0.9;
umb.vy+=260*dt;
let drag=umb.inv>0.5?1.8:0.6;
umb.vy-=umb.vy*dt*drag*0.5;umb.vx-=umb.vx*dt*0.2;
umb.x+=umb.vx*dt;umb.y+=umb.vy*dt;
umb.angle+=umb.spin*dt;umb.spin+=(wind*0.02-umb.spin)*dt*0.5;umb.lostTimer+=dt;
if(umb.y>groundY+40){umb.y=groundY+40;umb.vy*=-0.3;umb.vx*=0.7;umb.spin*=0.5;}
}
computePose();
if(umb.held){let len=108*S;umb.x=pose.handX+Math.sin(umb.angle)*len;umb.y=pose.handY-Math.cos(umb.angle)*len;}
let vx=wind*11,vy=540;
for(const d of drops){d.x+=vx*d.s*dt;d.y+=vy*d.s*dt;if(d.y>=groundY){if(Math.random()<0.3)ripples.push({x:d.x,y:groundY+4+Math.random()*20,r:2,a:0.5});d.y=-20-Math.random()*80;d.x=Math.random()*W-wind*15;}if(d.x>W+40){d.x=-20;d.y=Math.random()*H*0.5;}}
for(const c of clouds){c.x+=(20+wind*18)*c.s*dt;if(c.x>W+220)c.x=-220;}
for(const r of ripples){r.r+=40*dt;r.a-=dt*0.8;}ripples=ripples.filter(r=>r.a>0);
}
function drawClouds(){let f=clamp(wind/30,0,1);ctx.fillStyle=rgb(mix([205,215,230],[90,105,125],f),0.75);for(const c of clouds){ctx.beginPath();ctx.ellipse(c.x,c.y,70*c.s,18*c.s,0,0,Math.PI*2);ctx.ellipse(c.x+40*c.s,c.y+4*c.s,50*c.s,14*c.s,0,0,Math.PI*2);ctx.ellipse(c.x-45*c.s,c.y+5*c.s,45*c.s,12*c.s,0,0,Math.PI*2);ctx.fill();}}
function drawBackground(){
ctx.fillStyle='rgba(15,25,40,0.85)';
const houses=[{x:W*0.05,w:90*S,h:60*S},{x:W*0.18,w:70*S,h:45*S},{x:W*0.72,w:100*S,h:55*S},{x:W*0.86,w:80*S,h:40*S}];
for(const hs of houses){ctx.fillRect(hs.x,groundY-hs.h,hs.w,hs.h);ctx.beginPath();ctx.moveTo(hs.x-6*S,groundY-hs.h);ctx.lineTo(hs.x+hs.w/2,groundY-hs.h-22*S);ctx.lineTo(hs.x+hs.w+6*S,groundY-hs.h);ctx.closePath();ctx.fill();}
const trees=[W*0.62,W*0.08,W*0.92];
for(const tx of trees){let bend=wind*0.8*S;ctx.strokeStyle='#1a2a1e';ctx.lineWidth=5*S;ctx.beginPath();ctx.moveTo(tx,groundY);ctx.lineTo(tx+bend,groundY-70*S);ctx.stroke();ctx.fillStyle='rgba(30,55,35,0.9)';ctx.beginPath();ctx.arc(tx+bend+wind*0.4*S,groundY-82*S,22*S,0,Math.PI*2);ctx.arc(tx+bend-14*S,groundY-68*S,16*S,0,Math.PI*2);ctx.fill();}
}
function drawPerson(){
ctx.lineCap='round';ctx.lineJoin='round';
ctx.fillStyle='rgba(0,0,0,0.25)';ctx.beginPath();ctx.ellipse(personX,groundY+6*S,55*S,8*S,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#1e2f42';ctx.lineWidth=11*S;ctx.beginPath();ctx.moveTo(pose.hipsX,pose.hipsY);ctx.lineTo(pose.kneeBackX,pose.kneeBackY);ctx.lineTo(pose.footBackX,pose.footY-4*S);ctx.stroke();
ctx.strokeStyle='#2a405a';ctx.beginPath();ctx.moveTo(pose.hipsX,pose.hipsY);ctx.lineTo(pose.kneeFrontX,pose.kneeFrontY);ctx.lineTo(pose.footFrontX,pose.footY-4*S);ctx.stroke();
ctx.fillStyle='#111';ctx.fillRect(pose.footFrontX-14*S,pose.footY-6*S,26*S,6*S);ctx.fillRect(pose.footBackX-12*S,pose.footY-6*S,26*S,6*S);
ctx.strokeStyle='#1f4d5a';ctx.lineWidth=9*S;ctx.beginPath();ctx.moveTo(pose.shoulderX,pose.shoulderY+6*S);ctx.lineTo(pose.freeElbowX,pose.freeElbowY);ctx.lineTo(pose.freeHandX,pose.freeHandY);ctx.stroke();
ctx.fillStyle='#e8b88a';ctx.beginPath();ctx.arc(pose.freeHandX,pose.freeHandY,5*S,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#20505e';ctx.strokeStyle='#14333d';ctx.lineWidth=2*S;
ctx.beginPath();ctx.moveTo(pose.shoulderX-15*S,pose.shoulderY+4*S);ctx.lineTo(pose.shoulderX+15*S,pose.shoulderY+8*S);ctx.lineTo(pose.hipsX+22*S,pose.hipsY+30*S);ctx.lineTo(pose.hipsX-22*S,pose.hipsY+30*S);ctx.closePath();ctx.fill();ctx.stroke();
let neckX=pose.shoulderX-4*S,neckY=pose.shoulderY+2*S,scarfLen=34*S+wind*1.2*S;
ctx.strokeStyle='#d94f30';ctx.lineWidth=7*S;ctx.beginPath();ctx.moveTo(neckX,neckY);ctx.quadraticCurveTo(neckX+scarfLen*0.5,neckY-10*S+Math.sin(time*8)*6*S,neckX+scarfLen,neckY-6*S+Math.sin(time*6)*4*S-wind*0.2*S);ctx.stroke();
ctx.fillStyle='#e8b88a';ctx.beginPath();ctx.arc(pose.headX,pose.headY,15*S,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#222';ctx.beginPath();ctx.arc(pose.headX-6*S,pose.headY-1*S,1.8*S,0,Math.PI*2);ctx.fill();
ctx.save();ctx.translate(pose.headX,pose.headY-11*S);ctx.rotate(-0.15+clamp(wind*0.015,0,0.4));ctx.fillStyle='#2b2b2b';ctx.beginPath();ctx.ellipse(0,0,20*S,4*S,0,0,Math.PI*2);ctx.fill();ctx.fillRect(-11*S,-14*S,22*S,14*S);ctx.restore();
ctx.strokeStyle='#20505e';ctx.lineWidth=9*S;ctx.beginPath();ctx.moveTo(pose.shoulderX,pose.shoulderY+6*S);ctx.lineTo(pose.elbowX,pose.elbowY);ctx.lineTo(pose.handX,pose.handY);ctx.stroke();
ctx.fillStyle='#e8b88a';ctx.beginPath();ctx.arc(pose.handX,pose.handY,5*S,0,Math.PI*2);ctx.fill();
}
function drawUmbrella(){
const hubX=umb.x,hubY=umb.y,angle=umb.angle,cosA=Math.cos(angle),sinA=Math.sin(angle);
const toWorld=(lx,ly)=>[hubX+lx*cosA-ly*sinA,hubY+lx*sinA+ly*cosA];
const R=115*S,strain=umb.strain,flex=clamp(strain,0,1.6);
const centerNormal=-20*S+flex*24*S,rimNormal=24*S+flex*7*S;
const centerY=lerp(centerNormal,50*S,umb.inv),rimY=lerp(rimNormal,-16*S,umb.inv);
const asym=clamp(wind/25,0,1)*13*S*(1-umb.inv*0.4);
const N=18,pts=[],ptsU=[];
for(let i=0;i<=N;i++){const t=-1+2*i/N,lx=t*R,baseY=rimY+(centerY-rimY)*(1-t*t),fa=clamp(wind/22,0,1)*5*S;const y=baseY+asym*t+Math.sin(time*18+t*6)*fa*Math.abs(t)+Math.sin(time*14+t*2)*fa*0.35*(1-Math.abs(t));pts.push(toWorld(lx,y));ptsU.push(toWorld(lx,y+8*S));}
const bend=clamp(strain,0,1.5)*7*S,mid=toWorld(bend,55*S),grip=toWorld(0,108*S),handle=toWorld(0,132*S);
ctx.strokeStyle='#141414';ctx.lineWidth=4*S;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(hubX,hubY);ctx.quadraticCurveTo(mid[0],mid[1],grip[0],grip[1]);ctx.lineTo(handle[0],handle[1]);ctx.stroke();
ctx.beginPath();ctx.moveTo(handle[0],handle[1]);ctx.quadraticCurveTo(handle[0]+10*S,handle[1]+2*S,handle[0]+12*S,handle[1]-8*S);ctx.stroke();
ctx.beginPath();ctx.moveTo(pts[0][0],pts[0][1]);for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i][0],pts[i][1]);for(let i=ptsU.length-1;i>=0;i--)ctx.lineTo(ptsU[i][0],ptsU[i][1]);ctx.closePath();
ctx.fillStyle=umb.inv>0.5?'#a93226':'#c0392b';ctx.fill();ctx.strokeStyle='#6e1a12';ctx.lineWidth=2*S;ctx.stroke();
const attach=toWorld(0,centerY+3*S);
for(let i=0;i<pts.length-1;i+=2){ctx.beginPath();ctx.moveTo(attach[0],attach[1]);ctx.lineTo(pts[i][0],pts[i][1]);ctx.lineTo(pts[i+1][0],pts[i+1][1]);ctx.closePath();ctx.fillStyle='rgba(255,255,255,0.07)';ctx.fill();}
ctx.strokeStyle='rgba(40,10,8,0.55)';ctx.lineWidth=1.6*S;
for(const idx of[0,3,6,9,12,15,18]){ctx.beginPath();ctx.moveTo(attach[0],attach[1]);ctx.lineTo(pts[idx][0],pts[idx][1]);ctx.stroke();}
ctx.fillStyle='#222';ctx.beginPath();ctx.arc(hubX,hubY,4*S,0,Math.PI*2);ctx.fill();
const tip=toWorld(0,centerY-12*S);ctx.strokeStyle='#222';ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(attach[0],attach[1]);ctx.lineTo(tip[0],tip[1]);ctx.stroke();
}
function drawGripHand(){if(!umb.held)return;ctx.fillStyle='#e8b88a';ctx.strokeStyle='#b07a4a';ctx.lineWidth=1*S;ctx.beginPath();ctx.arc(pose.handX,pose.handY,6*S,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.lineWidth=1.5*S;for(let i=-1;i<=1;i++){ctx.beginPath();ctx.moveTo(pose.handX+i*3*S,pose.handY-4*S);ctx.lineTo(pose.handX+i*3*S,pose.handY+4*S);ctx.stroke();}}
function beaufort(v){if(v<0.5)return'Calm';if(v<1.6)return'Light air';if(v<3.4)return'Light breeze';if(v<5.5)return'Gentle breeze';if(v<8)return'Moderate breeze';if(v<10.8)return'Fresh breeze';if(v<13.9)return'Strong breeze';if(v<17.2)return'Near gale';if(v<20.8)return'Gale';if(v<24.5)return'Strong gale';if(v<28.5)return'Storm';return'Violent storm';}
function updateHUD(){windVal.textContent=wind.toFixed(1);sliderVal.textContent=windTarget.toFixed(1)+' m/s';beaufortEl.textContent=beaufort(wind);statusEl.textContent=!umb.held?'Umbrella lost — press R':umb.inverted?'INVERTED':umb.strain>1?'Straining — inversion imminent':umb.strain>0.7?'Canopy flexing':'Holding';strainBar.style.width=clamp(umb.strain/1.4*100,0,100)+'%';gripBar.style.width=clamp(umb.grip*100,0,100)+'%';}
function draw(){
let f=clamp(wind/30,0,1),top=mix([88,130,170],[16,26,46],f),bot=mix([185,205,225],[72,92,118],f);
let g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,rgb(top));g.addColorStop(1,rgb(bot));ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
if(flash>0){ctx.fillStyle='rgba(255,255,255,'+(flash*0.22)+')';ctx.fillRect(0,0,W,H);}
drawClouds();drawBackground();
ctx.fillStyle=rgb(mix([38,52,68],[18,26,38],f));ctx.fillRect(0,groundY,W,H-groundY);
ctx.fillStyle='rgba(140,170,200,0.18)';for(const p of[{x:0.2,rx:80},{x:0.55,rx:110},{x:0.82,rx:70}]){ctx.beginPath();ctx.ellipse(W*p.x,groundY+18*S,p.rx*S,9*S,0,0,Math.PI*2);ctx.fill();}
ctx.strokeStyle='rgba(180,210,240,0.5)';for(const r of ripples){ctx.globalAlpha=Math.max(0,r.a);ctx.beginPath();ctx.ellipse(r.x,r.y,r.r,r.r*0.3,0,0,Math.PI*2);ctx.stroke();}ctx.globalAlpha=1;
drawPerson();drawUmbrella();drawGripHand();
let vx=wind*11,vy=540;ctx.strokeStyle='rgba(180,210,240,0.55)';ctx.lineWidth=1.2;ctx.beginPath();for(const d of drops){ctx.moveTo(d.x,d.y);ctx.lineTo(d.x-vx*0.028*d.s,d.y-vy*0.028*d.s);}ctx.stroke();
if(wind>8){ctx.strokeStyle='rgba(255,255,255,0.10)';ctx.lineWidth=2;ctx.beginPath();for(let i=0;i<12;i++){let y=(i*97+Math.sin(time+i)*20)%H,x=(time*(200+wind*20)+i*200)%(W+200)-100;ctx.moveTo(x,y);ctx.lineTo(x+40+wind*3,y);}ctx.stroke();}
if(!umb.held){ctx.font='bold '+(20*S)+'px system-ui,sans-serif';ctx.textAlign='center';ctx.fillStyle='rgba(0,0,0,0.45)';ctx.fillRect(W/2-190*S,H*0.20-24*S,380*S,36*S);ctx.fillStyle='#fff';ctx.fillText('Umbrella gone — press R or click to reset',W/2,H*0.20);}
if(umb.inverted&&umb.held){ctx.font='bold '+(16*S)+'px system-ui,sans-serif';ctx.textAlign='center';ctx.fillStyle='#ff8a75';ctx.fillText('INVERTED!',umb.x,umb.y-70*S);}
updateHUD();
}
slider.addEventListener('input',()=>{windTarget=parseFloat(slider.value);autoHold=5;});
document.getElementById('autoChk').addEventListener('change',e=>{auto=e.target.checked;});
document.getElementById('gustBtn').addEventListener('click',()=>{gust=Math.min(15,gust+9);});
document.getElementById('resetBtn').addEventListener('click',resetUmbrella);
canvas.addEventListener('click',()=>{if(!umb.held)resetUmbrella();});
window.addEventListener('keydown',e=>{
if(e.code==='ArrowUp'||e.code==='ArrowRight'){windTarget=clamp(windTarget+1,0,30);slider.value=windTarget;autoHold=4;e.preventDefault();}
else if(e.code==='ArrowDown'||e.code==='ArrowLeft'){windTarget=clamp(windTarget-1,0,30);slider.value=windTarget;autoHold=4;e.preventDefault();}
else if(e.code==='Space'){gust=Math.min(15,gust+9);e.preventDefault();}
else if(e.code==='KeyR'){resetUmbrella();}
});
let last=performance.now();
function frame(now){let dt=Math.min(0.033,(now-last)/1000);last=now;update(dt);draw();requestAnimationFrame(frame);}
requestAnimationFrame(frame);
</script>
</body>
</html>
Where it looks right: rain slants and speeds up with the wind, the canopy flattens, lifts on the windward side and flutters at the rim before it snaps inside out, the shaft bends and the whole umbrella is wrenched downwind, the person leans into it, crouches, braces an elbow and flails the free arm, the scarf, trees and clouds stream downwind, and once inverted the umbrella pulls up, grip drains fast and it usually tears away and tumbles off downwind — all recognisable gale behaviour, with the auto-rising wind, slider, arrow keys, Space for a gust and R to reset giving you full control including driving it back down.
Where it looks wrong, and you would spot it first: it is a flat side view, so a round canopy becomes a 2D arc, ribs never twist or fold asymmetrically, and panels cannot billow in depth; the person's feet shuffle in place rather than actually stepping, sliding or being lifted, and their strength is a single grip number rather than arms, shoulders and stance; the lost umbrella is a simple ballistic tumble with parachute drag, not a spinning, flapping, bouncing wreck; rain does not hit the canopy, run off the rim or splash on the person, puddle ripples are decorative, and the wind is uniform across the scene with no eddies, shelter behind houses, or sound; inversion is a clean deterministic threshold, whereas a real cheap umbrella inverts at a different gust each time, often one panel first, and often stays half-inverted and flapping rather than snapping neatly to a bowl.