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 set of curved fabric panels stretched over ribs; each panel’s curve flexes like a damped spring as the wind loads it. Inversion is a change from a downward-rimmed dome to an upward-rimmed bowl. It happens after sufficiently strong wind has loaded the frame for long enough, rather than at one instantaneous speed. A still stronger, sustained pull can break the person’s grip.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hold On — an umbrella in the wind</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
background: #101e2b;
color: #f1ede2;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
canvas { display: block; width: 100vw; height: 100vh; }
.hud {
position: fixed;
top: 22px;
left: 24px;
width: min(355px, calc(100vw - 48px));
padding: 18px 20px 17px;
background: rgba(12, 27, 39, .79);
border: 1px solid rgba(204, 229, 227, .22);
border-radius: 15px;
box-shadow: 0 15px 45px #07121b55;
backdrop-filter: blur(9px);
}
.eyebrow { color: #9ec6c7; font-size: 11px; font-weight: 800; letter-spacing: .17em; }
h1 { margin: 5px 0 7px; font-size: 26px; line-height: 1; letter-spacing: -.04em; }
.reading { display: flex; align-items: baseline; gap: 9px; margin: 10px 0 2px; }
#speed { font-size: 49px; line-height: 1; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -.065em; }
.unit { color: #afd1d0; font-size: 14px; font-weight: 700; }
#condition { min-height: 20px; color: #f4ca88; font-size: 13px; font-weight: 650; }
.control-label { display: flex; justify-content: space-between; margin: 15px 0 8px; color: #c5d8d7; font-size: 11px; font-weight: 750; letter-spacing: .09em; }
input[type=range] { width: 100%; margin: 0; accent-color: #f3b76d; cursor: pointer; }
.buttons { display: flex; gap: 8px; margin-top: 13px; }
button {
padding: 9px 12px;
color: #eef5ef;
background: #314e59;
border: 1px solid #76969b;
border-radius: 8px;
font: 700 11px ui-sans-serif, system-ui, sans-serif;
letter-spacing: .06em;
cursor: pointer;
}
button:hover { background: #456b73; }
button.active { color: #172b32; background: #f3b76d; border-color: #f3b76d; }
.hint {
position: fixed;
right: 22px;
bottom: 18px;
max-width: 310px;
color: #e4f0e9;
background: #102332a8;
padding: 9px 12px;
border-radius: 7px;
font-size: 11px;
line-height: 1.5;
text-align: right;
}
@media (max-width: 600px) {
.hud { top: 12px; left: 12px; width: min(315px, calc(100vw - 24px)); padding: 13px 15px; }
h1 { font-size: 22px; }
#speed { font-size: 39px; }
.hint { right: 10px; bottom: 10px; font-size: 10px; }
}
</style>
</head>
<body>
<canvas id="scene" aria-label="Animated person bracing against wind and rain while holding an umbrella"></canvas>
<div class="hud">
<div class="eyebrow">A SMALL STORM SIMULATOR</div>
<h1>Hold on.</h1>
<div class="reading"><span id="speed">12</span><span class="unit">km/h →</span></div>
<div id="condition" role="status">A manageable breeze</div>
<div class="control-label"><label for="wind">WIND CONTROL</label><span id="mode">AUTO RISING</span></div>
<input id="wind" type="range" min="0" max="110" value="12" aria-label="Wind speed in kilometres per hour">
<div class="buttons">
<button id="auto" class="active" type="button">AUTO RISING</button>
<button id="reset" type="button">NEW UMBRELLA</button>
</div>
</div>
<div class="hint">Drag the slider or use ← → to take control. Space resumes the rising wind; R starts over.</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const slider = document.getElementById("wind");
const speedLabel = document.getElementById("speed");
const condition = document.getElementById("condition");
const modeLabel = document.getElementById("mode");
const autoButton = document.getElementById("auto");
const resetButton = document.getElementById("reset");
let W = 0, H = 0, dpr = 1;
let rain = [], time = 0, last = 0;
let auto = true, autoAge = 0, autoStart = 12, target = 12, wind = 12;
let flex = 0, flexVelocity = 0, angle = 0.08, angleVelocity = 0;
let inverted = false, detached = false, invertClock = 0, recoverClock = 0, releaseClock = 0;
let flyer = { x: 0, y: 0, vx: 0, vy: 0, angle: 0, spin: 0 };
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = innerWidth; H = innerHeight;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
rain = Array.from({ length: Math.min(310, Math.round(W * H / 2900)) }, () => ({
x: Math.random() * W, y: Math.random() * H,
depth: .45 + Math.random() * 1.3,
length: 8 + Math.random() * 16
}));
}
addEventListener("resize", resize);
resize();
function setAuto(on) {
auto = on;
if (on) { autoAge = 0; autoStart = clamp(target, 0, 110); }
autoButton.classList.toggle("active", on);
modeLabel.textContent = on ? "AUTO RISING" : "YOU'RE IN CONTROL";
}
slider.addEventListener("input", () => {
target = Number(slider.value);
setAuto(false);
});
autoButton.addEventListener("click", () => setAuto(!auto));
function reset() {
time = 0; autoAge = 0; autoStart = 12; target = 12; wind = 12;
flex = flexVelocity = invertClock = recoverClock = releaseClock = 0;
angle = .08; angleVelocity = 0;
inverted = detached = false;
slider.value = "12";
setAuto(true);
}
resetButton.addEventListener("click", reset);
addEventListener("keydown", e => {
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
e.preventDefault();
target = clamp((auto ? wind : target) + (e.key === "ArrowRight" ? 5 : -5), 0, 110);
slider.value = String(Math.round(target));
setAuto(false);
} else if (e.code === "Space" && e.target !== slider && e.target.tagName !== "BUTTON") {
e.preventDefault();
setAuto(!auto);
} else if (e.key.toLowerCase() === "r" && e.target !== slider) reset();
});
function geometry() {
const scale = clamp(Math.min(W / 900, H / 610), .57, 1.24);
const ground = H * .81;
const hipX = W * (W < 600 ? .42 : .43);
const lean = clamp(wind / 95, 0, 1) * 37 * scale;
const shoulderX = hipX - lean;
const shoulderY = ground - 131 * scale;
const gripX = shoulderX + 45 * scale + lean * .18;
const gripY = shoulderY + 4 * scale;
return { scale, ground, hipX, lean, shoulderX, shoulderY, gripX, gripY };
}
function update(dt) {
time += dt;
if (auto) {
autoAge += dt;
target = Math.min(110, autoStart + autoAge * 1.05 + autoAge * autoAge * .010);
slider.value = String(Math.round(target));
}
const gust = Math.max(0, wind / 110) *
(4 * Math.sin(time * 2.1) + 2.7 * Math.sin(time * 4.7 + 1.1) + 2 * Math.sin(time * .67));
wind += (clamp(target + gust, 0, 115) - wind) * Math.min(1, dt * 2.7);
// Two damped degrees of freedom: canopy deformation and shaft rotation.
const load = clamp(wind / 95, 0, 1.25);
flexVelocity += ((load - flex) * 17 - flexVelocity * 7.5) * dt;
flex += flexVelocity * dt;
const wantedAngle = clamp(.045 + wind * .0064 + gust * .002, -.05, .79);
angleVelocity += ((wantedAngle - angle) * 13 - angleVelocity * 6) * dt;
angle += angleVelocity * dt;
if (!detached) {
invertClock = wind > 54 && flex > .43 ? invertClock + dt : Math.max(0, invertClock - dt * 2);
if (!inverted && invertClock > .55) {
inverted = true;
angleVelocity += 1.25;
}
recoverClock = inverted && wind < 24 ? recoverClock + dt : 0;
if (recoverClock > 1.2) {
inverted = false; invertClock = recoverClock = 0;
angleVelocity -= .5;
}
releaseClock = inverted && wind > 88 ? releaseClock + dt : Math.max(0, releaseClock - dt * 2);
if (releaseClock > .9) {
const g = geometry();
detached = true;
flyer = {
x: g.gripX, y: g.gripY,
vx: wind * 1.7, vy: -85 - wind * .4,
angle, spin: 2.8
};
}
} else {
// A crude airborne umbrella: drag pushes it sideways; lift briefly fights gravity.
flyer.vx += ((wind * 2.45 - flyer.vx) * .9) * dt;
flyer.vy += (170 - wind * .65) * dt;
flyer.x += flyer.vx * dt;
flyer.y += flyer.vy * dt;
flyer.angle += flyer.spin * dt;
flyer.spin *= Math.exp(-.23 * dt);
}
for (const drop of rain) {
drop.x += (wind * 2.3 + 18) * drop.depth * dt;
drop.y += (430 + wind * 1.1) * drop.depth * dt;
if (drop.y > H + 35 || drop.x > W + 90) {
drop.x = drop.x > W + 90 ? -90 - Math.random() * 80 : Math.random() * W;
drop.y = -30 - Math.random() * 100;
}
}
speedLabel.textContent = String(Math.round(wind));
condition.textContent = detached ? "The umbrella got away!" :
inverted ? "Inside out — don't let go!" :
wind > 43 ? "The frame is straining" :
wind > 24 ? "Leaning into it" : "A manageable breeze";
}
function strokePath(points, color, width) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
ctx.strokeStyle = color; ctx.lineWidth = width;
ctx.lineCap = "round"; ctx.lineJoin = "round";
ctx.stroke();
}
function background(g) {
const sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, "#132838");
sky.addColorStop(.55, "#42606b");
sky.addColorStop(1, "#809395");
ctx.fillStyle = sky; ctx.fillRect(0, 0, W, H);
// Slowly drifting, layered cloud bands.
for (let layer = 0; layer < 5; layer++) {
const y = H * (.13 + layer * .105);
const drift = (time * (5 + layer * 3)) % 280;
ctx.fillStyle = layer % 2 ? "#253e4a58" : "#0d22305c";
for (let x = -360 + drift; x < W + 360; x += 275) {
ctx.beginPath();
ctx.ellipse(x, y, 175, 23 + layer * 7, -.035, 0, Math.PI * 2);
ctx.ellipse(x + 92, y - 12, 96, 24 + layer * 3, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Distant roofs and windows.
const horizon = g.ground - 67 * g.scale;
ctx.fillStyle = "#2b4650";
for (let x = -45; x < W + 90; x += 93) {
const height = (30 + 13 * Math.sin(x * .081)) * g.scale;
ctx.fillRect(x, horizon - height, 83, g.ground - horizon + height);
ctx.beginPath();
ctx.moveTo(x - 7, horizon - height);
ctx.lineTo(x + 41, horizon - height - 17 * g.scale);
ctx.lineTo(x + 90, horizon - height);
ctx.fill();
}
ctx.fillStyle = "#bed0bd42";
for (let x = 22; x < W; x += 93) {
ctx.fillRect(x, horizon - 16 * g.scale, 4, 6);
ctx.fillRect(x + 25, horizon - 16 * g.scale, 4, 6);
}
const groundGrad = ctx.createLinearGradient(0, g.ground, 0, H);
groundGrad.addColorStop(0, "#29434b");
groundGrad.addColorStop(1, "#102833");
ctx.fillStyle = groundGrad;
ctx.fillRect(0, g.ground, W, H - g.ground);
ctx.fillStyle = "#b5ccbb5c";
ctx.fillRect(0, g.ground, W, 2);
ctx.strokeStyle = "#a6cac04d";
ctx.lineWidth = 1.5;
for (let i = 0; i < 26; i++) {
const x = ((i * 157 + 31) % (W + 80)) - 20;
const y = g.ground + 10 + ((i * 47) % Math.max(20, H - g.ground - 12));
ctx.beginPath();
ctx.ellipse(x, y, 13 + (i % 4) * 9, 2.5, 0, 0, Math.PI * 2);
ctx.stroke();
}
}
function drawRain(front) {
ctx.lineCap = "round";
for (let i = front ? 0 : 1; i < rain.length; i += 2) {
const d = rain[i];
const length = d.length * d.depth * (front ? 1.3 : 1);
const slant = (wind * 2.3 + 18) / (430 + wind * 1.1);
ctx.strokeStyle = front ? "rgba(213,241,240,.42)" : "rgba(204,234,234,.25)";
ctx.lineWidth = front ? 1.35 : .85;
ctx.beginPath();
ctx.moveTo(d.x, d.y);
ctx.lineTo(d.x - length * slant, d.y - length);
ctx.stroke();
}
}
function drawWind(g) {
if (wind < 17) return;
ctx.lineCap = "round";
for (let i = 0; i < 21; i++) {
const x = ((i * 137 + time * (65 + wind * 1.5)) % (W + 240)) - 120;
const y = H * (.20 + ((i * 37) % 67) / 100);
const length = 12 + wind * .48 + (i % 5) * 9;
ctx.strokeStyle = `rgba(218,241,233,${.045 + wind / 1600})`;
ctx.lineWidth = 1 + i % 3 * .3;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + length * .5, y - 5, x + length, y - 2);
ctx.stroke();
}
}
function person(g) {
const s = g.scale, x = g.hipX, y = g.ground;
const sx = g.shoulderX, sy = g.shoulderY;
const brace = clamp(wind / 95, 0, 1);
const rearFoot = x - (23 + brace * 24) * s;
const frontFoot = x + (30 + brace * 12) * s;
ctx.fillStyle = "#0b1d2690";
ctx.beginPath();
ctx.ellipse(x + 12 * s, y + 5 * s, 78 * s, 8 * s, 0, 0, Math.PI * 2);
ctx.fill();
// Planted, widening stance.
strokePath([[x - 8*s,y-76*s],[x-17*s,y-40*s],[rearFoot,y-7*s]], "#142b38", 17*s);
strokePath([[x+9*s,y-75*s],[x+18*s,y-42*s],[frontFoot,y-7*s]], "#193341", 18*s);
strokePath([[rearFoot-12*s,y-5*s],[rearFoot+10*s,y-5*s]], "#0c1d28", 11*s);
strokePath([[frontFoot-10*s,y-5*s],[frontFoot+15*s,y-5*s]], "#0c1d28", 11*s);
// Coat, with its loose hem pulled downwind.
ctx.fillStyle = "#db9d65";
ctx.strokeStyle = "#263541";
ctx.lineWidth = 3*s;
ctx.beginPath();
ctx.moveTo(sx-15*s, sy+4*s);
ctx.lineTo(sx+15*s, sy-3*s);
ctx.lineTo(x+28*s, y-70*s);
ctx.lineTo(x+30*s+brace*29*s, y-(56+brace*14)*s);
ctx.lineTo(x+8*s, y-67*s);
ctx.lineTo(x-20*s, y-65*s);
ctx.closePath();
ctx.fill(); ctx.stroke();
strokePath([[sx+4*s,sy+9*s],[x+5*s,y-69*s]], "#f1c08b", 2*s);
// Head and wind-tugged hood.
ctx.fillStyle = "#243946";
ctx.beginPath();
ctx.arc(sx+1*s, sy-23*s, 19*s, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#e6bc95";
ctx.beginPath();
ctx.arc(sx+4*s, sy-24*s, 13*s, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#20313b";
ctx.beginPath();
ctx.arc(sx-1*s, sy-29*s, 14*s, Math.PI, Math.PI*1.96);
ctx.lineTo(sx-15*s,sy-22*s);
ctx.fill();
ctx.fillRect(sx+12*s,sy-27*s,2*s,2*s);
// Scarf tails track wind independently of the rigid body.
strokePath([[sx-5*s,sy-6*s],[sx+19*s,sy+2*s],
[sx+(37+brace*29)*s,sy+(4+Math.sin(time*6)*4)*s]], "#e6c475", 8*s);
strokePath([[sx-7*s,sy-3*s],[sx+13*s,sy+6*s],
[sx+(27+brace*21)*s,sy+(16+Math.sin(time*5+1)*5)*s]], "#bb794f", 5*s);
// Far arm reaches toward the handle.
strokePath([[sx+12*s,sy+14*s],[sx+26*s,sy+24*s],
[g.gripX-5*s,g.gripY+5*s]], "#a87050", 11*s);
strokePath([[sx+12*s,sy+10*s],[sx+30*s,sy+18*s],
[g.gripX-5*s,g.gripY+5*s]], "#c68b5e", 8*s);
}
function hand(g) {
const s = g.scale;
ctx.fillStyle = "#d8a97d";
ctx.strokeStyle = "#35404a";
ctx.lineWidth = 2*s;
ctx.beginPath();
ctx.ellipse(g.gripX, g.gripY, 9*s, 7*s, -.4, 0, Math.PI*2);
ctx.fill(); ctx.stroke();
strokePath([[g.gripX-3*s,g.gripY-4*s],[g.gripX+4*s,g.gripY-2*s]],
"#f0c39a", 2*s);
}
function umbrella(x, y, rotation, s) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.scale(s, s);
// Shaft and hook are rigid; the canopy and shaft rotation are not.
strokePath([[0,21],[0,-111]], "#172a35", 6);
strokePath([[0,21],[0,-111]], "#c9d4cb", 2.4);
ctx.beginPath();
ctx.moveTo(0,19);
ctx.bezierCurveTo(0,34,-6,40,-16,35);
ctx.strokeStyle = "#192f39"; ctx.lineWidth = 6; ctx.stroke();
ctx.translate(0,-108);
const span = 104 * (1 + flex * .10);
function top(u) {
const a = Math.abs(u);
const flutter = 2.3 * Math.sin(time*8 + u*12) * a * clamp(wind/85,0,1);
return inverted ? 10 - (53 + flex*14)*Math.pow(a,1.75) + flutter
: -17 + (53 - flex*26)*Math.pow(a,1.75) + flutter;
}
function bottom(u) {
const scallop = 4 * Math.sin(Math.PI * (u+1)*4) ** 2;
return top(u) + (inverted ? 15 : 23) * (1-Math.abs(u)) + 6 + scallop;
}
// Eight fabric panels, individually shaded, make the rib structure visible.
for (let panel = 0; panel < 8; panel++) {
const a = -1 + panel/4, b = a + .25;
ctx.beginPath();
for (let j = 0; j <= 8; j++) {
const u = a + (b-a)*j/8;
if (!j) ctx.moveTo(u*span,top(u)); else ctx.lineTo(u*span,top(u));
}
for (let j = 8; j >= 0; j--) {
const u = a + (b-a)*j/8;
ctx.lineTo(u*span,bottom(u));
}
ctx.closePath();
ctx.fillStyle = panel % 2 ? "#dcad74" : "#eac38a";
ctx.fill();
ctx.strokeStyle = "#304b51";
ctx.lineWidth = 1.2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(a*span,top(a)+3);
ctx.lineTo(0,0);
ctx.lineTo(b*span,top(b)+3);
ctx.strokeStyle = "#344a50a8";
ctx.lineWidth = 1.4;
ctx.stroke();
}
// The exposed wire ends become especially conspicuous inside out.
for (let i = 0; i <= 8; i++) {
const u = -1 + i/4;
strokePath([[0,0],[u*span,top(u)+3]], inverted ? "#35444c" : "#526066", 1.5);
if (i === 0 || i === 8 || inverted) {
ctx.fillStyle = "#233740";
ctx.beginPath();
ctx.arc(u*span, top(u)+2, 2.9, 0, Math.PI*2);
ctx.fill();
}
}
ctx.fillStyle = "#243b42";
ctx.beginPath(); ctx.arc(0,-1,4.5,0,Math.PI*2); ctx.fill();
strokePath([[0,top(0)],[0,top(0)-11]], "#263b44", 3);
ctx.restore();
}
function draw() {
const g = geometry();
background(g);
drawWind(g);
drawRain(false);
person(g);
if (detached) umbrella(flyer.x, flyer.y, flyer.angle, g.scale);
else umbrella(g.gripX, g.gripY, angle, g.scale);
if (!detached) hand(g);
drawRain(true);
// Wind-driven spray at the feet.
ctx.fillStyle = "#c9e9df83";
for (let i = 0; i < 15; i++) {
const phase = (time * (75 + wind*2) + i*91) % 300;
const x = g.hipX - 95*g.scale + phase;
const y = g.ground - 2 - ((i*29 + Math.floor(time*5)*3) % 12);
ctx.fillRect(x, y, 2 + wind/75, 1.2);
}
}
function frame(ms) {
if (!last) last = ms;
const dt = Math.min((ms-last)/1000, .04);
last = ms;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The growing lean, widening stance, bending canopy, sudden inversion, and eventual loss of the umbrella read well in side view. The slider can also bring the wind back down; a held umbrella rights itself once the wind has stayed low long enough.
What it gets wrong: real umbrellas twist in three dimensions, ribs fail unevenly, and a person changes their grip and steps around. Here the canopy is a springy cross-section, the feet remain planted, and the escaped umbrella follows deliberately simple airborne motion.