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.
In the code the canopy is a ring of eight ribs radiating from a hub at the top of the shaft; each rib tip sits on an ellipse (the rim) and the fabric is drawn as eight scalloped panels between neighbouring tips. The whole dome is described by one signed number, the dome depth h: positive means the hub (peak) sits above the rim plane (a normal dome), negative means it sits below it (inside out). Inversion is a latch on that sign — I integrate a spring that drives an inversion coordinate inv from 0 to 1, and h = baseDepth·(1 − 2·inv), so as inv crosses 0.5 the peak passes through the rim plane and the canopy snaps from convex to concave. What decides it is a force balance: the aerodynamic load F = ½ρv²·Cd·A is compared against the frame's restoring threshold; above Fup the ribs "give" and inv is driven to 1, below a lower Fdown it springs back to 0, and the gap between the two is the hysteresis that stops a real umbrella flickering on the edge. Cd itself drops once inverted (a bowl spills more than a scoop catches), which is why an inverted umbrella gets dragged rather than flipping straight back.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Umbrella in a Gale</title>
<style>
:root{
--ink:#e9eef5;
--accent:#ff6a4d;
--panel:rgba(14,20,28,0.52);
--edge:rgba(255,255,255,0.12);
}
*{box-sizing:border-box;}
html,body{margin:0;height:100%;overflow:hidden;background:#0a0f15;color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;}
#scene{position:fixed;inset:0;display:block;}
.panel{position:fixed;background:var(--panel);border:1px solid var(--edge);
border-radius:14px;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
box-shadow:0 8px 30px rgba(0,0,0,0.35);}
#readout{top:16px;left:16px;padding:14px 16px 12px;min-width:210px;pointer-events:none;}
#title{font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.6;margin:0 0 6px;}
#windrow{display:flex;align-items:baseline;gap:8px;}
#windNum{font-size:52px;font-weight:700;line-height:.9;font-variant-numeric:tabular-nums;
transition:color .2s;}
#windUnit{font-size:15px;opacity:.7;}
#windMs{font-size:12px;opacity:.5;margin-left:2px;}
#windWord{font-size:14px;margin-top:4px;opacity:.85;letter-spacing:.02em;}
#gauge{margin-top:10px;height:7px;border-radius:4px;background:rgba(255,255,255,0.10);overflow:hidden;}
#gaugeFill{height:100%;width:0%;border-radius:4px;background:#5fd0a0;transition:width .08s linear,background .3s;}
#status{margin-top:10px;font-size:13px;font-weight:600;letter-spacing:.02em;min-height:16px;}
#controls{left:50%;transform:translateX(-50%);bottom:16px;padding:12px 16px;
display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center;max-width:94vw;}
#controls label{font-size:12px;opacity:.7;letter-spacing:.04em;}
#slider{-webkit-appearance:none;appearance:none;width:min(46vw,320px);height:6px;border-radius:4px;
background:linear-gradient(90deg,#5fd0a0,#f0c250,#ff5a44);outline:none;}
#slider::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;
background:#fff;border:2px solid #1a222c;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,.4);}
#slider::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:#fff;
border:2px solid #1a222c;cursor:pointer;}
button{font:inherit;font-size:13px;font-weight:600;color:var(--ink);cursor:pointer;
background:rgba(255,255,255,0.08);border:1px solid var(--edge);border-radius:9px;
padding:8px 12px;transition:background .15s;}
button:hover{background:rgba(255,255,255,0.16);}
button.on{background:var(--accent);border-color:transparent;color:#1a0f0b;}
#hint{font-size:11px;opacity:.5;width:100%;text-align:center;letter-spacing:.03em;}
@media (max-width:560px){ #windNum{font-size:40px;} #readout{min-width:170px;} }
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div id="readout" class="panel">
<p id="title">Umbrella in a Gale</p>
<div id="windrow">
<span id="windNum">0</span><span id="windUnit">km/h</span>
<span id="windMs">0 m/s</span>
</div>
<div id="windWord">Calm</div>
<div id="gauge"><div id="gaugeFill"></div></div>
<div id="status">Holding.</div>
</div>
<div id="controls" class="panel">
<label for="slider">WIND</label>
<input id="slider" type="range" min="0" max="42" step="0.1" value="3">
<button id="autoBtn" class="on">Auto: on</button>
<button id="resetBtn">New umbrella</button>
<div id="hint">Drag the slider or ↑/↓ to drive the wind · Space = auto · R = reset</div>
</div>
<script>
(function(){
"use strict";
/* ---------- helpers ---------- */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rand=(a,b)=>a+Math.random()*(b-a);
const TAU=Math.PI*2;
function lerpColor(c1,c2,t){return `rgb(${Math.round(lerp(c1[0],c2[0],t))},${Math.round(lerp(c1[1],c2[1],t))},${Math.round(lerp(c1[2],c2[2],t))})`;}
/* ---------- canvas ---------- */
const cv=document.getElementById('scene');
const ctx=cv.getContext('2d');
let W=0,H=0,DPR=1;
const L={}; // layout metrics
function layout(){
L.groundY = H*0.85;
L.footX = W*0.42;
L.legLen = H*0.150;
L.torsoLen = H*0.150;
L.headR = H*0.032;
L.upperArm = H*0.085;
L.foreArm = H*0.085;
L.shaftLen = H*0.220;
L.R = Math.min(H*0.140, W*0.20);
L.baseH = H*0.055;
L.squash = 0.34;
}
function resize(){
DPR=Math.min(window.devicePixelRatio||1,2);
W=window.innerWidth; H=window.innerHeight;
cv.width=Math.floor(W*DPR); cv.height=Math.floor(H*DPR);
cv.style.width=W+'px'; cv.style.height=H+'px';
ctx.setTransform(DPR,0,0,DPR,0,0);
layout();
initGrass(); initClouds();
}
/* ---------- physics constants ---------- */
const PHYS={
rho:1.2, area:0.55, cdNormal:1.30, cdInv:0.85,
Fup:120, Fdown:55, // inversion thresholds (N) with hysteresis
baseGrip:250, invGripMul:0.75, // grip strength (N), reduced when inverted
maxWind:42, autoRate:1.5 // m/s, m/s per second
};
/* ---------- state ---------- */
const sim={
t:0, auto:true, windTarget:3, windBase:3, wind:3,
inv:0, invVel:0,
tilt:-0.10, tiltVel:0,
lean:0, crouch:0, grip:0,
attached:true, F:0, q:0,
free:{active:false,x:0,y:0,vx:0,vy:0,ang:0,angVel:0},
flash:0, nextFlash:1e9
};
let pose=null;
/* ---------- wind ---------- */
function updateWind(dt){
if(sim.auto) sim.windTarget=Math.min(PHYS.maxWind, sim.windTarget+PHYS.autoRate*dt);
sim.windBase += (sim.windTarget-sim.windBase)*Math.min(1,dt*1.8);
const n=0.62*Math.sin(sim.t*0.9)+0.28*Math.sin(sim.t*2.17+1.3)
+0.16*Math.sin(sim.t*4.3+2.1)+0.10*Math.sin(sim.t*7.9+0.6);
const env=Math.pow(Math.max(0,Math.sin(sim.t*0.37+0.5)),6); // occasional strong gust
const gust=1+0.17*n+0.35*env;
sim.wind=Math.max(0, sim.windBase*gust);
}
/* ---------- forces ---------- */
function computeForces(){
const cd=lerp(PHYS.cdNormal,PHYS.cdInv,sim.inv);
sim.q=0.5*PHYS.rho*sim.wind*sim.wind;
sim.F=sim.q*cd*PHYS.area;
}
/* ---------- inversion (latched spring on the dome sign) ---------- */
function updateInversion(dt){
let target;
if(sim.inv>=0.5) target=(sim.F<PHYS.Fdown)?0:1;
else target=(sim.F>PHYS.Fup)?1:0;
sim.invVel += (target-sim.inv)*34*dt;
sim.invVel *= Math.exp(-4.5*dt);
sim.inv += sim.invVel*dt;
if(sim.inv<0){sim.inv=0; if(sim.invVel<0)sim.invVel*=-0.25;}
if(sim.inv>1){sim.inv=1; if(sim.invVel>0)sim.invVel*=-0.25;}
}
/* ---------- umbrella attitude (tilt + gust wrench) ---------- */
function updateAttitude(dt){
let tiltTarget=-0.10 + sim.F*0.0052 + sim.inv*0.15;
tiltTarget=Math.min(tiltTarget,1.45);
sim.tiltVel += (tiltTarget-sim.tilt)*26*dt;
sim.tiltVel *= Math.exp(-5*dt);
if(sim.attached && sim.F>90 && Math.random()<(sim.F-90)*0.0009)
sim.tiltVel += rand(-1,1)*2.2; // sudden sideways wrench
sim.tilt += sim.tiltVel*dt;
sim.tilt = clamp(sim.tilt,-0.5,1.6);
}
/* ---------- person compensation ---------- */
function updatePerson(dt){
const leanT=clamp(sim.F*0.0032,0,0.62);
sim.lean += (leanT-sim.lean)*Math.min(1,dt*4);
const cT=clamp((sim.F-20)*0.006,0,1);
sim.crouch += (cT-sim.crouch)*Math.min(1,dt*4);
sim.grip=clamp(sim.F/PHYS.baseGrip,0,1);
}
/* ---------- skeleton / umbrella pose ---------- */
function computePose(){
const lean=sim.lean, crouch=sim.crouch;
const up={x:-Math.sin(lean), y:-Math.cos(lean)}; // body "up", tilts into wind (-x)
const ankle={x:L.footX, y:L.groundY};
const legLen=L.legLen*(1-0.26*crouch);
const hip={x:ankle.x+up.x*legLen - crouch*L.legLen*0.18, y:ankle.y+up.y*legLen};
const knee={x:(ankle.x+hip.x)/2 - crouch*L.legLen*0.35 - L.legLen*0.06, y:(ankle.y+hip.y)/2};
const torsoLen=L.torsoLen*(1-0.08*crouch);
const neck={x:hip.x+up.x*torsoLen - crouch*L.torsoLen*0.10, y:hip.y+up.y*torsoLen};
const headC={x:neck.x+up.x*L.headR*1.05, y:neck.y+up.y*L.headR*1.05 - L.headR*0.15};
const shoulder={x:neck.x, y:neck.y+L.headR*0.30};
const handUp=(L.upperArm+L.foreArm)*(sim.attached?0.82:0.5);
const handFwd=L.foreArm*0.55;
const handDrag=clamp(sim.F*0.10,0,L.foreArm*1.1);
const grip={x:shoulder.x+up.x*handUp - handFwd + handDrag, y:shoulder.y+up.y*handUp};
const t=sim.tilt;
const shaft={x:Math.sin(t), y:-Math.cos(t)}; // grip -> hub
const perp ={x:Math.cos(t), y:Math.sin(t)}; // points downwind (+x) at t=0
const bendMag=clamp(sim.F*0.10,0,L.shaftLen*0.5);
const hub={x:grip.x+shaft.x*L.shaftLen+perp.x*bendMag,
y:grip.y+shaft.y*L.shaftLen+perp.y*bendMag};
const d={x:-shaft.x, y:-shaft.y}; // hub -> hand (down-shaft)
const p={x:d.y, y:-d.x}; // rim width axis
return {up,ankle,hip,knee,neck,headC,shoulder,grip,hub,shaft,perp,d,p,bendMag};
}
/* ---------- rip-away ---------- */
function detach(){
sim.attached=false;
const f=sim.free, hub=pose.hub;
f.active=true; f.x=hub.x; f.y=hub.y;
const wpx=sim.wind*(W*0.010);
f.vx=wpx*0.6+120+sim.F*0.4;
f.vy=-160-Math.random()*120;
f.ang=sim.tilt;
f.angVel=(Math.random()<0.5?-1:1)*rand(2.5,5.5);
sim.auto=false;
}
function checkRip(){
if(!sim.attached) return;
const gripLimit=PHYS.baseGrip*(1-(1-PHYS.invGripMul)*sim.inv);
const eff=gripLimit*(0.9+0.2*Math.random());
if(sim.F>eff) detach();
}
function updateFree(dt){
const f=sim.free;
const wpx=sim.wind*(W*0.010);
f.vx += (wpx-f.vx)*Math.min(1,dt*0.8);
f.vy += 1500*dt;
f.vy += (0-f.vy)*Math.min(1,dt*0.3);
f.x += f.vx*dt; f.y += f.vy*dt;
f.ang += f.angVel*dt;
f.angVel += wpx*0.0006*dt - f.angVel*0.02*dt;
if(f.y>H+320 || f.x>W+460) f.active=false;
}
/* ---------- rain ---------- */
const rain={drops:[],max:520};
function newDrop(any){return {x:rand(-0.15*W,1.05*W), y:any?rand(-40,H):-rand(10,60),
sp:rand(0.8,1.3), a:rand(0.25,0.6)};}
function initRain(){rain.drops.length=0; for(let i=0;i<rain.max;i++)rain.drops.push(newDrop(true));}
function updateRain(dt){
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
const windPx=sim.wind*(W*0.010), fall=H*0.9;
for(const dr of rain.drops){
dr.x += windPx*dr.sp*dt;
dr.y += fall*dr.sp*dt;
if(dr.y>H+20 || dr.x>W+40){ dr.x=rand(-0.15*W,1.0*W); dr.y=-rand(10,50); }
}
}
/* ---------- wind streaks ---------- */
const streaks=[];
function initStreaks(){streaks.length=0; for(let i=0;i<46;i++)streaks.push({x:rand(0,W),y:rand(0,L.groundY),len:rand(20,60),sp:rand(0.7,1.4),a:rand(0.05,0.18)});}
function updateStreaks(dt){
const windPx=sim.wind*(W*0.010);
for(const s of streaks){ s.x += windPx*s.sp*dt*1.5; if(s.x-s.len>W){ s.x=-s.len; s.y=rand(0,L.groundY);} }
}
/* ---------- grass ---------- */
let grass=[];
function initGrass(){grass.length=0; const n=Math.round(W/70); for(let i=0;i<n;i++){const bx=rand(0,W); const blades=[]; const nb=2+((Math.random()*3)|0); for(let b=0;b<nb;b++)blades.push({a:rand(-0.5,0.5),len:rand(0.02,0.045)*H,ph:rand(0,TAU)}); grass.push({x:bx,blades});}}
/* ---------- clouds ---------- */
let clouds=[];
function initClouds(){clouds.length=0; for(let i=0;i<5;i++)clouds.push({x:rand(0,W),y:rand(H*0.05,H*0.4),rx:rand(W*0.12,W*0.28),ry:rand(H*0.04,H*0.09),sp:rand(0.4,1.0)});}
/* ---------- scarf (verlet rope) ---------- */
const scarf={pts:[],seg:10};
function initScarf(){
scarf.pts.length=0; scarf.seg=L.headR*0.55;
const nx=L.footX, ny=L.groundY-L.legLen-L.torsoLen;
for(let i=0;i<9;i++)scarf.pts.push({x:nx+i*scarf.seg,y:ny,px:nx+i*scarf.seg,py:ny});
}
function updateScarf(dt){
if(!pose||scarf.pts.length<2)return;
const s=scarf, dt2=dt*dt;
const windAcc=sim.wind*sim.wind*2.2, grav=1400;
s.pts[0].x=pose.neck.x; s.pts[0].y=pose.neck.y; s.pts[0].px=s.pts[0].x; s.pts[0].py=s.pts[0].y;
for(let i=1;i<s.pts.length;i++){
const p=s.pts[i];
let vx=(p.x-p.px)*0.9, vy=(p.y-p.py)*0.9;
const vm=Math.hypot(vx,vy), vmax=s.seg*0.9;
if(vm>vmax){vx*=vmax/vm; vy*=vmax/vm;}
p.px=p.x; p.py=p.y;
const flut=Math.sin(sim.t*11+i*0.9)*windAcc*0.25;
p.x += vx + windAcc*dt2;
p.y += vy + (grav+flut)*dt2;
}
for(let k=0;k<4;k++){
s.pts[0].x=pose.neck.x; s.pts[0].y=pose.neck.y;
for(let i=0;i<s.pts.length-1;i++){
const a=s.pts[i], b=s.pts[i+1];
let dx=b.x-a.x, dy=b.y-a.y, d=Math.hypot(dx,dy)||1e-4;
const diff=(d-s.seg)/d, ma=(i===0?0:0.5), mb=(i===0?1:0.5);
a.x+=dx*diff*ma; a.y+=dy*diff*ma; b.x-=dx*diff*mb; b.y-=dy*diff*mb;
}
}
}
/* ---------- lightning ---------- */
function updateFlash(dt){
if(sim.wind>34 && sim.t>sim.nextFlash && Math.random()<0.02){ sim.flash=1; sim.nextFlash=sim.t+rand(2,6); }
sim.flash*=Math.exp(-6*dt);
}
/* ---------- master step ---------- */
function step(dt){
sim.t+=dt;
updateWind(dt);
computeForces();
updateInversion(dt);
updateAttitude(dt);
updatePerson(dt);
pose=computePose();
checkRip();
if(!sim.attached) updateFree(dt);
updateScarf(dt);
updateRain(dt);
updateStreaks(dt);
updateFlash(dt);
}
/* ================= RENDERING ================= */
const COL={
canopyA:'#e0574c', canopyB:'#c8463c', rib:'#7d2b25', edge:'#391512',
shaft:'#c9a26b', handle:'#6b4a2f', ferrule:'#d6d6dc',
coat:'#2f3a49', coatDark:'#26303d', trousers:'#222b37', boot:'#141a21',
skin:'#caa07d', beanie:'#e0b040', scarf:'#e6b64a'
};
function drawSky(){
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
const top=lerpColor([40,54,67],[9,13,20],wN);
const hor=lerpColor([96,110,122],[38,48,60],wN);
const g=ctx.createLinearGradient(0,0,0,L.groundY);
g.addColorStop(0,top); g.addColorStop(1,hor);
ctx.fillStyle=g; ctx.fillRect(0,0,W,L.groundY);
// clouds
for(const c of clouds){
c.x += (10+sim.wind*4)*c.sp*0.016;
if(c.x-c.rx>W){c.x=-c.rx; c.y=rand(H*0.05,H*0.4);}
ctx.fillStyle=`rgba(20,26,34,${0.18+0.22*wN})`;
ctx.beginPath(); ctx.ellipse(c.x,c.y,c.rx,c.ry,0,0,TAU); ctx.fill();
}
}
function drawGround(){
ctx.fillStyle='#0d1319'; ctx.fillRect(0,L.groundY,W,H-L.groundY);
ctx.strokeStyle='rgba(150,170,190,0.20)'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(0,L.groundY); ctx.lineTo(W,L.groundY); ctx.stroke();
// grass
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
ctx.lineCap='round';
for(const t of grass){
for(const b of t.blades){
const sway=(0.15+0.85*wN)*b.len*0.7 + Math.sin(sim.t*6+b.ph)*b.len*0.12*wN;
ctx.strokeStyle='rgba(70,96,86,0.55)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(t.x,L.groundY);
ctx.quadraticCurveTo(t.x+sway*0.4, L.groundY-b.len*0.6, t.x+b.len*Math.sin(b.a)+sway, L.groundY-Math.cos(b.a)*b.len);
ctx.stroke();
}
}
}
function drawStreaks(){
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
if(wN<0.12) return;
ctx.lineWidth=1;
for(const s of streaks){
ctx.strokeStyle=`rgba(220,232,245,${s.a*wN})`;
ctx.beginPath(); ctx.moveTo(s.x,s.y); ctx.lineTo(s.x-s.len*(0.5+wN),s.y); ctx.stroke();
}
}
function limb(pts,w,col){
ctx.strokeStyle=col; ctx.lineWidth=w; ctx.lineCap='round'; ctx.lineJoin='round';
ctx.beginPath(); ctx.moveTo(pts[0].x,pts[0].y);
for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i].x,pts[i].y);
ctx.stroke();
}
function drawPersonBody(){
const P=pose;
// far leg
limb([P.hip,{x:P.knee.x+6,y:P.knee.y}, {x:P.ankle.x+6,y:P.ankle.y}], L.headR*0.85, COL.trousers);
// near leg
limb([P.hip,P.knee,P.ankle], L.headR*0.95, COL.trousers);
// boots
ctx.fillStyle=COL.boot;
for(const dx of [6,0]){
ctx.beginPath();
ctx.ellipse(P.ankle.x+dx-2, L.groundY-3, L.headR*0.9, L.headR*0.5, 0,0,TAU);
ctx.fill();
}
// coat-tail (blows downwind)
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
const tailLen=L.torsoLen*(0.5+0.6*wN);
const flap=Math.sin(sim.t*9)*tailLen*0.12*wN;
ctx.fillStyle=COL.coatDark;
ctx.beginPath();
ctx.moveTo(P.hip.x-4,P.hip.y-4);
ctx.quadraticCurveTo(P.hip.x+tailLen*0.5, P.hip.y+4+flap, P.hip.x+tailLen, P.hip.y+10+flap);
ctx.lineTo(P.hip.x+4,P.hip.y+L.headR*1.4);
ctx.closePath(); ctx.fill();
// torso / coat
limb([P.hip,P.neck], L.torsoLen*0.95, COL.coat);
// far arm (balance)
const swing=0.35+1.15*wN;
const fd={x:Math.sin(swing),y:Math.cos(swing)};
const fE={x:P.shoulder.x+fd.x*L.upperArm*0.9+4, y:P.shoulder.y+fd.y*L.upperArm};
const fH={x:fE.x+fd.x*L.foreArm*0.85+6, y:fE.y+fd.y*L.foreArm*0.85};
limb([{x:P.shoulder.x+4,y:P.shoulder.y},fE,fH], L.headR*0.7, COL.coatDark);
ctx.fillStyle=COL.skin; ctx.beginPath(); ctx.arc(fH.x,fH.y,L.headR*0.4,0,TAU); ctx.fill();
// head
ctx.fillStyle=COL.skin; ctx.beginPath(); ctx.arc(P.headC.x,P.headC.y,L.headR,0,TAU); ctx.fill();
// nose (faces -x, into wind)
ctx.beginPath(); ctx.moveTo(P.headC.x-L.headR*0.9,P.headC.y);
ctx.lineTo(P.headC.x-L.headR*1.4,P.headC.y+L.headR*0.15);
ctx.lineTo(P.headC.x-L.headR*0.85,P.headC.y+L.headR*0.35); ctx.closePath(); ctx.fill();
// beanie
ctx.fillStyle=COL.beanie;
ctx.beginPath(); ctx.arc(P.headC.x,P.headC.y,L.headR*1.06,Math.PI*1.02,Math.PI*1.98); ctx.fill();
ctx.fillRect(P.headC.x-L.headR*1.06,P.headC.y-L.headR*0.28,L.headR*2.12,L.headR*0.32);
}
function rimPoint(th,hub,d,p,R,h,load,t){
const push=R*0.16*load*(0.5+0.5*Math.cos(th)); // whole canopy leans downwind
const flut=Math.sin(t*22+th*3)*R*0.03*load; // fabric flutter
return {
x:hub.x + d.x*h + p.x*(R*Math.cos(th)) + push + d.x*flut,
y:hub.y + d.y*h + p.y*(R*Math.cos(th)) + R*L.squash*Math.sin(th) + d.y*flut
};
}
function drawCanopy(hub,d,p,R,h,N,load,t){
const rim=[]; for(let i=0;i<N;i++)rim.push(rimPoint(i/N*TAU,hub,d,p,R,h,load,t));
// panels
for(let i=0;i<N;i++){
const a=rim[i], b=rim[(i+1)%N];
const mid={x:(a.x+b.x)/2 + d.x*R*0.14, y:(a.y+b.y)/2 + d.y*R*0.14}; // scallop dips outboard
ctx.beginPath(); ctx.moveTo(hub.x,hub.y);
ctx.lineTo(a.x,a.y); ctx.quadraticCurveTo(mid.x,mid.y,b.x,b.y); ctx.closePath();
ctx.fillStyle=(i%2?COL.canopyB:COL.canopyA); ctx.fill();
}
// ribs
ctx.strokeStyle=COL.rib; ctx.lineWidth=Math.max(1.5,R*0.03); ctx.lineCap='round';
for(let i=0;i<N;i++){ ctx.beginPath(); ctx.moveTo(hub.x,hub.y); ctx.lineTo(rim[i].x,rim[i].y); ctx.stroke(); }
// scalloped rim edge
ctx.strokeStyle=COL.edge; ctx.lineWidth=Math.max(1.5,R*0.035);
for(let i=0;i<N;i++){
const a=rim[i], b=rim[(i+1)%N];
const mid={x:(a.x+b.x)/2 + d.x*R*0.14, y:(a.y+b.y)/2 + d.y*R*0.14};
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.quadraticCurveTo(mid.x,mid.y,b.x,b.y); ctx.stroke();
}
// ferrule (tip beyond the peak, along -d)
ctx.strokeStyle=COL.ferrule; ctx.lineWidth=Math.max(2,R*0.05);
ctx.beginPath(); ctx.moveTo(hub.x,hub.y);
ctx.lineTo(hub.x-d.x*R*0.20, hub.y-d.y*R*0.20); ctx.stroke();
}
function drawUmbrella(){
if(!sim.attached){
if(sim.free.active) drawFreeUmbrella();
return;
}
const P=pose;
// shaft (grip -> hub) with bend
const mid={x:(P.grip.x+P.hub.x)/2 + P.perp.x*P.bendMag, y:(P.grip.y+P.hub.y)/2 + P.perp.y*P.bendMag};
ctx.strokeStyle=COL.shaft; ctx.lineWidth=Math.max(3,L.headR*0.28); ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(P.grip.x,P.grip.y); ctx.quadraticCurveTo(mid.x,mid.y,P.hub.x,P.hub.y); ctx.stroke();
// canopy
const load=clamp(sim.F/200,0,1);
const h=L.baseH*(1-2*sim.inv)*(1-0.35*load);
drawCanopy(P.hub,P.d,P.p,L.R,h,8,load,sim.t);
// handle hook below grip
ctx.strokeStyle=COL.handle; ctx.lineWidth=Math.max(3,L.headR*0.3);
const hx=-P.shaft.x, hy=-P.shaft.y;
ctx.beginPath(); ctx.moveTo(P.grip.x,P.grip.y);
ctx.quadraticCurveTo(P.grip.x+hx*L.headR*1.2 - hy*L.headR, P.grip.y+hy*L.headR*1.2 + hx*L.headR,
P.grip.x+hx*L.headR*1.4, P.grip.y+hy*L.headR*1.4);
ctx.stroke();
}
function drawNearArm(){
const P=pose;
// 2-bone IK, choose lower elbow
const a=P.shoulder, b=P.grip, l1=L.upperArm, l2=L.foreArm;
let dx=b.x-a.x, dy=b.y-a.y, dist=Math.hypot(dx,dy)||1e-4;
dist=clamp(dist,Math.abs(l1-l2)+0.5,l1+l2-0.5);
const baseAng=Math.atan2(dy,dx);
const cosA=clamp((l1*l1+dist*dist-l2*l2)/(2*l1*dist),-1,1), A=Math.acos(cosA);
const e1={x:a.x+Math.cos(baseAng+A)*l1,y:a.y+Math.sin(baseAng+A)*l1};
const e2={x:a.x+Math.cos(baseAng-A)*l1,y:a.y+Math.sin(baseAng-A)*l1};
const elbow=(e1.y>e2.y?e1:e2);
limb([a,elbow,b], L.headR*0.8, COL.coat);
// gripping hand
ctx.fillStyle=COL.skin; ctx.beginPath(); ctx.arc(b.x,b.y,L.headR*0.5,0,TAU); ctx.fill();
}
function drawFreeUmbrella(){
const f=sim.free;
ctx.save(); ctx.translate(f.x,f.y); ctx.rotate(f.ang);
const d={x:-Math.sin(0),y:1}, p={x:1,y:0}; // local: shaft down = +y
// shaft
ctx.strokeStyle=COL.shaft; ctx.lineWidth=Math.max(3,L.headR*0.28); ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,L.shaftLen); ctx.stroke();
ctx.strokeStyle=COL.handle; ctx.beginPath(); ctx.moveTo(0,L.shaftLen);
ctx.quadraticCurveTo(-L.headR,L.shaftLen+L.headR, -L.headR*0.3,L.shaftLen+L.headR*1.4); ctx.stroke();
const h=L.baseH*(1-2*sim.inv);
drawCanopy({x:0,y:0},d,p,L.R,h,8,0.4,sim.t);
ctx.restore();
}
function drawScarf(){
const s=scarf; if(s.pts.length<2)return;
ctx.lineCap='round'; ctx.strokeStyle=COL.scarf;
for(let i=0;i<s.pts.length-1;i++){
const a=s.pts[i], b=s.pts[i+1];
ctx.lineWidth=lerp(L.headR*0.7,2, i/(s.pts.length-1));
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
}
function drawRain(){
const wN=clamp(sim.wind/PHYS.maxWind,0,1);
const active=Math.floor(lerp(50,rain.max,wN));
const windPx=sim.wind*(W*0.010), fall=H*0.9, tr=0.05;
ctx.lineWidth=1.1; ctx.lineCap='round';
for(let i=0;i<active;i++){
const dr=rain.drops[i];
const vx=windPx*dr.sp, vy=fall*dr.sp;
ctx.strokeStyle=`rgba(190,210,235,${dr.a*(0.4+0.6*wN)})`;
ctx.beginPath(); ctx.moveTo(dr.x,dr.y); ctx.lineTo(dr.x-vx*tr, dr.y-vy*tr); ctx.stroke();
}
}
function drawFlash(){
if(sim.flash>0.01){ ctx.fillStyle=`rgba(220,230,255,${sim.flash*0.45})`; ctx.fillRect(0,0,W,H); }
}
function render(){
ctx.clearRect(0,0,W,H);
drawSky();
drawStreaks();
drawGround();
drawPersonBody();
drawUmbrella();
drawNearArm();
drawScarf();
drawRain();
drawFlash();
}
/* ================= HUD ================= */
const el={
num:document.getElementById('windNum'), ms:document.getElementById('windMs'),
word:document.getElementById('windWord'), fill:document.getElementById('gaugeFill'),
status:document.getElementById('status'), slider:document.getElementById('slider'),
auto:document.getElementById('autoBtn'), reset:document.getElementById('resetBtn')
};
function beaufort(kmh){
if(kmh<1)return'Calm'; if(kmh<6)return'Light air'; if(kmh<12)return'Light breeze';
if(kmh<20)return'Gentle breeze'; if(kmh<29)return'Moderate breeze'; if(kmh<39)return'Fresh breeze';
if(kmh<50)return'Strong breeze'; if(kmh<62)return'Near gale'; if(kmh<75)return'Gale';
if(kmh<89)return'Strong gale'; if(kmh<103)return'Storm'; if(kmh<118)return'Violent storm';
return'Hurricane';
}
function updateHUD(){
const kmh=sim.wind*3.6, wN=clamp(sim.wind/PHYS.maxWind,0,1);
el.num.textContent=Math.round(kmh);
el.ms.textContent=sim.wind.toFixed(0)+' m/s';
el.word.textContent=beaufort(kmh);
el.fill.style.width=(wN*100)+'%';
const col = wN<0.4? lerpColor([95,208,160],[240,194,80],wN/0.4)
: lerpColor([240,194,80],[255,90,68],(wN-0.4)/0.6);
el.fill.style.background=col; el.num.style.color=col;
let st;
if(!sim.attached) st = sim.free.active? "It's gone — off it tumbles." : "Gone. Press R for another.";
else if(sim.inv>0.5) st="Inside out — hanging on!";
else if(sim.F>90) st="Straining…";
else if(sim.F>35) st="Leaning in.";
else if(sim.wind>3) st="Holding.";
else st="Calm.";
el.status.textContent=st;
el.slider.value=sim.windTarget;
el.auto.textContent='Auto: '+(sim.auto?'on':'off');
el.auto.classList.toggle('on',sim.auto);
}
/* ================= input ================= */
el.slider.addEventListener('input',e=>{ sim.windTarget=parseFloat(e.target.value); sim.auto=false; });
el.auto.addEventListener('click',()=>{ sim.auto=!sim.auto; });
el.reset.addEventListener('click',()=>reset());
cv.addEventListener('pointerdown',()=>{ if(!sim.attached && !sim.free.active) reset(); });
window.addEventListener('keydown',e=>{
const k=e.key;
if(k==='ArrowUp'||k==='w'||k==='W'){ sim.windTarget=clamp(sim.windTarget+1.5,0,PHYS.maxWind); sim.auto=false; e.preventDefault(); }
else if(k==='ArrowDown'||k==='s'||k==='S'){ sim.windTarget=clamp(sim.windTarget-1.5,0,PHYS.maxWind); sim.auto=false; e.preventDefault(); }
else if(k===' '){ sim.auto=!sim.auto; e.preventDefault(); }
else if(k==='r'||k==='R'){ reset(); }
});
window.addEventListener('resize',resize);
/* ================= reset / init ================= */
function reset(){
sim.t=0; sim.auto=true; sim.windTarget=3; sim.windBase=3; sim.wind=3;
sim.inv=0; sim.invVel=0; sim.tilt=-0.10; sim.tiltVel=0;
sim.lean=0; sim.crouch=0; sim.grip=0; sim.attached=true;
sim.free.active=false; sim.flash=0; sim.nextFlash=1e9;
initScarf();
}
/* ================= loop ================= */
let last=performance.now();
function frame(now){
let dt=(now-last)/1000; last=now;
dt=Math.min(dt,0.05);
const steps=2;
for(let i=0;i<steps;i++) step(dt/steps);
render();
updateHUD();
requestAnimationFrame(frame);
}
resize();
initRain(); initStreaks();
reset();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it looks right: the broad beats you already know are all there and read correctly. The person leans into the wind (top of the body toward the source) while the umbrella is dragged downwind over their head, so the two form the opposing V you actually make in a gale; they crouch lower as it worsens, the near arm visibly straightens as the grip is pulled, and the feet stay planted. Rain slants and accelerates with the wind (its streak angle is just atan(windX / fallSpeed)), and the scarf, coat-tail and grass all stream downwind as extra cues. The canopy telegraphs before it goes — it flattens, the ribs load, the shaft bows — then snaps inside out with a spring overshoot, latches there via hysteresis, and springs back if you drop the wind below the lower threshold. Past the grip limit it rips free and tumbles off downwind as a spinning rigid body. The wind readout, Beaufort word and severity colour all track the gusty instantaneous value, and you can drive it with the slider, ↑/↓, or let Auto ramp it.
Where it's wrong, before you find it:
Fup, Fdown, grip strength and the force→pose gains were picked to make a nice ~15-second arc, so the wind speeds at which it inverts and tears free are plausible but not physically calibrated, and nothing ever breaks — the frame is infinitely strong, it only ever lets go at the hand.