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 two ribs seen edge-on, one for each half of the umbrella in the side view. Each rib is a short chain of point masses. The crown and the inner rib (crown to the stretcher joint) are rigid in the shaft's frame, because the crown–runner–stretcher triangle holds them there. The three outer segments are free Verlet particles. Torsion springs at each joint give the rib its curve. A tension-only hoop limit stands in for the fabric, so the rim cannot spread past the cloth.
Each segment feels the air relative to its own motion:
Inversion is a discrete state on the "knuckle", the joint where the stretcher meets the rib. That spring has two rest wells: normal (outer rib continues the dome, −10°) and inside-out (outer rib flipped up, +85°). When the outer rib is pushed upward past the over-centre angle of +35°, the state flips and the spring starts pulling toward the inverted well. That is the snap. It flips back only below +27° (hysteresis), which in practice means the person's hand pulls it back.
What decides when it happens is the lifting moment about the knuckle against the knuckle stiffness. That moment comes from:
Each inversion permanently bends the rib a few degrees, so the next inversion comes easier.
Three separate limits handle the rest:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Umbrella in a rising gale</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#0b0f16;color:#dde5ee;font:14px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
canvas{display:block;position:fixed;left:0;top:0;width:100vw;height:100vh}
#hud{position:fixed;left:14px;top:12px;padding:10px 14px;background:rgba(10,14,20,.6);border:1px solid rgba(255,255,255,.08);border-radius:10px;width:270px;pointer-events:none}
#spd{font-size:40px;font-weight:700;letter-spacing:-.5px;font-variant-numeric:tabular-nums;line-height:1.1}
#spd small{font-size:16px;font-weight:500;opacity:.7}
#sub{opacity:.88}
#gust,#mode{opacity:.7;font-variant-numeric:tabular-nums;font-size:13px}
.lbl{font-size:12px;opacity:.7;margin-top:7px}
.bar{height:6px;background:rgba(255,255,255,.1);border-radius:3px;margin:3px 0 2px;overflow:hidden}
.bar i{display:block;height:100%;width:0;background:#7fc4ff}
#stat{margin-top:8px;min-height:4.2em;white-space:pre-line;font-size:13px}
#ctl{position:fixed;left:14px;right:14px;bottom:12px;display:flex;gap:10px;align-items:center;padding:8px 12px;background:rgba(10,14,20,.6);border-radius:10px;border:1px solid rgba(255,255,255,.08)}
#ctl input{flex:1;accent-color:#e2525c}
button{background:#222b38;color:#dde5ee;border:1px solid #3a4658;border-radius:7px;padding:6px 10px;font:inherit;cursor:pointer;white-space:nowrap}
button.on{background:#7a2a30;border-color:#c24852}
#keys{font-size:12px;opacity:.6;white-space:nowrap}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div id="spd">0.0 <small>m/s</small></div>
<div id="sub"></div>
<div id="gust"></div>
<div id="mode"></div>
<div class="lbl">grip load</div><div class="bar"><i id="bGrip"></i></div>
<div class="lbl">frame load (worst stretcher)</div><div class="bar"><i id="bFrame"></i></div>
<div id="stat"></div>
</div>
<div id="ctl">
<button id="auto" class="on">Auto: rising</button>
<span>wind</span>
<input id="sl" type="range" min="0" max="40" step="0.1" value="2">
<button id="newu">New umbrella</button>
<span id="keys">↑/↓ wind · A auto · N new · R reset</span>
</div>
<script>
'use strict';
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const D2R=Math.PI/180,RHO=1.2,GRAV=9.81;
let DPR=1,CW=0,CH=0,S=100,OX=0,OY=0;
const X=x=>OX+x*S,Y=y=>OY-y*S;
const viewL=()=>-OX/S,viewR=()=>(CW-OX)/S,viewT=()=>OY/S;
function clamp(v,a,b){return v<a?a:v>b?b:v}
/* ---------------- background ---------------- */
const bgc=document.createElement('canvas'),bgx=bgc.getContext('2d');
function buildBG(){
bgc.width=cv.width;bgc.height=cv.height;const c=bgx;c.setTransform(DPR,0,0,DPR,0,0);
const g=c.createLinearGradient(0,0,0,OY);g.addColorStop(0,'#0c121d');g.addColorStop(1,'#283240');
c.fillStyle=g;c.fillRect(0,0,CW,OY);
let seed=11;const rnd=()=>{seed=(seed*16807)%2147483647;return seed/2147483647;};
for(let layer=0;layer<2;layer++){
let x=-30;const col=layer?'#131922':'#1c2532',sc=CH/800;
while(x<CW+30){
const w=(layer?70:45)+rnd()*(layer?110:80),hg=((layer?70:130)+rnd()*(layer?150:190))*sc;
c.fillStyle=col;c.fillRect(x,OY-hg,w,hg);
for(let wy=OY-hg+10*sc;wy<OY-14*sc;wy+=16*sc)for(let wx=x+6;wx<x+w-8;wx+=13){
if(rnd()<(layer?0.12:0.07)){c.fillStyle=layer?'rgba(255,200,120,0.32)':'rgba(255,210,150,0.16)';c.fillRect(wx,wy,6,8*sc);}
}
x+=w+rnd()*14;
}
}
const gg=c.createLinearGradient(0,OY,0,CH);gg.addColorStop(0,'#1e242d');gg.addColorStop(1,'#0b0e13');
c.fillStyle=gg;c.fillRect(0,OY,CW,CH-OY);
c.fillStyle='rgba(170,190,220,0.14)';c.fillRect(0,OY,CW,1.5);
}
function resize(){
DPR=Math.min(2,window.devicePixelRatio||1);CW=innerWidth;CH=innerHeight;
cv.width=Math.round(CW*DPR);cv.height=Math.round(CH*DPR);
S=Math.min(CW/9.5,CH/5.0);OX=CW*0.40;OY=CH*0.80;buildBG();
}
addEventListener('resize',resize);resize();
/* ---------------- wind ---------------- */
let windSet=2,Umean=2,auto=true,T=0,Dadv=0,LULL=1;
const RAMP=0.25,WMAX=40;
let WX=0,WY=0;
function gn1(z){return 0.5*Math.sin(0.61*z+1.3)+0.33*Math.sin(1.37*z+0.2)+0.22*Math.sin(2.71*z+2.1)+0.12*Math.sin(4.9*z+4.0);}
function gn2(z){return 0.5*Math.sin(0.83*z+0.7)+0.35*Math.sin(1.91*z+2.9)+0.2*Math.sin(3.7*z+1.1);}
function wind(x,y){
const U=Umean,z=(Dadv-x)/6,yy=y>0.05?y:0.05;
const prof=Math.min(1.1,Math.pow(yy/1.6,0.16));
WX=U*(1+0.22*gn1(z))*prof*LULL;
WY=U*0.17*gn2(z*1.3+5)*Math.min(1,yy/0.6);
}
/* ---------------- umbrella ---------------- */
const A_IN=0.22,L_OUT=0.11,PHI0=100*D2R,RUN=0.22,HAND_B=0.10,SHAFT=0.85,COM_B=0.64;
const TH_N=-10*D2R,TH_I=85*D2R,TH_B=35*D2R,TH_HYS=8*D2R,TH_J=-12*D2R;
const K_KN=3.2,K_J=1.8,C_KN=0.02,C_J=0.012;
const MN=0.03,M_K=0.35,M_FREE=0.53,I_HAND=0.24,I_FREE=0.06,KG=60,CG=4;
const SPAN=[0.75,0.62,0.62,0.62];
const C_TOP=0.8,C_UND=1.5,C_SUC=0.3;
const STRUT_ARM=0.168,STRUT_BREAK=320,HSTEP=1/720;
const RA=[0],RR=[0],RMAX=[0,0,0,0,0];
(function(){let a=A_IN*Math.cos(PHI0),r=A_IN*Math.sin(PHI0);RA.push(a);RR.push(r);
let phi=PHI0-TH_N;for(let j=2;j<=4;j++){a+=L_OUT*Math.cos(phi);r+=L_OUT*Math.sin(phi);RA.push(a);RR.push(r);RMAX[j]=r*1.1;phi-=TH_J;}})();
const G={};
function geom(u,Hx,Hy){
const ux=Math.sin(u.alpha),uy=Math.cos(u.alpha);
G.ux=ux;G.uy=uy;G.px=uy;G.py=-ux;
if(u.held){G.bx=Hx-ux*HAND_B;G.by=Hy-uy*HAND_B;}else{G.bx=u.cx-ux*COM_B;G.by=u.cy-uy*COM_B;}
G.cx=G.bx+ux*SHAFT;G.cy=G.by+uy*SHAFT;G.rx=G.cx-ux*RUN;G.ry=G.cy-uy*RUN;
}
function newSide(s){const n=[];for(let j=0;j<5;j++)n.push({x:0,y:0,px:0,py:0,fx:0,fy:0});
return {s,n,state:0,broken:false,restKn:TH_N,thp:[0,TH_N,TH_J,TH_J,0],load:0,invT:0,inv:0,M:0};}
function makeUmb(){return {held:true,alpha:0,omega:0,alphaT:0,tauMax:14,fGrip:140,cx:0,cy:0,vx:0,vy:0,
sides:[newSide(-1),newSide(1)],slip:0,wrenchT:0,sat:false,why:'',freeT:0,gone:false,goneT:0,accFx:0,accFy:0,accN:0};}
function place(u,Hx,Hy){
geom(u,Hx,Hy);
for(const sd of u.sides)for(let j=0;j<5;j++){const nd=sd.n[j];
nd.x=nd.px=G.cx+G.ux*RA[j]+G.px*sd.s*RR[j];nd.y=nd.py=G.cy+G.uy*RA[j]+G.py*sd.s*RR[j];}
}
function bend(sd,j,k,rest,cd,h,lo,hi){
const s=sd.s,n=sd.n,p=n[j-1],c=n[j],q=n[j+1];
const d1x=c.x-p.x,d1y=c.y-p.y,d2x=q.x-c.x,d2y=q.y-c.y;
const L1=Math.hypot(d1x,d1y)||1e-6,L2=Math.hypot(d2x,d2y)||1e-6;
const th=s*Math.atan2(d1x*d2y-d1y*d2x,d1x*d2x+d1y*d2y);
const thd=clamp((th-sd.thp[j])/h,-300,300);sd.thp[j]=th;
let tau=-k*(th-rest)-cd*thd;
if(th<lo)tau+=-25*(th-lo);else if(th>hi)tau+=-25*(th-hi);
const tw=s*tau;
const fq=tw/L2,qx=-d2y/L2*fq,qy=d2x/L2*fq;
q.fx+=qx;q.fy+=qy;c.fx-=qx;c.fy-=qy;
const fp=-tw/L1,ppx=d1y/L1*fp,ppy=-d1x/L1*fp;
p.fx+=ppx;p.fy+=ppy;c.fx-=ppx;c.fy-=ppy;
return th;
}
function angAt(n,j,s){const p=n[j-1],c=n[j],q=n[j+1];const d1x=c.x-p.x,d1y=c.y-p.y,d2x=q.x-c.x,d2y=q.y-c.y;
return s*Math.atan2(d1x*d2y-d1y*d2x,d1x*d2x+d1y*d2y);}
function rootBroken(sd,h){
const s=sd.s,n=sd.n,a=n[0],b=n[1];
const rx=G.ux*Math.cos(PHI0)+G.px*s*Math.sin(PHI0),ry=G.uy*Math.cos(PHI0)+G.py*s*Math.sin(PHI0);
const dx=b.x-a.x,dy=b.y-a.y,L=Math.hypot(dx,dy)||1e-6;
const th=s*Math.atan2(rx*dy-ry*dx,rx*dx+ry*dy);
const thd=clamp((th-sd.thp[0])/h,-300,300);sd.thp[0]=th;
let tau=-0.35*th-0.01*thd;if(th<-1.2)tau+=-6*(th+1.2);if(th>1.6)tau+=-6*(th-1.6);
const f=s*tau/L;b.fx+=-dy/L*f;b.fy+=dx/L*f;
}
function distFix(a,b,L){const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1e-6;b.x=a.x+dx/d*L;b.y=a.y+dy/d*L;}
function distBoth(a,b,L){const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1e-6,k=(d-L)/d*0.5;a.x+=dx*k;a.y+=dy*k;b.x-=dx*k;b.y-=dy*k;}
function contact(u,x,y){
if(y>=0)return;
const rx=x-u.cx,ry=y-u.cy;u.cy-=y;
const wc=-u.omega,vpx=u.vx-wc*ry,vpy=u.vy+wc*rx;
if(vpy<0){
const kn=1/M_FREE+rx*rx/I_FREE,jn=-1.25*vpy/kn,kt=1/M_FREE+ry*ry/I_FREE;
let jt=clamp(-vpx/kt,-0.6*jn,0.6*jn);
u.vx+=jt/M_FREE;u.vy+=jn/M_FREE;u.omega-=(rx*jn-ry*jt)/I_FREE;
}
}
function release(u,why,Hx,Hy,Hvx,Hvy){
geom(u,Hx,Hy);
const comx=G.bx+G.ux*COM_B,comy=G.by+G.uy*COM_B,rx=comx-Hx,ry=comy-Hy,wc=-u.omega;
u.held=false;u.why=why;u.cx=comx;u.cy=comy;u.vx=Hvx-wc*ry;u.vy=Hvy+wc*rx;u.freeT=0;
P.reach=1.4;P.fix=null;
}
function stepUmb(u,h,Hx,Hy,Hvx,Hvy){
geom(u,Hx,Hy);
const ux=G.ux,uy=G.uy,pX=G.px,pY=G.py,cx=G.cx,cy=G.cy,c0=Math.cos(PHI0),s0=Math.sin(PHI0);
for(const sd of u.sides){const s=sd.s,n=sd.n;
n[0].px=n[0].x;n[0].py=n[0].y;n[0].x=cx;n[0].y=cy;
if(!sd.broken){const a=n[1];a.px=a.x;a.py=a.y;a.x=cx+(ux*c0+pX*s*s0)*A_IN;a.y=cy+(uy*c0+pY*s*s0)*A_IN;}
}
const pvx=u.held?Hx:u.cx,pvy=u.held?Hy:u.cy;
let Fx=0,Fy=0,Tq=0;
for(const sd of u.sides){const s=sd.s,n=sd.n;let M=0;
for(let j=0;j<5;j++){n[j].fx=0;n[j].fy=(j>=2||(j===1&&sd.broken))?-MN*GRAV:0;}
for(let j=0;j<4;j++){const a=n[j],b=n[j+1];
let tx=b.x-a.x,ty=b.y-a.y;const L=Math.hypot(tx,ty)||1e-6;tx/=L;ty/=L;
const mx=(a.x+b.x)/2,my=(a.y+b.y)/2;
const vmx=((a.x-a.px)+(b.x-b.px))/(2*h),vmy=((a.y-a.py)+(b.y-b.py))/(2*h);
wind(mx,my);const Vx=WX-vmx,Vy=WY-vmy;
const nx=-s*ty,ny=s*tx,q=Vx*nx+Vy*ny,vt=Vx*tx+Vy*ty;
const fn=0.5*RHO*SPAN[j]*L*((q>0?C_UND:C_TOP)*Math.abs(q)*q+C_SUC*vt*vt);
const fx=fn*nx,fy=fn*ny;
a.fx+=fx/2;a.fy+=fy/2;b.fx+=fx/2;b.fy+=fy/2;
Fx+=fx;Fy+=fy;Tq+=(mx-pvx)*fy-(my-pvy)*fx;M+=(mx-cx)*fy-(my-cy)*fx;
}
for(let j=1;j<5;j++){if(j===1&&!sd.broken)continue;const w=-MN*GRAV;Fy+=w;Tq+=(n[j].x-pvx)*w;M+=(n[j].x-cx)*w;}
sd.M=M;
}
if(P.fix&&u.held&&P.fix.t>0.45){const sd=u.sides[P.fix.i],s=sd.s,k=260*Math.min(1,(P.fix.t-0.45)*3);
for(let j=2;j<5;j++){const nd=sd.n[j],tx=cx+ux*RA[j]+pX*s*RR[j],ty=cy+uy*RA[j]+pY*s*RR[j];
nd.fx+=k*(tx-nd.x)-1.5*(nd.x-nd.px)/h;nd.fy+=k*(ty-nd.y)-1.5*(nd.y-nd.py)/h;}}
for(const sd of u.sides){
if(sd.broken)rootBroken(sd,h);
const th=angAt(sd.n,1,sd.s);
if(sd.state===0&&th>TH_B){sd.state=1;sd.inv++;sd.restKn=Math.min(TH_N+18*D2R,sd.restKn+3*D2R);}
else if(sd.state===1&&th<TH_B-TH_HYS){sd.state=0;}
bend(sd,1,K_KN,sd.state?TH_I:sd.restKn,C_KN,h,-60*D2R,125*D2R);
bend(sd,2,K_J,TH_J,C_J,h,-75*D2R,75*D2R);
bend(sd,3,K_J,TH_J,C_J,h,-75*D2R,75*D2R);
sd.invT=sd.state?sd.invT+h:0;
}
if(u.held){
const comx=G.bx+ux*COM_B,tg=(comx-Hx)*(-M_K*GRAV);
const Gext=-(Tq+tg);
let gr=KG*(u.alphaT-u.alpha)-CG*u.omega;
u.sat=Math.abs(gr)>u.tauMax*0.98;gr=clamp(gr,-u.tauMax,u.tauMax);
u.omega+=(Gext+gr)/I_HAND*h;u.alpha+=u.omega*h;
const fy=Fy-M_K*GRAV;u.accFx+=Fx;u.accFy+=fy;u.accN++;
const Fm=Math.hypot(Fx,fy);
if(Fm>u.fGrip)u.slip+=(Fm/u.fGrip-1)*h*4;else u.slip=Math.max(0,u.slip-h*0.5);
if(Math.abs(u.alpha-u.alphaT)>1.7)u.wrenchT+=h;else u.wrenchT=Math.max(0,u.wrenchT-h);
if(u.slip>0.3)release(u,'ripped out of their hands',Hx,Hy,Hvx,Hvy);
else if(u.wrenchT>0.35)release(u,'wrenched out of their grip',Hx,Hy,Hvx,Hvy);
}else{
u.vx+=(Fx/M_FREE-0.05*u.vx)*h;u.vy+=((Fy-M_K*GRAV)/M_FREE)*h;
u.cx+=u.vx*h;u.cy+=u.vy*h;
u.omega+=-Tq/I_FREE*h;u.omega*=1-0.2*h;u.alpha+=u.omega*h;
geom(u,0,0);
contact(u,G.bx,G.by);geom(u,0,0);contact(u,G.cx,G.cy);
for(const sd of u.sides){geom(u,0,0);const s=sd.s;
contact(u,G.cx+(G.ux*c0+G.px*s*s0)*A_IN,G.cy+(G.uy*c0+G.py*s*s0)*A_IN);}
}
const h2=h*h;
for(const sd of u.sides)for(let j=sd.broken?1:2;j<5;j++){const nd=sd.n[j];
const vx=(nd.x-nd.px)*0.9985,vy=(nd.y-nd.py)*0.9985;nd.px=nd.x;nd.py=nd.y;
nd.x+=vx+nd.fx/MN*h2;nd.y+=vy+nd.fy/MN*h2;}
for(let it=0;it<4;it++)for(const sd of u.sides){const s=sd.s,n=sd.n;
if(sd.broken)distFix(n[0],n[1],A_IN);
for(let j=1;j<4;j++){if(j===1&&!sd.broken)distFix(n[1],n[2],L_OUT);else distBoth(n[j],n[j+1],L_OUT);}
for(let j=2;j<5;j++){const nd=n[j],r=s*((nd.x-cx)*pX+(nd.y-cy)*pY);
if(r>RMAX[j]){const d=(r-RMAX[j])*0.6;nd.x-=s*pX*d;nd.y-=s*pY*d;}}
for(let j=1;j<5;j++){if(j===1&&!sd.broken)continue;const nd=n[j];
if(nd.y<0){nd.y=0;nd.py=0;nd.px=nd.x-(nd.x-nd.px)*0.6;}}
}
for(const sd of u.sides){
if(sd.broken){sd.load*=0.99;continue;}
const f=Math.abs(sd.M)/STRUT_ARM;sd.load+=(f-sd.load)*Math.min(1,h/0.03);
if(sd.load>STRUT_BREAK){sd.broken=true;sd.thp[0]=0;}
}
}
/* ---------------- person ---------------- */
const P={beta:0,bdot:0,stance:0,crouch:0,two:0,Us:2,Ug:2,Vg:0,tauS:0,Fsx:0,Fsy:0,Fax:0,Fay:0,
Hx:0,Hy:1.5,Hnx:0,Hny:1.5,at:0,reach:0,hitRate:0,hits:0,fix:null,fixCool:0,J:null};
let umb=null,umbCount=0;
function ik(ax,ay,bx,by,l1,l2,sgn){
const dx=bx-ax,dy=by-ay,d=Math.hypot(dx,dy)||1e-6;
const dn=Math.max(Math.min(d,l1+l2-1e-4),Math.abs(l1-l2)+1e-4);
const ux=dx/d,uy=dy/d,a=(l1*l1-l2*l2+dn*dn)/(2*dn),hh=Math.sqrt(Math.max(0,l1*l1-a*a));
return {ex:ax+ux*a-uy*hh*sgn,ey:ay+uy*a+ux*hh*sgn,hx:ax+ux*dn,hy:ay+uy*dn};
}
function armIK(sh,tx,ty){const a=ik(sh.x,sh.y,tx,ty,0.30,0.29,1),b=ik(sh.x,sh.y,tx,ty,0.30,0.29,-1);return a.ey<b.ey?a:b;}
function pose(){
const st=P.stance,cr=P.crouch,b=P.beta;
const aF={x:-0.13-0.12*st,y:0.08},aR={x:0.13+0.30*st,y:0.08};
const pivx=(aF.x+aR.x)/2,pivy=0.07,hipH=0.92-0.17*cr;
const hip={x:pivx-Math.sin(b)*hipH,y:pivy+Math.cos(b)*hipH};
for(let k=0;k<3;k++)for(const a of [aF,aR]){const dx=hip.x-a.x,dy=hip.y-a.y,d=Math.hypot(dx,dy);
if(d>0.88){hip.x=a.x+dx/d*0.88;hip.y=a.y+dy/d*0.88;}}
const ta=b+0.18*cr+0.05;
const neck={x:hip.x-Math.sin(ta)*0.54,y:hip.y+Math.cos(ta)*0.54};
const sh={x:neck.x+Math.sin(ta)*0.05,y:neck.y-Math.cos(ta)*0.05};
const hd=ta+0.25*cr+0.12;
const head={x:neck.x-Math.sin(hd)*0.15,y:neck.y+Math.cos(hd)*0.15};
const kF=ik(hip.x,hip.y,aF.x,aF.y,0.45,0.45,-1),kR=ik(hip.x,hip.y,aR.x,aR.y,0.45,0.45,-1);
return {aF,aR,pivx,pivy,hip,neck,sh,head,ta,kF:{x:kF.ex,y:kF.ey},kR:{x:kR.ex,y:kR.ey}};
}
function personPrep(dt){
wind(-0.2,1.4);
P.Us+=(Umean-P.Us)*Math.min(1,dt/1.0);
P.Ug+=(WX-P.Ug)*Math.min(1,dt/0.7);
P.Vg+=(WY-P.Vg)*Math.min(1,dt/0.7);
P.crouch+=(clamp((P.Us-9)/18,0,1)-P.crouch)*Math.min(1,dt/1.5);
P.stance+=(clamp((P.Us-4)/14,0,1)-P.stance)*Math.min(1,dt/1.2);
const held=!!(umb&&umb.held&&!umb.gone);
let inv=false;if(held)for(const sd of umb.sides)if(sd.state)inv=true;
const Fm=Math.hypot(P.Fsx,P.Fsy);
P.two+=(((held&&(P.Us>10||Fm>45||inv))?1:0)-P.two)*Math.min(1,dt/0.35);
P.at=clamp(Math.atan2(-P.Ug,6.5-P.Vg),-1.25,0.35);
if(held){
umb.alphaT=inv?Math.max(-1.45,P.at-0.3):P.at;
umb.tauMax=14+21*P.two;umb.fGrip=140+150*P.two;
}
P.fixCool-=dt;
if(P.fix){P.fix.t+=dt;const sd=held?umb.sides[P.fix.i]:null;
if(!sd||sd.state===0||sd.broken||P.fix.t>2.4||P.Us>16){P.fix=null;P.fixCool=1.5;}}
else if(held&&P.fixCool<=0&&P.Us<15){for(let i=0;i<2;i++){const sd=umb.sides[i];
if(sd.state&&!sd.broken&&sd.invT>1.2){P.fix={i,t:0};break;}}}
P.reach=Math.max(0,P.reach-dt);
P.J=pose();
const sh=P.J.sh,at=held?umb.alphaT:P.at,tilt=clamp(-at,0,1.45);
let hx=sh.x-0.17-0.10*tilt,hy=sh.y+0.24-0.26*tilt;
const kA=1600+1600*P.two;
hx+=clamp(P.Fsx/kA,-0.16,0.16);hy+=clamp(P.Fsy/kA,-0.16,0.16);
const dx=hx-sh.x,dy=hy-sh.y,d=Math.hypot(dx,dy);
if(d>0.56){hx=sh.x+dx/d*0.56;hy=sh.y+dy/d*0.56;}
P.Hnx=hx;P.Hny=hy;
}
function personDyn(dt){
const J=P.J;
wind(J.hip.x,1.1);
const A=0.72-0.2*P.crouch,Fb=0.5*RHO*A*WX*Math.abs(WX);
let tw=-(1.05-0.15*P.crouch-J.pivy)*Fb;
if(umb&&umb.held&&!umb.gone)tw+=(P.Hx-J.pivx)*P.Fay-(P.Hy-J.pivy)*P.Fax;
const Mgh=72*GRAV*1.0;
P.tauS+=(tw-P.tauS)*Math.min(1,dt/0.5);
const bref=Math.asin(clamp(-P.tauS/Mgh,-0.35,0.62));
const acc=(tw+Mgh*Math.sin(P.beta)-1500*(P.beta-bref)-420*P.bdot)/85;
P.bdot+=acc*dt;P.beta+=P.bdot*dt;
if(P.beta>0.62){P.beta=0.62;if(P.bdot>0)P.bdot=0;}
if(P.beta<-0.3){P.beta=-0.3;if(P.bdot<0)P.bdot=0;}
}
function newUmb(){
umb=makeUmb();umbCount++;umb.alpha=umb.alphaT=P.at;P.fix=null;P.reach=0;
personPrep(0);P.Hx=P.Hnx;P.Hy=P.Hny;place(umb,P.Hx,P.Hy);
}
/* ---------------- rain, splashes, air streaks ---------------- */
const NR=700,rx=new Float32Array(NR),ry=new Float32Array(NR),rvx=new Float32Array(NR),rvy=new Float32Array(NR);
const splashes=[];
const NS=50,sx=new Float32Array(NS),sy=new Float32Array(NS);
function spawnDrop(i){
const L=viewL()-0.5,R=viewR()+0.5,Tp=viewT()+0.3;
wind(0,2);const vx=WX,hfl=Math.abs(vx)*Tp,vfl=6.5*(R-L);
if(Math.random()<hfl/(hfl+vfl)){rx[i]=L;ry[i]=Math.random()*Tp;}
else{const back=Math.min(30,vx/6.5*Tp);rx[i]=L-back*Math.random()+Math.random()*(R-L+back*0.0);ry[i]=Tp;}
rvx[i]=vx;rvy[i]=-6.5+WY;
}
function rainInit(){for(let i=0;i<NR;i++){spawnDrop(i);rx[i]=viewL()+Math.random()*(viewR()-viewL());ry[i]=Math.random()*viewT();}
for(let i=0;i<NS;i++){sx[i]=viewL()+Math.random()*(viewR()-viewL());sy[i]=0.2+Math.random()*(viewT()-0.2);}}
function segHit(x0,y0,x1,y1,ax,ay,bx,by){
const rX=x1-x0,rY=y1-y0,sX=bx-ax,sY=by-ay,den=rX*sY-rY*sX;
if(Math.abs(den)<1e-12)return -1;
const qx=ax-x0,qy=ay-y0,t=(qx*sY-qy*sX)/den,v=(qx*rY-qy*rX)/den;
return (t>=0&&t<=1&&v>=0&&v<=1)?t:-1;
}
function distSeg(px,py,ax,ay,bx,by){const dx=bx-ax,dy=by-ay,l=dx*dx+dy*dy||1e-9;
const t=clamp(((px-ax)*dx+(py-ay)*dy)/l,0,1);return Math.hypot(px-ax-dx*t,py-ay-dy*t);}
function addSplash(x,y,n){if(splashes.length>400)return;for(let k=0;k<n;k++)
splashes.push({x,y,vx:(Math.random()-0.5)*1.5+Umean*0.05,vy:0.8+Math.random()*1.4,l:0.25+Math.random()*0.15});}
function rainStep(dt){
const k=Math.min(1,dt/0.1),segs=[];let bx0=1e9,by0=1e9,bx1=-1e9,by1=-1e9;
if(umb&&!umb.gone)for(const sd of umb.sides)for(let j=0;j<4;j++){const a=sd.n[j],b=sd.n[j+1];
segs.push(a.x,a.y,b.x,b.y);bx0=Math.min(bx0,a.x,b.x);bx1=Math.max(bx1,a.x,b.x);by0=Math.min(by0,a.y,b.y);by1=Math.max(by1,a.y,b.y);}
const J=P.J;let hits=0;const R=viewR()+1;
for(let i=0;i<NR;i++){
wind(rx[i],ry[i]);rvx[i]+=(WX-rvx[i])*k;rvy[i]+=(-6.5+WY-rvy[i])*k;
const x0=rx[i],y0=ry[i],x1=x0+rvx[i]*dt,y1=y0+rvy[i]*dt;
let hit=false,hx=0,hy=0;
if(segs.length&&Math.max(x0,x1)>=bx0-0.05&&Math.min(x0,x1)<=bx1+0.05&&Math.max(y0,y1)>=by0-0.05&&Math.min(y0,y1)<=by1+0.05){
for(let s=0;s<segs.length;s+=4){const t=segHit(x0,y0,x1,y1,segs[s],segs[s+1],segs[s+2],segs[s+3]);
if(t>=0){hit=true;hx=x0+(x1-x0)*t;hy=y0+(y1-y0)*t;break;}}
}
if(!hit&&y1<2&&y1>0.6&&Math.abs(x1-J.hip.x)<0.6){
if(Math.hypot(x1-J.head.x,y1-J.head.y)<0.13||distSeg(x1,y1,J.hip.x,J.hip.y,J.neck.x,J.neck.y)<0.16){hit=true;hx=x1;hy=y1;hits++;}
}
if(!hit&&y1<=0){hit=true;hx=x1;hy=0;}
if(hit){if(Math.random()<0.5)addSplash(hx,hy,hy===0?1:2);spawnDrop(i);}
else{rx[i]=x1;ry[i]=y1;if(x1>R||x1<viewL()-40)spawnDrop(i);}
}
P.hitRate+=(hits/dt-P.hitRate)*Math.min(1,dt/1.0);
for(let i=splashes.length-1;i>=0;i--){const s=splashes[i];s.vy-=9.8*dt;s.x+=s.vx*dt;s.y+=s.vy*dt;s.l-=dt;
if(s.l<=0||s.y<-0.02)splashes.splice(i,1);}
for(let i=0;i<NS;i++){wind(sx[i],sy[i]);sx[i]+=WX*dt;sy[i]+=WY*dt*0.5;
if(sx[i]>viewR()+1||sy[i]<0.1||sy[i]>viewT()){sx[i]=viewL()-1-Math.random()*2;sy[i]=0.2+Math.random()*(viewT()-0.2);}}
}
/* ---------------- drawing ---------------- */
function line(ax,ay,bx,by){ctx.beginPath();ctx.moveTo(X(ax),Y(ay));ctx.lineTo(X(bx),Y(by));ctx.stroke();}
function limb(a,b,c,w,col){ctx.strokeStyle=col;ctx.lineWidth=w*S;ctx.beginPath();ctx.moveTo(X(a.x),Y(a.y));ctx.lineTo(X(b.x),Y(b.y));ctx.lineTo(X(c.x),Y(c.y));ctx.stroke();}
function dot(x,y,r,col){ctx.fillStyle=col;ctx.beginPath();ctx.arc(X(x),Y(y),r*S,0,Math.PI*2);ctx.fill();}
function drawLamp(){
const lx=-2.9;
const g=ctx.createRadialGradient(X(lx+0.5),Y(3.2),0,X(lx+0.5),Y(3.2),2.6*S);
g.addColorStop(0,'rgba(255,214,150,0.22)');g.addColorStop(1,'rgba(255,214,150,0)');
ctx.fillStyle=g;ctx.fillRect(X(lx-2.3),Y(5.8),5.6*S,5.8*S);
ctx.fillStyle='rgba(255,210,150,0.10)';ctx.beginPath();ctx.ellipse(X(lx+0.5),Y(0),1.6*S,0.12*S,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#161b23';ctx.lineCap='round';ctx.lineWidth=0.07*S;line(lx,0,lx,3.25);
ctx.lineWidth=0.045*S;line(lx,3.25,lx+0.5,3.3);
dot(lx+0.5,3.22,0.06,'#ffe2b0');
}
function drawStreaks(){
const a=clamp((Umean-3)/45,0,0.22);if(a<=0.005)return;
ctx.strokeStyle='rgba(210,225,245,'+a+')';ctx.lineWidth=1;ctx.beginPath();
for(let i=0;i<NS;i++){wind(sx[i],sy[i]);ctx.moveTo(X(sx[i]),Y(sy[i]));ctx.lineTo(X(sx[i]-WX*0.12),Y(sy[i]-WY*0.06));}
ctx.stroke();
}
function smoothPath(pts){
ctx.moveTo(X(pts[0].x),Y(pts[0].y));
for(let i=1;i<pts.length-1;i++){const p=pts[i],q=pts[i+1];
ctx.quadraticCurveTo(X(p.x),Y(p.y),X((p.x+q.x)/2),Y((p.y+q.y)/2));}
const l=pts[pts.length-1];ctx.lineTo(X(l.x),Y(l.y));
}
function drawUmb(u){
if(u.gone)return;
geom(u,P.Hx,P.Hy);
ctx.lineCap='round';ctx.lineJoin='round';
const Ln=u.sides[0].n,Rn=u.sides[1].n;
const pts=[Ln[4],Ln[3],Ln[2],Ln[1],Ln[0],Rn[1],Rn[2],Rn[3],Rn[4]];
ctx.strokeStyle='#2c3138';ctx.lineWidth=Math.max(2,0.016*S);
line(G.bx,G.by,G.cx+G.ux*0.07,G.cy+G.uy*0.07);
// J handle
ctx.lineWidth=Math.max(3,0.03*S);ctx.strokeStyle='#4a2f1f';
const hk=0.05;ctx.beginPath();
const hcx=G.bx+G.px*hk,hcy=G.by+G.py*hk,ang0=Math.atan2(-(G.by-hcy),(G.bx-hcx));
ctx.arc(X(hcx),Y(hcy),hk*S,ang0,ang0+Math.PI*0.95,false);ctx.stroke();
line(G.bx,G.by,G.bx+G.ux*0.1,G.by+G.uy*0.1);
// stretchers
for(const sd of u.sides){
const f=clamp(sd.load/STRUT_BREAK,0,1);
ctx.lineWidth=Math.max(1.2,0.009*S);
if(!sd.broken){ctx.strokeStyle='rgb('+(90+165*f|0)+','+(95-50*f|0)+','+(105-60*f|0)+')';line(G.rx,G.ry,sd.n[1].x,sd.n[1].y);}
else{ctx.strokeStyle='#c45a3a';const dx=sd.n[1].x-G.rx,dy=sd.n[1].y-G.ry,d=Math.hypot(dx,dy)||1;
line(G.rx,G.ry,G.rx+dx/d*0.09,G.ry+dy/d*0.09-0.03);}
}
ctx.fillStyle='#3a3f47';const rw=0.02;ctx.beginPath();
ctx.moveTo(X(G.rx-G.px*rw-G.ux*0.03),Y(G.ry-G.py*rw-G.uy*0.03));ctx.lineTo(X(G.rx+G.px*rw-G.ux*0.03),Y(G.ry+G.py*rw-G.uy*0.03));
ctx.lineTo(X(G.rx+G.px*rw+G.ux*0.03),Y(G.ry+G.py*rw+G.uy*0.03));ctx.lineTo(X(G.rx-G.px*rw+G.ux*0.03),Y(G.ry-G.py*rw+G.uy*0.03));ctx.fill();
// canopy
ctx.beginPath();smoothPath(pts);ctx.closePath();
ctx.fillStyle='rgba(158,34,44,0.9)';ctx.fill('evenodd');
ctx.beginPath();smoothPath(pts);ctx.strokeStyle='#e0525c';ctx.lineWidth=Math.max(2,0.02*S);ctx.stroke();
ctx.strokeStyle='rgba(30,10,12,0.55)';ctx.lineWidth=Math.max(1,0.006*S);ctx.beginPath();smoothPath(pts);ctx.stroke();
for(const p of [Ln[4],Rn[4]])dot(p.x,p.y,0.012,'#ddd');
dot(G.cx+G.ux*0.07,G.cy+G.uy*0.07,0.014,'#888');
}
function drawPerson(){
const J=P.J;ctx.lineCap='round';ctx.lineJoin='round';
const coat='#d39a26',coatD='#93681a',trou='#2a3343',trouD='#1b222d',boot='#11151b',skin='#deb08e';
const held=umb&&umb.held&&!umb.gone;
if(held)geom(umb,P.Hx,P.Hy);
// targets
const sh=J.sh,tuck={x:sh.x-0.15,y:sh.y-0.22},hood={x:J.head.x-0.06,y:J.head.y+0.02};
let nT,fT;
if(held){
nT={x:P.Hx,y:P.Hy};
const h2={x:P.Hx+G.ux*0.2,y:P.Hy+G.uy*0.2};
fT={x:tuck.x+(h2.x-tuck.x)*P.two,y:tuck.y+(h2.y-tuck.y)*P.two};
if(P.fix){const tip=umb.sides[P.fix.i].n[4],r=clamp(P.fix.t/0.45,0,1);fT={x:fT.x+(tip.x-fT.x)*r,y:fT.y+(tip.y-fT.y)*r};}
}else if(P.reach>0&&umb&&!umb.gone){const dx=umb.cx-sh.x,dy=umb.cy-sh.y,d=Math.hypot(dx,dy)||1;
nT={x:sh.x+dx/d*0.6,y:sh.y+dy/d*0.6};fT={x:sh.x+dx/d*0.55,y:sh.y+dy/d*0.5};}
else{nT=hood;fT=tuck;}
const fa=armIK({x:sh.x+0.03,y:sh.y},fT.x,fT.y),na=armIK(sh,nT.x,nT.y);
// far leg & boot
limb(J.hip,J.kR,J.aR,0.12,trouD);
ctx.strokeStyle=boot;ctx.lineWidth=0.08*S;line(J.aR.x+0.03,0.035,J.aR.x-0.1,0.035);
// far arm
limb({x:sh.x+0.03,y:sh.y},{x:fa.ex,y:fa.ey},{x:fa.hx,y:fa.hy},0.085,coatD);dot(fa.hx,fa.hy,0.04,'#c39778');
// near leg
limb(J.hip,J.kF,J.aF,0.125,trou);
ctx.strokeStyle=boot;ctx.lineWidth=0.085*S;line(J.aF.x+0.03,0.038,J.aF.x-0.11,0.038);
// coat
const ta=J.ta,tdx=-Math.sin(ta),tdy=Math.cos(ta),pxv=Math.cos(ta),pyv=Math.sin(ta);
const fl=clamp(Umean/28,0,1),flap=fl*(0.13+0.05*Math.sin(T*13)+0.03*Math.sin(T*29));
ctx.fillStyle=coat;ctx.beginPath();
ctx.moveTo(X(J.neck.x-pxv*0.11),Y(J.neck.y-pyv*0.11));
ctx.lineTo(X(J.neck.x+pxv*0.12),Y(J.neck.y+pyv*0.12));
ctx.lineTo(X(J.hip.x+pxv*0.15),Y(J.hip.y+pyv*0.15));
ctx.lineTo(X(J.hip.x-tdx*0.22+pxv*0.17+flap),Y(J.hip.y-tdy*0.22+pyv*0.17+0.05*fl));
ctx.lineTo(X(J.hip.x-tdx*0.2-pxv*0.15+flap*0.4),Y(J.hip.y-tdy*0.2-pyv*0.15));
ctx.lineTo(X(J.hip.x-pxv*0.14),Y(J.hip.y-pyv*0.14));
ctx.closePath();ctx.fill();
ctx.strokeStyle=coat;ctx.lineWidth=0.25*S;line(J.hip.x+tdx*0.08,J.hip.y+tdy*0.08,J.neck.x-tdx*0.04,J.neck.y-tdy*0.04);
// head with hood
dot(J.head.x+0.015,J.head.y+0.005,0.125,coat);
dot(J.head.x-0.035,J.head.y-0.012,0.078,skin);
ctx.strokeStyle=coatD;ctx.lineWidth=0.02*S;ctx.beginPath();ctx.arc(X(J.head.x+0.015),Y(J.head.y+0.005),0.125*S,Math.PI*0.65,Math.PI*1.45);ctx.stroke();
// near arm
limb(sh,{x:na.ex,y:na.ey},{x:na.hx,y:na.hy},0.09,coat);dot(na.hx,na.hy,0.043,skin);
}
function drawRain(){
ctx.strokeStyle='rgba(185,205,235,0.38)';ctx.lineWidth=1;ctx.beginPath();
for(let i=0;i<NR;i++){ctx.moveTo(X(rx[i]),Y(ry[i]));ctx.lineTo(X(rx[i]-rvx[i]*0.025),Y(ry[i]-rvy[i]*0.025));}
ctx.stroke();
ctx.fillStyle='rgba(200,220,245,0.6)';
for(const s of splashes){ctx.fillRect(X(s.x)-1,Y(s.y)-1,2,2);}
}
function draw(){
ctx.setTransform(1,0,0,1,0,0);ctx.drawImage(bgc,0,0);
ctx.setTransform(DPR,0,0,DPR,0,0);
drawLamp();drawStreaks();
if(umb)drawUmb(umb);
drawPerson();
drawRain();
}
/* ---------------- HUD & input ---------------- */
const elS=document.getElementById('spd'),elSub=document.getElementById('sub'),elG=document.getElementById('gust'),
elM=document.getElementById('mode'),elSt=document.getElementById('stat'),bGrip=document.getElementById('bGrip'),
bFrame=document.getElementById('bFrame'),sl=document.getElementById('sl'),bAuto=document.getElementById('auto');
const BF=[0.5,1.6,3.4,5.5,8,10.8,13.9,17.2,20.8,24.5,28.5,32.7];
const BN=['Calm','Light air','Light breeze','Gentle breeze','Moderate breeze','Fresh breeze','Strong breeze','Near gale','Gale','Strong gale','Storm','Violent storm','Hurricane force'];
function beaufort(v){let b=0;while(b<BF.length&&v>=BF[b])b++;return b;}
function setAuto(on){auto=on;bAuto.classList.toggle('on',on);bAuto.textContent=on?'Auto: rising':'Auto: off';}
bAuto.onclick=()=>setAuto(!auto);
sl.oninput=()=>{windSet=+sl.value;setAuto(false);};
document.getElementById('newu').onclick=()=>newUmb();
function resetAll(){windSet=2;Umean=2;setAuto(true);sl.value=2;umbCount=0;P.beta=0;P.bdot=0;P.tauS=0;P.Fsx=P.Fsy=0;newUmb();}
addEventListener('keydown',e=>{
const k=e.key;
if(k==='ArrowUp'||k==='+'||k==='='){windSet=Math.min(WMAX,windSet+1);setAuto(false);sl.value=windSet;e.preventDefault();}
else if(k==='ArrowDown'||k==='-'){windSet=Math.max(0,windSet-1);setAuto(false);sl.value=windSet;e.preventDefault();}
else if(k==='a'||k==='A')setAuto(!auto);
else if(k==='n'||k==='N')newUmb();
else if(k==='r'||k==='R')resetAll();
});
function hud(){
const b=beaufort(Umean);
elS.innerHTML=Umean.toFixed(1)+' <small>m/s mean</small>';
elSub.textContent=Math.round(Umean*3.6)+' km/h · Beaufort '+b+' — '+BN[b];
let gx=-0.2,gy=1.6;if(umb&&!umb.gone){geom(umb,P.Hx,P.Hy);gx=G.cx;gy=G.cy;}
wind(gx,gy);elG.textContent='gust at the canopy: '+Math.hypot(WX,WY).toFixed(1)+' m/s ('+Math.round(Math.hypot(WX,WY)*3.6)+' km/h)';
elM.textContent=(auto?'wind rising on its own':'manual wind')+' · rain on you: '+P.hitRate.toFixed(0)+' drops/s';
const lines=[];let grip=0,frame=0;
if(umb){
if(umb.gone)lines.push('Umbrella #'+umbCount+' is gone — '+umb.why+'.\nFetching another…');
else if(!umb.held)lines.push('Umbrella #'+umbCount+' '+umb.why+'.\nIt is tumbling away.');
else{
lines.push(P.two>0.5?'Two hands, leaning in, braced':'One hand, relaxed');
grip=Math.hypot(P.Fsx,P.Fsy)/umb.fGrip;
for(const sd of umb.sides){
frame=Math.max(frame,sd.load/STRUT_BREAK);
const where=sd.n[4].x<sd.n[0].x?'upwind':'downwind';
if(sd.broken)lines.push(where+' rib snapped');
else if(sd.state)lines.push(where+' half inside out'+(sd.inv>1?' (×'+sd.inv+')':''));
else if(sd.inv>0)lines.push(where+' rib bent from '+sd.inv+' inversion'+(sd.inv>1?'s':''));
}
if(umb.sat)lines.push('being wrenched sideways — wrist at its limit');
if(umb.slip>0.05)lines.push('handle slipping…');
if(P.fix)lines.push('reaching up to pull it back the right way');
}
}
elSt.textContent=lines.join('\n');
bGrip.style.width=Math.min(100,grip*100)+'%';bGrip.style.background=grip>1?'#ff6a5a':grip>0.75?'#ffb35a':'#7fc4ff';
bFrame.style.width=Math.min(100,frame*100)+'%';bFrame.style.background=frame>0.8?'#ff6a5a':frame>0.55?'#ffb35a':'#7fc4ff';
}
/* ---------------- loop ---------------- */
let last=performance.now(),acc=0,hudT=0;
personPrep(0);newUmb();rainInit();
function frame(now){
let dt=(now-last)/1000;last=now;if(!(dt>0))dt=1/60;if(dt>1/30)dt=1/30;
if(auto){windSet=Math.min(WMAX,windSet+RAMP*dt);sl.value=windSet;}
Umean+=(windSet-Umean)*Math.min(1,dt/1.5);
LULL=1+0.06*Math.sin(0.05*T+1);
personPrep(dt);
acc+=dt;let n=Math.floor(acc/HSTEP);if(n>40){n=40;acc=0;}else acc-=n*HSTEP;
const H0x=P.Hx,H0y=P.Hy,H1x=P.Hnx,H1y=P.Hny,hvx=(H1x-H0x)/dt,hvy=(H1y-H0y)/dt;
umb.accFx=umb.accFy=0;umb.accN=0;
for(let k=0;k<n;k++){T+=HSTEP;Dadv+=Umean*HSTEP;const f=(k+1)/n;
if(!umb.gone)stepUmb(umb,HSTEP,H0x+(H1x-H0x)*f,H0y+(H1y-H0y)*f,hvx,hvy);}
P.Hx=H1x;P.Hy=H1y;
if(umb.held&&umb.accN>0){P.Fax=umb.accFx/umb.accN;P.Fay=umb.accFy/umb.accN;}else{P.Fax=0;P.Fay=0;}
P.Fsx+=(P.Fax-P.Fsx)*Math.min(1,dt/0.08);P.Fsy+=(P.Fay-P.Fsy)*Math.min(1,dt/0.08);
personDyn(dt);
if(!umb.held){
umb.freeT+=dt;
if(!umb.gone&&(umb.cx>viewR()+1.5||umb.cx<viewL()-2||umb.cy>viewT()+3))umb.gone=true;
if(umb.gone){umb.goneT+=dt;if(umb.goneT>2.5)newUmb();}
else if(umb.freeT>8)newUmb();
}
rainStep(dt);
draw();
hudT-=dt;if(hudT<=0){hud();hudT=0.12;}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
These notes describe what the model is built to produce. I have not watched it run, and I cannot test it here. The thresholds (knuckle stiffness, grip limits, stretcher break load) are estimates from rough force arithmetic, not values tuned on screen. The timings below are where I expect things to land, not where I saw them land.
Where it should look right
Where it is wrong or simplified
K_KN makes it fold earlier.