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 eight rigid ribs radiating from a hub, each a massless rod with a point mass at the tip; adjacent tips are connected by damped springs that represent fabric tension, and each rib has a torsional spring at the hub pulling it toward its rest angle (≈ 30° below horizontal). Wind pressure acts normal to each rib segment, integrated along its length, producing a distributed load that bends the rib tips inward and twists the hub. Inversion is a discrete state change: when the average rib angle crosses above horizontal (θ > 0) and the instantaneous wind pressure on the upper surface exceeds a threshold, the hub torsional springs flip their rest angle to +60° (inside-out) and their stiffness drops by 80%, locking the canopy inverted until the wind drops below a lower hysteresis threshold and the ribs have swung back past −10°. The person is a three-link chain (torso, upper arm, forearm) with the feet pinned; a PD controller drives the torso lean to cancel the net moment at the feet, the grip force ramps with shaft tension, and if shaft tension exceeds a breakaway limit the umbrella detaches and becomes a free rigid body subject to drag and gravity.
<!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:#1a2332; }
*{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}
.panel{background:rgba(15,22,38,0.95);border:1px solid rgba(107,122,148,0.3);border-radius:12px;padding:16px 24px;display:flex;flex-direction:column;align-items:center;gap:10px;min-width:280px;backdrop-filter:blur(8px)}
.row{display:flex;align-items:center;gap:12px;width:100%;justify-content:center;flex-wrap:wrap}
label{font-size:13px;color:var(--muted);white-space:nowrap}
input[type=range]{flex:1;max-width:300px;accent-color:var(--accent);cursor:pointer}
.value{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600;color:var(--accent);min-width:55px;text-align:right}
.value.inverted{color:var(--accent2)}
.btn{background:linear-gradient(135deg,var(--accent),#e85d2a);border:none;color:#fff;padding:10px 22px;border-radius:8px;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s}
.btn:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(255,107,53,0.4)}
.btn:active{transform:translateY(0)}
.btn.secondary{background:linear-gradient(135deg,var(--muted),#5a6a85)}
.status{font-size:12px;color:var(--muted);text-align:center;min-height:18px}
.status.warning{color:var(--accent);font-weight:600}
.status.danger{color:#ff3366;font-weight:600;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
.hint{font-size:11px;color:rgba(107,122,148,0.7);text-align:center;max-width:400px}
kbd{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.1);padding:2px 6px;border-radius:4px;font-family:monospace;font-size:10px}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="ui">
<div class="panel">
<div class="row">
<label for="wind">Wind</label>
<input type="range" id="wind" min="0" max="100" value="0" step="1">
<span class="value" id="windVal">0</span>
<span style="color:var(--muted);font-size:13px">m/s</span>
</div>
<div class="row">
<button class="btn" id="autoBtn">Auto Ramp</button>
<button class="btn secondary" id="resetBtn">Reset</button>
</div>
<div class="status" id="status">Calm</div>
<div class="hint">← → or drag slider • Space: gust • R: reset • Auto ramps wind up/down</div>
</div>
</div>
<script>
/* ============ PHYSICS CONSTANTS ============ */
const G = 9.81;
const RHO_AIR = 1.225;
const CD_RIB = 1.2;
const CD_CANOPY = 1.4;
const RIB_COUNT = 8;
const RIB_LENGTH = 1.1;
const HUB_RADIUS = 0.08;
const CANOPY_HEIGHT = 0.35;
const SHAFT_LENGTH = 1.0;
const HANDLE_LENGTH = 0.35;
const PERSON_HEIGHT = 1.75;
const FOOT_WIDTH = 0.35;
/* ============ STATE ============ */
let windSpeed = 0;
let targetWind = 0;
let autoRamp = false;
let autoDir = 1;
let time = 0;
let dt = 1/60;
/* Umbrella */
let ribs = [];
let hub = { x:0, y:0, vx:0, vy:0, angle:0, av:0, mass:0.3, inertia:0.05 };
let shaft = { angle:0, av:0, attached:true, grip:1 };
let inverted = false;
let inversionLock = 0;
let detached = false;
let detachTime = 0;
/* Person */
const person = {
footL: { x:-FOOT_WIDTH/2, y:0, fixed:true },
footR: { x:FOOT_WIDTH/2, y:0, fixed:true },
hip: { x:0, y:0.95, vx:0, vy:0, mass:35 },
shoulder: { x:0, y:1.45, vx:0, vy:0, mass:15 },
elbow: { x:0, y:1.15, vx:0, vy:0, mass:3 },
hand: { x:0, y:1.0, vx:0, vy:0, mass:1 },
torsoAngle: 0,
targetLean: 0,
leanRate: 0,
maxTorque: 180,
gripStrength: 800
};
/* Rain */
const rainDrops = [];
const MAX_RAIN = 800;
/* Wind gust noise */
let gustPhase = 0;
/* ============ INIT ============ */
function initRibs() {
ribs = [];
for (let i=0;i<RIB_COUNT;i++) {
const baseAngle = (i/RIB_COUNT)*Math.PI*2;
ribs.push({
baseAngle,
angle: -0.52, // -30 deg
av: 0,
tipMass: 0.04,
hubSpring: 45,
hubDamping: 2.5,
fabricK: 180,
fabricD: 8,
restAngle: -0.52,
inverted: false
});
}
}
initRibs();
/* ============ CANVAS SETUP ============ */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, scale, cx, cy;
function resize() {
W = canvas.width = innerWidth;
H = canvas.height = innerHeight;
scale = Math.min(W, H) * 0.38;
cx = W/2;
cy = H * 0.72;
}
resize();
addEventListener('resize', resize);
/* ============ WIND MODEL ============ */
function getWindAtHeight(y) {
// y in world meters, 0 at ground
const gradient = 1 + 0.15 * Math.log(Math.max(0.1, y/0.5));
const gust = Math.sin(gustPhase*0.7) * 0.3 + Math.sin(gustPhase*2.3) * 0.15 + Math.sin(gustPhase*5.1) * 0.07;
return windSpeed * gradient * (1 + gust * 0.4);
}
function getWindVector(y) {
const spd = getWindAtHeight(y);
// slight upward component near ground, downward at canopy top
const vy = -spd * 0.08 * Math.sin(y * 2);
return { x: spd, y: vy };
}
/* ============ PERSON CONTROL ============ */
function updatePerson() {
// Calculate wind moment on umbrella about feet
let windMoment = 0;
if (!detached) {
for (const rib of ribs) {
const wx = hub.x + Math.cos(rib.baseAngle) * HUB_RADIUS;
const wy = hub.y + Math.sin(rib.baseAngle) * HUB_RADIUS;
const tipX = wx + Math.cos(rib.baseAngle + rib.angle) * RIB_LENGTH;
const tipY = wy + Math.sin(rib.baseAngle + rib.angle) * RIB_LENGTH;
const midY = (wy + tipY)/2;
const wind = getWindVector(midY);
const ribDirX = Math.cos(rib.baseAngle + rib.angle);
const ribDirY = Math.sin(rib.baseAngle + rib.angle);
const normalX = -ribDirY;
const normalY = ribDirX;
const vrelX = wind.x - hub.vx;
const vrelY = wind.y - hub.vy;
const vnorm = vrelX * normalX + vrelY * normalY;
const force = 0.5 * RHO_AIR * CD_RIB * (RIB_LENGTH * 0.08) * vnorm * Math.abs(vnorm);
const fx = force * normalX;
const fy = force * normalY;
const rx = (wx + tipX)/2 - person.footL.x;
const ry = (wy + tipY)/2 - person.footL.y;
windMoment += rx * fy - ry * fx;
}
// Shaft drag
const shaftWind = getWindVector(hub.y - SHAFT_LENGTH/2);
const shaftDrag = 0.5 * RHO_AIR * 1.0 * (SHAFT_LENGTH * 0.025) * shaftWind.x * Math.abs(shaftWind.x);
windMoment += (hub.x - person.footL.x) * 0 - (hub.y - SHAFT_LENGTH/2 - person.footL.y) * shaftDrag;
}
// PD control for lean
const totalMass = person.hip.mass + person.shoulder.mass + person.elbow.mass + person.hand.mass;
const comHeight = (person.hip.y * person.hip.mass + person.shoulder.y * person.shoulder.mass + person.elbow.y * person.elbow.mass + person.hand.y * person.hand.mass) / totalMass;
person.targetLean = Math.atan2(windMoment, totalMass * G * comHeight);
person.targetLean = Math.max(-0.6, Math.min(0.6, person.targetLean));
const leanError = person.targetLean - person.torsoAngle;
person.leanRate += leanError * 12 * dt - person.leanRate * 4 * dt;
person.leanRate = Math.max(-3, Math.min(3, person.leanRate));
person.torsoAngle += person.leanRate * dt;
// Update body positions
person.hip.x = Math.sin(person.torsoAngle) * 0.05;
person.hip.y = 0.95 + Math.cos(person.torsoAngle) * 0.05;
person.shoulder.x = person.hip.x + Math.sin(person.torsoAngle) * 0.5;
person.shoulder.y = person.hip.y + Math.cos(person.torsoAngle) * 0.5;
// Arm reaches for handle
const handleX = hub.x;
const handleY = hub.y - SHAFT_LENGTH;
const dx = handleX - person.shoulder.x;
const dy = handleY - person.shoulder.y;
const dist = Math.hypot(dx, dy);
const upperLen = 0.35, lowerLen = 0.35;
if (dist > upperLen + lowerLen - 0.02) {
// Stretch - elbow locks
const ang = Math.atan2(dy, dx);
person.elbow.x = person.shoulder.x + Math.cos(ang) * upperLen;
person.elbow.y = person.shoulder.y + Math.sin(ang) * upperLen;
person.hand.x = handleX;
person.hand.y = handleY;
} else {
// IK
const cosAng = (upperLen*upperLen + lowerLen*lowerLen - dist*dist) / (2*upperLen*lowerLen);
const elbowAng = Math.acos(Math.max(-1, Math.min(1, cosAng)));
const shoulderAng = Math.atan2(dy, dx) - Math.asin(Math.sin(elbowAng) * lowerLen / dist);
person.elbow.x = person.shoulder.x + Math.cos(shoulderAng) * upperLen;
person.elbow.y = person.shoulder.y + Math.sin(shoulderAng) * upperLen;
person.hand.x = person.elbow.x + Math.cos(shoulderAng + Math.PI - elbowAng) * lowerLen;
person.hand.y = person.elbow.y + Math.sin(shoulderAng + Math.PI - elbowAng) * lowerLen;
}
// Grip force
const shaftTension = Math.hypot(hub.vx, hub.vy) * hub.mass + Math.abs(windMoment) * 0.5;
shaft.grip = Math.min(person.gripStrength, shaftTension * 1.2);
// Breakaway
if (shaftTension > person.gripStrength * 1.5 && !detached) {
detached = true;
detachTime = time;
hub.vx += shaftWind.x * 0.5;
hub.vy += shaftWind.y * 0.5;
}
}
/* ============ UMBRELLA PHYSICS ============ */
function updateUmbrella() {
if (detached) {
// Free flight
const wind = getWindVector(hub.y);
const dragX = 0.5 * RHO_AIR * CD_CANOPY * (RIB_LENGTH*RIB_LENGTH*1.5) * (wind.x - hub.vx) * Math.abs(wind.x - hub.vx);
const dragY = 0.5 * RHO_AIR * CD_CANOPY * (RIB_LENGTH*RIB_LENGTH*1.5) * (wind.y - hub.vy) * Math.abs(wind.y - hub.vy);
hub.vx += dragX / hub.mass * dt;
hub.vy += dragY / hub.mass * dt - G * dt;
hub.x += hub.vx * dt;
hub.y += hub.vy * dt;
hub.angle += hub.av * dt;
hub.av *= 0.99;
if (hub.y < -2) { resetUmbrella(); }
return;
}
// Hub follows hand
hub.x = person.hand.x;
hub.y = person.hand.y;
hub.vx = 0; hub.vy = 0;
// Wind at hub height
const wind = getWindVector(hub.y);
// Rib dynamics
let avgAngle = 0;
let maxTipSpeed = 0;
for (let i=0;i<RIB_COUNT;i++) {
const rib = ribs[i];
const wx = hub.x + Math.cos(rib.baseAngle) * HUB_RADIUS;
const wy = hub.y + Math.sin(rib.baseAngle) * HUB_RADIUS;
const tipX = wx + Math.cos(rib.baseAngle + rib.angle) * RIB_LENGTH;
const tipY = wy + Math.sin(rib.baseAngle + rib.angle) * RIB_LENGTH;
const midX = (wx + tipX)/2;
const midY = (wy + tipY)/2;
// Wind force on rib
const ribDirX = Math.cos(rib.baseAngle + rib.angle);
const ribDirY = Math.sin(rib.baseAngle + rib.angle);
const normalX = -ribDirY;
const normalY = ribDirX;
const vrelX = wind.x;
const vrelY = wind.y;
const vnorm = vrelX * normalX + vrelY * normalY;
const area = RIB_LENGTH * 0.08;
const force = 0.5 * RHO_AIR * CD_RIB * area * vnorm * Math.abs(vnorm);
const fx = force * normalX;
const fy = force * normalY;
// Torque about hub
const rx = tipX - wx;
const ry = tipY - wy;
const torque = rx * fy - ry * fx;
// Hub spring torque
const springTorque = -rib.hubSpring * (rib.angle - rib.restAngle) - rib.hubDamping * rib.av;
// Fabric springs to neighbors
let fabricTorque = 0;
const prev = ribs[(i-1+RIB_COUNT)%RIB_COUNT];
const next = ribs[(i+1)%RIB_COUNT];
for (const nrib of [prev, next]) {
const nTipX = wx + Math.cos(nrib.baseAngle + nrib.angle) * RIB_LENGTH;
const nTipY = wy + Math.sin(nrib.baseAngle + nrib.angle) * RIB_LENGTH;
const dx = nTipX - tipX;
const dy = nTipY - tipY;
const dist = Math.hypot(dx, dy);
const restDist = 2 * RIB_LENGTH * Math.sin(Math.PI/RIB_COUNT);
if (dist > 0.001) {
const fmag = rib.fabricK * (dist - restDist) + rib.fabricD * ((nrib.av - rib.av) * RIB_LENGTH);
const ftx = fmag * dx / dist;
const fty = fmag * dy / dist;
fabricTorque += rx * fty - ry * ftx;
}
}
// Gravity on tip
const gravTorque = rx * (-rib.tipMass * G) - ry * 0;
// Total torque
const totalTorque = torque + springTorque + fabricTorque + gravTorque;
const inertia = rib.tipMass * RIB_LENGTH * RIB_LENGTH;
rib.av += totalTorque / inertia * dt;
rib.av *= 0.985;
rib.angle += rib.av * dt;
// Limit
if (!inverted) {
rib.angle = Math.max(-1.4, Math.min(1.2, rib.angle));
} else {
rib.angle = Math.max(0.2, Math.min(1.8, rib.angle));
}
avgAngle += rib.angle;
maxTipSpeed = Math.max(maxTipSpeed, Math.abs(rib.av * RIB_LENGTH));
}
avgAngle /= RIB_COUNT;
// Inversion logic
const windPressure = 0.5 * RHO_AIR * wind.x * Math.abs(wind.x);
if (!inverted && avgAngle > 0 && windPressure > 35 && inversionLock <= 0) {
inverted = true;
inversionLock = 1.5; // hysteresis lock time
for (const rib of ribs) {
rib.restAngle = 1.05; // 60 deg up
rib.hubSpring *= 0.2;
rib.hubDamping *= 0.5;
rib.inverted = true;
}
} else if (inverted && windSpeed < 12 && avgAngle < -0.17 && inversionLock <= 0) {
inverted = false;
inversionLock = 1.0;
for (const rib of ribs) {
rib.restAngle = -0.52;
rib.hubSpring = 45;
rib.hubDamping = 2.5;
rib.inverted = false;
}
}
if (inversionLock > 0) inversionLock -= dt;
// Shaft angle follows average rib tilt
shaft.angle = avgAngle * 0.3;
}
/* ============ RAIN ============ */
function spawnRain() {
while (rainDrops.length < MAX_RAIN) {
const wx = getWindVector(0).x;
rainDrops.push({
x: (Math.random()-0.5) * 30,
y: (Math.random()-0.5) * 20 + 10,
vx: wx * 0.8,
vy: -12 - Math.random() * 8,
len: 8 + Math.random() * 12,
alpha: 0.3 + Math.random() * 0.5
});
}
}
function updateRain() {
const wind = getWindVector(0);
for (const drop of rainDrops) {
drop.vx += (wind.x * 0.9 - drop.vx) * 0.02;
drop.vy += (-14 - drop.vy) * 0.02;
drop.x += drop.vx * dt;
drop.y += drop.vy * dt;
if (drop.y < -2 || drop.x < -18 || drop.x > 18) {
drop.x = (Math.random()-0.5) * 30;
drop.y = 12 + Math.random() * 8;
drop.vx = wind.x * 0.8;
drop.vy = -12 - Math.random() * 8;
}
}
}
/* ============ RENDER ============ */
function worldToScreen(x, y) {
return { x: cx + x * scale, y: cy - y * scale };
}
function drawGround() {
ctx.fillStyle = '#0d1426';
ctx.fillRect(0, cy, W, H-cy);
// Grid lines
ctx.strokeStyle = 'rgba(107,122,148,0.08)';
ctx.lineWidth = 1;
for (let i=-10;i<=10;i++) {
const sx = cx + i * scale;
ctx.beginPath(); ctx.moveTo(sx, cy); ctx.lineTo(sx, H); ctx.stroke();
}
for (let y=0;y<5;y++) {
const sy = cy - y * scale;
ctx.beginPath(); ctx.moveTo(0, sy); ctx.lineTo(W, sy); ctx.stroke();
}
}
function drawRain() {
ctx.lineWidth = 1.2;
for (const drop of rainDrops) {
const p1 = worldToScreen(drop.x, drop.y);
const p2 = worldToScreen(drop.x + drop.vx * 0.04, drop.y + drop.vy * 0.04);
ctx.strokeStyle = `rgba(137,180,250,${drop.alpha * 0.6})`;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
}
function drawWindStreaks() {
if (windSpeed < 2) return;
ctx.lineWidth = 0.8;
ctx.strokeStyle = 'rgba(137,180,250,0.15)';
const t = time * 20;
for (let i=0;i<30;i++) {
const y = (Math.sin(i*0.7 + t*0.01) * 0.5 + 0.5) * 8;
const x = ((t + i*1.3) % 36) - 18;
const p1 = worldToScreen(x, y);
const p2 = worldToScreen(x - windSpeed*0.08, y);
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
}
}
function drawPerson() {
const hip = worldToScreen(person.hip.x, person.hip.y);
const shoulder = worldToScreen(person.shoulder.x, person.shoulder.y);
const elbow = worldToScreen(person.elbow.x, person.elbow.y);
const hand = worldToScreen(person.hand.x, person.hand.y);
const footL = worldToScreen(person.footL.x, person.footL.y);
const footR = worldToScreen(person.footR.x, person.footR.y);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Legs
ctx.strokeStyle = '#3a4a6b';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(hip.x, hip.y); ctx.lineTo(footL.x, footL.y);
ctx.moveTo(hip.x, hip.y); ctx.lineTo(footR.x, footR.y);
ctx.stroke();
// Feet
ctx.fillStyle = '#2a3a5a';
ctx.beginPath();
ctx.ellipse(footL.x, footL.y+2, 14*scale/100, 6*scale/100, 0, 0, Math.PI*2);
ctx.ellipse(footR.x, footR.y+2, 14*scale/100, 6*scale/100, 0, 0, Math.PI*2);
ctx.fill();
// Torso
ctx.strokeStyle = '#4a5a7a';
ctx.lineWidth = 10;
ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(shoulder.x, shoulder.y); ctx.stroke();
// Arm
ctx.strokeStyle = '#5a6a8a';
ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(shoulder.x, shoulder.y); ctx.lineTo(elbow.x, elbow.y); ctx.lineTo(hand.x, hand.y); ctx.stroke();
// Joints
ctx.fillStyle = '#6a7a9a';
for (const p of [hip, shoulder, elbow, hand]) {
ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill();
}
// Head
const headY = shoulder.y - 0.22 * scale;
ctx.fillStyle = '#d4a57a';
ctx.beginPath(); ctx.arc(shoulder.x, headY, 10, 0, Math.PI*2); ctx.fill();
// Hair/wind effect
if (windSpeed > 5) {
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 2;
for (let i=0;i<5;i++) {
const ang = -Math.PI/2 + (i-2)*0.3 + Math.sin(time*5+i)*0.2*windSpeed/20;
ctx.beginPath();
ctx.moveTo(shoulder.x, headY-8);
ctx.lineTo(shoulder.x + Math.cos(ang)*15, headY-8 + Math.sin(ang)*15);
ctx.stroke();
}
}
}
function drawUmbrella() {
const hx = hub.x, hy = hub.y;
const hubS = worldToScreen(hx, hy);
// Shaft
const shaftTop = worldToScreen(hx, hy);
const shaftBot = worldToScreen(hx, hy - SHAFT_LENGTH);
const handleEnd = worldToScreen(hx + Math.sin(shaft.angle) * HANDLE_LENGTH, hy - SHAFT_LENGTH - Math.cos(shaft.angle) * HANDLE_LENGTH);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Shaft
const shaftGrad = ctx.createLinearGradient(shaftTop.x, shaftTop.y, shaftBot.x, shaftBot.y);
shaftGrad.addColorStop(0, '#2a3a5a');
shaftGrad.addColorStop(1, '#1a2a4a');
ctx.strokeStyle = shaftGrad;
ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(shaftTop.x, shaftTop.y); ctx.lineTo(shaftBot.x, shaftBot.y); ctx.stroke();
// Handle
ctx.strokeStyle = '#3d2a1a';
ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(shaftBot.x, shaftBot.y); ctx.lineTo(handleEnd.x, handleEnd.y); ctx.stroke();
// Grip
ctx.fillStyle = '#2a1a0a';
ctx.beginPath(); ctx.arc(handleEnd.x, handleEnd.y, 6, 0, Math.PI*2); ctx.fill();
// Hub
ctx.fillStyle = inverted ? '#8b4a3a' : '#2a3a5a';
ctx.beginPath(); ctx.arc(hubS.x, hubS.y, 6, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#1a2a3a';
ctx.lineWidth = 2;
ctx.stroke();
// Ribs and canopy
const ribPoints = [];
for (let i=0;i<RIB_COUNT;i++) {
const rib = ribs[i];
const wx = hx + Math.cos(rib.baseAngle) * HUB_RADIUS;
const wy = hy + Math.sin(rib.baseAngle) * HUB_RADIUS;
const tipX = wx + Math.cos(rib.baseAngle + rib.angle) * RIB_LENGTH;
const tipY = wy + Math.sin(rib.baseAngle + rib.angle) * RIB_LENGTH;
const wS = worldToScreen(wx, wy);
const tS = worldToScreen(tipX, tipY);
ribPoints.push({ wx:wS.x, wy:wS.y, tx:tS.x, ty:tS.y, angle:rib.angle, baseAngle:rib.baseAngle });
// Rib
const ribGrad = ctx.createLinearGradient(wS.x, wS.y, tS.x, tS.y);
ribGrad.addColorStop(0, inverted ? '#a05040' : '#3a4a6a');
ribGrad.addColorStop(1, inverted ? '#804030' : '#2a3a5a');
ctx.strokeStyle = ribGrad;
ctx.lineWidth = inverted ? 3 : 2.5;
ctx.beginPath(); ctx.moveTo(wS.x, wS.y); ctx.lineTo(tS.x, tS.y); ctx.stroke();
// Stretcher (inner support)
if (!inverted) {
const strutX = wx + Math.cos(rib.baseAngle + rib.angle) * RIB_LENGTH * 0.45;
const strutY = wy + Math.sin(rib.baseAngle + rib.angle) * RIB_LENGTH * 0.45;
const strutS = worldToScreen(strutX, strutY);
const runnerX = hx;
const runnerY = hy - 0.15;
const runnerS = worldToScreen(runnerX, runnerY);
ctx.strokeStyle = 'rgba(60,70,90,0.6)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(strutS.x, strutS.y); ctx.lineTo(runnerS.x, runnerS.y); ctx.stroke();
}
}
// Canopy fabric (triangles between ribs)
for (let i=0;i<RIB_COUNT;i++) {
const a = ribPoints[i];
const b = ribPoints[(i+1)%RIB_COUNT];
const hubS = worldToScreen(hx, hy);
// Curved fabric using quadratic curves
const midX = (a.tx + b.tx)/2;
const midY = (a.ty + b.ty)/2;
const centerPull = inverted ? -0.15 : 0.25;
const curveX = midX + (hubS.x - midX) * centerPull;
const curveY = midY + (hubS.y - midY) * centerPull;
const panelGrad = ctx.createLinearGradient(hubS.x, hubS.y, midX, midY);
const baseHue = inverted ? 15 : 210;
const saturation = inverted ? 60 : 40;
const lightness = 25 + Math.abs(Math.sin(a.baseAngle*2 + time)) * 8;
panelGrad.addColorStop(0, `hsla(${baseHue},${saturation}%,${lightness}%,0.9)`);
panelGrad.addColorStop(1, `hsla(${baseHue},${saturation}%,${lightness-10}%,0.7)`);
ctx.fillStyle = panelGrad;
ctx.strokeStyle = `hsla(${baseHue},${saturation}%,${lightness-20}%,0.5)`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(hubS.x, hubS.y);
ctx.lineTo(a.tx, a.ty);
ctx.quadraticCurveTo(curveX, curveY, b.tx, b.ty);
ctx.lineTo(hubS.x, hubS.y);
ctx.fill();
ctx.stroke();
}
// Vent hole at top
if (!inverted) {
ctx.fillStyle = 'rgba(10,15,25,0.8)';
ctx.beginPath(); ctx.arc(hubS.x, hubS.y, 4, 0, Math.PI*2); ctx.fill();
}
// Tip caps
ctx.fillStyle = inverted ? '#a05040' : '#4a5a7a';
for (const p of ribPoints) {
ctx.beginPath(); ctx.arc(p.tx, p.ty, 3, 0, Math.PI*2); ctx.fill();
}
}
function drawUI() {
const windEl = document.getElementById('windVal');
const statusEl = document.getElementById('status');
const autoBtn = document.getElementById('autoBtn');
windEl.textContent = windSpeed.toFixed(1);
windEl.classList.toggle('inverted', inverted);
if (detached) {
statusEl.textContent = 'UMBRELLA LOST — Press R';
statusEl.className = 'status danger';
} else if (inverted) {
statusEl.textContent = 'INVERTED! Wind: ' + windSpeed.toFixed(1) + ' m/s';
statusEl.className = 'status danger';
} else if (windSpeed > 20) {
statusEl.textContent = 'High wind — bracing hard';
statusEl.className = 'status warning';
} else if (windSpeed > 10) {
statusEl.textContent = 'Moderate wind — leaning in';
statusEl.className = 'status warning';
} else {
statusEl.textContent = windSpeed > 1 ? 'Light breeze' : 'Calm';
statusEl.className = 'status';
}
autoBtn.textContent = autoRamp ? 'Stop Auto' : 'Auto Ramp';
}
/* ============ MAIN LOOP ============ */
function step() {
time += dt;
gustPhase += dt;
// Wind control
if (autoRamp) {
targetWind += autoDir * 8 * dt;
if (targetWind >= 100) { targetWind = 100; autoDir = -1; }
if (targetWind <= 0) { targetWind = 0; autoDir = 1; }
}
windSpeed += (targetWind - windSpeed) * 3 * dt;
updatePerson();
updateUmbrella();
updateRain();
drawUI();
render();
requestAnimationFrame(step);
}
function render() {
ctx.clearRect(0, 0, W, H);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, cy);
skyGrad.addColorStop(0, '#080c14');
skyGrad.addColorStop(1, '#0d1426');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, cy);
drawWindStreaks();
drawRain();
drawGround();
drawPerson();
drawUmbrella();
}
/* ============ INPUT ============ */
const windSlider = document.getElementById('wind');
windSlider.addEventListener('input', e => {
targetWind = parseFloat(e.target.value);
autoRamp = false;
});
document.getElementById('autoBtn').addEventListener('click', () => {
autoRamp = !autoRamp;
if (autoRamp) { targetWind = windSpeed; autoDir = windSpeed > 50 ? -1 : 1; }
});
document.getElementById('resetBtn').addEventListener('click', resetAll);
function resetAll() {
windSpeed = targetWind = 0;
windSlider.value = 0;
autoRamp = false;
autoDir = 1;
time = 0;
gustPhase = 0;
inverted = false;
inversionLock = 0;
detached = false;
detachTime = 0;
shaft = { angle:0, av:0, attached:true, grip:1 };
initRibs();
person.torsoAngle = 0;
person.leanRate = 0;
person.targetLean = 0;
person.hip.x = 0; person.hip.y = 0.95;
person.shoulder.x = 0; person.shoulder.y = 1.45;
person.elbow.x = 0; person.elbow.y = 1.15;
person.hand.x = 0; person.hand.y = 1.0;
rainDrops.length = 0;
}
addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'ArrowRight') { targetWind = Math.min(100, targetWind + 2); windSlider.value = targetWind; autoRamp = false; }
else if (e.key === 'ArrowLeft') { targetWind = Math.max(0, targetWind - 2); windSlider.value = targetWind; autoRamp = false; }
else if (e.key === ' ') { targetWind = Math.min(100, windSpeed + 15); windSlider.value = targetWind; autoRamp = false; e.preventDefault(); }
else if (e.key === 'r' || e.key === 'R') { resetAll(); }
});
/* ============ START ============ */
spawnRain();
step();
</script>
</body>
</html>
Where it looks right:
Where it looks wrong (and I know it):
If you’ve stood in a gale, you’ll miss the sound — the fabric drumming, the rib clicks, the sudden thwack of inversion — but the visual rhythm of loading, buckling, and loss is there.