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 umbrella is a small parametric structure, not a cloth simulation. Its canopy is a flexible shell drawn from two quadratic Bézier curves between rim points, and the rim, crown, and belly points are continuously morphed between a normal dome and an inverted scoop. Wind load is dynamic pressure (∝ wind speed²) times a projected-area multiplier; frame stress is that load divided by a fixed frame capacity. Inversion is a smooth state variable: when stress passes capacity the canopy flips toward its inside-out scoop shape, which increases drag, and below a lower hysteresis threshold it relaxes back. A rotational degree of freedom lets the shaft pivot in the person’s hand, resisted by grip stiffness, damping, and body lean. If load or deflection exceeds the grip limit, the umbrella detaches and becomes a wind-driven projectile.
<!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>
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: #0f172a;
}
body { font-family: system-ui, -apple-system, sans-serif; }
canvas { display: block; }
#hud {
position: fixed;
top: 0; left: 0; right: 0;
padding: 14px 18px;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px;
pointer-events: none;
z-index: 10;
color: #fff;
text-shadow: 0 2px 5px rgba(0,0,0,.5);
}
#windbox {
font-size: 28px;
font-weight: 700;
background: rgba(0,0,0,.45);
padding: 10px 18px;
border-radius: 16px;
letter-spacing: 1px;
}
#windbox span { color: #ffd166; }
#controls {
display: flex;
gap: 10px;
align-items: center;
pointer-events: auto;
background: rgba(0,0,0,.45);
padding: 8px 14px;
border-radius: 16px;
flex-wrap: wrap;
justify-content: center;
backdrop-filter: blur(4px);
}
#controls button {
background: #e2e8f0;
border: none;
border-radius: 8px;
padding: 8px 12px;
font-weight: 600;
cursor: pointer;
color: #0f172a;
}
#controls button:hover { background: #f8fafc; }
#windSlider {
width: 140px;
accent-color: #ffd166;
}
#status {
position: fixed;
bottom: 76px;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
color: #eee;
background: rgba(0,0,0,.45);
padding: 6px 16px;
border-radius: 999px;
pointer-events: none;
z-index: 10;
text-align: center;
white-space: nowrap;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div id="windbox">🌬️ Wind <span id="windVal">10</span> mph</div>
<div id="controls">
<button id="autoBtn">AUTO ON</button>
<button id="resetBtn">RESET</button>
<input type="range" id="windSlider" min="0" max="80" step="0.5" value="10">
</div>
</div>
<div id="status">Holding steady</div>
<script>
"use strict";
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('windSlider');
const autoBtn = document.getElementById('autoBtn');
const resetBtn = document.getElementById('resetBtn');
const windVal = document.getElementById('windVal');
const statusDiv = document.getElementById('status');
let WIDTH = 0, HEIGHT = 0, S = 1, groundY = 0, personX = 0;
let wind = 10, autoOn = true, time = 0;
let theta = 0, omega = 0, inversion = 0, inversionTarget = 0, flex = 0;
let drag = 0, stress = 0;
let bodyLean = 0, stance = 0;
let shoulder = {x: 0, y: 0}, hand = {x: 0, y: 0}, apex = {x: 0, y: 0};
let detached = false, lost = false;
let fly = null;
let drops = [];
const LEG_LEN = 95;
const TORSO_LEN = 76;
const HEAD_R = 16;
const SHAFT_LEN = 108;
const RIM_DROP = 42;
const HALF_WIDTH = 76;
function resize() {
WIDTH = window.innerWidth || 800;
HEIGHT = window.innerHeight || 600;
canvas.width = WIDTH;
canvas.height = HEIGHT;
S = Math.min(WIDTH, HEIGHT) / 650;
groundY = HEIGHT - 90 * S;
personX = WIDTH * 0.24;
}
function initRain() {
const count = Math.max(60, Math.floor(WIDTH * HEIGHT / 3000));
drops = [];
for (let i = 0; i < count; i++) {
drops.push({
x: Math.random() * WIDTH,
y: Math.random() * HEIGHT
});
}
}
function clamp(v, a, b) {
return Math.max(a, Math.min(b, v));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function damp(cur, target, rate, dt) {
return lerp(cur, target, 1 - Math.exp(-rate * dt));
}
function quadPoint(p0, p1, p2, t) {
const mt = 1 - t;
return {
x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y
};
}
function updateHUD() {
windVal.textContent = Math.round(wind);
autoBtn.textContent = autoOn ? 'AUTO ON' : 'AUTO OFF';
if (lost) {
statusDiv.textContent = 'Lost the umbrella — press RESET';
} else if (detached) {
statusDiv.textContent = 'Umbrella got away!';
} else if (inversion > 0.5) {
statusDiv.textContent = 'Inside out!';
} else if (stress > 0.8) {
statusDiv.textContent = 'Frame straining…';
} else {
statusDiv.textContent = 'Holding steady';
}
}
function updatePose(dt) {
const desiredLean = -clamp(drag * 0.0045, 0, 0.55);
const desiredStance = 32 * S + clamp(drag * 0.18, 0, 58) * S;
bodyLean = damp(bodyLean, desiredLean, 3.5, dt);
stance = damp(stance, desiredStance, 3.5, dt);
const hipY = groundY - LEG_LEN * S;
const hipX = personX + Math.sin(bodyLean) * LEG_LEN * S * 0.15;
shoulder.x = hipX + Math.sin(bodyLean) * TORSO_LEN * S;
shoulder.y = hipY - Math.cos(bodyLean) * TORSO_LEN * S;
if (detached) {
hand.x = shoulder.x + 14 * S;
hand.y = shoulder.y + 22 * S;
} else {
hand.x = shoulder.x + 6 * S + drag * 0.012 * S;
hand.y = shoulder.y - (56 * S + 5 * S * clamp(stress, 0, 1));
}
}
function updateUmbrella(dt) {
const len = SHAFT_LEN * S;
const cosT = Math.cos(theta);
const sinT = Math.sin(theta);
apex.x = hand.x + sinT * len;
apex.y = hand.y - cosT * len;
const torqueWind = drag * len * cosT;
const torqueGrav = 38 * S * len * sinT;
const targetTheta = -0.10 * clamp(wind / 60, 0, 1);
const kp = 32000 * S * (1 - 0.7 * clamp(stress - 1, 0, 1));
const kd = 1100 * S;
const tremor = stress > 1 ? Math.sin(time * 45) * stress * 12 * S : 0;
const alpha = (torqueWind + torqueGrav - kp * (theta - targetTheta) - kd * omega + tremor) / (2300 * S * S);
omega += alpha * dt;
theta += omega * dt;
apex.x = hand.x + Math.sin(theta) * len;
apex.y = hand.y - Math.cos(theta) * len;
if (inversion > 0.4 && (drag > 175 || Math.abs(theta) > 2.0)) {
detachUmbrella();
}
}
function detachUmbrella() {
if (detached) return;
detached = true;
fly = {
x: hand.x,
y: hand.y,
theta: theta,
omega: omega,
vx: wind * 1.6 + 50 * S,
vy: -90 * S
};
}
function updateFly(dt) {
if (lost) return;
const q = 0.04 * wind * wind;
const area = 0.8 + 0.5 * inversion;
const fd = q * area;
fly.vx += (wind * 0.55 + fd * 0.02) * dt;
fly.vy += 460 * S * dt;
fly.x += fly.vx * dt;
fly.y += fly.vy * dt;
fly.omega += (wind * 0.014 - fly.omega * 0.08) * dt;
fly.theta += fly.omega * dt;
if (fly.x > WIDTH + 350 || fly.x < -350 || fly.y > HEIGHT + 350 || fly.y < -350) {
lost = true;
}
}
function updateRain(dt) {
const drift = wind * 2.4 * S;
const fall = 420 * S;
for (const d of drops) {
d.x += drift * dt;
d.y += fall * dt;
if (d.y > groundY - 4) {
d.y = -20 * S - Math.random() * 20 * S;
d.x = Math.random() * WIDTH;
}
if (d.x > WIDTH + 20) d.x -= WIDTH + 40;
if (d.x < -20) d.x += WIDTH + 40;
}
}
function update(dt) {
time += dt;
if (autoOn) {
wind += 1.3 * dt;
if (wind > 80) wind = 80;
slider.value = wind.toFixed(1);
}
const q = 0.04 * wind * wind;
const area = 0.8 + 0.5 * inversion;
drag = q * area;
stress = drag / 60;
const flexTarget = clamp(stress * 0.8, 0, 1);
flex = damp(flex, flexTarget, 2.2, dt);
if (stress > 1.0) inversionTarget = 1;
else if (stress < 0.6) inversionTarget = 0;
inversion = damp(inversion, inversionTarget, 2.5, dt);
updatePose(dt);
if (detached) {
if (!lost) updateFly(dt);
} else {
updateUmbrella(dt);
}
updateRain(dt);
updateHUD();
}
function drawSky() {
const g = ctx.createLinearGradient(0, 0, 0, groundY);
g.addColorStop(0, '#0f172a');
g.addColorStop(1, '#3b4a63');
ctx.fillStyle = g;
ctx.fillRect(0, 0, WIDTH, groundY + 1);
}
function drawClouds() {
const span = WIDTH + 400;
const move = (time * (6 + wind * 0.5) * S) % span;
ctx.fillStyle = 'rgba(120,135,160,0.45)';
for (let i = 0; i < 7; i++) {
const cx = ((i * 190 - move) % span + span) % span - 120;
const cy = 60 * S + (i % 3) * 35 * S;
ctx.beginPath();
ctx.ellipse(cx, cy, 100 * S + wind * 0.4 * S, 30 * S, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawGround() {
ctx.fillStyle = '#29382e';
ctx.fillRect(0, groundY, WIDTH, HEIGHT - groundY);
ctx.strokeStyle = '#4a5f4a';
ctx.lineWidth = 2 * S;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(WIDTH, groundY);
ctx.stroke();
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(personX, groundY + 5 * S, 70 * S + stance, 7 * S, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawRain() {
const drift = wind * 2.4 * S;
const fall = 420 * S;
const hyp = Math.hypot(drift, fall) || 1;
const nx = drift / hyp;
const ny = fall / hyp;
const len = Math.min(22 * S, 4 * S + hyp * 0.03);
ctx.lineWidth = 1.3 * S;
ctx.strokeStyle = 'rgba(190,215,255,0.5)';
ctx.beginPath();
for (const d of drops) {
ctx.moveTo(d.x, d.y);
ctx.lineTo(d.x - nx * len, d.y - ny * len);
}
ctx.stroke();
}
function drawPerson() {
const hipY = groundY - LEG_LEN * S;
const hipX = personX + Math.sin(bodyLean) * LEG_LEN * S * 0.15;
const footL = personX - stance / 2;
const footR = personX + stance / 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 9 * S;
ctx.beginPath();
ctx.moveTo(footL, groundY);
ctx.lineTo(hipX, hipY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(footR, groundY);
ctx.lineTo(hipX, hipY);
ctx.stroke();
ctx.fillStyle = '#1f2a32';
ctx.fillRect(footL - 9 * S, groundY - 4 * S, 22 * S, 6 * S);
ctx.fillRect(footR - 13 * S, groundY - 4 * S, 22 * S, 6 * S);
ctx.strokeStyle = '#2980b9';
ctx.lineWidth = 15 * S;
ctx.beginPath();
ctx.moveTo(hipX, hipY);
ctx.lineTo(shoulder.x, shoulder.y);
ctx.stroke();
const headR = HEAD_R * S;
const headX = shoulder.x + Math.sin(bodyLean) * headR * 0.3;
const headY = shoulder.y - headR * 1.1;
ctx.fillStyle = '#f1c27d';
ctx.beginPath();
ctx.arc(headX, headY, headR, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#1f2a32';
ctx.lineWidth = 1.5 * S;
ctx.stroke();
ctx.strokeStyle = '#4a2c17';
ctx.lineWidth = 4 * S;
ctx.beginPath();
ctx.moveTo(headX, headY - headR * 0.9);
ctx.quadraticCurveTo(
headX + 12 * S + wind * 0.3 * S,
headY - headR * 1.5,
headX + 24 * S + wind * 0.5 * S,
headY - headR * 0.6
);
ctx.stroke();
if (wind > 5) {
ctx.strokeStyle = '#d63031';
ctx.lineWidth = 5 * S;
ctx.beginPath();
ctx.moveTo(headX, headY + headR * 0.6);
ctx.quadraticCurveTo(
headX + wind * 0.4 * S,
headY + headR * 1.5,
headX + wind * 0.8 * S,
headY + headR * 1.8
);
ctx.stroke();
}
ctx.strokeStyle = '#f1c27d';
ctx.lineWidth = 6 * S;
ctx.beginPath();
ctx.moveTo(shoulder.x, shoulder.y);
ctx.lineTo(hand.x, hand.y);
ctx.stroke();
if (!detached && stress > 0.25) {
const bend = flex * 16 * S;
const ctrl = {
x: (hand.x + apex.x) / 2 + Math.cos(theta) * bend,
y: (hand.y + apex.y) / 2 + Math.sin(theta) * bend
};
const g = quadPoint(hand, ctrl, apex, 0.35);
ctx.beginPath();
ctx.moveTo(shoulder.x, shoulder.y);
ctx.lineTo(g.x, g.y);
ctx.stroke();
}
}
function drawUmbrellaAt(handlePos, thetaVal, inversionVal, flexVal) {
const len = SHAFT_LEN * S;
const sx = Math.sin(thetaVal);
const sy = -Math.cos(thetaVal);
const px = Math.cos(thetaVal);
const py = Math.sin(thetaVal);
const downx = -sx;
const downy = -sy;
const A = {
x: handlePos.x + sx * len,
y: handlePos.y + sy * len
};
const bend = flexVal * 16 * S;
const midX = (handlePos.x + A.x) / 2 + px * bend;
const midY = (handlePos.y + A.y) / 2 + py * bend;
ctx.beginPath();
ctx.moveTo(handlePos.x, handlePos.y);
ctx.quadraticCurveTo(midX, midY, A.x, A.y);
ctx.lineWidth = 4.5 * S;
ctx.strokeStyle = stress > 1 ? '#c0392b' : '#475569';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(handlePos.x, handlePos.y);
ctx.quadraticCurveTo(
handlePos.x - 8 * S,
handlePos.y + 7 * S,
handlePos.x - 2 * S,
handlePos.y + 15 * S
);
ctx.lineWidth = 4 * S;
ctx.strokeStyle = '#475569';
ctx.stroke();
const rimDrop = (RIM_DROP * S) * (1 - 0.15 * flexVal);
const halfWidth = (HALF_WIDTH * S) * (1 - 0.12 * flexVal);
const rimCenter = {
x: A.x - sx * rimDrop,
y: A.y - sy * rimDrop
};
const Ln = {
x: rimCenter.x - px * halfWidth,
y: rimCenter.y - py * halfWidth
};
const Rn = {
x: rimCenter.x + px * halfWidth,
y: rimCenter.y + py * halfWidth
};
const u = clamp(inversionVal, 0, 1);
const Lv = {
x: A.x - px * (0.4 * halfWidth) + sx * (0.4 * halfWidth),
y: A.y - py * (0.4 * halfWidth) + sy * (0.4 * halfWidth)
};
const Rv = {
x: A.x - px * (0.4 * halfWidth) - sx * (0.6 * halfWidth),
y: A.y - py * (0.4 * halfWidth) - sy * (0.6 * halfWidth)
};
const L = { x: lerp(Ln.x, Lv.x, u), y: lerp(Ln.y, Lv.y, u) };
const R = { x: lerp(Rn.x, Rv.x, u), y: lerp(Rn.y, Rv.y, u) };
const Ctn = {
x: 2 * A.x - rimCenter.x + downx * flexVal * 10 * S,
y: 2 * A.y - rimCenter.y + downy * flexVal * 10 * S
};
const invBulge = 1 + 0.25 * flexVal;
const Ctv = {
x: A.x + px * (1.2 * halfWidth * invBulge) - sx * (0.15 * halfWidth),
y: A.y + py * (1.2 * halfWidth * invBulge) - sy * (0.15 * halfWidth)
};
const cTop = {
x: lerp(Ctn.x, Ctv.x, u),
y: lerp(Ctn.y, Ctv.y, u)
};
const Cbn = {
x: rimCenter.x + downx * 8 * S,
y: rimCenter.y + downy * 8 * S
};
const Cbv = {
x: A.x + px * (1.2 * halfWidth * invBulge) - sx * (0.6 * halfWidth),
y: A.y + py * (1.2 * halfWidth * invBulge) - sy * (0.6 * halfWidth)
};
const cBottom = {
x: lerp(Cbn.x, Cbv.x, u),
y: lerp(Cbn.y, Cbv.y, u)
};
ctx.beginPath();
ctx.moveTo(L.x, L.y);
ctx.quadraticCurveTo(cTop.x, cTop.y, R.x, R.y);
ctx.quadraticCurveTo(cBottom.x, cBottom.y, L.x, L.y);
ctx.closePath();
const grad = ctx.createLinearGradient(L.x, L.y, R.x, R.y);
grad.addColorStop(0, u > 0.5 ? '#e67e22' : '#c0392b');
grad.addColorStop(1, u > 0.5 ? '#c0392b' : '#e74c3c');
ctx.fillStyle = grad;
ctx.fill();
ctx.lineWidth = 2 * S;
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.stroke();
ctx.beginPath();
for (let i = 0; i <= 5; i++) {
const t = i / 5;
const pt = quadPoint(L, cTop, R, t);
ctx.moveTo(A.x, A.y);
ctx.lineTo(pt.x, pt.y);
}
ctx.lineWidth = 1.5 * S;
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.stroke();
}
function drawGrip() {
if (detached) {
ctx.strokeStyle = '#f1c27d';
ctx.lineWidth = 3 * S;
ctx.beginPath();
ctx.arc(hand.x, hand.y, 6 * S, 0, Math.PI * 2);
ctx.stroke();
return;
}
ctx.fillStyle = '#f1c27d';
ctx.beginPath();
ctx.arc(hand.x, hand.y, 6 * S, 0, Math.PI * 2);
ctx.fill();
if (stress > 0.25) {
const bend = flex * 16 * S;
const ctrl = {
x: (hand.x + apex.x) / 2 + Math.cos(theta) * bend,
y: (hand.y + apex.y) / 2 + Math.sin(theta) * bend
};
const g = quadPoint(hand, ctrl, apex, 0.35);
ctx.beginPath();
ctx.arc(g.x, g.y, 5 * S, 0, Math.PI * 2);
ctx.fill();
}
}
function draw() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
drawSky();
drawClouds();
drawGround();
drawRain();
drawPerson();
if (detached && !lost && fly) {
drawUmbrellaAt({ x: fly.x, y: fly.y }, fly.theta, inversion, flex);
} else if (!detached) {
drawUmbrellaAt(hand, theta, inversion, flex);
}
drawGrip();
}
function resetAll() {
detached = false;
lost = false;
fly = null;
theta = 0;
omega = 0;
inversion = 0;
inversionTarget = 0;
flex = 0;
drag = 0;
stress = 0;
bodyLean = 0;
stance = 0;
wind = 0;
autoOn = true;
slider.value = 0;
updateHUD();
}
slider.addEventListener('input', () => {
autoOn = false;
wind = parseFloat(slider.value);
updateHUD();
});
autoBtn.addEventListener('click', () => {
autoOn = !autoOn;
updateHUD();
});
resetBtn.addEventListener('click', resetAll);
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
e.preventDefault();
autoOn = false;
wind = Math.min(80, wind + 1);
slider.value = wind;
updateHUD();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
autoOn = false;
wind = Math.max(0, wind - 1);
slider.value = wind;
updateHUD();
} else if (e.key.toLowerCase() === 'a') {
autoOn = !autoOn;
updateHUD();
} else if (e.key.toLowerCase() === 'r') {
resetAll();
}
});
resize();
initRain();
updateHUD();
let last = performance.now();
function loop(t) {
const dt = Math.min((t - last) / 1000, 0.05);
last = t;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
window.addEventListener('resize', () => {
resize();
initRain();
});
</script>
</body>
</html>
Where it looks right: the rain slant follows the current wind, the person leans and widens their stance as the load increases, the shaft bends and the canopy flattens before inversion, and the inversion happens at a repeatable stress threshold and produces an obvious inside-out “scoop” shape. The umbrella can then be wrenched sideways and, if the wind keeps rising, it will tear away from the hand and tumble offscreen as a projectile. The auto-ramp, slider, and arrow keys all drive the displayed wind speed.
Where it looks wrong: the canopy is a stylized parametric shell, not real fabric, so it cannot flutter, tear, or wrap around individual ribs. The inside-out shape is a geometric morph, not a buckling of actual struts. The person is a kinematic figure and never slips or loses footing; their grip is just a single scalar threshold. The released umbrella behaves as a rigid tumbling body, ignoring the fabric’s real aerodynamics. The mph numbers are theatrical, not meteorologically calibrated.