16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 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 hand-drawn, segmented membrane supported by flexible ribs and a bending shaft; aerodynamic load is approximated from the square of wind speed, while accumulated overload flattens and skews the canopy, twists the shaft, and exhausts the person’s grip. Inversion is a discrete, irreversible state change for the current umbrella—followed by a short animated transition from a dome to a wind-facing cup—and occurs when sustained effective wind above the frame’s threshold fills an overload accumulator; after inversion, continued load drains grip much faster until the umbrella detaches and becomes a simple tumbling projectile.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Umbrella in a Rising Gale</title>
<style>
:root {
color-scheme: dark;
--glass: rgba(8, 18, 27, .74);
--line: rgba(255,255,255,.16);
--text: #eef7fa;
--muted: #a9bdc7;
--accent: #73d3ee;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #101b24;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
.panel {
position: fixed;
z-index: 2;
left: max(14px, env(safe-area-inset-left));
top: max(14px, env(safe-area-inset-top));
width: min(390px, calc(100vw - 28px));
padding: 14px 15px 13px;
border: 1px solid var(--line);
border-radius: 15px;
color: var(--text);
background: var(--glass);
box-shadow: 0 12px 38px rgba(0,0,0,.3);
backdrop-filter: blur(10px);
user-select: none;
}
.topline {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.speed {
font-size: clamp(28px, 5vw, 42px);
font-weight: 760;
line-height: 1;
letter-spacing: -.04em;
}
.speed small {
font-size: .4em;
margin-left: 4px;
color: var(--muted);
letter-spacing: 0;
}
.condition {
text-align: right;
font-size: 13px;
font-weight: 700;
color: #d7f6ff;
}
.condition span {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
}
.row {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-top: 13px;
}
label {
font-size: 12px;
color: var(--muted);
}
output {
min-width: 43px;
text-align: right;
font-variant-numeric: tabular-nums;
font-size: 12px;
color: #d9f7ff;
}
input[type="range"] {
width: 100%;
height: 24px;
margin: 0;
accent-color: var(--accent);
cursor: ew-resize;
}
.meterline {
display: grid;
grid-template-columns: 45px 1fr 44px;
align-items: center;
gap: 10px;
margin-top: 7px;
font-size: 11px;
color: var(--muted);
}
.meter {
height: 6px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,.11);
}
.meter > i {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
background: #89e3b2;
transition: width .12s linear, background .2s;
}
.buttons {
display: flex;
gap: 8px;
margin-top: 12px;
}
button {
appearance: none;
border: 1px solid rgba(255,255,255,.16);
border-radius: 9px;
padding: 7px 10px;
color: var(--text);
background: rgba(255,255,255,.08);
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
button:hover { background: rgba(255,255,255,.14); }
button:active { transform: translateY(1px); }
.hint {
flex: 1;
align-self: center;
text-align: right;
font-size: 10px;
color: #8ea4ae;
}
.badge {
position: fixed;
z-index: 2;
right: max(14px, env(safe-area-inset-right));
bottom: max(12px, env(safe-area-inset-bottom));
padding: 7px 10px;
border: 1px solid rgba(255,255,255,.12);
border-radius: 999px;
color: rgba(235,248,252,.7);
background: rgba(8,18,27,.5);
font-size: 10px;
letter-spacing: .08em;
text-transform: uppercase;
pointer-events: none;
}
@media (max-height: 520px) {
.panel { transform: scale(.88); transform-origin: top left; }
.badge { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.meter > i { transition: none; }
}
</style>
</head>
<body>
<canvas id="scene" role="img" aria-label="Animated side view of a person bracing an umbrella against increasingly strong wind and rain"></canvas>
<section class="panel" aria-label="Wind controls">
<div class="topline">
<div class="speed"><span id="speed">0.0</span><small>m/s</small></div>
<div class="condition" id="condition">Light wind<span id="umbrellaState">Umbrella sound</span></div>
</div>
<div class="row">
<label for="wind">Wind</label>
<input id="wind" type="range" min="0" max="38" step="0.5" value="6" aria-label="Wind target in metres per second">
<output id="target">6.0</output>
</div>
<div class="meterline">
<span>Grip</span>
<div class="meter"><i id="gripBar"></i></div>
<span id="gripText">firm</span>
</div>
<div class="buttons">
<button id="minus" type="button" aria-label="Reduce wind">− Wind</button>
<button id="plus" type="button" aria-label="Increase wind">+ Wind</button>
<button id="replace" type="button">New umbrella</button>
<span class="hint">← → or A D<br>Wind resumes rising</span>
</div>
</section>
<div class="badge">Wind blows left to right</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const windSlider = document.getElementById("wind");
const speedEl = document.getElementById("speed");
const targetEl = document.getElementById("target");
const conditionEl = document.getElementById("condition");
const umbrellaStateEl = document.getElementById("umbrellaState");
const gripBar = document.getElementById("gripBar");
const gripText = document.getElementById("gripText");
let W = 0, H = 0, dpr = 1;
let time = 0;
let last = performance.now();
let targetWind = 6;
let meanWind = 6;
let effectiveWind = 6;
let manualPause = 0;
let canopyState = "sound";
let inversionDose = 0;
let inversionMix = 0;
let grip = 1;
let attached = true;
let escaped = false;
let shaftAngle = -0.28;
let shaftVelocity = 0;
const loose = {
x: 0, y: 0,
vx: 0, vy: 0,
angle: 0, spin: 0
};
const rain = [];
const grass = [];
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
W = innerWidth;
H = innerHeight;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!rain.length) {
for (let i = 0; i < 260; i++) {
rain.push({
x: Math.random() * W,
y: Math.random() * H,
z: .35 + Math.random() * .95,
phase: Math.random() * 10
});
}
for (let i = 0; i < 85; i++) {
grass.push({
x: Math.random() * W,
h: 4 + Math.random() * 14,
phase: Math.random() * Math.PI * 2
});
}
}
}
function setTarget(v) {
targetWind = clamp(v, 0, 38);
windSlider.value = targetWind;
targetEl.value = targetWind.toFixed(1);
manualPause = 3.2;
}
windSlider.addEventListener("input", () => setTarget(+windSlider.value));
document.getElementById("minus").addEventListener("click", () => setTarget(targetWind - 3));
document.getElementById("plus").addEventListener("click", () => setTarget(targetWind + 3));
function replaceUmbrella() {
canopyState = "sound";
inversionDose = 0;
inversionMix = 0;
grip = 1;
attached = true;
escaped = false;
shaftAngle = -0.28;
shaftVelocity = 0;
}
document.getElementById("replace").addEventListener("click", replaceUmbrella);
addEventListener("keydown", e => {
if (e.key === "ArrowLeft" || e.key.toLowerCase() === "a") {
setTarget(targetWind - 1);
e.preventDefault();
} else if (e.key === "ArrowRight" || e.key.toLowerCase() === "d") {
setTarget(targetWind + 1);
e.preventDefault();
} else if (e.key.toLowerCase() === "r") {
replaceUmbrella();
}
});
function windDescription(v) {
if (v < 3) return "Light air";
if (v < 8) return "Fresh breeze";
if (v < 14) return "Strong wind";
if (v < 21) return "Near gale";
if (v < 28) return "Severe gale";
if (v < 34) return "Storm force";
return "Violent storm";
}
function update(dt) {
time += dt;
manualPause = Math.max(0, manualPause - dt);
if (manualPause <= 0) {
targetWind = Math.min(38, targetWind + .34 * dt);
windSlider.value = targetWind;
targetEl.value = targetWind.toFixed(1);
}
meanWind += (targetWind - meanWind) * (1 - Math.exp(-dt * 1.4));
const gust =
Math.sin(time * .72) * 1.15 +
Math.sin(time * 1.83 + 1.1) * .62 +
Math.sin(time * .19 + 3.4) * 1.4;
const gustScale = .12 + meanWind / 18;
effectiveWind = Math.max(0, meanWind + gust * gustScale);
const load = Math.pow(effectiveWind / 17.5, 2);
const desiredAngle =
-0.30 +
Math.min(1.02, load * .235) +
Math.sin(time * 4.1) * Math.min(.075, load * .025);
shaftVelocity += (desiredAngle - shaftAngle) * 8.5 * dt;
shaftVelocity *= Math.exp(-4.3 * dt);
shaftAngle += shaftVelocity * dt;
if (attached && canopyState === "sound") {
const filling = Math.max(0, (effectiveWind - 18.5) / 8);
inversionDose += filling * .35 * dt;
inversionDose -= Math.max(0, (18 - effectiveWind) / 18) * .18 * dt;
inversionDose = clamp(inversionDose, 0, 1.2);
if (inversionDose >= 1) {
canopyState = "inverted";
shaftVelocity += .72;
}
}
const inversionTarget = canopyState === "inverted" ? 1 : 0;
inversionMix += (inversionTarget - inversionMix) *
(1 - Math.exp(-dt * (inversionTarget ? 8 : 3)));
if (attached) {
let drain = 0;
if (canopyState === "sound") {
drain = Math.max(0, (effectiveWind - 30) / 8) * .07;
} else {
drain = .035 + Math.max(0, (effectiveWind - 20) / 11) * .19;
}
grip -= drain * dt;
if (effectiveWind < 15 && canopyState === "sound") {
grip += .055 * dt;
}
grip = clamp(grip, 0, 1);
if (grip <= 0) {
attached = false;
const pose = personPose();
loose.x = pose.hand.x;
loose.y = pose.hand.y;
loose.angle = shaftAngle;
loose.vx = 80 + effectiveWind * 5.2;
loose.vy = -95 - effectiveWind * 1.8;
loose.spin = 1.5 + effectiveWind * .09;
}
} else if (!escaped) {
const drag = 1 - Math.exp(-dt * 1.2);
loose.vx += ((effectiveWind * 8.2 + 55) - loose.vx) * drag;
loose.vy += 360 * dt;
loose.vy -= effectiveWind * 2.1 * dt;
loose.x += loose.vx * dt;
loose.y += loose.vy * dt;
loose.angle += loose.spin * dt;
loose.spin += Math.sin(time * 2.4) * .025;
const ground = H * .82;
if (loose.y > ground - 22) {
loose.y = ground - 22;
loose.vy *= -.25;
loose.vx *= .93;
loose.spin *= .91;
}
if (loose.x > W + 330 || loose.y < -400) escaped = true;
}
updateRain(dt);
updateUI();
}
function updateRain(dt) {
const vx = effectiveWind * 10.5;
const vy = 430 + effectiveWind * 4;
for (const p of rain) {
p.x += vx * p.z * dt;
p.y += vy * p.z * dt;
if (p.y > H + 30 || p.x > W + 80) {
if (Math.random() < .6) {
p.x = -80 - Math.random() * W * .35;
p.y = Math.random() * H;
} else {
p.x = Math.random() * W;
p.y = -30 - Math.random() * 160;
}
p.z = .35 + Math.random() * .95;
}
}
}
function updateUI() {
speedEl.textContent = effectiveWind.toFixed(1);
conditionEl.firstChild.nodeValue = windDescription(effectiveWind);
let stateText = "Umbrella sound";
if (canopyState === "sound" && inversionDose > .4) stateText = "Frame loading";
if (canopyState === "inverted") stateText = attached ? "Inside out" : "Grip lost";
if (!attached && !escaped) stateText = "Umbrella away";
if (escaped) stateText = "Umbrella gone";
umbrellaStateEl.textContent = stateText;
gripBar.style.width = (grip * 100).toFixed(0) + "%";
gripBar.style.background =
grip > .55 ? "#89e3b2" :
grip > .25 ? "#ffd36e" : "#ff7676";
gripText.textContent =
!attached ? "gone" :
grip > .72 ? "firm" :
grip > .38 ? "taxed" :
grip > .12 ? "slipping" : "failing";
}
function personPose() {
const ground = H * .82;
const brace = clamp(effectiveWind / 31, 0, 1.18);
const hip = {
x: W * .57 - Math.min(25, Math.max(0, W - 850) * .025),
y: ground - 92 + brace * 12
};
const shoulder = {
x: hip.x - 27 - brace * 43,
y: hip.y - 95 + brace * 17
};
const hand = {
x: shoulder.x - 3 + brace * 9,
y: shoulder.y + 34
};
return { ground, brace, hip, shoulder, hand };
}
function mapLocal(origin, angle, x, y) {
const tx = Math.cos(angle), ty = Math.sin(angle);
const ux = Math.sin(angle), uy = -Math.cos(angle);
return {
x: origin.x + tx * x + ux * y,
y: origin.y + ty * x + uy * y
};
}
function canopyLocalY(x, radius, flex, inv) {
const q = clamp(Math.abs(x) / radius, 0, 1);
const normal = (30 + 12 * (1 - flex)) * (1 - Math.pow(q, 1.58));
const inverted = -29 * (1 - Math.pow(q, 1.7)) + q * 10;
const windWarp = flex * 12 * (x / radius);
return lerp(normal + windWarp, inverted + windWarp * .35, inv);
}
function drawUmbrella(handle, angle, detached) {
const scale = clamp(Math.min(W / 1000, H / 650), .72, 1.22);
const length = 185 * scale;
const radius = 116 * scale;
const load = Math.pow(effectiveWind / 18, 2);
const flex = clamp(load * .34 + inversionDose * .3, 0, 1);
const hub = mapLocal(handle, angle, 0, length);
const tx = Math.cos(angle), ty = Math.sin(angle);
const ux = Math.sin(angle), uy = -Math.cos(angle);
ctx.save();
ctx.lineCap = "round";
ctx.lineJoin = "round";
// Curved shaft
const bend = (detached ? 6 : 17 * flex) * scale;
const control = {
x: (handle.x + hub.x) / 2 + tx * bend,
y: (handle.y + hub.y) / 2 + ty * bend
};
ctx.beginPath();
ctx.moveTo(handle.x, handle.y + 10 * scale);
ctx.quadraticCurveTo(control.x, control.y, hub.x, hub.y);
ctx.strokeStyle = "#b9c5c9";
ctx.lineWidth = 4.1 * scale;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(handle.x, handle.y);
ctx.quadraticCurveTo(
handle.x - tx * 19 * scale + ux * 8 * scale,
handle.y - ty * 19 * scale + uy * 8 * scale,
handle.x - tx * 8 * scale + ux * 23 * scale,
handle.y - ty * 8 * scale + uy * 23 * scale
);
ctx.strokeStyle = "#40372f";
ctx.lineWidth = 6 * scale;
ctx.stroke();
// Fabric silhouette
const samples = 34;
const points = [];
for (let i = 0; i <= samples; i++) {
let x = -radius + radius * 2 * i / samples;
const pinch = flex * (1 - Math.abs(x) / radius) * 10 * scale;
x -= pinch;
const y = canopyLocalY(x, radius, flex, inversionMix) * scale;
points.push(mapLocal(hub, angle, x, y));
}
const right = points[points.length - 1];
const left = points[0];
ctx.beginPath();
ctx.moveTo(left.x, left.y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
for (let i = points.length - 1; i >= 0; i--) {
ctx.lineTo(points[i].x - ux * 7 * scale, points[i].y - uy * 7 * scale);
}
ctx.closePath();
const grad = ctx.createLinearGradient(left.x, left.y, right.x, right.y);
grad.addColorStop(0, "#0d7894");
grad.addColorStop(.46, "#22b2c9");
grad.addColorStop(.53, "#e3a14a");
grad.addColorStop(1, "#b84e48");
ctx.fillStyle = grad;
ctx.globalAlpha = detached ? .92 : .97;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = "rgba(4,25,32,.8)";
ctx.lineWidth = 2.1 * scale;
ctx.stroke();
// Seams and ribs
const ribs = [-1, -.67, -.34, 0, .34, .67, 1];
for (let i = 0; i < ribs.length; i++) {
const x = ribs[i] * radius;
const y = canopyLocalY(x, radius, flex, inversionMix) * scale;
const p = mapLocal(hub, angle, x, y);
ctx.beginPath();
ctx.moveTo(hub.x, hub.y);
const bow = inversionMix * 20 * scale;
ctx.quadraticCurveTo(
lerp(hub.x, p.x, .58) - ux * bow,
lerp(hub.y, p.y, .58) - uy * bow,
p.x, p.y
);
ctx.strokeStyle = i === 3 ? "rgba(230,242,244,.75)" : "rgba(22,42,47,.65)";
ctx.lineWidth = (i === 3 ? 2 : 1.35) * scale;
ctx.stroke();
}
// Hub cap
ctx.beginPath();
ctx.arc(hub.x, hub.y, 4.2 * scale, 0, Math.PI * 2);
ctx.fillStyle = "#d6e0e2";
ctx.fill();
ctx.restore();
return { hub, ux, uy, tx, ty, scale };
}
function drawLimb(a, b, width, color) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.stroke();
}
function elbowFor(shoulder, hand, bendSign) {
const dx = hand.x - shoulder.x;
const dy = hand.y - shoulder.y;
const d = Math.max(1, Math.hypot(dx, dy));
const nx = -dy / d, ny = dx / d;
return {
x: (shoulder.x + hand.x) / 2 + nx * 18 * bendSign,
y: (shoulder.y + hand.y) / 2 + ny * 18 * bendSign
};
}
function drawPerson(pose, umbrellaInfo) {
const { ground, brace, hip, shoulder, hand } = pose;
const s = clamp(Math.min(W / 1000, H / 650), .72, 1.22);
const rearFoot = {
x: hip.x + 68 + brace * 45,
y: ground
};
const frontFoot = {
x: hip.x - 68 - brace * 24,
y: ground
};
const rearKnee = {
x: hip.x + 34 + brace * 22,
y: ground - 53
};
const frontKnee = {
x: hip.x - 30 - brace * 10,
y: ground - 49 + brace * 8
};
// Rear leg
drawLimb(hip, rearKnee, 22 * s, "#233542");
drawLimb(rearKnee, rearFoot, 20 * s, "#1b2a34");
// Rear shoe
ctx.beginPath();
ctx.ellipse(rearFoot.x + 9, rearFoot.y - 3, 31 * s, 8 * s, -.04, 0, Math.PI * 2);
ctx.fillStyle = "#10171c";
ctx.fill();
// Front leg
drawLimb({x: hip.x - 9, y: hip.y}, frontKnee, 24 * s, "#2b4050");
drawLimb(frontKnee, frontFoot, 21 * s, "#20313d");
ctx.beginPath();
ctx.ellipse(frontFoot.x - 7, frontFoot.y - 3, 33 * s, 8 * s, .04, 0, Math.PI * 2);
ctx.fillStyle = "#10171c";
ctx.fill();
// Coat body, leaning into wind
const coatBottomL = { x: hip.x - 30, y: hip.y + 24 };
const coatBottomR = { x: hip.x + 38 + brace * 18, y: hip.y + 18 };
ctx.beginPath();
ctx.moveTo(shoulder.x - 26, shoulder.y + 12);
ctx.quadraticCurveTo(shoulder.x - 42, hip.y - 25, coatBottomL.x, coatBottomL.y);
ctx.lineTo(coatBottomR.x, coatBottomR.y);
ctx.quadraticCurveTo(
hip.x + 73 + brace * 42,
hip.y - 30 + Math.sin(time * 7) * 4,
shoulder.x + 29,
shoulder.y + 11
);
ctx.closePath();
const coatGrad = ctx.createLinearGradient(shoulder.x, shoulder.y, hip.x, hip.y);
coatGrad.addColorStop(0, "#dfb345");
coatGrad.addColorStop(1, "#98712b");
ctx.fillStyle = coatGrad;
ctx.fill();
ctx.strokeStyle = "#533f24";
ctx.lineWidth = 2;
ctx.stroke();
// Scarf streaming downwind
const neck = { x: shoulder.x - 4, y: shoulder.y - 12 };
ctx.beginPath();
ctx.moveTo(neck.x, neck.y);
ctx.bezierCurveTo(
neck.x + 40, neck.y + 7,
neck.x + 72 + brace * 46, neck.y - 10 + Math.sin(time * 5) * 7,
neck.x + 102 + brace * 64, neck.y + 8 + Math.sin(time * 4.3) * 9
);
ctx.strokeStyle = "#bd5052";
ctx.lineWidth = 11 * s;
ctx.lineCap = "round";
ctx.stroke();
// Head
const head = {
x: shoulder.x - 11,
y: shoulder.y - 38
};
ctx.beginPath();
ctx.arc(head.x, head.y, 21 * s, 0, Math.PI * 2);
ctx.fillStyle = "#d49a78";
ctx.fill();
// Hair and nose facing into wind
ctx.beginPath();
ctx.arc(head.x + 4, head.y - 5, 20 * s, Math.PI * .95, Math.PI * 2.04);
ctx.lineTo(head.x + 17, head.y - 4);
ctx.fillStyle = "#312c2b";
ctx.fill();
ctx.beginPath();
ctx.moveTo(head.x - 19 * s, head.y - 2);
ctx.lineTo(head.x - 28 * s, head.y + 3);
ctx.lineTo(head.x - 18 * s, head.y + 6);
ctx.fillStyle = "#d49a78";
ctx.fill();
// Arms and hands
let target1, target2;
if (attached && umbrellaInfo) {
target1 = hand;
target2 = {
x: hand.x + umbrellaInfo.ux * 42 * s,
y: hand.y + umbrellaInfo.uy * 42 * s
};
} else {
target1 = { x: shoulder.x - 26, y: shoulder.y + 42 };
target2 = { x: shoulder.x + 3, y: shoulder.y + 49 };
}
const shoulderBack = { x: shoulder.x + 15, y: shoulder.y + 9 };
const shoulderFront = { x: shoulder.x - 11, y: shoulder.y + 8 };
const elbowBack = elbowFor(shoulderBack, target2, -1);
const elbowFront = elbowFor(shoulderFront, target1, 1);
drawLimb(shoulderBack, elbowBack, 16 * s, "#a97d32");
drawLimb(elbowBack, target2, 14 * s, "#b88837");
drawLimb(shoulderFront, elbowFront, 17 * s, "#d5a43d");
drawLimb(elbowFront, target1, 15 * s, "#c89435");
for (const h of [target2, target1]) {
ctx.beginPath();
ctx.arc(h.x, h.y, 7 * s, 0, Math.PI * 2);
ctx.fillStyle = "#d49a78";
ctx.fill();
}
// Coat buttons
ctx.fillStyle = "#5d4728";
for (let i = 0; i < 3; i++) {
const t = .3 + i * .21;
ctx.beginPath();
ctx.arc(
lerp(shoulder.x, hip.x, t) + 2,
lerp(shoulder.y, hip.y, t),
2.2 * s, 0, Math.PI * 2
);
ctx.fill();
}
}
function drawBackground() {
const ground = H * .82;
const storm = clamp(effectiveWind / 38, 0, 1);
const sky = ctx.createLinearGradient(0, 0, 0, ground);
sky.addColorStop(0, `rgb(${Math.round(31 - storm * 15)},${Math.round(57 - storm * 22)},${Math.round(73 - storm * 22)})`);
sky.addColorStop(.62, `rgb(${Math.round(76 - storm * 28)},${Math.round(101 - storm * 34)},${Math.round(111 - storm * 31)})`);
sky.addColorStop(1, "#708188");
ctx.fillStyle = sky;
ctx.fillRect(0, 0, W, ground);
// Layered storm clouds
ctx.save();
ctx.globalAlpha = .18 + storm * .2;
for (let layer = 0; layer < 4; layer++) {
const y = H * (.12 + layer * .105);
const drift = (time * effectiveWind * (.6 + layer * .15)) % 240;
ctx.beginPath();
ctx.moveTo(-250, y + 35);
for (let x = -250; x <= W + 300; x += 90) {
const yy = y + Math.sin((x + drift) * .014 + layer) * 22;
ctx.quadraticCurveTo(x + 45, yy - 34, x + 90, yy);
}
ctx.lineTo(W + 300, y + 90);
ctx.lineTo(-250, y + 90);
ctx.closePath();
ctx.fillStyle = layer % 2 ? "#172935" : "#c7d4d7";
ctx.fill();
}
ctx.restore();
// Distant horizon
ctx.fillStyle = "rgba(35,58,64,.56)";
ctx.beginPath();
ctx.moveTo(0, ground);
for (let x = 0; x <= W; x += 80) {
ctx.lineTo(x, ground - 18 - Math.sin(x * .019) * 12);
}
ctx.lineTo(W, ground);
ctx.closePath();
ctx.fill();
// Ground
const earth = ctx.createLinearGradient(0, ground, 0, H);
earth.addColorStop(0, "#384a47");
earth.addColorStop(1, "#172521");
ctx.fillStyle = earth;
ctx.fillRect(0, ground, W, H - ground);
// Wet sheen
ctx.fillStyle = "rgba(191,225,226,.12)";
ctx.fillRect(0, ground, W, 3);
// Grass bends downwind
const bend = clamp(effectiveWind * 1.25, 0, 46);
ctx.strokeStyle = "rgba(31,65,51,.9)";
ctx.lineWidth = 1.4;
for (const g of grass) {
const wave = Math.sin(time * 4 + g.phase) * Math.min(5, effectiveWind * .12);
ctx.beginPath();
ctx.moveTo(g.x, ground + 2);
ctx.quadraticCurveTo(
g.x + bend * .35,
ground - g.h * .55,
g.x + bend + wave,
ground - g.h * .45
);
ctx.stroke();
}
// Puddles and wind-rippled reflections
ctx.strokeStyle = "rgba(153,205,211,.18)";
ctx.lineWidth = 2;
for (let i = 0; i < 10; i++) {
const x = (i * 173 + 55) % Math.max(1, W);
const y = ground + 20 + (i % 3) * 34;
const width = 28 + effectiveWind * 1.8;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.stroke();
}
}
function drawRain(backLayer) {
const vx = effectiveWind * 10.5;
const vy = 430 + effectiveWind * 4;
const mag = Math.hypot(vx, vy);
const ux = vx / mag, uy = vy / mag;
ctx.save();
ctx.lineCap = "round";
for (const p of rain) {
const isBack = p.z < .76;
if (isBack !== backLayer) continue;
const len = (9 + effectiveWind * .42) * p.z;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - ux * len, p.y - uy * len);
ctx.strokeStyle = `rgba(191,229,239,${.16 + p.z * .3})`;
ctx.lineWidth = .65 + p.z * .75;
ctx.stroke();
}
ctx.restore();
}
function drawWindMarks() {
if (effectiveWind < 9) return;
ctx.save();
ctx.lineCap = "round";
const count = Math.floor(effectiveWind / 4);
for (let i = 0; i < count; i++) {
const lane = (i * 83 + 37) % Math.max(1, H * .65);
const x = ((time * effectiveWind * (14 + i % 3) + i * 211) % (W + 260)) - 130;
const length = 20 + effectiveWind * 1.8;
ctx.beginPath();
ctx.moveTo(x, lane + 75);
ctx.quadraticCurveTo(x + length * .5, lane + 70, x + length, lane + 75);
ctx.strokeStyle = `rgba(220,241,244,${.03 + effectiveWind / 650})`;
ctx.lineWidth = 1.3;
ctx.stroke();
}
ctx.restore();
}
function render() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawWindMarks();
drawRain(true);
const pose = personPose();
let info = null;
if (attached) {
info = drawUmbrella(pose.hand, shaftAngle, false);
} else if (!escaped) {
drawUmbrella({ x: loose.x, y: loose.y }, loose.angle, true);
}
drawPerson(pose, info);
drawRain(false);
// Foot contact shadows emphasize that the person remains planted.
ctx.save();
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "rgba(8,15,14,.18)";
ctx.beginPath();
ctx.ellipse(pose.hip.x, pose.ground + 3, 145, 13, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function frame(now) {
const dt = Math.min(.033, Math.max(.001, (now - last) / 1000));
last = now;
update(dt);
render();
requestAnimationFrame(frame);
}
resize();
addEventListener("resize", resize);
updateUI();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
It looks best from a moderate breeze through the first severe gusts: the rain slants progressively, the person widens their stance and leans upwind, the coat and scarf stream downwind, the shaft is pulled sideways, and the canopy visibly flattens before snapping into its inverted shape. The overload, inversion, grip failure, and free-flying umbrella are deterministic enough to be understood while still being modulated by gusts.
It is not a real membrane or multibody simulation. The canopy cannot wrinkle, flutter independently between ribs, tear, shed water, partially invert one panel at a time, or pop back by being pointed into the wind. The hands use drawn inverse-kinematics-like elbows but do not have real joints or changing finger placement; the person cannot stumble, rotate their shoulders properly, reposition a foot in discrete steps, or decide to lower the umbrella before losing it. Aerodynamic force is reduced to wind-squared thresholds rather than being calculated from canopy angle, projected area, pressure distribution, and turbulent wake, and a released umbrella gets a deliberately theatrical amount of lift and stable downwind travel.