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 pair of six‑segment rib chains (a "rib" is a mass‑spring‑bend polyline of point masses) hinged at the hub on top of a rigid four‑particle shaft, with an inextensible‑in‑tension stretcher rope from the mid‑shaft runner to each rib's third node — the tension member that actually stops a real umbrella folding up. Every rib joint is a positional bend constraint with a rest turn angle and a stiffness graded stiffest at the root, and every rib segment is an aerodynamic panel: force = ½ρC_d·A·|u·n|·(u·n)·n against the relative air velocity, with a cheap upwind ray test so the lee rib sits in the windward rib's shadow. Inversion is a discrete per‑rib state flip (inv), not a shape: it changes the rest angles from "out and down" to "up and out", so the frame's preferred shape snaps through and the solver drags the fabric there over a few frames. What decides it is the accumulated signed curvature error D = Σ(θ_j − θ_rest,j)·side summed over the rib's joints — i.e. how far the wind has bent the frame against its rest curvature; cross −1.25 rad and it snaps inside out, cross +1.2 rad the other way and it snaps back, with a cooldown so it can't chatter, and past 2.7 rad the worst joint takes a permanent kink (a broken spoke, stiffness ×0.12). Each rib decides independently, so one side usually goes first. The hand is a damped spring plus a torque‑limited grip on the shaft: exceed the wrist torque and the umbrella weathervanes downwind; exceed the grip force for 0.18 s and it's gone.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Gale — one person, one umbrella</title>
<style>
html,body{margin:0;height:100%;background:#0b0f14;overflow:hidden;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#cfe3ef;-webkit-user-select:none;user-select:none}
canvas{display:block;position:fixed;inset:0}
#hud{position:fixed;left:14px;top:12px;padding:10px 14px 12px;background:rgba(9,15,21,.55);
border:1px solid rgba(160,200,225,.18);border-radius:10px;min-width:236px;pointer-events:none}
.big{font-size:30px;font-weight:600;letter-spacing:-1px;color:#eaf6ff;line-height:1}
.big span.u{font-size:14px;opacity:.6;font-weight:400;letter-spacing:0}
.sub{font-size:11px;opacity:.65;margin-top:3px}
.row{display:flex;justify-content:space-between;font-size:10.5px;margin-top:8px;opacity:.85}
.bar{height:5px;background:rgba(255,255,255,.09);border-radius:3px;overflow:hidden;margin-top:3px}
.bar>i{display:block;height:100%;width:0;background:#6fd3ff}
#st{margin-top:10px;font-size:11px;letter-spacing:.5px;color:#9fd8ee}
#msg{font-size:11px;letter-spacing:1px;color:#ffb27a;height:13px;margin-top:2px}
#ctl{position:fixed;left:50%;transform:translateX(-50%);bottom:14px;display:flex;gap:9px;align-items:center;
background:rgba(9,15,21,.6);border:1px solid rgba(160,200,225,.18);padding:8px 13px;border-radius:10px;font-size:11px}
input[type=range]{width:210px;accent-color:#6fd3ff}
button{font:inherit;font-size:11px;background:rgba(255,255,255,.07);color:#cfe3ef;
border:1px solid rgba(160,200,225,.22);border-radius:6px;padding:4px 9px;cursor:pointer}
button:hover{background:rgba(255,255,255,.15)}
button.on{background:#20607d;border-color:#5fc0e6;color:#eaf8ff}
#tip{position:fixed;right:14px;bottom:14px;font-size:10px;opacity:.38;text-align:right;line-height:1.6;pointer-events:none}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div id="hud">
<div class="big"><span id="ws">0.0</span> <span class="u">m/s</span></div>
<div class="sub"><span id="wk">0</span> km/h · <span id="wm">0</span> mph</div>
<div class="sub" id="bf">Force 0 — Calm</div>
<div class="row"><span>gust</span><span id="gv">+0.0</span></div><div class="bar"><i id="gb"></i></div>
<div class="row"><span>frame load</span><span id="lv">0%</span></div><div class="bar"><i id="lb" style="background:#ffd06f"></i></div>
<div class="row"><span>grip</span><span id="hv">0 N</span></div><div class="bar"><i id="hb" style="background:#ff8f6f"></i></div>
<div id="st">CANOPY: INTACT</div>
<div id="msg"></div>
</div>
<div id="ctl">
<span style="opacity:.6">wind</span>
<input id="sl" type="range" min="0" max="42" step="0.1" value="3">
<span id="slv" style="width:34px;display:inline-block">3.0</span>
<button id="bauto" class="on">auto ↑</button>
<button id="bgust">gust</button>
<button id="breset">new brolly</button>
</div>
<div id="tip">↑↓ wind · A auto · G gust · R new umbrella · P pause</div>
<script>
'use strict';
(function(){
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
let CW=800,CH=600,DPR=1,PPM=110,SW=8;
const SH=5.6,GY=4.98,RHO=1.2,G=9.81;
/* ---------- utils ---------- */
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
function smooth(a,b,x){const t=clamp((x-a)/(b-a),0,1);return t*t*(3-2*t);}
function wrap(a){while(a>Math.PI)a-=6.283185307;while(a<-Math.PI)a+=6.283185307;return a;}
function mk(x,y,m){return{x:x,y:y,px:x,py:y,fx:0,fy:0,m:m,im:1/m};}
function segInt(ax,ay,bx,by,cx,cy,dx,dy){
const r1=bx-ax,r2=by-ay,s1=dx-cx,s2=dy-cy,den=r1*s2-r2*s1;
if(Math.abs(den)<1e-12)return false;
const t=((cx-ax)*s2-(cy-ay)*s1)/den,u=((cx-ax)*r2-(cy-ay)*r1)/den;
return t>=0&&t<=1&&u>=0&&u<=1;
}
function ik(ax,ay,bx,by,l1,l2,sgn){
let dx=bx-ax,dy=by-ay,d=Math.hypot(dx,dy)||1e-6;
const nx=dx/d,ny=dy/d;
d=clamp(d,Math.abs(l1-l2)+1e-3,l1+l2-1e-3);
const a=(l1*l1-l2*l2+d*d)/(2*d),h=Math.sqrt(Math.max(0,l1*l1-a*a));
return{x:ax+nx*a-ny*h*sgn,y:ay+ny*a+nx*h*sgn};
}
/* ---------- global state ---------- */
let tt=0,paused=false,lostCount=0,msgT=0,msgTxt='';
const shk={m:0,x:0,y:0};
function kick(m){shk.m=Math.max(shk.m,m);}
function say(s){msgTxt=s;msgT=2.2;}
const wind={base:3,target:3,auto:true,speed:3,vx:3,vy:0};
let gustT=1,gustPh=0,gustDur=1,gustA=0;
function updateWind(dt){
if(wind.auto){wind.target=Math.min(42,wind.target+0.30*dt);sl.value=wind.target.toFixed(1);}
wind.base+=(wind.target-wind.base)*clamp(dt*2.0,0,1);
gustT-=dt;
if(gustT<=0){gustT=1.0+Math.random()*4.0;gustPh=0;gustDur=0.6+Math.random()*1.9;gustA=0.12+Math.random()*0.55;}
gustPh+=dt;
const env=gustPh<gustDur?Math.sin(Math.PI*gustPh/gustDur):0;
const rip=Math.sin(tt*2.9)*0.34+Math.sin(tt*7.1+1.2)*0.2+Math.sin(tt*1.27+.5)*0.42+Math.sin(tt*0.41)*0.3;
const amp=0.10*wind.base+0.55;
wind.speed=Math.max(0.05,wind.base*(1+gustA*env*0.5)+rip*amp);
wind.vx=wind.speed;
wind.vy=(Math.sin(tt*1.83)*0.05+Math.sin(tt*0.63+2)*0.045)*wind.speed-0.02*wind.speed;
}
const BFT=[[0.5,'0 — Calm'],[1.6,'1 — Light air'],[3.4,'2 — Light breeze'],[5.5,'3 — Gentle breeze'],
[8.0,'4 — Moderate breeze'],[10.8,'5 — Fresh breeze'],[13.9,'6 — Strong breeze'],[17.2,'7 — Near gale'],
[20.8,'8 — Gale'],[24.5,'9 — Strong gale'],[28.5,'10 — Storm'],[32.7,'11 — Violent storm'],[999,'12 — Hurricane']];
function beau(v){for(let i=0;i<BFT.length;i++)if(v<BFT[i][0])return'Force '+BFT[i][1];return'';}
/* ---------- person ---------- */
const P={home:3,x:3,vx:0,lean:0,brace:0,load:0,fatigue:1,bodyDrag:0,aimT:-0.25,
Tmax:12,gripMax:90,init:false,reach:0,skid:0};
let umb=null;
function updatePerson(dt){
const w=wind.speed;
P.brace+=(smooth(3,15,w)-P.brace)*Math.min(1,dt*2.2);
const A=0.62-0.14*P.brace;
P.bodyDrag=0.5*RHO*1.1*A*w*w*(1+0.07*Math.sin(tt*4.1));
const hf=(umb&&umb.attached)?umb.handF:{x:0,y:0};
P.load=Math.hypot(hf.x,hf.y);
const Fx=P.bodyDrag-hf.x;
const M=78;
const leanT=clamp(Math.atan2(Fx,M*G)*1.3,-0.06,0.82);
P.lean+=(leanT-P.lean)*Math.min(1,dt*5.5);
/* footing: slide when friction is beaten, walk back when it isn't */
const fric=0.72*M*G;
if(Fx>fric){P.vx+=(Fx-fric)/M*dt;}
else{if(P.x>P.home+0.01)P.x-=Math.min(P.x-P.home,0.22*dt);}
P.x+=P.vx*dt;P.vx*=Math.pow(0.35,dt);
P.x=clamp(P.x,P.home-0.25,P.home+2.6);
P.skid=Math.abs(P.vx);
const hipH=0.92-0.18*P.brace;
P.hip={x:P.x,y:GY-hipH};
const ud={x:-Math.sin(P.lean),y:-Math.cos(P.lean)};
const pd={x:-ud.y,y:ud.x};
P.ud=ud;P.pd=pd;
const torso=0.56-0.03*P.brace;
P.sh={x:P.hip.x+ud.x*torso,y:P.hip.y+ud.y*torso};
P.neck={x:P.sh.x+ud.x*0.07,y:P.sh.y+ud.y*0.07};
P.head={x:P.neck.x+ud.x*0.13+pd.x*0.015,y:P.neck.y+ud.y*0.13+pd.y*0.015};
P.footF={x:P.x-0.10+0.58*Math.sin(P.lean),y:GY};
P.footB={x:P.x+0.22+1.02*Math.sin(P.lean)+0.16*P.brace,y:GY};
const gone=!umb||!umb.attached;
P.reach+=((gone?1:0)-P.reach)*Math.min(1,dt*3);
const hx=lerp(-0.16-0.11*P.brace,-0.34,P.reach);
const hy=lerp(0.10+0.15*P.brace,-0.16,P.reach);
P.hand={x:P.sh.x+pd.x*hx+ud.x*(-hy),y:P.sh.y+pd.y*hx+ud.y*(-hy)};
const aim=smooth(3,17,w);
let th=-(0.22+1.02*aim);
if(umb&&umb.inverted)th=lerp(th,-0.12,0.65);
P.aimT=th+Math.sin(tt*0.77)*0.045+Math.sin(tt*2.3)*0.012;
if(P.load>0.62*P.gripMax)P.fatigue-=dt*0.055;else P.fatigue+=dt*0.035;
P.fatigue=clamp(P.fatigue,0.66,1);
P.Tmax=(11+21*P.brace)*((umb&&umb.inverted)?0.72:1)*P.fatigue;
P.gripMax=(80+92*P.brace)*P.fatigue;
}
/* ---------- umbrella ---------- */
const N_SEG=6,SEG=0.095,A0=0.17,CURVE=0.135,AINV=-0.72,CINV=-0.16;
const SHAFT=0.92,TIPL=0.15,CWID=0.52,CDN=1.25,BSTIFF=0.44;
const DCRIT=1.25,DCRIT2=1.2,DBREAK=2.7;
function buildUmbrella(hx,hy,theta){
const dir={x:Math.sin(theta),y:-Math.cos(theta)};
const rv={x:-dir.y,y:dir.x};
const u={attached:true,inverted:false,anyBroken:false,load:0,over:0,lostTimer:0,fixT:0,
handF:{x:0,y:0},born:0,flash:0};
const S0=mk(hx,hy,0.16),S1=mk(hx+dir.x*SHAFT*.5,hy+dir.y*SHAFT*.5,0.06),
S2=mk(hx+dir.x*SHAFT,hy+dir.y*SHAFT,0.06),
S3=mk(hx+dir.x*(SHAFT+TIPL),hy+dir.y*(SHAFT+TIPL),0.02);
u.S0=S0;u.S1=S1;u.S2=S2;u.S3=S3;
u.shaft=[S0,S1,S2,S3];
u.parts=[S0,S1,S2,S3];
u.dist=[];u.ropes=[];u.ribs=[];
const D=(a,b,k)=>u.dist.push({a:a,b:b,L:Math.hypot(a.x-b.x,a.y-b.y),k:k===undefined?1:k});
D(S0,S1);D(S1,S2);D(S2,S3);D(S0,S2);D(S1,S3);D(S0,S3);
[-1,1].forEach(function(s){
const pts=[S2];let cx=S2.x,cy=S2.y;
for(let k=0;k<N_SEG;k++){
const a=A0+k*CURVE;
cx+=(rv.x*s*Math.cos(a)-dir.x*Math.sin(a))*SEG;
cy+=(rv.y*s*Math.cos(a)-dir.y*Math.sin(a))*SEG;
pts.push(mk(cx,cy,k===N_SEG-1?0.022:0.02));
}
for(let k=1;k<pts.length;k++){u.parts.push(pts[k]);D(pts[k-1],pts[k],1);}
const rib={s:s,pts:pts,inv:false,err:new Array(N_SEG).fill(0),shadow:new Array(N_SEG).fill(1),
D:0,broken:-1,brokenRest:0,cool:0,flash:0,stiff:[]};
for(let j=0;j<N_SEG;j++)rib.stiff.push(BSTIFF*(N_SEG-j)/N_SEG);
u.ribs.push(rib);
u.ropes.push({a:S1,b:pts[2],L:Math.hypot(S1.x-pts[2].x,S1.y-pts[2].y),broken:false,strain:0,rib:rib});
});
return u;
}
function ribRest(rib,j){
if(rib.broken===j)return rib.brokenRest;
if(!rib.inv)return j===0?-rib.s*(Math.PI/2-A0):rib.s*CURVE;
return j===0?-rib.s*(Math.PI/2-AINV):rib.s*CINV;
}
function canopyLine(u){
const L=u.ribs[0],R=u.ribs[1],a=[];
for(let i=L.pts.length-1;i>=1;i--)a.push(L.pts[i]);
a.push(u.S2);
for(let i=1;i<R.pts.length;i++)a.push(R.pts[i]);
return a;
}
function bendSolve(A,B,C,rest,stiff){
let v1x=B.x-A.x,v1y=B.y-A.y;const l1=Math.hypot(v1x,v1y);if(l1<1e-9)return 0;
v1x/=l1;v1y/=l1;
let v2x=C.x-B.x,v2y=C.y-B.y;const l2=Math.hypot(v2x,v2y);if(l2<1e-9)return 0;
const cur=Math.atan2(v1x*v2y-v1y*v2x,v1x*v2x+v1y*v2y);
const err=wrap(cur-rest);
const ca=Math.cos(rest),sa=Math.sin(rest);
const tx=B.x+(v1x*ca-v1y*sa)*l2,ty=B.y+(v1x*sa+v1y*ca)*l2;
const dx=(tx-C.x)*stiff,dy=(ty-C.y)*stiff;
const wA=A.im,wB=B.im,wC=C.im,su=wC+0.35*(wA+wB)+1e-9;
C.x+=dx*wC/su;C.y+=dy*wC/su;
A.x-=dx*0.35*wA/su;A.y-=dy*0.35*wA/su;
B.x-=dx*0.35*wB/su;B.y-=dy*0.35*wB/su;
return err;
}
function solveDist(c){
const a=c.a,b=c.b;let dx=b.x-a.x,dy=b.y-a.y;const d=Math.hypot(dx,dy)||1e-9;
const diff=(d-c.L)/d*c.k,ws=a.im+b.im;
const ka=a.im/ws,kb=b.im/ws;
a.x+=dx*diff*ka;a.y+=dy*diff*ka;b.x-=dx*diff*kb;b.y-=dy*diff*kb;
}
function solveRope(r){
const a=r.a,b=r.b;let dx=b.x-a.x,dy=b.y-a.y;const d=Math.hypot(dx,dy)||1e-9;
r.strain=(d-r.L)/r.L;
if(d<=r.L)return;
const diff=(d-r.L)/d*0.85,ws=a.im+b.im;
a.x+=dx*diff*a.im/ws;a.y+=dy*diff*a.im/ws;
b.x-=dx*diff*b.im/ws;b.y-=dy*diff*b.im/ws;
}
function aeroSeg(A,B,wx,wy,width,cd,sh,dt){
let ex=B.x-A.x,ey=B.y-A.y;const L=Math.hypot(ex,ey);if(L<1e-6)return;
ex/=L;ey/=L;const nx=-ey,ny=ex;
const vx=((A.x-A.px)+(B.x-B.px))*0.5/dt,vy=((A.y-A.py)+(B.y-B.py))*0.5/dt;
const ux=wx-vx,uy=wy-vy;
const un=ux*nx+uy*ny,ut=ux*ex+uy*ey,area=L*width;
let fn=0.5*RHO*cd*area*Math.abs(un)*un*sh;
fn=clamp(fn,-500,500);
const ft=0.5*RHO*0.06*area*Math.abs(ut)*ut;
const fx=(fn*nx+ft*ex)*0.5,fy=(fn*ny+ft*ey)*0.5;
A.fx+=fx;A.fy+=fy;B.fx+=fx;B.fy+=fy;
}
function computeShadow(u){
const poly=canopyLine(u);
const wl=Math.hypot(wind.vx,wind.vy)||1;
const ux=-wind.vx/wl*0.85,uy=-wind.vy/wl*0.85;
for(let ri=0;ri<u.ribs.length;ri++){
const rib=u.ribs[ri];
for(let j=0;j<rib.pts.length-1;j++){
const A=rib.pts[j],B=rib.pts[j+1];
const mx=(A.x+B.x)*0.5+ux*0.03,my=(A.y+B.y)*0.5+uy*0.03;
let sh=1;
for(let i=0;i<poly.length-1;i++){
const C=poly[i],Dp=poly[i+1];
if(C===A||C===B||Dp===A||Dp===B)continue;
if(segInt(mx,my,mx+ux,my+uy,C.x,C.y,Dp.x,Dp.y)){sh=0.3;break;}
}
rib.shadow[j]=sh;
}
}
}
function stepUmbrella(u,dt){
const wx=wind.vx,wy=wind.vy;
for(let i=0;i<u.parts.length;i++){const p=u.parts[i];p.fx=0;p.fy=p.m*G;}
for(let ri=0;ri<u.ribs.length;ri++){
const rib=u.ribs[ri];
for(let j=0;j<rib.pts.length-1;j++){
const A=rib.pts[j],B=rib.pts[j+1];
const mx=(A.x+B.x)*0.5,my=(A.y+B.y)*0.5;
const turb=1+0.20*Math.sin(tt*6.3+mx*3.7+my*2.1)+0.13*Math.sin(tt*11.7+my*5.1);
aeroSeg(A,B,wx*turb,wy*turb+0.3*Math.sin(tt*8+mx*4),CWID,CDN,rib.shadow[j],dt);
}
}
for(let i=0;i<3;i++)aeroSeg(u.shaft[i],u.shaft[i+1],wx,wy,0.05,1.0,1,dt);
if(u.attached){
const S0=u.S0,S2=u.S2;
const dxh=P.hand.x-S0.x,dyh=P.hand.y-S0.y;
const vx=(S0.x-S0.px)/dt,vy=(S0.y-S0.py)/dt;
let fx=2600*dxh-30*vx,fy=2600*dyh-30*vy;
u.handF.x=fx;u.handF.y=fy;
S0.fx+=fx;S0.fy+=fy;
let dx=S2.x-S0.x,dy=S2.y-S0.y;const L=Math.hypot(dx,dy)||1e-6;
const th=Math.atan2(dx/L,-dy/L);
const px=Math.cos(th),py=Math.sin(th);
const rvx=((S2.x-S2.px)-(S0.x-S0.px))/dt,rvy=((S2.y-S2.py)-(S0.y-S0.py))/dt;
const om=(rvx*px+rvy*py)/L;
let T=clamp(90*wrap(P.aimT-th)-7*om,-P.Tmax,P.Tmax);
if(th<-1.62)T+=(-1.62-th)*300;
if(th>1.78)T+=(1.78-th)*300;
u.shaftAng=th;
const f=T/L;
S2.fx+=px*f;S2.fy+=py*f;S0.fx-=px*f;S0.fy-=py*f;
}else{u.handF.x=0;u.handF.y=0;}
for(let i=0;i<u.parts.length;i++){
const p=u.parts[i];
const vx=(p.x-p.px)*0.9985,vy=(p.y-p.py)*0.9985;
p.px=p.x;p.py=p.y;
p.x+=vx+p.fx*p.im*dt*dt;
p.y+=vy+p.fy*p.im*dt*dt;
}
for(let it=0;it<3;it++){
for(let i=0;i<u.dist.length;i++)solveDist(u.dist[i]);
for(let i=0;i<u.ropes.length;i++)if(!u.ropes[i].broken)solveRope(u.ropes[i]);
for(let ri=0;ri<u.ribs.length;ri++){
const rib=u.ribs[ri],pts=rib.pts;
for(let j=0;j<pts.length-1;j++){
const A=(j===0)?u.S3:pts[j-1],B=pts[j],C=pts[j+1];
rib.err[j]=bendSolve(A,B,C,ribRest(rib,j),rib.stiff[j]*(rib.broken===j?0.12:1));
}
}
}
for(let i=0;i<u.parts.length;i++){
const p=u.parts[i];
if(p.y>GY){const vy=p.y-p.py,vx=p.x-p.px;p.y=GY;p.py=GY+vy*0.35;p.px=p.x-vx*0.72;}
}
}
function updateRibStates(u,dt){
u.load=0;u.inverted=false;u.anyBroken=false;
for(let ri=0;ri<u.ribs.length;ri++){
const rib=u.ribs[ri];
let d=0;for(let j=0;j<rib.err.length;j++)d+=rib.err[j]*rib.s;
rib.D=d;rib.cool=Math.max(0,rib.cool-dt);rib.flash=Math.max(0,rib.flash-dt*3.5);
u.load=Math.max(u.load,Math.abs(d)/DCRIT);
if(rib.cool<=0){
if(!rib.inv&&d<-DCRIT){rib.inv=true;rib.cool=0.3;rib.flash=1;kick(0.05);say('INVERTED');
for(let k=0;k<6;k++)spawnBit(rib.pts[4].x,rib.pts[4].y,(Math.random()-.5)*2,-Math.random()*2,'spark');}
else if(rib.inv&&d>DCRIT2){rib.inv=false;rib.cool=0.3;rib.flash=1;kick(0.03);say('SNAPPED BACK');}
}
if(rib.broken<0&&Math.abs(d)>DBREAK){
let mj=0,mv=0;
for(let j=0;j<rib.err.length;j++){if(Math.abs(rib.err[j])>mv){mv=Math.abs(rib.err[j]);mj=j;}}
rib.broken=mj;rib.brokenRest=ribRest(rib,mj)+rib.err[mj];
kick(0.07);say('RIB BROKEN');
for(let k=0;k<8;k++)spawnBit(rib.pts[mj+1].x,rib.pts[mj+1].y,(Math.random()-.5)*3,-Math.random()*3,'spark');
}
if(rib.inv)u.inverted=true;
if(rib.broken>=0)u.anyBroken=true;
}
for(let i=0;i<u.ropes.length;i++){
const r=u.ropes[i];
if(!r.broken&&r.strain>0.11){r.broken=true;say('STRETCHER GAVE WAY');kick(0.04);}
}
if(u.attached){
const f=Math.hypot(u.handF.x,u.handF.y);
u.over=(f>P.gripMax)?u.over+dt*(f/P.gripMax):Math.max(0,u.over-dt*2.2);
if(u.over>0.18){u.attached=false;u.over=0;lostCount++;say('GONE');kick(0.09);}
}else{
u.lostTimer+=dt;
let off=true;
for(let i=0;i<u.parts.length;i++){const p=u.parts[i];if(p.x<SW+1.4&&p.x>-1.6)off=false;}
if(off||u.lostTimer>7){umb=buildUmbrella(P.hand.x,P.hand.y,P.aimT);umb.born=0.35;say('SPARE');}
}
if(u.attached&&(u.inverted||u.anyBroken)){
if(wind.speed<8){u.fixT+=dt;if(u.fixT>1.7){umb=buildUmbrella(P.hand.x,P.hand.y,P.aimT);umb.born=0.3;say('RESET');}}
else u.fixT=Math.max(0,u.fixT-dt);
}else u.fixT=0;
u.born=Math.max(0,u.born-dt);
}
/* ---------- rain & bits ---------- */
const RMAX=1000,rain=[];let rainN=300;
function resetDrop(d,any){
d.v=7.5+Math.random()*4.5;d.z=Math.random();d.j=(Math.random()-0.5)*1.4;d.seed=Math.random();
if(any){d.x=Math.random()*(SW+2)-1;d.y=Math.random()*(GY+0.5)-0.5;return;}
const fallT=(SH)/d.v,dxw=Math.abs(wind.vx)*fallT;
if(Math.random()<SW/(SW+dxw)){d.x=Math.random()*(SW+1.2)-0.6;d.y=-0.4;}
else{d.x=wind.vx>=0?-0.4:SW+0.4;d.y=Math.random()*(GY+0.4)-0.4;}
d.px=d.x;d.py=d.y;
}
for(let i=0;i<RMAX;i++){const d={x:0,y:0,px:0,py:0,vx:0,vy:0,v:9,z:0,j:0,seed:0};resetDrop(d,true);rain.push(d);}
const bits=[];
function spawnBit(x,y,vx,vy,t){
if(bits.length>420)bits.shift();
bits.push({x:x,y:y,vx:vx,vy:vy,life:t==='spark'?0.35:0.5+Math.random()*0.4,t:t,r:Math.random()*6.28,
vr:(Math.random()-.5)*14});
}
function updateBits(dt){
for(let i=bits.length-1;i>=0;i--){
const b=bits[i];b.life-=dt;
if(b.life<=0){bits.splice(i,1);continue;}
b.vy+=G*dt*(b.t==='leaf'?0.35:1);
if(b.t==='leaf'){b.vx+=(wind.vx*0.9-b.vx)*dt*3;b.r+=b.vr*dt;}
else b.vx+=(wind.vx*0.5-b.vx)*dt*2;
b.x+=b.vx*dt;b.y+=b.vy*dt;
if(b.y>GY&&b.t!=='leaf'){b.y=GY;b.vy*=-0.25;b.vx*=0.6;}
}
}
function updateRain(dt){
rainN=Math.floor(clamp(200+27*wind.speed,200,RMAX));
let poly=null,bx0=0,by0=0,bx1=0,by1=0;
if(umb){
poly=canopyLine(umb);
bx0=by0=1e9;bx1=by1=-1e9;
for(let i=0;i<poly.length;i++){const p=poly[i];
if(p.x<bx0)bx0=p.x;if(p.x>bx1)bx1=p.x;if(p.y<by0)by0=p.y;if(p.y>by1)by1=p.y;}
}
for(let i=0;i<rainN;i++){
const d=rain[i];
d.px=d.x;d.py=d.y;
const g=1+0.09*Math.sin(tt*5+d.seed*13);
d.vx=wind.vx*0.96*g+d.j;d.vy=d.v+wind.vy*0.5;
d.x+=d.vx*dt;d.y+=d.vy*dt;
if(poly&&d.x>bx0-0.25&&d.x<bx1+0.25&&d.y>by0-0.25&&d.y<by1+0.25){
let hit=false;
for(let k=0;k<poly.length-1;k++){
if(segInt(d.px,d.py,d.x,d.y,poly[k].x,poly[k].y,poly[k+1].x,poly[k+1].y)){hit=true;break;}
}
if(hit){
if(Math.random()<0.5)spawnBit(d.x,d.y,d.vx*0.15+(Math.random()-.3)*1.5,-Math.random()*1.2,'drop');
resetDrop(d);continue;
}
}
if(d.y>GY){
if(Math.random()<0.35)spawnBit(d.x,GY,(Math.random()-.5)*1.2+wind.vx*0.06,-0.5-Math.random()*1.4,'drop');
resetDrop(d);
}else if(d.x>SW+1.6||d.x<-2.2)resetDrop(d);
}
if(wind.speed>13&&Math.random()<dt*wind.speed*0.35){
spawnBit(-0.4,GY-Math.random()*2.2,wind.vx*0.7,-Math.random()*1.5,'leaf');
}
}
/* ---------- scenery ---------- */
let grass=[],hills=[],puddles=[],clouds=[];
function buildScenery(){
grass=[];puddles=[];clouds=[];
for(let i=0;i<70;i++)grass.push({x:Math.random()*(SW+2)-1,h:0.10+Math.random()*0.22,
ph:Math.random()*6.28,z:Math.random()});
for(let i=0;i<9;i++)puddles.push({x:Math.random()*(SW+1)-0.5,y:GY+0.04+Math.random()*0.4,
w:0.3+Math.random()*1.1});
for(let i=0;i<7;i++)clouds.push({x:Math.random()*(SW+6)-3,y:0.2+Math.random()*1.5,
w:1.5+Math.random()*3,h:0.3+Math.random()*0.5,a:0.05+Math.random()*0.10});
hills=[];
for(let i=0;i<3;i++){
const pts=[];const base=GY-0.25-i*0.12;
for(let x=-1;x<SW+2;x+=0.35)pts.push({x:x,y:base-Math.abs(Math.sin(x*0.35+i*2.1))*(0.25+i*0.1)
-Math.sin(x*0.9+i)*0.06});
hills.push(pts);
}
}
/* ---------- drawing ---------- */
function pathSmooth(pts){
ctx.moveTo(pts[0].x,pts[0].y);
for(let i=1;i<pts.length-1;i++){
const xc=(pts[i].x+pts[i+1].x)*0.5,yc=(pts[i].y+pts[i+1].y)*0.5;
ctx.quadraticCurveTo(pts[i].x,pts[i].y,xc,yc);
}
ctx.lineTo(pts[pts.length-1].x,pts[pts.length-1].y);
}
function limb(a,b,c,w,col){
ctx.strokeStyle=col;ctx.lineWidth=w;ctx.lineCap='round';ctx.lineJoin='round';
ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.lineTo(c.x,c.y);ctx.stroke();
}
function drawSky(){
ctx.setTransform(DPR,0,0,DPR,0,0);
const g=ctx.createLinearGradient(0,0,0,CH);
const dark=clamp(wind.speed/40,0,1);
g.addColorStop(0,'rgb('+(26-8*dark)+','+(33-9*dark)+','+(41-9*dark)+')');
g.addColorStop(0.55,'rgb('+(48-14*dark)+','+(58-16*dark)+','+(67-16*dark)+')');
g.addColorStop(1,'rgb('+(70-22*dark)+','+(80-24*dark)+','+(88-24*dark)+')');
ctx.fillStyle=g;ctx.fillRect(0,0,CW,CH);
}
function drawBack(){
for(let i=0;i<clouds.length;i++){
const c=clouds[i];
c.x-=wind.vx*0.035*0.016;
if(c.x<-4)c.x=SW+4;if(c.x>SW+5)c.x=-4;
ctx.fillStyle='rgba(215,230,240,'+c.a+')';
ctx.beginPath();ctx.ellipse(c.x,c.y,c.w,c.h,0,0,6.283);ctx.fill();
}
for(let i=0;i<hills.length;i++){
ctx.fillStyle='rgba('+(30+i*6)+','+(38+i*7)+','+(45+i*8)+',0.85)';
ctx.beginPath();ctx.moveTo(-1,GY+1);
for(let k=0;k<hills[i].length;k++)ctx.lineTo(hills[i][k].x,hills[i][k].y);
ctx.lineTo(SW+2,GY+1);ctx.closePath();ctx.fill();
}
const gg=ctx.createLinearGradient(0,GY,0,SH);
gg.addColorStop(0,'#1b2128');gg.addColorStop(1,'#0e1216');
ctx.fillStyle=gg;ctx.fillRect(-1,GY,SW+2,SH-GY+1);
ctx.strokeStyle='rgba(160,200,225,0.13)';ctx.lineWidth=0.008;
ctx.beginPath();ctx.moveTo(-1,GY);ctx.lineTo(SW+2,GY);ctx.stroke();
for(let i=0;i<puddles.length;i++){
const p=puddles[i];
ctx.fillStyle='rgba(150,190,215,0.10)';
ctx.beginPath();ctx.ellipse(p.x,p.y,p.w,0.035,0,0,6.283);ctx.fill();
}
}
function drawGrass(front){
const bend=clamp(wind.speed/26,0,1.5);
for(let i=0;i<grass.length;i++){
const g=grass[i];
if((g.z>0.5)!==front)continue;
const sway=Math.sin(tt*(2.2+g.z*2)+g.ph)*0.03*(0.4+bend);
const tipx=g.x+(bend*0.8+sway)*g.h*2.4,tipy=GY-g.h*(1-bend*0.25);
ctx.strokeStyle=front?'rgba(60,74,64,0.85)':'rgba(44,56,50,0.7)';
ctx.lineWidth=0.012;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(g.x,GY);
ctx.quadraticCurveTo(g.x+bend*0.15*g.h,GY-g.h*0.7,tipx,tipy);ctx.stroke();
}
}
function drawRain(back){
ctx.lineCap='butt';
for(let i=0;i<rainN;i++){
const d=rain[i];
if((d.z<0.45)!==back)continue;
const sp=Math.hypot(d.vx,d.vy)||1;
const len=clamp(sp*0.030,0.05,0.5)*(0.55+d.z*0.8);
ctx.strokeStyle='rgba(188,216,235,'+(0.08+0.30*d.z)+')';
ctx.lineWidth=0.004+0.008*d.z;
ctx.beginPath();ctx.moveTo(d.x,d.y);
ctx.lineTo(d.x-d.vx/sp*len,d.y-d.vy/sp*len);ctx.stroke();
}
}
function drawBits(){
for(let i=0;i<bits.length;i++){
const b=bits[i];
if(b.t==='leaf'){
ctx.fillStyle='rgba(120,88,52,0.8)';
ctx.save();ctx.translate(b.x,b.y);ctx.rotate(b.r);
ctx.beginPath();ctx.ellipse(0,0,0.045,0.018,0,0,6.283);ctx.fill();ctx.restore();
}else if(b.t==='spark'){
ctx.fillStyle='rgba(255,220,170,'+clamp(b.life*2.5,0,1)+')';
ctx.beginPath();ctx.arc(b.x,b.y,0.012,0,6.283);ctx.fill();
}else{
ctx.strokeStyle='rgba(190,220,240,'+clamp(b.life*1.6,0,0.6)+')';
ctx.lineWidth=0.006;
ctx.beginPath();ctx.moveTo(b.x,b.y);ctx.lineTo(b.x-b.vx*0.02,b.y-b.vy*0.02);ctx.stroke();
}
}
}
function drawPerson(){
const hip=P.hip,sh=P.sh,ud=P.ud,pd=P.pd;
const FAR='#131b25',NEAR='#1e2a38',COAT='#26394c',SKIN='#c79b78';
ctx.save();
/* ground shadow */
ctx.fillStyle='rgba(0,0,0,0.25)';
ctx.beginPath();ctx.ellipse(P.x+0.15,GY+0.02,0.55,0.05,0,0,6.283);ctx.fill();
/* far leg */
const kB=ik(hip.x,hip.y,P.footB.x,P.footB.y,0.52,0.52,1);
limb(hip,kB,P.footB,0.115,FAR);
ctx.strokeStyle=FAR;ctx.lineWidth=0.075;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(P.footB.x,P.footB.y-0.02);ctx.lineTo(P.footB.x-0.16,P.footB.y-0.02);ctx.stroke();
/* far arm */
let fh;
if(umb&&umb.attached&&P.brace>0.35){
const S0=umb.S0,S2=umb.S2;let dx=S2.x-S0.x,dy=S2.y-S0.y;const L=Math.hypot(dx,dy)||1;
fh={x:S0.x+dx/L*0.27,y:S0.y+dy/L*0.27};
}else fh={x:sh.x+pd.x*0.14+ud.x*(-0.40),y:sh.y+pd.y*0.14+ud.y*(-0.40)};
const feb=ik(sh.x,sh.y,fh.x,fh.y,0.30,0.29,-1);
limb(sh,feb,fh,0.075,FAR);
/* torso */
ctx.strokeStyle=NEAR;ctx.lineWidth=0.30;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(hip.x,hip.y);ctx.lineTo(sh.x,sh.y);ctx.stroke();
/* coat */
const wf=clamp(wind.speed/28,0,1.2);
const hemA={x:hip.x-pd.x*0.15,y:hip.y-pd.y*0.15};
const flap=Math.sin(tt*(5+wf*7))*0.05*wf;
const hemB={x:hip.x+pd.x*(0.16+0.85*wf)+ud.x*(-0.30+0.34*wf),
y:hip.y+pd.y*(0.16+0.85*wf)+ud.y*(-0.30+0.34*wf)+flap};
const hemC={x:hip.x+pd.x*(0.30+1.5*wf)+ud.x*(-0.10+0.42*wf),
y:hip.y+pd.y*(0.30+1.5*wf)+ud.y*(-0.10+0.42*wf)-flap*1.7};
ctx.fillStyle=COAT;
ctx.beginPath();
ctx.moveTo(sh.x-pd.x*0.16+ud.x*0.02,sh.y-pd.y*0.16+ud.y*0.02);
ctx.lineTo(hemA.x+ud.x*-0.30,hemA.y+ud.y*-0.30);
ctx.quadraticCurveTo(hemB.x,hemB.y,hemC.x,hemC.y);
ctx.quadraticCurveTo(hemB.x+ud.x*0.16,hemB.y+ud.y*0.16,sh.x+pd.x*0.17,sh.y+pd.y*0.17);
ctx.closePath();ctx.fill();
/* near leg */
const kF=ik(hip.x,hip.y,P.footF.x,P.footF.y,0.52,0.52,1);
limb(hip,kF,P.footF,0.12,NEAR);
ctx.strokeStyle=NEAR;ctx.lineWidth=0.08;
ctx.beginPath();ctx.moveTo(P.footF.x,P.footF.y-0.02);ctx.lineTo(P.footF.x-0.17,P.footF.y-0.02);ctx.stroke();
/* head + hood */
const hd=P.head;
ctx.fillStyle=SKIN;ctx.beginPath();ctx.arc(hd.x,hd.y,0.112,0,6.283);ctx.fill();
ctx.fillStyle=COAT;
ctx.beginPath();ctx.arc(hd.x,hd.y,0.125,0,6.283);
ctx.fill('evenodd');
ctx.beginPath();
ctx.arc(hd.x,hd.y,0.128,-2.0+P.lean,1.5+P.lean,false);
ctx.lineTo(hd.x+pd.x*0.10+ud.x*(-0.14),hd.y+pd.y*0.10+ud.y*(-0.14));
ctx.closePath();ctx.fill();
ctx.fillStyle=SKIN;
ctx.beginPath();ctx.arc(hd.x-0.02,hd.y+0.02,0.095,0,6.283);ctx.fill();
/* scarf */
ctx.strokeStyle='#b8543f';ctx.lineWidth=0.05;ctx.lineCap='round';
let sx0=P.neck.x,sy0=P.neck.y;
ctx.beginPath();ctx.moveTo(sx0,sy0);
for(let k=1;k<=3;k++){
const t=k/3,amp=Math.sin(tt*(6+wf*8)-k*1.1)*0.05*(0.4+wf);
ctx.lineTo(sx0+pd.x*(0.10+0.55*wf)*k+ud.x*(amp- (1-wf)*0.10*k),
sy0+pd.y*(0.10+0.55*wf)*k+ud.y*(amp-(1-wf)*0.10*k)+0.03*k*(1-wf));
}
ctx.stroke();
/* near arm */
const nh=P.hand;
const ne=ik(sh.x,sh.y,nh.x,nh.y,0.30,0.29,-1);
limb(sh,ne,nh,0.085,NEAR);
ctx.fillStyle=SKIN;ctx.beginPath();ctx.arc(nh.x,nh.y,0.045,0,6.283);ctx.fill();
if(umb&&umb.attached&&P.brace>0.35){ctx.beginPath();ctx.arc(fh.x,fh.y,0.042,0,6.283);ctx.fill();}
ctx.restore();
if(P.skid>0.05){
ctx.strokeStyle='rgba(180,200,215,0.25)';ctx.lineWidth=0.01;
ctx.beginPath();ctx.moveTo(P.footB.x-0.2,GY-0.01);ctx.lineTo(P.footB.x-0.2-P.skid*0.6,GY-0.01);ctx.stroke();
}
}
function drawUmbrella(u){
const poly=canopyLine(u),hub=u.S2;
const T0=poly[0],T1=poly[poly.length-1];
const mx=(T0.x+T1.x)*0.5,my=(T0.y+T1.y)*0.5;
let bx=mx-hub.x,by=my-hub.y;const bl=Math.hypot(bx,by)||1;bx/=bl;by/=bl;
const chord=Math.hypot(T1.x-T0.x,T1.y-T0.y);
const cpx=mx+bx*chord*0.28,cpy=my+by*chord*0.28;
ctx.save();
if(u.born>0){ctx.globalAlpha=clamp(1-u.born*2.2,0.15,1);}
ctx.beginPath();pathSmooth(poly);
ctx.quadraticCurveTo(cpx,cpy,T0.x,T0.y);ctx.closePath();
const g=ctx.createLinearGradient(hub.x,hub.y,mx+bx*0.4,my+by*0.4);
g.addColorStop(0,'#46b0bb');g.addColorStop(0.55,'#2b8593');g.addColorStop(1,'#164d59');
ctx.fillStyle=g;ctx.fill();
ctx.strokeStyle='rgba(8,18,24,0.5)';ctx.lineWidth=0.012;ctx.stroke();
/* foreshortened ghost ribs + panel shading */
const L=u.ribs[0].pts,R=u.ribs[1].pts;
[0.3,0.72].forEach(function(t){
ctx.beginPath();ctx.moveTo(hub.x,hub.y);
for(let k=1;k<L.length;k++){
const x=hub.x+(L[k].x-hub.x)*(1-t)+(R[k].x-hub.x)*t;
const y=hub.y+(L[k].y-hub.y)*(1-t)+(R[k].y-hub.y)*t;
ctx.lineTo(x,y);
}
ctx.strokeStyle='rgba(10,30,36,0.22)';ctx.lineWidth=0.014;ctx.stroke();
});
/* ribs */
for(let ri=0;ri<u.ribs.length;ri++){
const rib=u.ribs[ri];
ctx.beginPath();ctx.moveTo(hub.x,hub.y);
for(let k=1;k<rib.pts.length;k++)ctx.lineTo(rib.pts[k].x,rib.pts[k].y);
const st=clamp(Math.abs(rib.D)/DCRIT,0,1);
ctx.strokeStyle=rib.flash>0?'rgba(255,240,200,'+rib.flash+')'
:'rgba('+Math.round(20+180*st)+','+Math.round(30+40*st)+','+Math.round(38-10*st)+',0.85)';
ctx.lineWidth=0.017;ctx.lineCap='round';ctx.stroke();
/* tip ferrules */
ctx.fillStyle='#0f1a20';
const tp=rib.pts[rib.pts.length-1];
ctx.beginPath();ctx.arc(tp.x,tp.y,0.018,0,6.283);ctx.fill();
if(rib.broken>=0){
const bp=rib.pts[rib.broken+1];
ctx.strokeStyle='rgba(255,140,90,0.9)';ctx.lineWidth=0.012;
ctx.beginPath();ctx.arc(bp.x,bp.y,0.035,0,6.283);ctx.stroke();
}
}
/* stretchers */
for(let i=0;i<u.ropes.length;i++){
const r=u.ropes[i];
if(r.broken){
ctx.strokeStyle='rgba(120,130,140,0.5)';ctx.lineWidth=0.008;
ctx.beginPath();ctx.moveTo(r.a.x,r.a.y);
ctx.lineTo(r.a.x+(r.b.x-r.a.x)*0.45+0.04,r.a.y+(r.b.y-r.a.y)*0.45+0.05);ctx.stroke();
}else{
const ten=clamp(r.strain*14,0,1);
ctx.strokeStyle='rgba('+Math.round(150+100*ten)+','+Math.round(160-60*ten)+',170,0.75)';
ctx.lineWidth=0.010;
ctx.beginPath();ctx.moveTo(r.a.x,r.a.y);ctx.lineTo(r.b.x,r.b.y);ctx.stroke();
}
}
/* shaft, tip, handle */
ctx.strokeStyle='#2a3238';ctx.lineWidth=0.022;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(u.S0.x,u.S0.y);ctx.lineTo(u.S3.x,u.S3.y);ctx.stroke();
ctx.fillStyle='#8f9aa2';ctx.beginPath();ctx.arc(u.S3.x,u.S3.y,0.022,0,6.283);ctx.fill();
ctx.fillStyle='#3c464e';ctx.beginPath();ctx.arc(u.S1.x,u.S1.y,0.028,0,6.283);ctx.fill();
let dx=u.S0.x-u.S2.x,dy=u.S0.y-u.S2.y;const dl=Math.hypot(dx,dy)||1;dx/=dl;dy/=dl;
let px=-dy,py=dx;if(px<0){px=-px;py=-py;}
ctx.strokeStyle='#6b4a32';ctx.lineWidth=0.032;
ctx.beginPath();ctx.moveTo(u.S0.x+dx*0.02,u.S0.y+dy*0.02);
ctx.quadraticCurveTo(u.S0.x+dx*0.17,u.S0.y+dy*0.17,
u.S0.x+dx*0.15+px*0.13,u.S0.y+dy*0.15+py*0.13);
ctx.stroke();
ctx.restore();
}
function vignette(){
ctx.setTransform(DPR,0,0,DPR,0,0);
const g=ctx.createRadialGradient(CW*0.5,CH*0.5,CH*0.35,CW*0.5,CH*0.5,CH*0.95);
g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(0,0,0,0.45)');
ctx.fillStyle=g;ctx.fillRect(0,0,CW,CH);
const h=clamp((wind.speed-14)/26,0,1);
if(h>0){ctx.fillStyle='rgba(150,175,195,'+(h*0.10)+')';ctx.fillRect(0,0,CW,CH);}
}
function draw(){
drawSky();
ctx.setTransform(DPR*PPM,0,0,DPR*PPM,DPR*shk.x,DPR*shk.y);
drawBack();
drawGrass(false);
drawRain(true);
drawPerson();
if(umb)drawUmbrella(umb);
drawBits();
drawRain(false);
drawGrass(true);
vignette();
}
/* ---------- HUD ---------- */
const ws=document.getElementById('ws'),wk=document.getElementById('wk'),wm=document.getElementById('wm'),
bf=document.getElementById('bf'),gv=document.getElementById('gv'),gb=document.getElementById('gb'),
lv=document.getElementById('lv'),lb=document.getElementById('lb'),hv=document.getElementById('hv'),
hb=document.getElementById('hb'),st=document.getElementById('st'),msgEl=document.getElementById('msg'),
sl=document.getElementById('sl'),slv=document.getElementById('slv'),
bauto=document.getElementById('bauto');
function updateHUD(){
ws.textContent=wind.speed.toFixed(1);
wk.textContent=(wind.speed*3.6).toFixed(0);
wm.textContent=(wind.speed*2.2369).toFixed(0);
bf.textContent=beau(wind.speed);
const gust=wind.speed-wind.base;
gv.textContent=(gust>=0?'+':'')+gust.toFixed(1);
gb.style.width=clamp(Math.abs(gust)/8*100,0,100)+'%';
const load=umb?umb.load:0;
lv.textContent=Math.round(load*100)+'%';
lb.style.width=clamp(load*100,0,100)+'%';
lb.style.background=load>0.85?'#ff7a5a':'#ffd06f';
hv.textContent=Math.round(P.load)+' / '+Math.round(P.gripMax)+' N';
hb.style.width=clamp(P.load/P.gripMax*100,0,100)+'%';
let s='CANOPY: ';
if(!umb||!umb.attached)s+='GONE (lost '+lostCount+')';
else{
const n=umb.ribs[0].inv+umb.ribs[1].inv;
s+=umb.anyBroken?'BROKEN':(n===2?'INSIDE OUT':(n===1?'HALF INVERTED':'INTACT'));
s+=' · lost '+lostCount;
}
st.textContent=s;
msgEl.textContent=msgT>0?msgTxt:'';
slv.textContent=wind.target.toFixed(1);
}
/* ---------- input ---------- */
sl.addEventListener('input',function(){wind.target=parseFloat(sl.value);wind.auto=false;bauto.classList.remove('on');});
bauto.addEventListener('click',function(){wind.auto=!wind.auto;bauto.classList.toggle('on',wind.auto);});
document.getElementById('bgust').addEventListener('click',function(){gustT=0;gustPh=0;gustDur=1.3;gustA=0.8;});
document.getElementById('breset').addEventListener('click',function(){
umb=buildUmbrella(P.hand.x,P.hand.y,P.aimT);umb.born=0.3;say('FRESH BROLLY');});
addEventListener('keydown',function(e){
const k=e.key.toLowerCase();
if(k==='arrowup'||k==='w'){wind.target=Math.min(42,wind.target+1);wind.auto=false;bauto.classList.remove('on');e.preventDefault();}
else if(k==='arrowdown'||k==='s'){wind.target=Math.max(0,wind.target-1);wind.auto=false;bauto.classList.remove('on');e.preventDefault();}
else if(k==='a'){wind.auto=!wind.auto;bauto.classList.toggle('on',wind.auto);}
else if(k==='g'){gustT=0;gustPh=0;gustDur=1.3;gustA=0.8;}
else if(k==='r'){umb=buildUmbrella(P.hand.x,P.hand.y,P.aimT);umb.born=0.3;say('FRESH BROLLY');}
else if(k==='p'){paused=!paused;}
if(!wind.auto)sl.value=wind.target.toFixed(1);
});
/* ---------- boot & loop ---------- */
function resize(){
DPR=Math.min(2,window.devicePixelRatio||1);
CW=window.innerWidth;CH=window.innerHeight;
cv.width=Math.round(CW*DPR);cv.height=Math.round(CH*DPR);
cv.style.width=CW+'px';cv.style.height=CH+'px';
PPM=CH/SH;SW=CW/PPM;
P.home=clamp(SW*0.34,1.6,SW-2.6);
if(!P.init){P.x=P.home;P.init=true;}
buildScenery();
}
addEventListener('resize',resize);
resize();
updatePerson(0.016);
umb=buildUmbrella(P.hand.x,P.hand.y,P.aimT);
let last=performance.now(),acc=0;
const FIX=1/360;
function frame(now){
let dt=(now-last)/1000;last=now;
dt=clamp(dt,0,0.05);
if(!paused){
tt+=dt;
msgT=Math.max(0,msgT-dt);
updateWind(dt);
updatePerson(dt);
if(umb){
computeShadow(umb);
acc+=dt;
let n=0;
while(acc>=FIX&&n<14){stepUmbrella(umb,FIX);acc-=FIX;n++;}
if(acc>FIX)acc=0;
updateRibStates(umb,dt);
}
updateRain(dt);
updateBits(dt);
shk.m*=Math.pow(0.02,dt);
const jitter=clamp((wind.speed-22)/40,0,0.5)*0.004;
shk.x=((Math.random()-0.5)*shk.m+ (Math.random()-0.5)*jitter)*PPM;
shk.y=((Math.random()-0.5)*shk.m+ (Math.random()-0.5)*jitter)*PPM;
}
draw();
updateHUD();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it looks right. The load builds before anything dramatic happens: the canopy visibly flexes and the windward side flattens while the frame-load bar creeps up, and somewhere around Force 7–8 (usually on a gust, not on the steady value) one rib snaps through with a bang and the other follows a beat later — the asymmetric half-inverted stage is the bit most sims skip and it's the bit that feels true. The stretcher goes visibly taut before it happens and turns red; break it and that rib gives up almost immediately. Weathervaning works properly: because the wrist torque is capped, the umbrella slowly gets wrenched round from "pointed into the wind" to "streaming downwind over the shoulder" as the wind climbs, and the person's aim error is the thing that saturates first, not the grip force. The handle slides a few centimetres out of the drawn hand under load before the release triggers, the person leans on an arctangent of the actual horizontal force (so they stand up again when you wind it back down), and the feet skid and then walk back upwind when the gust passes. Rain follows the wind vector, spawns off the upwind edge at the right flux so it doesn't leave a diagonal hole, and is genuinely blocked by the canopy polyline — you can see the dry cone move as the umbrella is wrenched around, and watch it vanish the moment the thing inverts.
Where it looks wrong. It's a single plane. A real umbrella's worst behaviour is out of plane — the yaw twist that rolls the canopy edge-on, the sideways skew where one half stays down and the other rides up, the spin as it escapes. Mine can't do any of that; the "escaped" umbrella tumbles like a flat plate with the same rib aero, so it flies too predictably and never generates the bowl-shaped lift that makes real ones climb. The fabric isn't fabric: there is no membrane, no billow between ribs, no ripple, no snapping panel noise — the profile you see is the two simulated ribs and everything else is drawn interpolation, so the far side of the canopy is a lie. Inversion is triggered by a summed-curvature threshold, not a real Euler buckling load, so the critical wind speed is a tuned number rather than a derived one; gusts flip it earlier than a steady wind of the same peak, which is correct, but the exact threshold is my choice. The person is kinematic, not dynamic: they can lean past what a real centre-of-mass would allow, they never stagger, never take a recovery step, never get lifted, and their feet slide without the ankles or knees complaining. The "repair" when the wind drops below 8 m/s just rebuilds the umbrella after 1.7 s — a real person wrestling a bent frame back is much uglier and often fails. Rain doesn't pool, run to the rim, or sheet off the downwind edge, only the occasional bounce particle. And the PBD stiffnesses are tied to the fixed 1/360 substep; if your machine can't keep up, the frame gets softer rather than just slower.