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 a segmented, flexible surface drawn over ribs that bend with the wind load; its edge sags in the normal state and turns upward in the inverted state. Inversion is a one-way state change for the current umbrella, triggered when wind load stays above a threshold long enough (or exceeds a higher emergency threshold briefly). The umbrella’s tilt, sway, grip strain, and eventual breakaway are lightweight spring-and-threshold approximations, not a structural simulation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weathering the Wind</title>
<style>
:root {
color-scheme: dark;
--ink: #e9f2ef;
--muted: #9bb2b4;
--panel: rgba(14, 29, 39, .86);
--line: rgba(213, 239, 233, .15);
--mint: #8ee1c5;
--amber: #ffcb75;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
padding: 24px 18px 28px;
color: var(--ink);
background:
radial-gradient(ellipse at 50% -10%, #274958 0, #142a38 45%, #0d1b27 100%);
font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
}
main { max-width: 1040px; margin: 0 auto; }
header {
display: flex;
justify-content: space-between;
align-items: end;
gap: 20px;
margin: 0 2px 16px;
}
h1 {
margin: 0;
font-size: clamp(24px, 4vw, 36px);
letter-spacing: -.04em;
line-height: 1.05;
}
.subtitle { color: var(--muted); margin-top: 6px; }
.tag {
flex: 0 0 auto;
color: #b8d6d2;
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 11px;
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
}
.controls {
display: grid;
grid-template-columns: minmax(210px, 1fr) auto auto;
gap: 16px;
align-items: center;
padding: 13px 16px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 15px 15px 0 0;
}
.slider-wrap { min-width: 0; }
.slider-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
color: var(--muted);
font-size: 13px;
}
.slider-label strong { color: var(--ink); font-weight: 600; }
input[type="range"] {
display: block;
width: 100%;
accent-color: var(--mint);
cursor: ew-resize;
}
button {
color: var(--ink);
background: #233d49;
border: 1px solid rgba(208, 235, 230, .2);
border-radius: 10px;
padding: 10px 14px;
font: inherit;
font-size: 13px;
cursor: pointer;
transition: background .15s, transform .15s;
white-space: nowrap;
}
button:hover { background: #315564; }
button:active { transform: translateY(1px); }
button.primary { color: #102b2c; background: var(--mint); border-color: transparent; font-weight: 700; }
button.primary:hover { background: #a8efd8; }
.button-group { display: flex; gap: 8px; }
.stage {
position: relative;
overflow: hidden;
border: 1px solid var(--line);
border-top: 0;
border-radius: 0 0 15px 15px;
background: #9bb9ba;
box-shadow: 0 22px 60px rgba(0,0,0,.28);
}
canvas { display: block; width: 100%; height: auto; aspect-ratio: 900 / 560; }
.hud {
position: absolute;
top: 15px;
left: 16px;
display: flex;
align-items: center;
gap: 10px;
pointer-events: none;
}
.speed-card, .status {
backdrop-filter: blur(8px);
background: rgba(15, 34, 42, .72);
border: 1px solid rgba(235, 250, 246, .18);
box-shadow: 0 5px 18px rgba(10, 30, 36, .16);
}
.speed-card { padding: 8px 12px; border-radius: 11px; min-width: 132px; }
.speed-card .small { display: block; color: #b7cecc; font-size: 10px; letter-spacing: .13em; text-transform: uppercase; }
#windReadout { font-size: 23px; line-height: 1.2; font-weight: 750; letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
#windReadout span { color: #c3d9d7; font-size: 13px; font-weight: 500; letter-spacing: 0; }
.status { border-radius: 999px; padding: 8px 11px; color: #d8eee8; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.status.warn { color: #ffdc98; }
.status.bad { color: #ffaaa0; }
.grip {
position: absolute;
right: 16px;
top: 17px;
width: min(168px, 28%);
color: #dcebe7;
font-size: 11px;
text-align: right;
pointer-events: none;
}
.grip-label { margin-bottom: 5px; text-shadow: 0 1px 3px #35515b; }
.meter { height: 6px; overflow: hidden; background: rgba(24, 48, 54, .55); border-radius: 99px; }
#gripFill { height: 100%; width: 100%; border-radius: inherit; background: #a3e0c7; transition: width .15s, background .2s; }
.help {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 12px 3px 0;
color: var(--muted);
font-size: 12px;
}
kbd {
font: inherit;
color: #d6e7e3;
background: rgba(224, 244, 237, .08);
border: 1px solid rgba(224, 244, 237, .17);
border-radius: 5px;
padding: 2px 6px;
}
@media (max-width: 650px) {
body { padding: 16px 10px 22px; }
header { align-items: start; }
.tag { display: none; }
.controls { grid-template-columns: 1fr auto; gap: 11px; padding: 12px; }
.slider-wrap { grid-column: 1 / -1; }
.button-group { justify-content: end; }
.help { display: block; }
.help span { display: block; margin-bottom: 5px; }
.grip { top: 68px; }
}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>Weathering the Wind</h1>
<div class="subtitle">A small storm, one stubborn umbrella.</div>
</div>
<div class="tag">A little wind lab</div>
</header>
<section class="controls" aria-label="Wind controls">
<div class="slider-wrap">
<div class="slider-label">
<label for="windSlider">Wind speed</label>
<strong><span id="sliderValue">4.0</span> m/s</strong>
</div>
<input id="windSlider" type="range" min="0" max="40" step="0.1" value="4">
</div>
<div class="button-group">
<button id="autoButton" class="primary" type="button">Auto: rising</button>
<button id="gustButton" type="button">Send a gust</button>
</div>
<button id="resetButton" type="button">Reset umbrella</button>
</section>
<div class="stage">
<canvas id="scene" aria-label="A person holding an umbrella as wind and rain intensify"></canvas>
<div class="hud">
<div class="speed-card">
<span class="small">Wind speed</span>
<div id="windReadout">4.0 <span>m/s</span></div>
</div>
<div id="status" class="status">Holding on</div>
</div>
<div class="grip">
<div class="grip-label">Grip & footing</div>
<div class="meter"><div id="gripFill"></div></div>
</div>
</div>
<div class="help">
<span><kbd>←</kbd> ease the wind <kbd>→</kbd> raise it <kbd>Space</kbd> gust <kbd>R</kbd> reset</span>
<span>Use the slider to take control; Auto resumes the rising storm.</span>
</div>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const slider = document.getElementById("windSlider");
const sliderValue = document.getElementById("sliderValue");
const windReadout = document.getElementById("windReadout");
const statusEl = document.getElementById("status");
const gripFill = document.getElementById("gripFill");
const autoButton = document.getElementById("autoButton");
const W = 900, H = 560, ground = 478;
let dpr = 1, scale = 1, offsetX = 0, offsetY = 0;
let auto = true;
let targetWind = 4;
let wind = 4;
let gust = 0;
let gustClock = 0;
let time = 0;
let lastTime = 0;
let inversion = 0;
let inverted = false;
let invertedTimer = 0;
let frameStress = 0;
let grip = 1;
let angle = -Math.PI / 2 + 0.08;
let angularVelocity = 0;
let escaped = false;
let escapeX = 0, escapeY = 0, escapeVX = 0, escapeVY = 0, escapeAngle = 0;
let particles = [];
let randomSeed = 43921;
function rand() {
randomSeed = (randomSeed * 16807) % 2147483647;
return (randomSeed - 1) / 2147483646;
}
function resize() {
const rect = canvas.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.width * (H / W) * dpr);
scale = canvas.width / W;
offsetX = 0;
offsetY = 0;
ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
initRain();
}
function initRain() {
particles = [];
for (let i = 0; i < 150; i++) {
particles.push({
x: rand() * W,
y: rand() * H,
length: 8 + rand() * 15,
alpha: .13 + rand() * .36,
phase: rand() * 10
});
}
}
function resetUmbrella() {
inversion = 0;
inverted = false;
invertedTimer = 0;
frameStress = 0;
grip = 1;
angle = -Math.PI / 2 + .08;
angularVelocity = 0;
escaped = false;
escapeX = escapeY = escapeVX = escapeVY = 0;
escapeAngle = 0;
}
function update(dt) {
time += dt;
if (auto) targetWind = Math.min(36, targetWind + .42 * dt);
const ambientGust = 1.25 * Math.sin(time * 1.7) + .65 * Math.sin(time * 3.9 + 1.4);
const gustBoost = gust * (1.7 + 2.1 * Math.sin(Math.min(1, gustClock / 1.6) * Math.PI));
gust = Math.max(0, gust - dt * 1.35);
gustClock += dt;
const desired = Math.max(0, targetWind + ambientGust + gustBoost);
wind += (desired - wind) * Math.min(1, dt * 1.7);
wind = Math.max(0, wind);
const normalizedLoad = Math.pow(wind / 23, 2) * (1 + Math.max(0, ambientGust + gustBoost) * .025);
const load = Math.min(2.4, normalizedLoad);
frameStress += ((load * (inverted ? .82 : 1)) - frameStress) * Math.min(1, dt * 2.5);
if (!inverted && !escaped) {
if (load > .78) invertedTimer += dt * (load > 1.16 ? 2.4 : 1);
else invertedTimer = Math.max(0, invertedTimer - dt * .8);
if (invertedTimer > 1.05 || load > 1.78) {
inverted = true;
invertedTimer = 0;
}
}
inversion += ((inverted ? 1 : 0) - inversion) * Math.min(1, dt * 3.8);
if (!escaped) {
const targetAngle = -Math.PI / 2 + .07 + Math.min(.82, wind / 34 * .78) + (inverted ? .14 : 0);
const spring = 8.0;
angularVelocity += ((targetAngle - angle) * spring - angularVelocity * 3.3) * dt;
angularVelocity += Math.sin(time * 5.4) * load * .012;
angle += angularVelocity * dt;
if (wind > 15) grip -= dt * (wind - 15) * (inverted ? .0053 : .0025);
else grip += dt * .045;
grip = Math.max(0, Math.min(1, grip));
const breakThreshold = inverted ? 23.5 : 32;
if ((wind > breakThreshold && grip < .28) || grip <= .035) {
escaped = true;
const hand = handPoint();
const crown = canopyCenter(hand, angle);
escapeX = crown.x;
escapeY = crown.y;
escapeVX = 80 + wind * 5.3;
escapeVY = -110 - wind * 1.6;
escapeAngle = angle;
}
} else {
escapeVX += 22 * dt;
escapeVY += 200 * dt;
escapeX += escapeVX * dt;
escapeY += escapeVY * dt;
escapeAngle += dt * (1.2 + wind * .018);
if (escapeY > ground + 100 || escapeX > W + 180) {
// The runaway umbrella is gone; the person is left bracing in the rain.
escapeX = Math.min(escapeX, W + 180);
}
}
updateRain(dt);
updateReadout();
}
function updateRain(dt) {
const horizontal = 45 + wind * 8.2;
const vertical = 260 + wind * 3.6;
for (const p of particles) {
p.x += horizontal * dt;
p.y += vertical * dt;
if (p.x > W + 30 || p.y > H + 25) {
p.x = rand() * (W * .65) - 40;
p.y = -10 - rand() * 170;
}
}
}
function updateReadout() {
const value = wind.toFixed(1);
windReadout.innerHTML = value + ' <span>m/s</span>';
sliderValue.textContent = Number(slider.value).toFixed(1);
gripFill.style.width = (grip * 100).toFixed(0) + "%";
gripFill.style.background = grip > .55 ? "#a3e0c7" : grip > .25 ? "#ffcb75" : "#ff8e82";
if (escaped) {
statusEl.textContent = "Umbrella away";
statusEl.className = "status bad";
} else if (inverted) {
statusEl.textContent = "Turned inside out";
statusEl.className = "status bad";
} else if (frameStress > .6 || grip < .55) {
statusEl.textContent = "Straining";
statusEl.className = "status warn";
} else {
statusEl.textContent = "Holding on";
statusEl.className = "status";
}
}
function handPoint() {
const lean = Math.min(21, wind * .55);
return { x: 395 - lean * .33, y: 320 + Math.min(8, wind * .16) };
}
function canopyCenter(hand, a) {
return { x: hand.x + Math.cos(a) * 167, y: hand.y + Math.sin(a) * 167 };
}
function pathRoundRect(c, x, y, w, h, r) {
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function draw() {
ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
drawSky();
drawRain(true);
drawLandscape();
drawPerson();
if (escaped) drawUmbrella(escapeX, escapeY, escapeAngle, false);
else drawHeldUmbrella();
drawRain(false);
drawWindLines();
}
function drawSky() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#607f91");
g.addColorStop(.54, "#a8c1c0");
g.addColorStop(1, "#d3d4c7");
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// A pale, clouded break in the storm light.
const glow = ctx.createRadialGradient(670, 145, 10, 670, 145, 340);
glow.addColorStop(0, "rgba(255,241,202,.40)");
glow.addColorStop(1, "rgba(255,241,202,0)");
ctx.fillStyle = glow;
ctx.fillRect(250, 0, 650, 420);
drawCloud(160 + Math.sin(time * .12) * 8, 105, .9, .40);
drawCloud(620 + Math.sin(time * .1) * 11, 74, 1.15, .34);
drawCloud(820, 190, .72, .25);
}
function drawCloud(x, y, s, alpha) {
ctx.save();
ctx.translate(x, y);
ctx.scale(s, s);
ctx.fillStyle = `rgba(48,72,83,${alpha})`;
ctx.beginPath();
ctx.ellipse(0, 14, 74, 20, 0, 0, Math.PI * 2);
ctx.ellipse(-40, 8, 34, 23, -.2, 0, Math.PI * 2);
ctx.ellipse(-7, -5, 40, 32, 0, 0, Math.PI * 2);
ctx.ellipse(34, 4, 44, 27, .12, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawLandscape() {
// Distant low hills and a quiet, simplified town.
ctx.fillStyle = "#849e9a";
ctx.beginPath();
ctx.moveTo(0, 348);
ctx.quadraticCurveTo(150, 305, 295, 352);
ctx.quadraticCurveTo(470, 300, 610, 349);
ctx.quadraticCurveTo(770, 300, 900, 347);
ctx.lineTo(900, ground);
ctx.lineTo(0, ground);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgba(59,83,86,.34)";
const buildings = [
[30, 333, 40, 82], [77, 348, 34, 67], [128, 322, 48, 93],
[189, 345, 32, 70], [690, 337, 44, 81], [743, 319, 55, 99],
[812, 346, 35, 72], [860, 328, 48, 90]
];
for (const [x, y, w, h] of buildings) {
ctx.fillRect(x, y, w, h);
ctx.fillStyle = "rgba(226,222,184,.35)";
for (let wy = y + 11; wy < y + h - 7; wy += 17)
for (let wx = x + 8; wx < x + w - 5; wx += 15)
ctx.fillRect(wx, wy, 5, 7);
ctx.fillStyle = "rgba(59,83,86,.34)";
}
ctx.fillStyle = "#405e59";
ctx.beginPath();
ctx.moveTo(0, 406);
ctx.quadraticCurveTo(180, 378, 350, 411);
ctx.quadraticCurveTo(600, 379, 900, 408);
ctx.lineTo(900, ground);
ctx.lineTo(0, ground);
ctx.closePath();
ctx.fill();
// Wind-bent roadside grass.
for (let i = 0; i < 40; i++) {
const x = (i * 79 + 19) % W;
const y = 463 + (i % 4) * 3;
const lean = Math.min(14, 3 + wind * .25);
ctx.strokeStyle = i % 2 ? "rgba(196,211,169,.42)" : "rgba(37,72,63,.6)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + lean * .4, y - 10, x + lean, y - 18 - (i % 3) * 4);
ctx.stroke();
}
const floor = ctx.createLinearGradient(0, ground, 0, H);
floor.addColorStop(0, "#455d56");
floor.addColorStop(1, "#263c3a");
ctx.fillStyle = floor;
ctx.fillRect(0, ground, W, H - ground);
ctx.strokeStyle = "rgba(222,228,199,.28)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, ground);
ctx.lineTo(W, ground);
ctx.stroke();
}
function drawRain(back) {
ctx.save();
ctx.lineCap = "round";
for (const p of particles) {
// Front layer is sparse; most rain sits behind the figure and umbrella.
const isFront = p.phase > 7.6;
if (isFront !== !back) continue;
const dx = (45 + wind * 8.2) * .045;
const dy = p.length;
ctx.strokeStyle = `rgba(228,244,241,${p.alpha * (back ? .74 : .9)})`;
ctx.lineWidth = back ? 1.15 : 1.45;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x + dx, p.y + dy);
ctx.stroke();
}
ctx.restore();
}
function drawWindLines() {
const count = Math.floor(2 + wind / 8);
ctx.save();
for (let i = 0; i < count; i++) {
const phase = (time * (90 + wind * 4) + i * 137) % (W + 160);
const x = phase - 80;
const y = 170 + ((i * 83 + 42) % 235);
const len = 15 + wind * 1.2;
ctx.strokeStyle = `rgba(237,249,239,${Math.min(.25, .07 + wind * .004)})`;
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + len * .45, y - 3, x + len, y);
ctx.stroke();
}
ctx.restore();
}
function drawPerson() {
const lean = Math.min(18, wind * .48);
const hipX = 343 - lean * .35;
const hipY = 385;
const hand = handPoint();
const windBrace = Math.min(1, wind / 25);
// Contact shadow and firmly planted boots.
ctx.fillStyle = "rgba(18,35,34,.28)";
ctx.beginPath();
ctx.ellipse(345, ground + 1, 52, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Back leg, trousers, and boot.
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = "#263d48";
ctx.lineWidth = 17;
ctx.beginPath();
ctx.moveTo(hipX - 3, hipY - 1);
ctx.lineTo(hipX - 21, 433);
ctx.lineTo(hipX - 27, 466);
ctx.stroke();
ctx.fillStyle = "#202e34";
pathRoundRect(ctx, hipX - 47, 463, 34, 12, 5);
ctx.fill();
// Front leg braced forward.
ctx.strokeStyle = "#334b55";
ctx.lineWidth = 18;
ctx.beginPath();
ctx.moveTo(hipX + 5, hipY);
ctx.lineTo(hipX + 18, 428);
ctx.lineTo(hipX + 30, 466);
ctx.stroke();
ctx.fillStyle = "#202e34";
pathRoundRect(ctx, hipX + 15, 463, 39, 12, 5);
ctx.fill();
// Jacket torso, angled into the wind.
ctx.fillStyle = "#b95f48";
ctx.beginPath();
ctx.moveTo(hipX - 17, 383);
ctx.lineTo(hipX - 24 - lean * .25, 326);
ctx.quadraticCurveTo(hipX - 19 - lean * .35, 292, hipX + 2 - lean, 283);
ctx.lineTo(hipX + 30 - lean, 292);
ctx.quadraticCurveTo(hipX + 44 - lean, 330, hipX + 35, 382);
ctx.quadraticCurveTo(hipX + 8, 395, hipX - 17, 383);
ctx.fill();
ctx.strokeStyle = "rgba(255,211,169,.34)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(hipX + 3 - lean, 292);
ctx.lineTo(hipX + 8, 376);
ctx.stroke();
// Neck and head, turned slightly toward the weather.
ctx.fillStyle = "#d9a982";
ctx.fillRect(hipX + 3 - lean, 270, 17, 22);
ctx.fillStyle = "#e4b892";
ctx.beginPath();
ctx.ellipse(hipX + 8 - lean, 251, 22, 27, -.12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#344650";
ctx.beginPath();
ctx.moveTo(hipX - 15 - lean, 247);
ctx.quadraticCurveTo(hipX - 10 - lean, 217, hipX + 15 - lean, 223);
ctx.quadraticCurveTo(hipX + 32 - lean, 230, hipX + 31 - lean, 247);
ctx.lineTo(hipX + 17 - lean, 240);
ctx.lineTo(hipX - 7 - lean, 244);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#283b43";
ctx.beginPath();
ctx.ellipse(hipX + 8 - lean, 224, 22, 8, -.12, Math.PI, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#293e47";
ctx.beginPath();
ctx.arc(hipX + 16 - lean, 251, 2, 0, Math.PI * 2);
ctx.fill();
// Free arm held close to the body against the gust.
ctx.strokeStyle = "#d9a982";
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(hipX - 13, 310);
ctx.lineTo(hipX - 31 - windBrace * 5, 342);
ctx.lineTo(hipX - 18, 365);
ctx.stroke();
// Holding arm reaches for the shaft.
ctx.strokeStyle = "#b95f48";
ctx.lineWidth = 16;
ctx.beginPath();
ctx.moveTo(hipX + 26 - lean, 303);
ctx.quadraticCurveTo(hipX + 43, 310, hand.x - 11, hand.y + 7);
ctx.stroke();
ctx.strokeStyle = "#d9a982";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(hand.x - 17, hand.y + 9);
ctx.lineTo(hand.x - 2, hand.y - 1);
ctx.stroke();
// Rain jacket collar.
ctx.fillStyle = "#d78361";
ctx.beginPath();
ctx.moveTo(hipX - 3 - lean, 291);
ctx.lineTo(hipX + 13 - lean, 306);
ctx.lineTo(hipX + 24 - lean, 289);
ctx.closePath();
ctx.fill();
}
function drawHeldUmbrella() {
const hand = handPoint();
const center = canopyCenter(hand, angle);
drawUmbrella(center.x, center.y, angle, true, hand);
}
function drawUmbrella(cx, cy, a, held, handOverride) {
const r = 112;
const ux = Math.cos(a), uy = Math.sin(a);
const px = -uy, py = ux;
const flex = Math.min(20, frameStress * 10 + Math.max(0, wind - 12) * .35);
const inv = held ? inversion : (inverted ? 1 : 0);
function point(u, extra = 0) {
const t = Math.abs(u) / r;
const natural = 38 * Math.pow(t, 1.5);
const normalV = natural + (u / r) * flex * .45;
const invertedV = -natural * .88 + (u / r) * flex * .35;
const v = normalV * (1 - inv) + invertedV * inv + extra;
const windPush = Math.max(0, wind - 8) * .24 * t * t;
return {
x: cx + px * u - ux * v + windPush,
y: cy + py * u - uy * v
};
}
// Shaft and ferrule.
const gripPoint = handOverride || { x: cx - ux * 167, y: cy - uy * 167 };
ctx.save();
ctx.lineCap = "round";
ctx.strokeStyle = "#3b4141";
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(gripPoint.x, gripPoint.y);
ctx.lineTo(cx, cy);
ctx.stroke();
ctx.strokeStyle = "#d7c49a";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(gripPoint.x + 2, gripPoint.y);
ctx.lineTo(cx + ux * 7, cy + uy * 7);
ctx.stroke();
// Canopy outline, sampled so the load-dependent curve is visible.
const pts = [];
for (let i = 0; i <= 24; i++) pts.push(point(-r + (2 * r * i / 24)));
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.lineTo(cx, cy);
ctx.closePath();
ctx.fillStyle = inv > .5 ? "#d28a54" : "#274c61";
ctx.fill();
// Alternating fabric panels, clipped to the bowed canopy.
ctx.save();
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.lineTo(cx, cy);
ctx.closePath();
ctx.clip();
for (let i = 0; i < 8; i++) {
const u1 = -r + 2 * r * i / 8;
const u2 = -r + 2 * r * (i + 1) / 8;
const p1 = point(u1), p2 = point(u2);
ctx.fillStyle = i % 2 ? (inv > .5 ? "rgba(245,190,121,.66)" : "rgba(92,142,151,.55)")
: (inv > .5 ? "rgba(164,79,61,.42)" : "rgba(12,40,56,.38)");
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.closePath();
ctx.fill();
}
ctx.restore();
// Ribs and their flexing tips.
ctx.lineWidth = 1.6;
for (let i = 0; i <= 8; i++) {
const u = -r + 2 * r * i / 8;
const p = point(u);
ctx.strokeStyle = i % 2 ? "rgba(215,224,204,.66)" : "rgba(36,55,59,.75)";
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.quadraticCurveTo((cx + p.x) / 2 + (u / r) * flex * .22,
(cy + p.y) / 2 - 3, p.x, p.y);
ctx.stroke();
ctx.fillStyle = "#ead2a6";
ctx.beginPath();
ctx.arc(p.x, p.y, 2.3, 0, Math.PI * 2);
ctx.fill();
}
ctx.strokeStyle = inv > .5 ? "#f0bf7f" : "#adc8c1";
ctx.lineWidth = 2;
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();
ctx.fillStyle = "#e9d5a8";
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fill();
if (held && !escaped) {
ctx.strokeStyle = "#443d37";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(gripPoint.x - 4, gripPoint.y - 3);
ctx.lineTo(gripPoint.x + 5, gripPoint.y + 5);
ctx.stroke();
}
ctx.restore();
}
function frame(now) {
if (!lastTime) lastTime = now;
const dt = Math.min(.04, (now - lastTime) / 1000);
lastTime = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
slider.addEventListener("input", () => {
auto = false;
targetWind = Number(slider.value);
autoButton.textContent = "Auto: off";
autoButton.classList.remove("primary");
});
autoButton.addEventListener("click", () => {
auto = !auto;
if (auto) {
targetWind = wind;
autoButton.textContent = "Auto: rising";
autoButton.classList.add("primary");
} else {
targetWind = wind;
autoButton.textContent = "Auto: off";
autoButton.classList.remove("primary");
}
});
document.getElementById("gustButton").addEventListener("click", () => {
gust = Math.min(4, gust + 2.5);
gustClock = 0;
});
document.getElementById("resetButton").addEventListener("click", resetUmbrella);
window.addEventListener("keydown", event => {
if (event.target instanceof HTMLInputElement) return;
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
event.preventDefault();
auto = false;
targetWind = Math.max(0, Math.min(40, targetWind + (event.key === "ArrowRight" ? 1 : -1)));
slider.value = targetWind;
autoButton.textContent = "Auto: off";
autoButton.classList.remove("primary");
} else if (event.code === "Space") {
event.preventDefault();
gust = Math.min(4, gust + 2.5);
gustClock = 0;
} else if (event.key.toLowerCase() === "r") {
resetUmbrella();
}
});
window.addEventListener("resize", resize);
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The umbrella flexes and tilts as the wind picks up, the rain slants faster, and the person leans and braces while their grip gives way. A sustained hard load can flip the canopy inside out; enough strain after that can pull the umbrella free. The auto setting raises the wind gradually, and the slider, arrow keys, and gust button let you intervene.
The weak spots are deliberate simplifications: this is a side-on illustration, not a measured human or umbrella model. The canopy is a handful of stylized panels rather than cloth with real airflow, and the inversion and escape thresholds are tuned for readable drama—not engineering accuracy. The person’s feet stay planted, and the umbrella never catches on anything.