16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 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 2-D cross-section: two ribs hinged at the crown, each a chain of 8 segments. Each rib has only two degrees of freedom. The first is a hinge angle α, which stands in for the stretcher, runner and catch. The second is a curvature κ, a bending spring for the rib itself. Both are driven through their Jacobians by quasi-steady pressure on every segment. That pressure comes from two sources. The cavity side gets one pressure, which depends on how directly the wind blows into the canopy's opening. The exterior gets a pressure that depends on each segment's local angle to the wind: stagnation where it faces the wind, strong suction where the flow runs over the dome. The hinge's holding torque is not linear. It rises as the rib is lifted, peaks, then falls to zero over the next 0.2 rad. This is the stretcher going over centre. Inversion is a per-rib boolean state flip, triggered when α crosses that zero point. Once flipped, the rib obeys a different, much weaker hinge law that pulls it toward about 26°, pointing up. The cavity also swaps sides, so the wind now fills the tulip instead of the dome. The rib flips back only if pressure pushes α back past 100°. That happens when the tip is pointed into the wind, or when the person pulls it back in a lull. So what decides inversion is the pressure moment on the rib against a peak holding torque. That peak loses 14% on every inversion and half when a rib snaps. Tilt, gust lag and the shaft being wrenched around all decide whether the wind reaches the cavity at all.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Umbrella in a rising gale</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html,body{margin:0;height:100%;background:#1b2330;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#e8eef5}
canvas{display:block;width:100%;height:100%}
#hud{position:absolute;left:14px;top:10px;pointer-events:none;text-shadow:0 1px 3px #000c}
#spd{font-size:46px;font-weight:700;letter-spacing:-1px;line-height:1.05}
#sub{font-size:14px;opacity:.9}
#stat{font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;margin-top:8px;white-space:pre;opacity:.92}
#ev{font:13px/1.5 ui-monospace,Menlo,Consolas,monospace;margin-top:6px;color:#ffd27a;white-space:pre}
#ctl{position:absolute;left:0;right:0;bottom:0;padding:10px 14px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;background:linear-gradient(transparent,#000a);font-size:13px}
#ctl input[type=range]{width:min(360px,45vw);vertical-align:middle}
button{background:#2c3a4d;color:#e8eef5;border:1px solid #4a5d77;border-radius:6px;padding:5px 10px;font:inherit;cursor:pointer}
button:hover{background:#38506b}
.keys{opacity:.65}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud"><div id="spd">0.0 m/s</div><div id="sub"></div><div id="stat"></div><div id="ev"></div></div>
<div id="ctl">
<label>Mean wind <input id="sl" type="range" min="0" max="35" step="0.1" value="2"> <span id="slv"></span></label>
<label><input id="auto" type="checkbox" checked> keeps rising</label>
<button id="newu">New umbrella (N)</button>
<button id="pause">Pause (P)</button>
<span class="keys">↑/↓ ±1 m/s (↓ stops the rise) · A toggle rise · N new umbrella · P pause</span>
</div>
<script>
'use strict';
const $=id=>document.getElementById(id);
const cvs=$('c'),ctx=cvs.getContext('2d');
let DW=0,DH=0,DPR=1,S=100,GY=0;
const OX=0.36;
function resize(){DPR=Math.min(2,window.devicePixelRatio||1);DW=window.innerWidth;DH=window.innerHeight;
cvs.width=Math.round(DW*DPR);cvs.height=Math.round(DH*DPR);S=Math.min(DH/4.4,DW/4.6);GY=DH*0.83;}
window.addEventListener('resize',resize);resize();
const X=x=>x*S+DW*OX, Y=y=>GY-y*S;
const viewL=()=>-DW*OX/S, viewR=()=>DW*(1-OX)/S, viewT=()=>GY/S;
const clamp=(v,a,b)=>v<a?a:v>b?b:v, lerp=(a,b,t)=>a+(b-a)*t;
/* ---------------- constants ---------------- */
const G=9.81,RHO=1.225;
const L_GC=0.78,L_HANDLE=0.12,RIB_L=0.52,NSEG=8,DS=RIB_L/NSEG,DEPTH=0.72;
const M_RIB=0.14,I_RIB=M_RIB*RIB_L*RIB_L/3,I_K=M_RIB*Math.pow(RIB_L,4)/20;
const M_U=0.5,M_S=M_U-2*M_RIB,D_S=0.3,COM_D=0.5,I_G=0.27,I_C=I_G-M_U*COM_D*COM_D;
// hinge (stretcher + runner catch) law
const A_N=1.88,K_N=23,K_C=160,A_I=0.45,K_I=2.5,A_R=1.75,FLIP_W=0.2,A_MIN=0.12,A_MAX=2.75,C_H=0.25,T_PEAK0=6;
// rib bending
const KAPPA0=1.4,KB0=3,C_K=0.012,KAPPA_BREAK=3.5;
const STR_J=3,STR_L=0.3;
/* ---------------- state ---------------- */
let T=0,W=2,Wset=2,auto=true,paused=false,sliding=false;
let UMB=null,debris=[],lossT=0;
const events=[];
function ev(m){const l=events[events.length-1];if(l&&l.m===m&&T-l.t<1.2)return;events.push({m,t:T});if(events.length>5)events.shift();}
/* ---------------- wind ---------------- */
function hash(i){const s=Math.sin(i*127.1+311.7)*43758.5453;return s-Math.floor(s);}
function vn(x){const i=Math.floor(x),f=x-i,u=f*f*(3-2*f);return (hash(i)*(1-u)+hash(i+1)*u)*2-1;}
function gust(t){return 0.55*vn(t*0.35)+0.3*vn(t*1.1+17.3)+0.15*vn(t*3.1+41.7);}
function gustV(t){return 0.6*vn(t*0.5+91.1)+0.4*vn(t*1.7+57.9);}
function wind(x,y,out){
const tau=T-x/Math.max(W,2); // frozen turbulence: gusts travel downwind
const h=Math.max(y,0.05);
const prof=Math.min(1.25,Math.pow(h/1.8,0.16));
out[0]=W*prof*(1+0.32*gust(tau));
out[1]=W*0.11*gustV(tau)*Math.min(1,h/0.6);
return out;
}
const tw=[0,0],tw2=[0,0];
/* ---------------- umbrella ---------------- */
function makeRib(side){return{side,a:A_N,da:0,k:KAPPA0,dk:0,inv:false,Tpeak:T_PEAK0,Kb:KB0,broken:false,load:0,flips:0,Qa:0,Qk:0};}
function makeUmb(){return{mode:'held',th:0,w:0,cx:0,cy:0,vx:0,vy:0,ribs:[makeRib(-1),makeRib(1)],slip:0,Fh:[0,0],F:0,tg:0,g:null};}
function geom(u,gx,gy){
const ax=Math.sin(u.th),ay=Math.cos(u.th);
const cx=gx+ax*L_GC,cy=gy+ay*L_GC;
const psi=Math.atan2(ay,ax);
const g={gx,gy,ax,ay,cx,cy,ribs:[]};
for(const r of u.ribs){
const sg=r.side,phi0=psi-sg*r.a;
const R={px:[cx],py:[cy],dx:[],dy:[],mx:[],my:[],jkx:[],jky:[]};
let x=cx,y=cy,accx=0,accy=0;
for(let k=0;k<NSEG;k++){
const sm=(k+0.5)*DS,ph=phi0-sg*r.k*sm;
const dx=Math.cos(ph),dy=Math.sin(ph),f=-sg*sm*DS;
R.dx.push(dx);R.dy.push(dy);
R.mx.push(x+dx*DS*0.5);R.my.push(y+dy*DS*0.5);
R.jkx.push(accx-f*dy*0.5);R.jky.push(accy+f*dx*0.5);
accx+=-f*dy;accy+=f*dx;
x+=dx*DS;y+=dy*DS;R.px.push(x);R.py.push(y);
}
g.ribs.push(R);
}
return g;
}
const AO={Fx:0,Fy:0,T:0};
function aero(u,g,bvx,bvy,bw,pvx,pvy){
wind(g.cx,g.cy,tw);
let wcx=tw[0]-(bvx-bw*(g.cy-pvy)),wcy=tw[1]-(bvy+bw*(g.cx-pvx));
const wl=Math.hypot(wcx,wcy)+1e-9;wcx/=wl;wcy/=wl;
let Fx=0,Fy=0,Tq=0;
for(let i=0;i<2;i++){
const r=u.ribs[i],R=g.ribs[i],sg=r.side;
const ox=r.inv?g.ax:-g.ax,oy=r.inv?g.ay:-g.ay; // direction the cavity opens toward
const s=-(wcx*ox+wcy*oy); // +1 = wind blowing straight into the cavity
const Cc=-0.25+0.7*s+0.55*s*s; // cavity pressure coefficient
let Qa=0,Qk=0;
for(let k=0;k<NSEG;k++){
const mx=R.mx[k],my=R.my[k];
const Jax=sg*(my-g.cy),Jay=-sg*(mx-g.cx);
const vx=bvx-bw*(my-pvy)+Jax*r.da+R.jkx[k]*r.dk;
const vy=bvy+bw*(mx-pvx)+Jay*r.da+R.jky[k]*r.dk;
wind(mx,my,tw);
const wx=tw[0]-vx,wy=tw[1]-vy,U2=wx*wx+wy*wy;
let fx=0,fy=-M_RIB/NSEG*G;
if(U2>1e-6){
const Um=Math.sqrt(U2),ux=wx/Um,uy=wy/Um;
const q=0.5*RHO*U2*DS*DEPTH;
let nx=sg*(-R.dy[k]),ny=sg*R.dx[k];
if(r.inv){nx=-nx;ny=-ny;}
const c=ux*nx+uy*ny;
const Ce=c<0?-1.2+2.2*c*c:-1.2+0.8*c; // exterior: stagnation facing wind, suction over the dome
const dp=(Cc-Ce)*q;
fx+=dp*nx+q*0.3*Math.abs(c)*ux;
fy+=dp*ny+q*0.3*Math.abs(c)*uy;
}
Qa+=fx*Jax+fy*Jay;Qk+=fx*R.jkx[k]+fy*R.jky[k];
Fx+=fx;Fy+=fy;Tq+=(mx-pvx)*fy-(my-pvy)*fx;
}
r.Qa=Qa;r.Qk=Qk;
}
AO.Fx=Fx;AO.Fy=Fy;AO.T=Tq;
}
function stepRib(r,h,extra,log){
const ap=A_N-r.Tpeak/K_N,af=ap-FLIP_W;
let tq;
if(!r.inv){
if(r.a>=A_N) tq=K_C*(A_N-r.a);
else if(r.a>=ap) tq=K_N*(A_N-r.a);
else tq=r.Tpeak*Math.max(0,r.a-af)/(ap-af); // over-centre: holding torque collapses
r.load=clamp((A_N-r.a)/(A_N-af),0,1);
} else {tq=K_I*(A_I-r.a);r.load=0;}
tq+=extra-C_H*r.da;
if(r.a<A_MIN) tq+=300*(A_MIN-r.a)-(r.da<0?1.5*r.da:0);
if(r.a>A_MAX) tq-=300*(r.a-A_MAX)+(r.da>0?1.5*r.da:0);
r.da=clamp(r.da+(tq+r.Qa)/I_RIB*h,-250,250);r.a+=r.da*h;
const tk=-r.Kb*(r.k-KAPPA0)-C_K*r.dk+r.Qk;
r.dk=clamp(r.dk+tk/I_K*h,-400,400);r.k+=r.dk*h;
const dk=r.k-KAPPA0;
if(!r.broken&&Math.abs(dk)>KAPPA_BREAK){r.broken=true;r.Kb=KB0*0.1;r.Tpeak=Math.max(1.2,r.Tpeak*0.5);if(log)ev('a rib snapped');}
const lim=r.broken?4.5:KAPPA_BREAK+0.5;
if(dk>lim){r.k=KAPPA0+lim;if(r.dk>0)r.dk=0;}else if(dk<-lim){r.k=KAPPA0-lim;if(r.dk<0)r.dk=0;}
if(!r.inv&&r.a<af){r.inv=true;r.flips++;r.Tpeak=Math.max(1.5,r.Tpeak*0.86);if(log)ev('inside out ('+(r.side<0?'left':'right')+' half)');}
else if(r.inv&&r.a>A_R){r.inv=false;if(log)ev('popped back ('+(r.side<0?'left':'right')+' half)');}
}
const anyInv=u=>u.ribs[0].inv||u.ribs[1].inv;
function heldStep(u,h){
const gx=P.hand[0],gy=P.hand[1];
const g=geom(u,gx,gy);
aero(u,g,0,0,u.w,gx,gy);
let tq=AO.T+(g.ax*D_S)*(-M_S*G);
let tg=-P.Kg*(P.thT-u.th)-P.Cg*u.w;tg=clamp(tg,-P.tmax,P.tmax);u.tg=tg;tq+=tg;
if(u.th<-1.75) tq-=80*(-1.75-u.th)+2*u.w;
if(u.th>2.3) tq+=80*(u.th-2.3)-2*u.w;
u.w+=tq/I_G*h;u.th-=u.w*h;
const rx=g.ax*COM_D,ry=g.ay*COM_D,w2=u.w*u.w;
u.Fh[0]=AO.Fx+M_U*w2*rx;u.Fh[1]=AO.Fy-M_S*G+M_U*w2*ry;
const F=Math.hypot(u.Fh[0],u.Fh[1]);u.F=F;
if(F>P.gripMax) u.slip+=(F/P.gripMax-1)*h*5; else u.slip=Math.max(0,u.slip-h*0.7);
const fixT=P.fixing*6;
for(const r of u.ribs) stepRib(r,h,r.inv?fixT:0,true);
const err=u.th-P.thT;
if(!P.wrenched&&Math.abs(err)>0.75){P.wrenched=true;ev('wrenched sideways');}
else if(P.wrenched&&Math.abs(err)<0.3)P.wrenched=false;
if(u.slip>=1) release(u,g);
}
function release(u,g){
u.mode='free';
u.cx=g.gx+g.ax*COM_D;u.cy=g.gy+g.ay*COM_D;
u.vx=-u.w*(u.cy-g.gy);u.vy=u.w*(u.cx-g.gx);
u.Fh=[0,0];debris.push(u);if(debris.length>3)debris.shift();
UMB=null;lossT=0;P.wrenched=false;ev('it got away');
}
function freeStep(u,h){
const ax=Math.sin(u.th),ay=Math.cos(u.th);
const gx=u.cx-ax*COM_D,gy=u.cy-ay*COM_D;
const g=geom(u,gx,gy);
aero(u,g,u.vx,u.vy,u.w,u.cx,u.cy);
let fx=AO.Fx,fy=AO.Fy-M_S*G,tq=AO.T+((gx+ax*D_S)-u.cx)*(-M_S*G);
const pts=[[gx-ax*L_HANDLE,gy-ay*L_HANDLE],[gx,gy],[g.cx+ax*0.09,g.cy+ay*0.09],
[g.ribs[0].px[NSEG],g.ribs[0].py[NSEG]],[g.ribs[1].px[NSEG],g.ribs[1].py[NSEG]]];
for(const p of pts){
if(p[1]<0){
const rx=p[0]-u.cx,ry=p[1]-u.cy,vx=u.vx-u.w*ry,vy=u.vy+u.w*rx;
const cy_=3000*(-p[1])-(vy<0?30*vy:0);
const cx_=clamp(-8*vx,-0.7*cy_,0.7*cy_);
fx+=cx_;fy+=cy_;tq+=rx*cy_-ry*cx_;
}
}
u.vx+=fx/M_U*h;u.vy+=fy/M_U*h;u.w+=tq/I_C*h;u.w*=1-0.3*h;
u.cx+=u.vx*h;u.cy+=u.vy*h;u.th-=u.w*h;
for(const r of u.ribs) stepRib(r,h,0,false);
}
/* ---------------- person ---------------- */
const P={lam:0.02,dlam:0,crouch:0,sw:0.3,
feet:[{x:-0.13,t:-1,a:0,b:0,lift:0},{x:0.17,t:-1,a:0,b:0,lift:0}],
two:0,Kg:55,Cg:3,tmax:7,gripMax:170,thT:-0.3,uF:2,tef:0,strain:0,fixT:0,fixing:0,
hand:[0,1.6],h2:[0,1.2],geo:null,wrenched:false};
function personGeom(){
const f0=P.feet[0],f1=P.feet[1];
const xm=(f0.x+f1.x)/2,hh=0.93-0.3*P.crouch;
const sl=Math.sin(P.lam),cl=Math.cos(P.lam);
const hip=[xm-sl*hh,cl*hh];
for(const f of P.feet){ // keep legs able to reach the ground
const ax=f.x,ay=0.09+f.lift,dx=hip[0]-ax,dy=hip[1]-ay,d=Math.hypot(dx,dy);
if(d>0.93){hip[0]=ax+dx/d*0.93;hip[1]=ay+dy/d*0.93;}
}
const tang=P.lam*1.3+P.crouch*0.35+0.04;
const tx=-Math.sin(tang),ty=Math.cos(tang);
const sh=[hip[0]+tx*0.56,hip[1]+ty*0.56];
const fx=-ty,fy=tx;
const head=[sh[0]+tx*0.24+fx*0.03,sh[1]+ty*0.24+fy*0.03];
let hx,hy;
if(UMB){
const t2=P.two,up=lerp(0.2,0.06,t2),fw=lerp(0.27,0.31,t2);
const raise=0.32*clamp(UMB.th-0.2,0,1.3);
hx=sh[0]+fx*fw+tx*(up+raise);hy=sh[1]+fy*fw+ty*(up+raise);
} else {hx=head[0]+fx*0.17-tx*0.03;hy=head[1]+fy*0.17-ty*0.03;}
return{xm,hh,hip,sh,head,tx,ty,fx,fy,hand:[hx,hy]};
}
function personStep(h){
wind(0,2.0,tw2);P.uF+=(tw2[0]-P.uF)*Math.min(1,h/0.6);
const uF=Math.max(0,P.uF);
const wantTwo=UMB&&(uF>8||anyInv(UMB))?1:0;
P.two+=(wantTwo-P.two)*Math.min(1,h/0.4);
const t2=P.two;
P.Kg=lerp(55,170,t2);P.Cg=lerp(3,7,t2);P.tmax=lerp(7,22,t2);P.gripMax=lerp(170,320,t2);
if(UMB){
const inv=anyInv(UMB);
P.thT=clamp(inv?-Math.atan2(uF,1.0):-Math.atan2(uF,6.5),-1.4,0.2);
if(inv){P.fixT+=h;P.fixing=(P.fixT>1&&uF<7)?Math.min(1,(P.fixT-1)/0.8):0;}
else{P.fixT=0;P.fixing=0;}
} else {P.fixT=0;P.fixing=0;}
const g=personGeom();
wind(g.hip[0],1.1,tw2);const ub=tw2[0];
const Fb=0.5*RHO*ub*Math.abs(ub)*0.62;
let tau=-Fb*(g.hip[1]+0.2);
if(UMB&&UMB.mode==='held'){const rx=g.hand[0]-g.xm,ry=g.hand[1];tau+=rx*UMB.Fh[1]-ry*UMB.Fh[0];}
P.tef+=(tau-P.tef)*Math.min(1,h/0.3);
const M=75,hc=g.hh+0.14;
const ld=Math.asin(clamp(-P.tef/(M*G*hc),-0.3,0.55));
const tmax=M*G*(P.sw*0.5+0.1);
const ta=clamp(2200*(ld-P.lam)-450*P.dlam,-tmax,tmax);
let tt=M*G*hc*Math.sin(P.lam)+tau+ta;
if(P.lam>0.62) tt-=20000*(P.lam-0.62)+(P.dlam>0?800*P.dlam:0);
if(P.lam<-0.35) tt+=20000*(-0.35-P.lam)-(P.dlam<0?800*P.dlam:0);
P.dlam+=tt/(M*hc*hc+6)*h;P.lam+=P.dlam*h;
P.strain+=(Math.abs(ta)/tmax-P.strain)*Math.min(1,h/0.3);
P.geo=personGeom();P.hand=P.geo.hand;
}
function footing(dt){
const uF=Math.max(0,P.uF);
const swT=0.28+0.36*clamp((uF-6)/16,0,1)+0.12*P.strain;
P.sw+=(swT-P.sw)*Math.min(1,dt/0.5);
const tg=[-P.sw*0.42,P.sw*0.58];
const cT=clamp((uF-12)/18,0,0.5)+0.25*clamp(P.strain-0.5,0,1);
P.crouch+=(cT-P.crouch)*Math.min(1,dt/0.8);
if(!P.feet.some(f=>f.t>=0)){
let bi=-1,be=0.07;
P.feet.forEach((f,i)=>{const e=Math.abs(tg[i]-f.x);if(e>be){be=e;bi=i;}});
if(bi>=0){const f=P.feet[bi];f.t=0;f.a=f.x;f.b=tg[bi];}
}
for(const f of P.feet){
if(f.t>=0){f.t+=dt/0.3;const u=Math.min(1,f.t),s=u*u*(3-2*u);
f.x=f.a+(f.b-f.a)*s;f.lift=Math.sin(Math.PI*u)*0.07;if(f.t>=1){f.t=-1;f.lift=0;}}
}
}
/* ---------------- rain ---------------- */
const drops=[];let splashes=[];const NDROP=650;
function spawnDrop(d,init){
const L=viewL()-0.5,R=viewR()+0.3,Tp=viewT()+0.3;
wind(0,2,tw2);const u=Math.max(0,tw2[0]);
if(init){d.x=L+Math.random()*(R-L);d.y=Math.random()*Tp;}
else{const ft=6.5*(R-L),fl=u*Tp;
if(Math.random()*(ft+fl)<ft){d.x=L+Math.random()*(R-L);d.y=Tp+Math.random()*0.3;}
else{d.x=L-Math.random()*0.4;d.y=Math.random()*Tp;}}
wind(d.x,d.y,tw2);d.vx=tw2[0];d.vy=tw2[1]-6.5;
}
for(let i=0;i<NDROP;i++){const d={x:0,y:0,vx:0,vy:0};spawnDrop(d,true);drops.push(d);}
function segHit(ax,ay,bx,by,cx,cy,dx,dy){
const rx=bx-ax,ry=by-ay,sx=dx-cx,sy=dy-cy,den=rx*sy-ry*sx;
if(Math.abs(den)<1e-12)return -1;
const t=((cx-ax)*sy-(cy-ay)*sx)/den,uu=((cx-ax)*ry-(cy-ay)*rx)/den;
return(t>=0&&t<=1&&uu>=0&&uu<=1)?t:-1;
}
function distSeg(px,py,a,b){const vx=b[0]-a[0],vy=b[1]-a[1],t=clamp(((px-a[0])*vx+(py-a[1])*vy)/(vx*vx+vy*vy),0,1);return Math.hypot(px-a[0]-vx*t,py-a[1]-vy*t);}
function splash(x,y,d,p){const n=Math.random()<p?2:1;for(let i=0;i<n;i++){if(splashes.length>400)return;
splashes.push({x,y,vx:d.vx*0.15+(Math.random()-0.5)*1.5,vy:0.5+Math.random()*1.2,life:0.15+Math.random()*0.15});}}
function rain(dt){
const L=viewL()-0.6,R=viewR()+0.6,Tp=viewT()+0.4;
const segs=[];const us=UMB?[UMB,...debris]:debris;
for(const u of us){const g=u.g;if(!g)continue;for(const Rb of g.ribs)for(let j=0;j<NSEG;j++)segs.push(Rb.px[j],Rb.py[j],Rb.px[j+1],Rb.py[j+1]);}
const pg=P.geo,k=Math.min(1,dt/0.07);
for(const d of drops){
const ox=d.x,oy=d.y;
wind(d.x,d.y,tw2);
d.vx+=(tw2[0]-d.vx)*k;d.vy+=(tw2[1]-6.5-d.vy)*k;
d.x+=d.vx*dt;d.y+=d.vy*dt;
let hit=false;
for(let i=0;i<segs.length;i+=4){const t=segHit(ox,oy,d.x,d.y,segs[i],segs[i+1],segs[i+2],segs[i+3]);
if(t>=0){splash(ox+(d.x-ox)*t,oy+(d.y-oy)*t,d,0.6);hit=true;break;}}
if(!hit&&pg){const hx=d.x-pg.head[0],hy=d.y-pg.head[1];
if(hx*hx+hy*hy<0.015||distSeg(d.x,d.y,pg.hip,pg.sh)<0.15){splash(d.x,d.y,d,0.3);hit=true;}}
if(!hit&&d.y<0){if(Math.random()<0.5)splash(d.x,0.005,d,0.4);hit=true;}
if(hit||d.x>R||d.x<L-1||d.y>Tp+1)spawnDrop(d,false);
}
for(const s of splashes){s.vy-=G*dt;s.x+=s.vx*dt;s.y+=s.vy*dt;s.life-=dt;}
splashes=splashes.filter(s=>s.life>0&&s.y>-0.02);
}
/* ---------------- control ---------------- */
function newUmb(){UMB=makeUmb();UMB.th=-Math.atan2(Math.max(0,P.uF),6.5);lossT=0;ev('opens a fresh umbrella');}
UMB=makeUmb();
P.geo=personGeom();P.hand=P.geo.hand;
let acc=0;const HSTEP=1/720;
function substep(h){T+=h;personStep(h);if(UMB)heldStep(UMB,h);for(const u of debris)freeStep(u,h);}
function update(dt){
if(auto)Wset=Math.min(35,Wset+0.25*dt);
W+=(Wset-W)*Math.min(1,dt/1.5);
acc+=dt;let n=0;
while(acc>=HSTEP&&n<60){substep(HSTEP);acc-=HSTEP;n++;}
if(n>=60)acc=0;
footing(dt);
debris=debris.filter(u=>u.cx>viewL()-6&&u.cx<viewR()+6&&u.cy<20);
if(!UMB){lossT+=dt;if(lossT>3&&P.uF<15)newUmb();}
if(UMB)UMB.g=geom(UMB,P.hand[0],P.hand[1]);
for(const u of debris)u.g=geom(u,u.cx-Math.sin(u.th)*COM_D,u.cy-Math.cos(u.th)*COM_D);
// second hand target
const g=P.geo;let t2;
if(UMB&&P.fixing>0){const i=UMB.ribs[0].inv?0:1;t2=[UMB.g.ribs[i].px[NSEG],UMB.g.ribs[i].py[NSEG]];}
else if(UMB&&P.two>0.5){t2=[UMB.g.gx+UMB.g.ax*0.2,UMB.g.gy+UMB.g.ay*0.2];}
else if(!UMB){t2=[g.head[0]+g.fx*0.02+g.tx*0.1,g.head[1]+g.fy*0.02+g.ty*0.1];}
else{t2=[g.hip[0]+g.fx*0.05+g.tx*0.05,g.hip[1]+g.fy*0.05+g.ty*0.05];}
const kk=Math.min(1,dt/0.12);P.h2[0]+=(t2[0]-P.h2[0])*kk;P.h2[1]+=(t2[1]-P.h2[1])*kk;
rain(dt);
}
/* ---------------- drawing ---------------- */
function mix(a,b,t){return`rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;}
function ik(ax,ay,bx,by,l1,l2,bend){
const dx=bx-ax,dy=by-ay,d0=Math.hypot(dx,dy)||1e-6,ux=dx/d0,uy=dy/d0;
const d=Math.min(d0,l1+l2-1e-3),a=(l1*l1-l2*l2+d*d)/(2*d),h=Math.sqrt(Math.max(0,l1*l1-a*a));
return[[ax+ux*a-uy*h*bend,ay+uy*a+ux*h*bend],[ax+ux*d,ay+uy*d]];
}
function poly(pts,w,col){ctx.strokeStyle=col;ctx.lineWidth=w;ctx.beginPath();ctx.moveTo(X(pts[0][0]),Y(pts[0][1]));
for(let i=1;i<pts.length;i++)ctx.lineTo(X(pts[i][0]),Y(pts[i][1]));ctx.stroke();}
function circ(p,r,col){ctx.fillStyle=col;ctx.beginPath();ctx.arc(X(p[0]),Y(p[1]),r*S,0,Math.PI*2);ctx.fill();}
const bld=[];(function(){let x=-10,s=7;const rnd=()=>{s=(s*16807)%2147483647;return s/2147483647;};
while(x<22){const w=0.6+rnd()*1.2,h=1.2+rnd()*2.4;bld.push({x,w,h,win:rnd()});x+=w+0.05+rnd()*0.3;}})();
const tufts=[-2.3,-1.7,-0.9,0.55,1.4,2.1,2.8,3.6,4.4,5.3,6.1];
const NG=16,TILT=0.2,LIGHT=(()=>{const v=[-0.35,0.8,0.5],l=Math.hypot(...v);return v.map(x=>x/l);})();
function drawUmb(u){
const g=u.g;if(!g)return;
const ax=g.ax,ay=g.ay,ex=ay,ey=-ax;
const prof=[[],[]];
for(let i=0;i<2;i++){const R=g.ribs[i],sg=u.ribs[i].side;
for(let j=0;j<=NSEG;j++){const dx=R.px[j]-g.cx,dy=R.py[j]-g.cy;prof[i].push([sg*(dx*ex+dy*ey),dx*ax+dy*ay]);}}
wind(g.cx,g.cy,tw);const q=0.5*RHO*(tw[0]*tw[0]+tw[1]*tw[1]);
const flA=Math.min(0.03,q*0.00006)*(anyInv(u)?1:0.35);
const az=[];
for(let k=0;k<NG;k++){
const ph=2*Math.PI*k/NG,c=Math.cos(ph),s=Math.sin(ph),w=(1+c)/2,pts=[];
for(let j=0;j<=NSEG;j++){
const r=lerp(prof[0][j][0],prof[1][j][0],w);let z=lerp(prof[0][j][1],prof[1][j][1],w);
z+=flA*(j/NSEG)*Math.sin(T*26+k*1.9+j*0.8);
const wx=g.cx+ax*z+ex*r*c,wy=g.cy+ay*z+ey*r*c,d=r*s;
pts.push([X(wx),Y(wy-TILT*d),d]);
}
az.push({pts,c,s,w});
}
const gores=[];
for(let k=0;k<NG;k++){
const A=az[k],B=az[(k+1)%NG];
const pm=Math.PI*(2*k+1)/NG,cm=Math.cos(pm),sm=Math.sin(pm),wm=(1+cm)/2;
const r0=lerp(prof[0][3][0],prof[1][3][0],wm),z0=lerp(prof[0][3][1],prof[1][3][1],wm);
const r1=lerp(prof[0][5][0],prof[1][5][0],wm),z1=lerp(prof[0][5][1],prof[1][5][1],wm);
let nr=-(z1-z0),nz=r1-r0;const nl=Math.hypot(nr,nz)||1;nr/=nl;nz/=nl;
const Nx=ax*nz+ex*cm*nr,Ny=ay*nz+ey*cm*nr,Nz=sm*nr;
const br=Nx*LIGHT[0]+Ny*LIGHT[1]+Nz*LIGHT[2],facing=Nz+TILT*Ny;
gores.push({A,B,br,facing,dep:(A.pts[NSEG][2]+B.pts[NSEG][2])/2,k});
}
gores.sort((a,b)=>a.dep-b.dep);
const r0=u.ribs[0],r1=u.ribs[1];
const ribCol=w=>{const brk=(w<0.3&&r0.broken)||(w>0.7&&r1.broken);if(brk)return'#e6e6e6';
const ld=Math.pow(lerp(r0.load,r1.load,w),1.5);return mix([45,32,32],[255,175,40],ld);};
const drawGore=G=>{
const base=G.k%2?[186,38,46]:[160,30,40];
const f=G.facing>=0?0.5+0.5*Math.max(0,G.br):0.32+0.25*Math.max(0,-G.br);
ctx.fillStyle=`rgb(${base[0]*f|0},${base[1]*f|0},${base[2]*f|0})`;
ctx.beginPath();const a=G.A.pts,b=G.B.pts;ctx.moveTo(a[0][0],a[0][1]);
for(let j=1;j<=NSEG;j++)ctx.lineTo(a[j][0],a[j][1]);
for(let j=NSEG;j>=0;j--)ctx.lineTo(b[j][0],b[j][1]);
ctx.closePath();ctx.fill();
ctx.strokeStyle=ribCol(G.A.w);ctx.lineWidth=Math.max(1,S*0.008);ctx.beginPath();ctx.moveTo(a[0][0],a[0][1]);
for(let j=1;j<=NSEG;j++)ctx.lineTo(a[j][0],a[j][1]);ctx.stroke();
ctx.fillStyle='#222';ctx.beginPath();ctx.arc(a[NSEG][0],a[NSEG][1],Math.max(1.2,S*0.007),0,7);ctx.fill();
};
ctx.lineCap='round';ctx.lineJoin='round';
for(const G of gores)if(G.dep<0)drawGore(G);
// shaft, runner, stretchers
let dsum=0;for(let i=0;i<2;i++){const r=prof[i][STR_J][0],z=prof[i][STR_J][1];dsum+=-z+Math.sqrt(Math.max(0,STR_L*STR_L-r*r));}
const dr=clamp(dsum/2,0.05,0.42);
const Hx=g.gx-ax*L_HANDLE,Hy=g.gy-ay*L_HANDLE,Rx=g.cx-ax*dr,Ry=g.cy-ay*dr;
poly([[Hx,Hy],[g.cx,g.cy]],Math.max(1.5,S*0.018),'#3c3f44');
ctx.strokeStyle='#8a8f96';ctx.lineWidth=Math.max(0.8,S*0.005);ctx.beginPath();
for(const A of az){ctx.moveTo(X(Rx),Y(Ry));ctx.lineTo(A.pts[STR_J][0],A.pts[STR_J][1]);}ctx.stroke();
poly([[Rx-ax*0.03,Ry-ay*0.03],[Rx+ax*0.03,Ry+ay*0.03]],Math.max(2,S*0.03),'#55595f');
// J handle
ctx.strokeStyle='#5b3a1e';ctx.lineWidth=Math.max(2,S*0.035);ctx.beginPath();
ctx.moveTo(X(g.gx),Y(g.gy));ctx.lineTo(X(Hx),Y(Hy));
ctx.bezierCurveTo(X(Hx-ax*0.09),Y(Hy-ay*0.09),X(Hx-ax*0.09+ex*0.09),Y(Hy-ay*0.09+ey*0.09),X(Hx-ax*0.03+ex*0.09),Y(Hy-ay*0.03+ey*0.09));
ctx.stroke();
for(const G of gores)if(G.dep>=0)drawGore(G);
poly([[g.cx,g.cy],[g.cx+ax*0.1,g.cy+ay*0.1]],Math.max(1.5,S*0.016),'#2c2e31');
}
function drawLeg(f,g,col){
const ax=f.x,ay=0.09+f.lift;
const [kn]=ik(ax,ay,g.hip[0],g.hip[1],0.47,0.47,1);
poly([[ax,ay],kn,g.hip],S*0.12,col);
const l=f.lift;ctx.fillStyle='#15181d';ctx.beginPath();
ctx.moveTo(X(ax+0.06),Y(l));ctx.lineTo(X(ax-0.17),Y(l));
ctx.quadraticCurveTo(X(ax-0.2),Y(l+0.07),X(ax-0.08),Y(l+0.09));
ctx.lineTo(X(ax+0.06),Y(l+0.14));ctx.closePath();ctx.fill();
}
function drawArm(sh,target,col){
const [el,end]=ik(sh[0],sh[1],target[0],target[1],0.31,0.3,1);
poly([sh,el,end],S*0.095,col);circ(end,0.045,'#e2b08c');
}
function drawPersonBack(){
const g=P.geo;ctx.lineCap='round';ctx.lineJoin='round';
drawLeg(P.feet[1],g,'#232a38');
drawArm([g.sh[0]-g.fx*0.03,g.sh[1]-g.fy*0.03],P.h2,'#a87c14');
}
function drawPersonBody(){
const g=P.geo,{hip,sh,tx,ty,fx,fy}=g;
drawLeg(P.feet[0],g,'#2e3748');
wind(hip[0],hip[1],tw);const u=Math.max(0,tw[0]);
const flap=Math.min(0.2,u*u*0.0003)+Math.sin(T*17)*0.012*Math.min(1,u/10);
const pts=[[sh[0]+fx*0.12,sh[1]+fy*0.12],[hip[0]+fx*0.15,hip[1]+fy*0.15],
[hip[0]-tx*0.22+fx*0.15+flap*0.6,hip[1]-ty*0.22+fy*0.15],
[hip[0]-tx*0.2-fx*0.15+flap*1.4,hip[1]-ty*0.2-fy*0.15+flap*0.3],
[hip[0]-fx*0.15,hip[1]-fy*0.15],[sh[0]-fx*0.13,sh[1]-fy*0.13]];
ctx.fillStyle='#d9a521';ctx.strokeStyle='#d9a521';ctx.lineWidth=S*0.05;ctx.beginPath();
ctx.moveTo(X(pts[0][0]),Y(pts[0][1]));for(let i=1;i<pts.length;i++)ctx.lineTo(X(pts[i][0]),Y(pts[i][1]));
ctx.closePath();ctx.fill();ctx.stroke();
poly([[hip[0]+fx*0.14,hip[1]+fy*0.14],[hip[0]-tx*0.2+fx*0.14+flap*0.6,hip[1]-ty*0.2+fy*0.14]],Math.max(1,S*0.006),'#a87c14');
const hd=g.head;
circ(hd,0.13,'#c2921a');
circ([hd[0]+fx*0.045,hd[1]+fy*0.045],0.082,'#e2b08c');
ctx.strokeStyle='#d9a521';ctx.lineWidth=S*0.035;ctx.beginPath();
const ang=Math.atan2(-fy,fx);ctx.arc(X(hd[0]),Y(hd[1]),0.125*S,ang-1.2,ang+1.2);ctx.stroke();
const ex=hd[0]+fx*0.09+tx*0.02,ey=hd[1]+fy*0.09+ty*0.02;
if(P.strain>0.7||P.uF>18){poly([[ex-0.018,ey+0.004],[ex+0.018,ey-0.004]],Math.max(1,S*0.008),'#222');}
else circ([ex,ey],0.012,'#222');
poly([[hd[0]+fx*0.12,hd[1]+fy*0.12-0.005],[hd[0]+fx*0.14-tx*0.025,hd[1]+fy*0.14-ty*0.025]],Math.max(1,S*0.012),'#c9936f');
}
function drawPersonFront(){const g=P.geo;drawArm([g.sh[0]+g.fx*0.02,g.sh[1]+g.fy*0.02],P.hand,'#d9a521');}
function draw(){
ctx.setTransform(DPR,0,0,DPR,0,0);
const storm=clamp(W/30,0,1);
const sk=ctx.createLinearGradient(0,0,0,GY);
sk.addColorStop(0,mix([96,112,132],[26,32,44],storm));sk.addColorStop(1,mix([150,160,172],[58,66,80],storm));
ctx.fillStyle=sk;ctx.fillRect(0,0,DW,DH);
const kp=0.45,by=Y(0.25);
ctx.fillStyle=mix([110,120,135],[40,47,60],storm);
for(const b of bld){const x0=DW*OX+b.x*S*kp;ctx.fillRect(x0,by-b.h*S*kp,b.w*S*kp,b.h*S*kp+2);}
ctx.fillStyle=`rgba(255,215,140,${0.25+0.3*storm})`;
for(const b of bld){if(b.win<0.5)continue;const x0=DW*OX+b.x*S*kp;
for(let i=0;i<3;i++)for(let j=0;j<Math.floor(b.h*2);j++){if(hash(b.x*13+i*7+j)<0.6)continue;
ctx.fillRect(x0+(0.15+i*0.25)*b.w*S*kp,by-(0.3+j*0.45)*S*kp,0.07*S*kp,0.12*S*kp);}}
// rain behind
ctx.strokeStyle='rgba(195,210,230,0.35)';ctx.lineWidth=1;ctx.beginPath();
for(let i=0;i<drops.length;i+=2){const d=drops[i];ctx.moveTo(X(d.x),Y(d.y));ctx.lineTo(X(d.x-d.vx*0.022),Y(d.y-d.vy*0.022));}
ctx.stroke();
// ground
const gg=ctx.createLinearGradient(0,GY,0,DH);gg.addColorStop(0,'#232a33');gg.addColorStop(1,'#0e1116');
ctx.fillStyle=gg;ctx.fillRect(0,GY,DW,DH-GY);
ctx.fillStyle='rgba(170,190,210,0.25)';ctx.fillRect(0,GY,DW,1.5);
ctx.fillStyle='rgba(150,170,195,0.14)';ctx.beginPath();ctx.ellipse(X(0.9),Y(-0.08),0.8*S,0.05*S,0,0,7);ctx.fill();
// grass
wind(0,0.2,tw);const ug=Math.max(0,tw[0]);
ctx.strokeStyle='#2f4a2c';ctx.lineWidth=Math.max(1,S*0.008);ctx.beginPath();
for(const x of tufts){for(let b=0;b<4;b++){const xb=x+b*0.03;
const bend=clamp(ug*0.007,0,0.11)+0.012*Math.sin(T*14+xb*9)*Math.min(1,ug/6);
ctx.moveTo(X(xb),Y(0));ctx.quadraticCurveTo(X(xb+bend*0.3),Y(0.08),X(xb+bend),Y(0.13-bend*0.5));}}
ctx.stroke();
for(const u of debris)drawUmb(u);
drawPersonBack();
drawPersonBody();
if(UMB)drawUmb(UMB);
drawPersonFront();
// rain front
ctx.strokeStyle='rgba(215,228,245,0.5)';ctx.lineWidth=1.2;ctx.beginPath();
for(let i=1;i<drops.length;i+=2){const d=drops[i];ctx.moveTo(X(d.x),Y(d.y));ctx.lineTo(X(d.x-d.vx*0.025),Y(d.y-d.vy*0.025));}
ctx.stroke();
ctx.fillStyle='rgba(220,232,248,0.8)';
for(const s of splashes){ctx.fillRect(X(s.x)-1,Y(s.y)-1,2,2);}
}
/* ---------------- HUD ---------------- */
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'];
const beaufort=v=>{let b=0;while(b<BF.length&&v>=BF[b])b++;return b;};
const bar=(f,n=12)=>{const k=Math.round(clamp(f,0,1)*n);return'█'.repeat(k)+'░'.repeat(n-k);};
function hud(){
wind(0,1.8,tw);const gu=tw[0];
$('spd').textContent=W.toFixed(1)+' m/s';
const b=beaufort(W);
$('sub').textContent=`${Math.round(W*3.6)} km/h · Beaufort ${b}, ${BN[b]} · gust at head height ${gu.toFixed(1)} m/s${auto?' · rising':''}`;
let s='';
if(UMB){
const r0=UMB.ribs[0],r1=UMB.ribs[1];
s+=`umbrella held, ${P.two>0.5?'both hands':'one hand'}${P.wrenched?' — WRENCHED':''}${P.fixing>0?' — pulling it back':''}\n`;
s+=`canopy left ${r0.inv?'INSIDE OUT':'normal '} right ${r1.inv?'INSIDE OUT':'normal'}\n`;
const ld=Math.max(r0.load,r1.load);
s+=`frame ${bar(ld)} ${String(Math.round(ld*100)).padStart(3)}% to snap-through\n`;
s+=`damage ${(r0.broken?1:0)+(r1.broken?1:0)} ribs broken · ${r0.flips+r1.flips} inversions · catch strength ${Math.round(100*(r0.Tpeak+r1.Tpeak)/(2*T_PEAK0))}%\n`;
s+=`wrist ${bar(Math.abs(UMB.tg)/P.tmax)} ${Math.abs(UMB.tg).toFixed(1)} / ${P.tmax.toFixed(0)} N·m\n`;
s+=`pull ${bar(UMB.F/P.gripMax)} ${String(Math.round(UMB.F)).padStart(3)} / ${Math.round(P.gripMax)} N${UMB.slip>0.05?' slipping '+Math.round(UMB.slip*100)+'%':''}\n`;
} else {
s+=`umbrella gone${P.uF>=15?' — waiting for a lull before opening the spare':' — getting the spare out'}\n\n\n\n\n\n`;
}
s+=`body lean ${String(Math.round(P.lam*57.3)).padStart(3)}° · stance ${P.sw.toFixed(2)} m · crouch ${Math.round(P.crouch*100)}% · ankles ${bar(P.strain,8)}`;
$('stat').textContent=s;
$('ev').textContent=events.filter(e=>T-e.t<6).map(e=>'› '+e.m).join('\n');
$('slv').textContent=Wset.toFixed(1)+' m/s';
if(!sliding)$('sl').value=Wset;
}
/* ---------------- input ---------------- */
const sl=$('sl');
sl.addEventListener('pointerdown',()=>sliding=true);
window.addEventListener('pointerup',()=>sliding=false);
sl.addEventListener('input',()=>{const v=+sl.value;if(v<Wset-0.05){auto=false;$('auto').checked=false;}Wset=v;});
$('auto').addEventListener('change',e=>{auto=e.target.checked;});
$('newu').addEventListener('click',()=>newUmb());
$('pause').addEventListener('click',()=>{paused=!paused;});
window.addEventListener('keydown',e=>{
if(e.target===sl&&(e.key==='ArrowUp'||e.key==='ArrowDown'||e.key==='ArrowLeft'||e.key==='ArrowRight'))return;
if(e.key==='ArrowUp'){Wset=Math.min(35,Wset+1);e.preventDefault();}
else if(e.key==='ArrowDown'){Wset=Math.max(0,Wset-1);auto=false;$('auto').checked=false;e.preventDefault();}
else if(e.key==='a'||e.key==='A'){auto=!auto;$('auto').checked=auto;}
else if(e.key==='n'||e.key==='N'){newUmb();}
else if(e.key==='p'||e.key==='P'){paused=!paused;}
});
/* ---------------- loop ---------------- */
let last=performance.now(),hf=0;
function frame(now){
const dt=Math.min(0.05,(now-last)/1000);last=now;
if(!paused)update(dt);
draw();
if((hf++&3)===0)hud();
requestAnimationFrame(frame);
}
update(0.016);
requestAnimationFrame(frame);
</script>
</body>
</html>
This is the behaviour I designed it to produce. I haven't run and tuned it, so the wind speeds below are estimates from the constants, not measurements.
What should look right
What is wrong