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 curved, flexible 2D fabric profile supported by drawn ribs around a hub. Wind bends the profile and applies torque to the shaft; inversion is a latched state change that makes the rim spring upward past the hub. It triggers above a wind threshold and springs back only after the wind drops below a lower threshold. Sustained stronger wind builds grip strain until the umbrella is released.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Against the weather</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #101c27;
color: #eef4f3;
}
* { box-sizing: border-box; }
body { margin: 0; overflow: hidden; }
main {
position: relative;
width: 100%;
height: 100vh;
height: 100dvh;
min-height: 380px;
overflow: hidden;
background: #1d303d;
}
canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.top {
position: absolute;
top: clamp(20px, 4vw, 42px);
left: clamp(20px, 4vw, 48px);
right: clamp(20px, 4vw, 48px);
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
pointer-events: none;
}
.eyebrow {
margin: 0 0 7px;
color: #a9c4c6;
font-size: 10px;
font-weight: 750;
letter-spacing: .19em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(25px, 4.5vw, 48px);
line-height: 1;
letter-spacing: -.045em;
font-weight: 650;
}
.subtitle {
margin: 9px 0 0;
color: #b9cbcd;
font-size: 13px;
}
.readout { text-align: right; white-space: nowrap; }
.speed {
font-size: clamp(31px, 5vw, 52px);
line-height: .9;
font-weight: 600;
letter-spacing: -.055em;
font-variant-numeric: tabular-nums;
}
.speed span { font-size: .33em; letter-spacing: 0; color: #b9cbcd; }
.condition {
margin-top: 9px;
color: #f1bc81;
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.panel {
position: absolute;
left: clamp(16px, 3vw, 36px);
right: clamp(16px, 3vw, 36px);
bottom: clamp(16px, 3vw, 30px);
display: grid;
grid-template-columns: minmax(180px, 1fr) auto;
gap: 16px 28px;
align-items: end;
padding: 19px 22px;
border: 1px solid rgba(207, 227, 225, .18);
border-radius: 14px;
background: rgba(14, 26, 35, .87);
box-shadow: 0 12px 40px rgba(0, 0, 0, .16);
backdrop-filter: blur(12px);
}
.slider-heading {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
color: #e7efed;
font-size: 12px;
font-weight: 650;
}
.slider-heading span { color: #9fb6b9; font-weight: 450; }
input[type="range"] {
display: block;
width: 100%;
margin: 0;
accent-color: #eaa978;
cursor: pointer;
}
.range-labels {
display: flex;
justify-content: space-between;
margin-top: 7px;
color: #8da5a9;
font-size: 10px;
letter-spacing: .04em;
}
.buttons { display: flex; gap: 8px; align-items: center; }
button {
min-height: 40px;
padding: 0 15px;
border: 1px solid rgba(210, 229, 226, .25);
border-radius: 7px;
background: rgba(255,255,255,.055);
color: #edf4f1;
font: inherit;
font-size: 12px;
font-weight: 650;
white-space: nowrap;
cursor: pointer;
}
button:hover { background: rgba(255,255,255,.12); }
button:focus-visible, input:focus-visible { outline: 2px solid #ffcb9e; outline-offset: 3px; }
.hint {
grid-column: 1 / -1;
margin: 0;
color: #91a8aa;
font-size: 11px;
line-height: 1.4;
}
@media (max-width: 600px) {
.subtitle { max-width: 190px; }
.panel { grid-template-columns: 1fr; gap: 13px; padding: 16px; }
.buttons { width: 100%; }
.buttons button { flex: 1; }
.hint { grid-column: auto; }
}
@media (max-height: 480px) {
.subtitle, .hint { display: none; }
.panel { gap: 10px; padding: 12px 16px; }
.top { top: 15px; }
}
</style>
</head>
<body>
<main id="scene">
<canvas id="canvas" aria-label="A person bracing against wind and rain with an umbrella"></canvas>
<header class="top">
<div>
<p class="eyebrow">A little weather experiment</p>
<h1>Against the weather</h1>
<p class="subtitle">Hold on. The wind is picking up.</p>
</div>
<div class="readout">
<p class="eyebrow">Wind speed</p>
<div class="speed"><span id="speed">7.0</span> <span>m/s</span></div>
<div class="condition" id="condition">Holding steady</div>
</div>
</header>
<section class="panel" id="panel" aria-label="Weather controls">
<div>
<div class="slider-heading">
<label for="wind">Wind strength</label>
<span id="mode">Rising automatically</span>
</div>
<input id="wind" type="range" min="0" max="40" step="0.1" value="7">
<div class="range-labels"><span>CALM · 0</span><span>GALE · 40 m/s</span></div>
</div>
<div class="buttons">
<button id="auto" type="button" aria-pressed="true">Pause rise</button>
<button id="reset" type="button">Reset scene</button>
</div>
<p class="hint">Move the slider to take control. Arrow keys adjust wind; space toggles the rise; R resets.</p>
</section>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const panel = document.getElementById("panel");
const slider = document.getElementById("wind");
const speedEl = document.getElementById("speed");
const conditionEl = document.getElementById("condition");
const modeEl = document.getElementById("mode");
const autoButton = document.getElementById("auto");
const resetButton = document.getElementById("reset");
let width = 0, height = 0, scale = 1, ground = 0;
let wind = 7, automatic = true, time = 0;
let angle = 0, angularVelocity = 0;
let inverted = false, inversion = 0, inversionVelocity = 0;
let strain = 0, released = false;
let freeX = 0, freeY = 0, freeVX = 0, freeVY = 0;
let freeAngle = 0, freeSpin = 0;
let lastTime = 0;
const rain = Array.from({length: 150}, () => ({
x: Math.random(), y: Math.random(),
length: 8 + Math.random() * 13,
depth: .4 + Math.random() * .6
}));
function resize() {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
scale = Math.min(rect.width / 800, rect.height / 600);
scale = Math.max(.25, scale);
width = rect.width / scale;
height = rect.height / scale;
const panelTop = panel.getBoundingClientRect().top - rect.top;
ground = Math.min(height - 30, (panelTop - 14) / scale);
ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0);
}
function setAuto(value) {
automatic = value;
autoButton.textContent = automatic ? "Pause rise" : "Resume rise";
autoButton.setAttribute("aria-pressed", String(automatic));
modeEl.textContent = automatic ? "Rising automatically" : "Manual control";
}
function reset() {
wind = 7;
time = 0;
angle = 0;
angularVelocity = 0;
inverted = false;
inversion = 0;
inversionVelocity = 0;
strain = 0;
released = false;
freeX = freeY = freeVX = freeVY = freeAngle = freeSpin = 0;
slider.value = wind;
setAuto(true);
}
slider.addEventListener("input", () => {
wind = Number(slider.value);
setAuto(false);
});
autoButton.addEventListener("click", () => setAuto(!automatic));
resetButton.addEventListener("click", reset);
window.addEventListener("resize", resize);
window.addEventListener("keydown", event => {
if (event.target === slider && (event.key === "ArrowLeft" || event.key === "ArrowRight" ||
event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (event.target.tagName === "BUTTON" && (event.key === " " || event.key === "Enter")) return;
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
event.preventDefault();
wind = Math.min(40, wind + 1);
slider.value = wind;
setAuto(false);
} else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
event.preventDefault();
wind = Math.max(0, wind - 1);
slider.value = wind;
setAuto(false);
} else if (event.key === " ") {
event.preventDefault();
setAuto(!automatic);
} else if (event.key.toLowerCase() === "r") {
reset();
}
});
function personPose() {
const x = width * .48;
const load = released ? Math.min(wind / 40, 1) * .35 : Math.min(wind / 40, 1);
const lean = load * 28;
const hip = {x: x - lean * .25, y: ground - 75};
const shoulder = {x: x - lean, y: ground - 139};
const hand = {x: x + 35 - lean * .45, y: ground - 151 + load * 4};
return {x, hip, shoulder, hand, load};
}
function umbrellaHub(pose) {
return {
x: pose.hand.x + Math.sin(angle) * 88,
y: pose.hand.y - Math.cos(angle) * 88
};
}
function update(dt) {
time += dt;
if (automatic) wind = Math.min(40, wind + .34 * dt);
slider.value = wind.toFixed(1);
const gust = Math.max(0, wind + Math.sin(time * 1.25) * wind * .035 +
Math.sin(time * 2.8) * wind * .015);
if (!released) {
if (!inverted && gust > 22.5) inverted = true;
if (inverted && gust < 12) inverted = false;
const targetAngle = Math.min(1.1, (gust * gust / 1600) * 1.02 +
(inverted ? .12 : 0));
angularVelocity += ((targetAngle - angle) * 11 - angularVelocity * 5) * dt;
angle += angularVelocity * dt;
const targetInversion = inverted ? 1 : 0;
inversionVelocity += ((targetInversion - inversion) * 32 - inversionVelocity * 8) * dt;
inversion += inversionVelocity * dt;
inversion = Math.max(-.05, Math.min(1.08, inversion));
strain += Math.max(0, (gust - 30) / 9) * .48 * dt;
if (gust < 29) strain = Math.max(0, strain - .35 * dt);
if (strain >= 1) {
const pose = personPose();
const hub = umbrellaHub(pose);
released = true;
freeX = hub.x;
freeY = hub.y;
freeAngle = angle;
freeVX = 65 + wind * 3.4;
freeVY = -90;
freeSpin = 1.8 + angularVelocity;
}
} else {
freeVX += (wind * 10 - freeVX) * .45 * dt;
freeVY += (110 - freeVY) * .25 * dt;
freeX += freeVX * dt;
freeY += freeVY * dt;
freeSpin += (1.1 - freeSpin) * .35 * dt;
freeAngle += freeSpin * dt;
if (freeY > ground - 25) {
freeY = ground - 25;
freeVY = -Math.abs(freeVY) * .35;
freeSpin *= .8;
}
}
for (const drop of rain) {
drop.x += (wind * 9 + 15) * drop.depth * dt / width;
drop.y += (240 + wind * 2.5) * drop.depth * dt / height;
if (drop.y > 1.04) {
drop.y = -.04;
drop.x = Math.random() * 1.2 - .2;
}
if (drop.x > 1.08) {
drop.x = -.08;
drop.y = Math.random();
}
}
speedEl.textContent = wind.toFixed(1);
conditionEl.textContent = released ? "Umbrella released" :
strain > .45 ? "Grip slipping" :
inverted ? "Inside out" :
wind > 16 ? "Bracing" : "Holding steady";
}
function line(points, color, thickness) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.strokeStyle = color;
ctx.lineWidth = thickness;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
function circle(x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
function drawBackground() {
const sky = ctx.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#203746");
sky.addColorStop(.65, "#405866");
sky.addColorStop(1, "#263c42");
ctx.fillStyle = sky;
ctx.fillRect(0, 0, width, height);
// A soft break in the clouds, behind the figure.
const glow = ctx.createRadialGradient(width * .54, ground - 185, 5,
width * .54, ground - 185, width * .48);
glow.addColorStop(0, "rgba(162,185,180,.18)");
glow.addColorStop(1, "rgba(162,185,180,0)");
ctx.fillStyle = glow;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "rgba(19,37,44,.22)";
const horizon = ground - 65;
ctx.beginPath();
ctx.moveTo(0, ground);
ctx.lineTo(0, horizon + 10);
for (let x = 0; x <= width + 70; x += 70) {
const h = 12 + (Math.sin(x * .021) + 1) * 12;
ctx.lineTo(x, horizon - h);
ctx.lineTo(x + 45, horizon - h);
ctx.lineTo(x + 45, horizon + 5);
}
ctx.lineTo(width, ground);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#263d40";
ctx.fillRect(0, ground, width, height - ground);
ctx.fillStyle = "rgba(181,207,195,.23)";
ctx.fillRect(0, ground, width, 2);
// Ground marks and small tufts.
ctx.strokeStyle = "rgba(164,190,178,.18)";
ctx.lineWidth = 1.5;
for (let i = 0; i < 18; i++) {
const x = ((i * 143 + 30) % (width + 100)) - 30;
const y = ground + 13 + (i % 3) * 14;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + 30 + (i % 4) * 8, y);
ctx.stroke();
}
}
function drawRain(front) {
ctx.save();
ctx.lineCap = "round";
for (let i = 0; i < rain.length; i++) {
const drop = rain[i];
if ((i % 4 === 0) !== front) continue;
const x = drop.x * width;
const y = drop.y * height;
const dy = drop.length * drop.depth;
const dx = dy * (wind * 9 + 15) / (240 + wind * 2.5);
ctx.strokeStyle = front ? "rgba(199,225,224,.29)" : "rgba(192,220,222,.17)";
ctx.lineWidth = front ? 1.3 : .8;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - dx, y - dy);
ctx.stroke();
}
ctx.restore();
}
function drawWind() {
ctx.save();
ctx.strokeStyle = "rgba(208,226,220,.13)";
ctx.lineWidth = 1.3;
for (let i = 0; i < 8; i++) {
const y = ground - 45 - i * 37;
const x = ((time * (12 + wind * 2.2) + i * 161) % (width + 180)) - 100;
const len = 32 + wind * 1.6 + (i % 3) * 13;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(x + len * .55, y - 5, x + len, y - 3);
ctx.stroke();
}
ctx.restore();
}
function drawPerson(p) {
const {x, hip, shoulder, hand, load} = p;
const backFoot = {x: x - 42 - load * 12, y: ground - 4};
const frontFoot = {x: x + 37 + load * 12, y: ground - 4};
const backKnee = {x: x - 21 - load * 5, y: ground - 39};
const frontKnee = {x: x + 25 + load * 6, y: ground - 39};
// Back arm and legs.
line([shoulder, {x: x - 34, y: ground - 111},
{x: hand.x - 8, y: hand.y + 8}], "#263239", 11);
line([hip, backKnee, backFoot], "#182b32", 18);
line([hip, frontKnee, frontFoot], "#21343a", 18);
line([{x: backFoot.x - 10, y: ground - 4},
{x: backFoot.x + 9, y: ground - 4}], "#101e25", 9);
line([{x: frontFoot.x - 8, y: ground - 4},
{x: frontFoot.x + 14, y: ground - 4}], "#101e25", 9);
// Raincoat, with a hem that pulls downwind.
ctx.beginPath();
ctx.moveTo(shoulder.x - 13, shoulder.y - 4);
ctx.quadraticCurveTo(shoulder.x + 8, shoulder.y - 12, shoulder.x + 17, shoulder.y + 5);
ctx.lineTo(hip.x + 19 + load * 10, hip.y + 14);
ctx.quadraticCurveTo(hip.x, hip.y + 18, hip.x - 19, hip.y + 8);
ctx.closePath();
ctx.fillStyle = "#d49a6a";
ctx.fill();
line([{x: shoulder.x + 4, y: shoulder.y + 4},
{x: hip.x + 5, y: hip.y + 8}], "rgba(81,60,49,.38)", 1.5);
// Head and hood.
circle(shoulder.x - 2, shoulder.y - 18, 15, "#c48c69");
ctx.beginPath();
ctx.arc(shoulder.x - 5, shoulder.y - 20, 16, Math.PI * .7, Math.PI * 1.9);
ctx.lineTo(shoulder.x + 4, shoulder.y - 15);
ctx.quadraticCurveTo(shoulder.x - 10, shoulder.y - 24, shoulder.x - 11, shoulder.y - 7);
ctx.fillStyle = "#a66d50";
ctx.fill();
circle(shoulder.x + 9, shoulder.y - 20, 1.2, "#263239");
// Front arm and hand meet the shaft at exactly the same point.
const elbow = {x: shoulder.x + 23, y: shoulder.y + 21};
line([shoulder, elbow, hand], "#bd835e", 12);
circle(hand.x, hand.y, 5, "#e2b08a");
}
function drawUmbrella(hx, hy, theta, inv, flex, shaftStart) {
const ax = Math.sin(theta), ay = -Math.cos(theta);
const tx = Math.cos(theta), ty = Math.sin(theta);
const rim = -12 * (1 - inv) + 52 * inv;
const crown = 34 * (1 - inv) - 5 * inv;
function point(u) {
const along = crown * (1 - u * u) + rim * u * u +
flex * 7 * u * (1 - u * u);
return {
x: hx + tx * u * 88 + ax * along,
y: hy + ty * u * 88 + ay * along
};
}
const bottomX = shaftStart ? shaftStart.x : hx - ax * 88;
const bottomY = shaftStart ? shaftStart.y : hy - ay * 88;
line([{x: bottomX, y: bottomY}, {x: hx + ax * 7, y: hy + ay * 7}],
"#253039", 4);
line([{x: bottomX, y: bottomY}, {x: bottomX - tx * 3, y: bottomY - ty * 3}],
"#d8a76f", 5);
ctx.beginPath();
let p = point(-1);
ctx.moveTo(p.x, p.y);
for (let i = 1; i <= 48; i++) {
p = point(-1 + i / 24);
ctx.lineTo(p.x, p.y);
}
ctx.lineTo(hx, hy);
ctx.closePath();
ctx.fillStyle = "#d88261";
ctx.fill();
// Alternate fabric panels are visible in this deliberately flat side view.
for (let section = 0; section < 4; section++) {
const u0 = -1 + section * .5;
const u1 = u0 + .5;
if (section % 2 === 0) {
ctx.beginPath();
ctx.moveTo(hx, hy);
p = point(u0);
ctx.lineTo(p.x, p.y);
for (let j = 1; j <= 12; j++) {
p = point(u0 + (u1 - u0) * j / 12);
ctx.lineTo(p.x, p.y);
}
ctx.closePath();
ctx.fillStyle = "rgba(255,212,171,.17)";
ctx.fill();
}
}
for (const u of [-1, -.5, 0, .5, 1]) {
p = point(u);
line([{x: hx, y: hy}, p], "rgba(74,53,49,.66)", 1.5);
if (Math.abs(u) === 1) circle(p.x, p.y, 2.2, "#e6bb98");
}
ctx.beginPath();
p = point(-1);
ctx.moveTo(p.x, p.y);
for (let i = 1; i <= 48; i++) {
p = point(-1 + i / 24);
ctx.lineTo(p.x, p.y);
}
ctx.strokeStyle = "#f0b28b";
ctx.lineWidth = 2;
ctx.stroke();
circle(hx, hy, 3, "#303a3b");
}
function render() {
ctx.clearRect(0, 0, width, height);
drawBackground();
drawWind();
drawRain(false);
const pose = personPose();
drawPerson(pose);
if (released) {
drawUmbrella(freeX, freeY, freeAngle, inversion, Math.min(wind / 40, 1), null);
} else {
const hub = umbrellaHub(pose);
drawUmbrella(hub.x, hub.y, angle, inversion, Math.min(wind / 40, 1), pose.hand);
circle(pose.hand.x, pose.hand.y, 4, "#e2b08a");
}
drawRain(true);
}
function frame(timestamp) {
if (!lastTime) lastTime = timestamp;
const elapsed = Math.min((timestamp - lastTime) / 1000, .05);
lastTime = timestamp;
// Small fixed-size steps keep the springs stable after a slow frame.
const steps = Math.max(1, Math.ceil(elapsed / .016));
for (let i = 0; i < steps; i++) update(elapsed / steps);
render();
requestAnimationFrame(frame);
}
resize();
setAuto(true);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The umbrella bends, snaps inside out, and can tear free; lowering the wind before release lets it recover. The person leans and braces without sliding off the ground, and rain follows the wind.
It is a stylized side view, not a realistic fabric or fluid simulation. The ribs don’t buckle independently, the grip threshold is invented, and the released umbrella tumbles under a simple wind-and-gravity approximation rather than flying like a real one.