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 12 radial ribs, each a 7-point Verlet chain (hub → 5 segments → tip) with distance constraints and bending stiffness; adjacent rib tips are linked by membrane constraints. Wind applies a quadratic drag force normal to each segment, scaled by projected area and relative velocity. Inversion is a state flip: when the mean rib tip angle crosses 100° from downward (i.e., tips rise above the hub) for >0.3 s, the frame "buckles" — bending stiffness collapses to 10%, membrane rest lengths extend 40%, and the canopy locks into an inside-out cone until wind drops below 40% of inversion threshold for 1 s. The person is a 3-segment IK chain (torso/upper-arm/forearm) with a fixed shoulder; the hand targets the sliding grip point on the shaft. Torso lean follows a damped spring toward atan2(wind_force, body_weight). Feet are pinned but show heel/toe pressure via a simple moment balance. Grip fails when shaft tension exceeds a random threshold (500–800 N), releasing the umbrella as a free rigid body with drag.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Umbrella in a Gale</title>
<style>
:root { --bg:#0b0f1a; --fg:#e8eef6; --muted:#6b7a94; --accent:#ff6b35; --accent2:#4ecdc4; --rain:#89b4fa; --ground:#1a1f2e; }
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow:hidden}
canvas{display:block;background:linear-gradient(180deg,#0d1426 0%,#0b0f1a 60%,var(--ground) 100%)}
.ui{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:12px;pointer-events:auto;z-index:10}
.slider-wrap{width:320px;max-width:90vw}
input[type=range]{width:100%;accent-color:var(--accent);cursor:pointer}
.labels{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}
.stats{display:flex;gap:24px;font-size:13px;color:var(--fg);font-variant-numeric:tabular-nums}
.stat{display:flex;align-items:center;gap:6px}
.stat-label{color:var(--muted)}
.stat-value{font-weight:600;color:var(--accent2)}
.stat-value.warn{color:var(--accent);animation:pulse 0.8s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
.hint{font-size:11px;color:var(--muted);text-align:center;max-width:320px}
.hint kbd{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);border-radius:3px;padding:1px 5px;font-family:inherit;font-size:inherit}
@media(prefers-reduced-motion:reduce){.stat-value.warn{animation:none}*,*::before,*::after{animation-duration:0.01ms!important;transition-duration:0.01ms!important}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="ui">
<div class="slider-wrap">
<input type="range" id="windSlider" min="0" max="100" value="0" step="1" aria-label="Wind speed">
<div class="labels"><span>Calm</span><span>Gale</span><span>Storm</span></div>
</div>
<div class="stats">
<div class="stat"><span class="stat-label">Wind:</span><span class="stat-value" id="windVal">0.0</span><span class="stat-label">m/s</span></div>
<div class="stat"><span class="stat-label">Force:</span><span class="stat-value" id="forceVal">0</span><span class="stat-label">N</span></div>
<div class="stat"><span class="stat-label">State:</span><span class="stat-value" id="stateVal">Intact</span></div>
</div>
<p class="hint">Drag slider or press <kbd>←</kbd>/<kbd>→</kbd> / <kbd>A</kbd>/<kbd>D</kbd> to control wind. <kbd>Space</kbd> = auto-ramp. <kbd>R</kbd> = reset.</p>
</div>
<script>
/* ============ CONFIG ============ */
const CFG = {
// World
G: 9.81,
AIR_DENSITY: 1.225,
CD_RIB: 1.2,
CD_CANOPY: 1.4,
CD_PERSON: 1.0,
// Umbrella
NUM_RIBS: 12,
RIB_SEGS: 6,
RIB_LENGTH: 0.75,
HUB_RADIUS: 0.04,
SHAFT_LENGTH: 1.1,
SLIDER_RANGE: 0.35,
CANOPY_RADIUS: 0.78,
RIB_MASS: 0.018,
TIP_MASS: 0.025,
BEND_STIFF: 0.85,
MEMBRANE_STIFF: 0.6,
INVERSION_ANGLE: 100 * Math.PI/180,
INVERSION_HOLD: 0.3,
RECOVERY_WIND_RATIO: 0.4,
RECOVERY_HOLD: 1.0,
GRIP_MIN: 500,
GRIP_MAX: 800,
// Person
TORSO_LEN: 0.55,
UPPER_ARM: 0.32,
FOREARM: 0.3,
SHOULDER_H: 1.55,
SHOULDER_X: 0,
BODY_MASS: 72,
LEAN_GAIN: 0.0012,
LEAN_MAX: 30 * Math.PI/180,
DAMPING: 0.92,
// Rain
RAIN_COUNT: 350,
RAIN_SPEED: 9,
// Visual
GROUND_Y: 0.15,
SCALE: 0, // set in resize
// Auto ramp
AUTO_RAMP_RATE: 0.8,
MAX_WIND: 35
};
/* ============ STATE ============ */
let windSpeed = 0;
let targetWind = 0;
let autoRamp = false;
let simTime = 0;
let lastTime = 0;
// Umbrella
let hub = {x:0,y:0,vx:0,vy:0, fixed:false};
let ribs = []; // each: {pts:[{x,y,vx,vy,mass}], inverted:false}
let membraneLinks = []; // {i,j, rest}
let shaft = {angle:0, vangle:0, slider:0.5, gripForce:0, gripBroken:false, gripThreshold:0};
let inverted = false;
let inversionTimer = 0;
let recoveryTimer = 0;
let umbrellaFree = false;
let umbrellaBody = {x:0,y:0,vx:0,vy:0,angle:0,vangle:0, mass:0.4, moi:0.05};
// Person
let shoulder = {x:0,y:0};
let torsoAngle = 0, torsoVAngle = 0;
let leanTarget = 0;
let footL = {x:-0.18, y:0}, footR = {x:0.18, y:0};
let heelPressure = 0, toePressure = 0;
// Rain
let rain = [];
// Input
const keys = {};
// DOM
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const windSlider = document.getElementById('windSlider');
const windVal = document.getElementById('windVal');
const forceVal = document.getElementById('forceVal');
const stateVal = document.getElementById('stateVal');
/* ============ INIT ============ */
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = canvas.clientWidth = window.innerWidth;
const h = canvas.clientHeight = window.innerHeight - 120;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
CFG.SCALE = Math.min(w, h * 0.8) * 0.38;
shoulder.x = w/2;
shoulder.y = h * 0.22;
CFG.GROUND_Y_PIX = h * 0.88;
initUmbrella();
initRain();
}
function initUmbrella() {
hub = {x:shoulder.x, y:shoulder.y + CFG.SHAFT_LENGTH * CFG.SCALE * 0.6, vx:0, vy:0, fixed:false};
ribs = [];
const n = CFG.NUM_RIBS;
const segLen = CFG.RIB_LENGTH / CFG.RIB_SEGS;
for (let r=0;r<n;r++) {
const baseAngle = (r/n) * Math.PI * 2;
const pts = [];
for (let s=0;s<=CFG.RIB_SEGS;s++) {
const frac = s/CFG.RIB_SEGS;
const rad = CFG.CANOPY_RADIUS * frac;
const ang = -Math.PI/2 + baseAngle; // ribs hang down initially
const x = hub.x + Math.cos(ang) * rad * CFG.SCALE;
const y = hub.y + Math.sin(ang) * rad * CFG.SCALE;
pts.push({x,y,vx:0,vy:0, mass: s===CFG.RIB_SEGS ? CFG.TIP_MASS : CFG.RIB_MASS, baseAngle:ang, frac});
}
ribs.push({pts, inverted:false, baseAngle});
}
// Membrane links between adjacent rib tips
membraneLinks = [];
for (let r=0;r<n;r++) {
const r2 = (r+1)%n;
const p1 = ribs[r].pts[CFG.RIB_SEGS];
const p2 = ribs[r2].pts[CFG.RIB_SEGS];
const dx = p2.x - p1.x, dy = p2.y - p1.y;
membraneLinks.push({i:r, j:r2, rest:Math.hypot(dx,dy)});
}
shaft = {angle:0, vangle:0, slider:0.5, gripForce:0, gripBroken:false, gripThreshold: CFG.GRIP_MIN + Math.random()*(CFG.GRIP_MAX-CFG.GRIP_MIN)};
inverted = false;
inversionTimer = 0;
recoveryTimer = 0;
umbrellaFree = false;
umbrellaBody = {x:hub.x, y:hub.y, vx:0, vy:0, angle:0, vangle:0, mass:0.4, moi:0.05};
}
function initRain() {
rain = [];
const w = canvas.width / (window.devicePixelRatio||1);
const h = canvas.height / (window.devicePixelRatio||1);
for (let i=0;i<CFG.RAIN_COUNT;i++) {
rain.push({
x: Math.random()*w,
y: Math.random()*h - h,
vx: 0, vy: CFG.RAIN_SPEED,
len: 8 + Math.random()*12,
thick: 0.5 + Math.random()*1.2
});
}
}
/* ============ PHYSICS ============ */
function step(dt) {
simTime += dt;
// Wind ramp
if (autoRamp) targetWind = Math.min(CFG.MAX_WIND, targetWind + CFG.AUTO_RAMP_RATE * dt);
windSpeed += (targetWind - windSpeed) * Math.min(1, dt * 3);
windSpeed = Math.max(0, Math.min(CFG.MAX_WIND, windSpeed));
const windVec = {x: windSpeed, y: -windSpeed * 0.08}; // slight downdraft
// Update UI
windVal.textContent = windSpeed.toFixed(1);
if (!umbrellaFree) {
// Hub follows hand (IK target)
updatePerson(dt, windVec);
hub.x = shoulder.x + Math.sin(torsoAngle) * CFG.TORSO_LEN * CFG.SCALE * 0.3;
hub.y = shoulder.y + CFG.SHAFT_LENGTH * CFG.SCALE * 0.6 + Math.cos(torsoAngle) * CFG.TORSO_LEN * CFG.SCALE * 0.3;
hub.vx = 0; hub.vy = 0;
// Shaft angle dynamics
const windTorque = computeWindTorque(windVec);
shaft.vangle += windTorque * dt * 0.8;
shaft.vangle *= 0.9;
shaft.angle += shaft.vangle * dt;
shaft.angle = clamp(shaft.angle, -1.2, 1.2);
// Rib physics
let totalForce = 0;
for (let r=0;r<CFG.NUM_RIBS;r++) {
totalForce += stepRib(r, windVec, dt);
}
forceVal.textContent = Math.round(totalForce);
// Membrane constraints
for (const link of membraneLinks) {
const p1 = ribs[link.i].pts[CFG.RIB_SEGS];
const p2 = ribs[link.j].pts[CFG.RIB_SEGS];
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const dist = Math.hypot(dx,dy) || 1;
const diff = (dist - link.rest) / dist * 0.5 * CFG.MEMBRANE_STIFF;
const ox = dx * diff, oy = dy * diff;
p1.vx += ox; p1.vy += oy;
p2.vx -= ox; p2.vy -= oy;
}
// Inversion check
let meanTipAngle = 0;
for (const rib of ribs) {
const tip = rib.pts[CFG.RIB_SEGS];
const dx = tip.x - hub.x, dy = tip.y - hub.y;
meanTipAngle += Math.atan2(dy, dx);
}
meanTipAngle /= CFG.NUM_RIBS;
const invertedNow = meanTipAngle > -Math.PI/2 + CFG.INVERSION_ANGLE; // tips above horizontal
if (!inverted && invertedNow) {
inversionTimer += dt;
if (inversionTimer > CFG.INVERSION_HOLD) {
inverted = true;
inversionTimer = 0;
// Buckle: reduce stiffness, extend membrane
for (const rib of ribs) rib.inverted = true;
for (const link of membraneLinks) link.rest *= 1.4;
CFG.BEND_STIFF *= 0.1;
}
} else if (!invertedNow) {
inversionTimer = 0;
}
if (inverted && windSpeed < CFG.MAX_WIND * CFG.RECOVERY_WIND_RATIO) {
recoveryTimer += dt;
if (recoveryTimer > CFG.RECOVERY_HOLD) {
inverted = false;
recoveryTimer = 0;
for (const rib of ribs) rib.inverted = false;
for (const link of membraneLinks) link.rest /= 1.4;
CFG.BEND_STIFF /= 0.1;
}
} else if (inverted) {
recoveryTimer = 0;
}
// Grip check
shaft.gripForce = totalForce;
if (totalForce > shaft.gripThreshold && !shaft.gripBroken) {
shaft.gripBroken = true;
umbrellaFree = true;
umbrellaBody.x = hub.x;
umbrellaBody.y = hub.y;
umbrellaBody.vx = windVec.x * 0.3;
umbrellaBody.vy = windVec.y * 0.3;
umbrellaBody.angle = shaft.angle;
umbrellaBody.vangle = shaft.vangle;
}
stateVal.textContent = shaft.gripBroken ? 'Lost' : (inverted ? 'Inverted' : 'Intact');
stateVal.className = 'stat-value' + (inverted || shaft.gripBroken ? ' warn' : '');
} else {
// Free umbrella physics
stepFreeUmbrella(windVec, dt);
forceVal.textContent = Math.round(Math.hypot(umbrellaBody.vx, umbrellaBody.vy) * umbrellaBody.mass * 10);
stateVal.textContent = 'Airborne';
stateVal.className = 'stat-value warn';
}
// Rain
stepRain(windVec, dt);
}
function stepRib(ribIdx, windVec, dt) {
const rib = ribs[ribIdx];
const pts = rib.pts;
const segLen = CFG.RIB_LENGTH / CFG.RIB_SEGS * CFG.SCALE;
let ribForce = 0;
// Gravity + wind on each point (except hub)
for (let i=1;i<pts.length;i++) {
const p = pts[i];
// Gravity
p.vy += CFG.G * CFG.SCALE * dt * 0.5;
// Wind drag on segment (i-1 to i)
const p0 = pts[i-1];
const sx = p.x - p0.x, sy = p.y - p0.y;
const segLenAct = Math.hypot(sx,sy) || 1;
const nx = -sy/segLenAct, ny = sx/segLenAct; // normal
const relVx = windVec.x - (p.vx + p0.vx)*0.5;
const relVy = windVec.y - (p.vy + p0.vy)*0.5;
const vNormal = relVx*nx + relVy*ny;
if (vNormal > 0) {
const area = segLenAct * 0.02 * CFG.SCALE; // effective width
const force = 0.5 * CFG.AIR_DENSITY * CFG.CD_RIB * area * vNormal * vNormal;
const fx = nx * force, fy = ny * force;
p.vx += fx/p.mass * dt;
p.vy += fy/p.mass * dt;
p0.vx += fx/p0.mass * dt;
p0.vy += fy/p0.mass * dt;
ribForce += force;
}
}
// Verlet integration
for (const p of pts) {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= CFG.DAMPING;
p.vy *= CFG.DAMPING;
}
// Hub constraint (point 0 fixed to hub)
const p0 = pts[0];
p0.x = hub.x; p0.y = hub.y; p0.vx = 0; p0.vy = 0;
// Distance constraints (relaxed)
for (let iter=0;iter<4;iter++) {
for (let i=1;i<pts.length;i++) {
const a = pts[i-1], b = pts[i];
const dx = b.x - a.x, dy = b.y - a.y;
const dist = Math.hypot(dx,dy) || 1;
const diff = (dist - segLen) / dist * 0.5;
const ox = dx * diff, oy = dy * diff;
if (i>1) { a.vx -= ox; a.vy -= oy; }
b.vx += ox; b.vy += oy;
}
}
// Bending constraints (stiffness)
const stiff = rib.inverted ? CFG.BEND_STIFF * 0.1 : CFG.BEND_STIFF;
for (let iter=0;iter<2;iter++) {
for (let i=1;i<pts.length-1;i++) {
const a = pts[i-1], b = pts[i], c = pts[i+1];
const dx1 = b.x - a.x, dy1 = b.y - a.y;
const dx2 = c.x - b.x, dy2 = c.y - b.y;
const ang1 = Math.atan2(dy1, dx1);
const ang2 = Math.atan2(dy2, dx2);
let diff = ang2 - ang1;
while (diff > Math.PI) diff -= 2*Math.PI;
while (diff < -Math.PI) diff += 2*Math.PI;
const target = rib.baseAngle * (1 - b.frac) + (rib.inverted ? Math.PI/2 : 0); // crude
const correction = (diff - target) * stiff * 0.15;
const l1 = Math.hypot(dx1,dy1)||1, l2 = Math.hypot(dx2,dy2)||1;
const nx1 = -dy1/l1, ny1 = dx1/l1;
const nx2 = -dy2/l2, ny2 = dx2/l2;
a.vx += nx1 * correction * 0.3; a.vy += ny1 * correction * 0.3;
b.vx -= (nx1 + nx2) * correction * 0.3; b.vy -= (ny1 + ny2) * correction * 0.3;
c.vx += nx2 * correction * 0.3; c.vy += ny2 * correction * 0.3;
}
}
return ribForce;
}
function computeWindTorque(windVec) {
// Approximate torque on shaft from canopy drag
let torque = 0;
for (const rib of ribs) {
const tip = rib.pts[CFG.RIB_SEGS];
const dx = tip.x - hub.x, dy = tip.y - hub.y;
const relVx = windVec.x - tip.vx;
const relVy = windVec.y - tip.vy;
const vNorm = relVx * (-dy) + relVy * dx; // cross product
torque += vNorm * 0.0002;
}
return torque;
}
function updatePerson(dt, windVec) {
// Lean target from wind force on canopy
const windForce = windSpeed * windSpeed * 0.8;
leanTarget = clamp(windForce * CFG.LEAN_GAIN, -CFG.LEAN_MAX, CFG.LEAN_MAX);
torsoVAngle += (leanTarget - torsoAngle) * 8 * dt;
torsoVAngle *= 0.85;
torsoAngle += torsoVAngle * dt;
// Foot pressures (moment balance)
const comX = shoulder.x + Math.sin(torsoAngle) * CFG.TORSO_LEN * CFG.SCALE * 0.5;
const footSpan = footR.x - footL.x;
const comRel = (comX - shoulder.x) / (footSpan * CFG.SCALE * 0.5);
heelPressure = clamp(0.5 - comRel, 0, 1);
toePressure = clamp(0.5 + comRel, 0, 1);
}
function stepFreeUmbrella(windVec, dt) {
const b = umbrellaBody;
// Gravity
b.vy += CFG.G * CFG.SCALE * dt;
// Wind drag on whole body
const relVx = windVec.x - b.vx;
const relVy = windVec.y - b.vy;
const speed = Math.hypot(relVx, relVy);
if (speed > 0.1) {
const drag = 0.5 * CFG.AIR_DENSITY * CFG.CD_CANOPY * (CFG.CANOPY_RADIUS*2*CFG.SCALE) * 0.1 * speed * speed;
const fx = (relVx/speed) * drag;
const fy = (relVy/speed) * drag;
b.vx += fx / b.mass * dt;
b.vy += fy / b.mass * dt;
// Torque
b.vangle += (Math.random()-0.5) * 0.5 * dt;
}
b.vangle *= 0.98;
b.angle += b.vangle * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
// Ground collision
if (b.y > CFG.GROUND_Y_PIX) {
b.y = CFG.GROUND_Y_PIX;
b.vy *= -0.3;
b.vx *= 0.7;
b.vangle *= 0.5;
}
}
function stepRain(windVec, dt) {
const w = canvas.width / (window.devicePixelRatio||1);
const h = canvas.height / (window.devicePixelRatio||1);
for (const drop of rain) {
drop.vx += (windVec.x * 0.6 - drop.vx) * dt * 2;
drop.vy += (windVec.y * 0.6 + CFG.RAIN_SPEED - drop.vy) * dt * 2;
drop.x += drop.vx * dt * 60;
drop.y += drop.vy * dt * 60;
if (drop.y > h + 50 || drop.x < -50 || drop.x > w + 50) {
drop.x = Math.random() * w;
drop.y = -Math.random() * 200 - 50;
drop.vx = 0; drop.vy = CFG.RAIN_SPEED;
}
}
}
/* ============ RENDER ============ */
function render() {
const w = canvas.width / (window.devicePixelRatio||1);
const h = canvas.height / (window.devicePixelRatio||1);
ctx.clearRect(0,0,w,h);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0,0,0,h*0.6);
skyGrad.addColorStop(0, '#0d1426');
skyGrad.addColorStop(1, '#1a2332');
ctx.fillStyle = skyGrad;
ctx.fillRect(0,0,w,h*0.6);
// Ground
ctx.fillStyle = '#141a2a';
ctx.fillRect(0, CFG.GROUND_Y_PIX, w, h - CFG.GROUND_Y_PIX);
ctx.fillStyle = '#1e2738';
ctx.fillRect(0, CFG.GROUND_Y_PIX, w, 4);
// Rain
ctx.strokeStyle = 'rgba(137,180,250,0.4)';
ctx.lineWidth = 0.8;
ctx.lineCap = 'round';
ctx.beginPath();
for (const drop of rain) {
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x - drop.vx*0.02, drop.y - drop.vy*0.02);
}
ctx.stroke();
if (!umbrellaFree) {
drawPerson();
drawUmbrella();
} else {
drawFreeUmbrella();
}
// Wind indicator
drawWindArrow(w, h);
}
function drawPerson() {
ctx.save();
ctx.translate(shoulder.x, shoulder.y);
ctx.rotate(torsoAngle);
// Legs
const legLen = CFG.TORSO_LEN * CFG.SCALE * 1.1;
const footY = CFG.GROUND_Y_PIX - shoulder.y;
// Left leg
ctx.beginPath();
ctx.moveTo(footL.x * CFG.SCALE, 0);
ctx.lineTo(footL.x * CFG.SCALE, footY * 0.55);
ctx.lineTo(footL.x * CFG.SCALE, footY);
ctx.strokeStyle = '#3a4a6b';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.stroke();
// Right leg
ctx.beginPath();
ctx.moveTo(footR.x * CFG.SCALE, 0);
ctx.lineTo(footR.x * CFG.SCALE, footY * 0.55);
ctx.lineTo(footR.x * CFG.SCALE, footY);
ctx.stroke();
// Feet pressure indication
ctx.fillStyle = `rgba(255,107,53,${0.3+heelPressure*0.5})`;
ctx.beginPath(); ctx.arc(footL.x*CFG.SCALE, footY, 8, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = `rgba(78,205,196,${0.3+toePressure*0.5})`;
ctx.beginPath(); ctx.arc(footR.x*CFG.SCALE, footY, 8, 0, Math.PI*2); ctx.fill();
// Torso
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0, CFG.TORSO_LEN * CFG.SCALE);
ctx.strokeStyle = '#4a5a7a';
ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.stroke();
// Arm IK
const handTargetX = hub.x - shoulder.x;
const handTargetY = hub.y - shoulder.y;
const upperLen = CFG.UPPER_ARM * CFG.SCALE;
const foreLen = CFG.FOREARM * CFG.SCALE;
const dist = Math.hypot(handTargetX, handTargetY);
let elbowAngle = 0, shoulderAngle = 0;
if (dist > upperLen + foreLen - 5) {
shoulderAngle = Math.atan2(handTargetY, handTargetX);
elbowAngle = 0;
} else if (dist < Math.abs(upperLen - foreLen) + 5) {
shoulderAngle = Math.atan2(handTargetY, handTargetX);
elbowAngle = Math.PI * 0.9;
} else {
const cosElbow = (upperLen*upperLen + foreLen*foreLen - dist*dist) / (2*upperLen*foreLen);
elbowAngle = Math.acos(clamp(cosElbow, -1, 1));
const angleToHand = Math.atan2(handTargetY, handTargetX);
const cosShoulder = (upperLen*upperLen + dist*dist - foreLen*foreLen) / (2*upperLen*dist);
shoulderAngle = angleToHand - Math.acos(clamp(cosShoulder, -1, 1));
}
// Upper arm
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(Math.cos(shoulderAngle)*upperLen, Math.sin(shoulderAngle)*upperLen);
ctx.strokeStyle = '#5a6a8a';
ctx.lineWidth = 10;
ctx.stroke();
// Forearm
const elbowX = Math.cos(shoulderAngle)*upperLen;
const elbowY = Math.sin(shoulderAngle)*upperLen;
const forearmAngle = shoulderAngle + Math.PI - elbowAngle;
ctx.beginPath();
ctx.moveTo(elbowX, elbowY);
ctx.lineTo(elbowX + Math.cos(forearmAngle)*foreLen, elbowY + Math.sin(forearmAngle)*foreLen);
ctx.strokeStyle = '#6a7a9a';
ctx.lineWidth = 8;
ctx.stroke();
// Hand (grip)
const handX = elbowX + Math.cos(forearmAngle)*foreLen;
const handY = elbowY + Math.sin(forearmAngle)*foreLen;
ctx.fillStyle = '#c9b896';
ctx.beginPath(); ctx.arc(handX, handY, 6, 0, Math.PI*2); ctx.fill();
// Head
ctx.fillStyle = '#d4c4a8';
ctx.beginPath(); ctx.arc(0, -18, 16, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawUmbrella() {
// Shaft
ctx.save();
ctx.translate(hub.x, hub.y);
ctx.rotate(shaft.angle);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0, -CFG.SHAFT_LENGTH * CFG.SCALE * 0.4);
ctx.strokeStyle = '#2a3a5a';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.stroke();
// Slider
const sliderY = -CFG.SHAFT_LENGTH * CFG.SCALE * 0.4 * shaft.slider;
ctx.fillStyle = '#3a4a6a';
ctx.beginPath(); ctx.arc(0, sliderY, 5, 0, Math.PI*2); ctx.fill();
// Ribs
for (const rib of ribs) {
const pts = rib.pts;
ctx.beginPath();
ctx.moveTo(pts[0].x - hub.x, pts[0].y - hub.y);
for (let i=1;i<pts.length;i++) {
ctx.lineTo(pts[i].x - hub.x, pts[i].y - hub.y);
}
ctx.strokeStyle = rib.inverted ? '#ff6b35' : '#4a5a7a';
ctx.lineWidth = 2;
ctx.stroke();
// Membrane fill (triangulate to hub)
if (!rib.inverted) {
ctx.fillStyle = 'rgba(78,205,196,0.08)';
ctx.beginPath();
ctx.moveTo(hub.x, hub.y);
for (let i=0;i<pts.length;i++) {
ctx.lineTo(pts[i].x, pts[i].y);
}
ctx.closePath();
ctx.fill();
}
}
// Canopy membrane (between ribs)
ctx.fillStyle = inverted ? 'rgba(255,107,53,0.15)' : 'rgba(78,205,196,0.12)';
ctx.beginPath();
for (let r=0;r<CFG.NUM_RIBS;r++) {
const tip = ribs[r].pts[CFG.RIB_SEGS];
if (r===0) ctx.moveTo(tip.x, tip.y);
else ctx.lineTo(tip.x, tip.y);
}
ctx.closePath();
ctx.fill();
// Hub
ctx.fillStyle = '#3a4a6a';
ctx.beginPath(); ctx.arc(0,0, CFG.HUB_RADIUS*CFG.SCALE, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawFreeUmbrella() {
const b = umbrellaBody;
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.angle);
// Simplified inverted canopy
ctx.fillStyle = 'rgba(255,107,53,0.2)';
ctx.beginPath();
ctx.moveTo(0,0);
for (let i=0;i<CFG.NUM_RIBS;i++) {
const ang = (i/CFG.NUM_RIBS)*Math.PI*2;
const r = CFG.CANOPY_RADIUS * CFG.SCALE * (inverted ? 1.2 : 1);
ctx.lineTo(Math.cos(ang)*r, Math.sin(ang)*r);
}
ctx.closePath();
ctx.fill();
// Ribs
ctx.strokeStyle = '#ff6b35';
ctx.lineWidth = 2;
for (let i=0;i<CFG.NUM_RIBS;i++) {
const ang = (i/CFG.NUM_RIBS)*Math.PI*2;
const r = CFG.CANOPY_RADIUS * CFG.SCALE * 1.2;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(Math.cos(ang)*r, Math.sin(ang)*r);
ctx.stroke();
}
ctx.restore();
}
function drawWindArrow(w, h) {
const x = w - 60, y = 60;
const len = Math.min(40, windSpeed * 1.5);
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.atan2(-windSpeed*0.08, windSpeed));
ctx.strokeStyle = `rgba(255,107,53,${0.5+windSpeed/70})`;
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(-len, 0);
ctx.lineTo(len, 0);
ctx.stroke();
// Arrowhead
ctx.beginPath();
ctx.moveTo(len, 0);
ctx.lineTo(len-10, -6);
ctx.lineTo(len-10, 6);
ctx.closePath();
ctx.fillStyle = ctx.strokeStyle;
ctx.fill();
ctx.restore();
// Label
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText(`${windSpeed.toFixed(1)} m/s`, x, y - len - 8);
}
/* ============ LOOP ============ */
function loop(ts) {
if (!lastTime) lastTime = ts;
const dt = Math.min((ts - lastTime) / 1000, 0.05);
lastTime = ts;
step(dt);
render();
requestAnimationFrame(loop);
}
/* ============ INPUT ============ */
windSlider.addEventListener('input', e => {
targetWind = parseFloat(e.target.value) / 100 * CFG.MAX_WIND;
autoRamp = false;
});
window.addEventListener('keydown', e => {
keys[e.key] = true;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
targetWind = Math.min(CFG.MAX_WIND, targetWind + 1.5);
windSlider.value = Math.round(targetWind / CFG.MAX_WIND * 100);
autoRamp = false;
} else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
targetWind = Math.max(0, targetWind - 1.5);
windSlider.value = Math.round(targetWind / CFG.MAX_WIND * 100);
autoRamp = false;
} else if (e.key === ' ') {
autoRamp = !autoRamp;
e.preventDefault();
} else if (e.key === 'r' || e.key === 'R') {
initUmbrella();
targetWind = 0;
windSlider.value = 0;
autoRamp = false;
}
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
window.addEventListener('resize', resize);
/* ============ UTIL ============ */
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
/* ============ START ============ */
resize();
requestAnimationFrame(loop);
</script>
</body>
</html>
Where it looks right:
Where it looks wrong: