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 17-node elastic cross-section: each node has a rib-like spring toward its rest profile, neighbour springs keep the fabric continuous, and the crown is pinned to the shaft while relative wind loads each node locally. Inversion is a discrete state change, not merely a large bend—the rest profile switches from a dome to a reversed bowl, and the fabric and ribs settle into it over roughly half a second. Dynamic pressure, canopy angle, gusts, accumulated frame fatigue, and excessive rim lift decide when it trips; grip load is tracked separately, so inversion, sideways wrenching, slipping, and complete release do not all happen on the same threshold.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Hold On — An Umbrella in a Rising Wind</title>
<style>
:root {
color-scheme: dark;
--ink: #edf3ee;
--muted: #a9bbb9;
--line: rgba(218, 237, 231, .15);
--glass: rgba(8, 22, 29, .78);
--cyan: #9bd8d2;
--orange: #ff946d;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #0b1b24;
}
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
color: var(--ink);
user-select: none;
-webkit-user-select: none;
}
canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.interface {
position: fixed;
inset: 0;
pointer-events: none;
padding: clamp(18px, 3vw, 42px);
}
header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
}
.eyebrow {
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 9px;
color: var(--cyan);
font: 700 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .2em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 22px;
height: 1px;
background: currentColor;
}
h1 {
margin: 0;
font-size: clamp(28px, 4vw, 54px);
font-weight: 650;
line-height: .95;
letter-spacing: -.055em;
}
.subtitle {
margin: 12px 0 0;
color: var(--muted);
font-size: 12px;
letter-spacing: .02em;
}
.instrument {
min-width: 184px;
text-align: right;
}
.speed-line {
display: flex;
align-items: baseline;
justify-content: flex-end;
gap: 8px;
}
#windSpeed {
font: 500 clamp(40px, 5vw, 68px)/.9 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: -.08em;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 24px rgba(0, 0, 0, .25);
}
.unit {
color: var(--muted);
font: 600 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
}
#windDescription {
margin-top: 11px;
color: var(--cyan);
font: 700 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .14em;
text-transform: uppercase;
}
.wind-track {
width: 184px;
height: 2px;
margin-top: 12px;
margin-left: auto;
overflow: hidden;
background: rgba(255, 255, 255, .13);
}
#windFill {
display: block;
width: 10%;
height: 100%;
background: var(--cyan);
transition: background-color .2s;
}
.console {
position: absolute;
left: 50%;
bottom: max(18px, env(safe-area-inset-bottom));
width: min(680px, calc(100% - 32px));
padding: 16px 18px 14px;
transform: translateX(-50%);
pointer-events: auto;
border: 1px solid var(--line);
border-radius: 15px;
background: var(--glass);
box-shadow: 0 18px 55px rgba(0, 0, 0, .25);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.console-top, .slider-head, .button-row, .range-labels {
display: flex;
align-items: center;
justify-content: space-between;
}
.console-top {
gap: 18px;
padding-bottom: 13px;
margin-bottom: 13px;
border-bottom: 1px solid rgba(255, 255, 255, .09);
}
.state {
display: flex;
align-items: center;
min-width: 0;
gap: 9px;
color: #f3e8d9;
font: 700 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .11em;
text-transform: uppercase;
}
#stateDot {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-radius: 50%;
background: #9bd8d2;
box-shadow: 0 0 12px rgba(155, 216, 210, .55);
}
#stateText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.frame-meter {
flex: 0 0 150px;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 9px;
color: var(--muted);
font: 600 9px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .1em;
}
.load-track {
height: 3px;
overflow: hidden;
background: rgba(255, 255, 255, .12);
}
#loadFill {
display: block;
height: 100%;
width: 0;
background: var(--orange);
}
.slider-head {
margin-bottom: 8px;
color: var(--muted);
font: 650 9px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .14em;
text-transform: uppercase;
}
#modeLabel { color: var(--cyan); }
input[type="range"] {
--p: 11%;
width: 100%;
height: 20px;
margin: 0;
padding: 0;
appearance: none;
-webkit-appearance: none;
background: transparent;
cursor: ew-resize;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
border-radius: 4px;
background: linear-gradient(to right,
var(--cyan) 0 var(--p),
rgba(255, 255, 255, .15) var(--p) 100%);
}
input[type="range"]::-moz-range-track {
height: 4px;
border-radius: 4px;
background: linear-gradient(to right,
var(--cyan) 0 var(--p),
rgba(255, 255, 255, .15) var(--p) 100%);
}
input[type="range"]::-webkit-slider-thumb {
width: 17px;
height: 17px;
margin-top: -6.5px;
appearance: none;
-webkit-appearance: none;
border: 3px solid #102b34;
border-radius: 50%;
background: #e9f5ef;
box-shadow: 0 0 0 1px rgba(233, 245, 239, .35);
}
input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border: 3px solid #102b34;
border-radius: 50%;
background: #e9f5ef;
}
input[type="range"]:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 5px;
border-radius: 4px;
}
.range-labels {
margin-top: 1px;
color: rgba(210, 227, 222, .48);
font: 500 8px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
}
.button-row {
justify-content: flex-start;
gap: 8px;
margin-top: 12px;
}
button {
appearance: none;
padding: 8px 11px;
border: 1px solid rgba(224, 240, 234, .16);
border-radius: 7px;
background: rgba(255, 255, 255, .045);
color: #dfeae5;
font: 650 9px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
button:hover { background: rgba(255, 255, 255, .1); }
button:active { transform: translateY(1px); }
button:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
button[aria-pressed="true"] {
border-color: rgba(155, 216, 210, .35);
color: var(--cyan);
background: rgba(155, 216, 210, .08);
}
kbd {
margin-left: 5px;
color: rgba(223, 234, 229, .5);
font: inherit;
}
.hint {
margin-left: auto;
color: rgba(210, 227, 222, .5);
font: 500 9px/1.3 ui-monospace, SFMono-Regular, Consolas, monospace;
}
@media (max-width: 620px) {
.interface { padding: 18px; }
.subtitle { display: none; }
.instrument { min-width: 130px; }
.wind-track { width: 130px; }
.console { padding: 13px 14px 12px; }
.frame-meter { flex-basis: 105px; }
.frame-meter span { display: none; }
.frame-meter { grid-template-columns: 1fr; }
.hint { display: none; }
.button-row { margin-top: 9px; }
button { padding: 8px 9px; }
}
@media (max-height: 590px) {
.subtitle { display: none; }
.console-top { padding-bottom: 8px; margin-bottom: 8px; }
.console { padding-top: 11px; padding-bottom: 10px; }
.button-row { margin-top: 7px; }
}
</style>
</head>
<body>
<canvas id="scene" role="img" aria-label="A person in a rainstorm braces against a rising wind while holding a flexible umbrella.">
A person holding an umbrella in wind and rain.
</canvas>
<div class="interface">
<header>
<div>
<div class="eyebrow">Field study № 04</div>
<h1>Hold on.</h1>
<p class="subtitle">One umbrella. Two hands. A deteriorating situation.</p>
</div>
<div class="instrument" aria-label="Current wind speed">
<div class="speed-line">
<output id="windSpeed">5.0</output>
<span class="unit">m/s</span>
</div>
<div id="windDescription">Breeze · 18 km/h</div>
<div class="wind-track"><i id="windFill"></i></div>
</div>
</header>
<section class="console" aria-label="Storm controls">
<div class="console-top">
<div class="state" role="status">
<i id="stateDot"></i>
<span id="stateText">Two hands · canopy holding</span>
</div>
<div class="frame-meter">
<span>FRAME LOAD</span>
<div class="load-track"><i id="loadFill"></i></div>
</div>
</div>
<div class="slider-head">
<label for="windSlider">Driving wind</label>
<span id="modeLabel">Auto · rising</span>
</div>
<input id="windSlider" type="range" min="0" max="45" step="0.1" value="5"
aria-label="Driving wind in metres per second">
<div class="range-labels"><span>0</span><span>15</span><span>30</span><span>45 m/s</span></div>
<div class="button-row">
<button id="autoButton" type="button" aria-pressed="true">Auto rise <kbd>A</kbd></button>
<button id="gustButton" type="button">Gust <kbd>G</kbd></button>
<button id="resetButton" type="button">Reset <kbd>R</kbd></button>
<span class="hint">↑ / ↓ drive the wind</span>
</div>
</section>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const slider = document.getElementById("windSlider");
const windSpeedEl = document.getElementById("windSpeed");
const windDescriptionEl = document.getElementById("windDescription");
const windFill = document.getElementById("windFill");
const stateText = document.getElementById("stateText");
const stateDot = document.getElementById("stateDot");
const loadFill = document.getElementById("loadFill");
const modeLabel = document.getElementById("modeLabel");
const autoButton = document.getElementById("autoButton");
const gustButton = document.getElementById("gustButton");
const resetButton = document.getElementById("resetButton");
const TAU = Math.PI * 2;
const NODE_COUNT = 17;
const FIXED_DT = 1 / 120;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const lerp = (a, b, t) => a + (b - a) * t;
let W = 0, H = 0, DPR = 1, PPM = 100;
let groundY = 0, personX = 0;
let drops = [], clouds = [], splashes = [];
let baseWind = 5;
let actualWind = 5;
let autoWind = true;
let gustPulse = 0;
let simTime = 0;
let effort = 0;
let flash = 0;
let uiClock = 0;
let lastState = "";
let sim;
function makeState() {
const nodes = [];
for (let i = 0; i < NODE_COUNT; i++) {
const q = -1 + 2 * i / (NODE_COUNT - 1);
nodes.push({
q,
x: q * .66,
y: -.24 * Math.pow(Math.abs(q), 1.75),
vx: 0,
vy: 0
});
}
return {
attached: true,
canopy: "normal",
nodes,
theta: -.04,
omega: 0,
invertBlend: 0,
inversionCharge: 0,
recovery: 0,
fatigue: 0,
gripHealth: 1,
gripCap: 220,
slip: 0,
load: 0,
drag: 0,
wrench: 0,
hub: { x: .06, y: 2.14 },
hubVel: { x: 0, y: 0 },
freePos: { x: .06, y: 2.14 },
freeVel: { x: 0, y: 0 },
releaseAge: 0
};
}
function resetAll() {
sim = makeState();
baseWind = 5;
actualWind = 5;
gustPulse = 0;
simTime = 0;
effort = 0;
autoWind = true;
splashes.length = 0;
lastState = "";
updateUI();
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
PPM = clamp(Math.min(W / (W < 600 ? 4.4 : 8.4), H / 5.4), 58, 145);
groundY = H * (W < 600 ? .75 : .82);
personX = W * (W < 600 ? .47 : .51);
seedAtmosphere();
}
function seedAtmosphere() {
const count = W < 600 ? 175 : 310;
drops = Array.from({ length: count }, () => ({
x: Math.random() * W,
y: Math.random() * H,
depth: .35 + Math.random() * .65,
speed: 4.8 + Math.random() * 3.8,
front: Math.random() < .28,
phase: Math.random() * TAU
}));
clouds = Array.from({ length: 9 }, (_, i) => ({
x: Math.random() * (W + 500) - 250,
y: H * (.04 + Math.random() * .35),
w: 160 + Math.random() * 280,
h: 28 + Math.random() * 55,
depth: .3 + Math.random() * .7,
seed: i * 1.73
}));
}
function getGrip() {
const shake = sim.attached
? Math.sin(simTime * 39) * .009 * sim.slip
: 0;
return {
x: .10 - .09 * effort + shake,
y: 1.20 - .025 * effort + shake * .35
};
}
function computeAero(wind, theta) {
const pressure = .6125 * wind * wind;
const area = sim.canopy === "inverted" ? .46 : .70;
const angle = Math.abs(Math.sin(theta + .18));
const exposure = .42 + .58 * Math.min(1, angle * 1.45);
const drag = pressure * area * exposure;
const lift = pressure * area *
(sim.canopy === "inverted" ? .12 : .26) *
(.55 + .65 * angle);
return {
drag,
lift,
load: Math.hypot(drag, lift * .7)
};
}
function invertCanopy() {
sim.canopy = "inverted";
sim.inversionCharge = 0;
sim.recovery = 0;
sim.fatigue = clamp(sim.fatigue + .08, 0, .5);
sim.omega += 1 + Math.min(1.2, actualWind * .025);
sim.wrench = 1.5;
for (const n of sim.nodes) {
n.vy += Math.abs(n.q) * 1.05;
}
}
function releaseUmbrella() {
if (!sim.attached) return;
sim.attached = false;
sim.freePos = { x: sim.hub.x, y: sim.hub.y };
sim.freeVel = {
x: 1.6 + actualWind * .075 + sim.hubVel.x * .15,
y: 2.1 + Math.max(0, sim.hubVel.y * .12)
};
sim.omega += 1.2;
sim.releaseAge = 0;
}
function updateInversion(dt) {
const attack = Math.abs(Math.sin(sim.theta + .28));
let index = actualWind * actualWind * (.62 + .9 * attack) / 640;
index += sim.fatigue * .22;
if (sim.canopy === "normal") {
if (index > .88) {
sim.inversionCharge += dt * (index - .82) * 1.45;
} else {
sim.inversionCharge -= dt * .35;
}
sim.inversionCharge = clamp(sim.inversionCharge, 0, 1.2);
sim.fatigue = clamp(
sim.fatigue + Math.max(0, index - .5) * dt * .012,
0, .5
);
const rimHeight = (sim.nodes[0].y + sim.nodes[NODE_COUNT - 1].y) * .5;
if (sim.inversionCharge > .72 || (index > 1.05 && rimHeight > -.055)) {
invertCanopy();
}
} else {
sim.fatigue = Math.max(0, sim.fatigue - dt * .0015);
if (sim.attached && actualWind < 7) {
sim.recovery += dt;
if (sim.recovery > 2.4) {
sim.canopy = "normal";
sim.inversionCharge = 0;
sim.recovery = 0;
for (const n of sim.nodes) n.vy -= Math.abs(n.q) * .35;
}
} else {
sim.recovery = 0;
}
}
}
const accX = new Array(NODE_COUNT).fill(0);
const accY = new Array(NODE_COUNT).fill(0);
const restX = new Array(NODE_COUNT).fill(0);
const restY = new Array(NODE_COUNT).fill(0);
function updateNodes(dt) {
sim.invertBlend += ((sim.canopy === "inverted" ? 1 : 0) - sim.invertBlend) *
(1 - Math.exp(-dt * 7));
const c = Math.cos(sim.theta);
const s = Math.sin(sim.theta);
const relX = actualWind - sim.hubVel.x;
const relY = -sim.hubVel.y;
const localWindX = relX * c - relY * s;
const localWindY = relX * s + relY * c;
const speed = Math.hypot(localWindX, localWindY);
const pressure = Math.min(1600, speed * speed);
const blend = sim.invertBlend;
for (let i = 0; i < NODE_COUNT; i++) {
const n = sim.nodes[i];
const a = Math.abs(n.q);
const normalX = n.q * .66;
const normalY = -.24 * Math.pow(a, 1.75);
const invertedX = n.q * .55;
const invertedY = .40 * Math.pow(a, 1.25);
const flutter = Math.sin(simTime * 11 + n.q * 4.2) *
.014 * a * Math.min(1, actualWind / 20) * blend;
restX[i] = lerp(normalX, invertedX, blend);
restY[i] = lerp(normalY, invertedY, blend) + flutter;
const edge = .15 + .85 * Math.pow(a, .7);
const spring = 72 * (1 - .22 * sim.fatigue);
let fx = (restX[i] - n.x) * spring - n.vx * 8.5;
let fy = (restY[i] - n.y) * spring - n.vy * 8.5;
fx += .0045 * localWindX * Math.abs(localWindX) * edge;
fy += (1 - blend) * .012 * pressure * edge *
(.45 + .55 * Math.abs(Math.sin(sim.theta + .15)));
fy -= blend * .0025 * pressure * edge;
fy += .002 * localWindY * Math.abs(localWindY) * (1 - a * .4);
fy += Math.sin(simTime * (7 + actualWind * .08) + i * 2.1) *
pressure * .0007 * edge;
accX[i] = fx;
accY[i] = fy;
}
for (let i = 0; i < NODE_COUNT - 1; i++) {
const a = sim.nodes[i];
const b = sim.nodes[i + 1];
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.max(.0001, Math.hypot(dx, dy));
const restLength = Math.hypot(restX[i + 1] - restX[i], restY[i + 1] - restY[i]);
const force = (distance - restLength) * 32;
const fx = dx / distance * force;
const fy = dy / distance * force;
accX[i] += fx;
accY[i] += fy;
accX[i + 1] -= fx;
accY[i + 1] -= fy;
}
for (let i = 0; i < NODE_COUNT; i++) {
const n = sim.nodes[i];
if (i === (NODE_COUNT - 1) / 2) {
n.x = 0;
n.y = 0;
n.vx = 0;
n.vy = 0;
continue;
}
n.vx += accX[i] * dt;
n.vy += accY[i] * dt;
n.x += n.vx * dt;
n.y += n.vy * dt;
n.x = clamp(n.x, -1.05, 1.05);
n.y = clamp(n.y, -.65, .75);
}
}
function updateAttached(dt) {
const oldHub = { x: sim.hub.x, y: sim.hub.y };
const aero = computeAero(actualWind, sim.theta);
sim.drag = aero.drag;
sim.load = aero.load;
const desired = -.04 - .24 * effort;
const turbulence =
Math.sin(simTime * 5.3) * Math.min(.35, actualWind * actualWind * .00035) +
Math.sin(simTime * 9.7 + 1.4) * Math.min(.15, actualWind * .004);
const torque = aero.drag * .021 -
6.8 * (sim.theta - desired) -
3.25 * sim.omega + turbulence;
sim.omega = clamp(sim.omega + torque * dt, -6, 6);
sim.theta += sim.omega * dt;
if (sim.theta > 1.58) {
sim.theta = 1.58;
sim.omega *= -.2;
} else if (sim.theta < -.75) {
sim.theta = -.75;
sim.omega *= -.25;
}
const grip = getGrip();
const dir = { x: Math.sin(sim.theta), y: Math.cos(sim.theta) };
sim.hub = {
x: grip.x + dir.x * .94,
y: grip.y + dir.y * .94
};
sim.hubVel = {
x: (sim.hub.x - oldHub.x) / dt,
y: (sim.hub.y - oldHub.y) / dt
};
if (Math.abs(sim.omega) > 1.25 && actualWind > 14) sim.wrench = .3;
updateInversion(dt);
updateNodes(dt);
sim.gripCap = 210 + 25 * effort;
const ratio = sim.load / sim.gripCap;
if (ratio > 1) {
sim.gripHealth -= dt * (.06 + (ratio - 1) * .55);
} else {
sim.gripHealth += dt * .045;
}
sim.gripHealth = clamp(sim.gripHealth, 0, 1);
sim.slip = clamp(1 - sim.gripHealth + Math.max(0, ratio - 1) * .3, 0, 1);
if (sim.gripHealth <= 0 ||
(sim.theta > 1.51 && sim.load > sim.gripCap * .95)) {
releaseUmbrella();
}
}
function updateFree(dt) {
sim.releaseAge += dt;
const relativeWind = actualWind - sim.freeVel.x;
const aero = computeAero(Math.abs(relativeWind), sim.theta);
sim.drag = aero.drag;
sim.load = aero.load;
const ax = clamp(relativeWind * 1.1, -25, 25);
const lift = Math.min(13,
.010 * actualWind * actualWind *
(sim.canopy === "inverted" ? .68 : 1));
const ay = -9.81 + lift - sim.freeVel.y * .28;
sim.freeVel.x += ax * dt;
sim.freeVel.y += ay * dt;
sim.freePos.x += sim.freeVel.x * dt;
sim.freePos.y += sim.freeVel.y * dt;
sim.omega += (Math.sin(simTime * 3.1) * .8 - sim.omega * .25) * dt;
sim.theta += sim.omega * dt;
if (sim.freePos.y < .22) {
sim.freePos.y = .22;
if (sim.freeVel.y < 0) sim.freeVel.y *= -.28;
sim.freeVel.x *= .985;
sim.omega *= .96;
}
sim.hubVel = { x: sim.freeVel.x, y: sim.freeVel.y };
sim.hub = { x: sim.freePos.x, y: sim.freePos.y };
updateInversion(dt);
updateNodes(dt);
}
function resetDrop(p, fromSide) {
if (fromSide && actualWind > 8) {
p.x = -25;
p.y = Math.random() * groundY;
} else {
p.x = Math.random() * (W + 160) - 80;
p.y = -20 - Math.random() * 130;
}
p.depth = .35 + Math.random() * .65;
p.speed = 4.8 + Math.random() * 3.8;
}
function updateWeather(dt) {
for (const c of clouds) {
c.x += actualWind * (1 + c.depth * 2.1) * dt;
if (c.x - c.w > W + 100) c.x = -c.w * 1.4;
}
for (const p of drops) {
const vx = actualWind * PPM * .48 * p.depth;
const vy = p.speed * PPM * (.72 + .28 * p.depth);
p.x += (vx + Math.sin(simTime * 2 + p.phase) * 5) * dt;
p.y += vy * dt;
if (p.y > groundY + 5) {
if (p.x > 0 && p.x < W && Math.random() < .12 && splashes.length < 45) {
splashes.push({ x: p.x, age: 0, size: .6 + p.depth });
}
resetDrop(p, false);
} else if (p.x > W + 40 || p.x < -60) {
resetDrop(p, true);
}
}
for (let i = splashes.length - 1; i >= 0; i--) {
splashes[i].age += dt;
if (splashes[i].age > .65) splashes.splice(i, 1);
}
}
function step(dt) {
simTime += dt;
sim.wrench = Math.max(0, sim.wrench - dt);
if (autoWind && baseWind < 42) {
baseWind = Math.min(42, baseWind + dt * (.34 + baseWind * .004));
}
gustPulse *= Math.exp(-dt * .72);
const waves = (Math.sin(simTime * .71) +
.45 * Math.sin(simTime * 1.93 + 1.2)) * baseWind * .035;
actualWind = Math.max(0, baseWind + gustPulse + waves);
const targetEffort = clamp(actualWind / 32, 0, 1);
effort += (targetEffort - effort) * (1 - Math.exp(-dt * 3));
if (sim.attached) updateAttached(dt);
else updateFree(dt);
updateWeather(dt);
if (actualWind > 31 && Math.random() < dt * .018) flash = .8;
flash *= Math.exp(-dt * 7);
uiClock -= dt;
if (uiClock <= 0) {
uiClock = .08;
updateUI();
}
}
function colorMix(a, b, t) {
const c = a.map((v, i) => Math.round(lerp(v, b[i], t)));
return `rgb(${c[0]},${c[1]},${c[2]})`;
}
function drawSky() {
const storm = clamp(actualWind / 40, 0, 1);
const gradient = ctx.createLinearGradient(0, 0, 0, H);
gradient.addColorStop(0, colorMix([27, 53, 64], [6, 16, 27], storm));
gradient.addColorStop(.58, colorMix([66, 91, 96], [29, 48, 57], storm));
gradient.addColorStop(1, colorMix([116, 128, 121], [61, 76, 78], storm));
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
const glow = ctx.createRadialGradient(
W * .25, H * .28, 0, W * .25, H * .28, Math.max(W, H) * .65
);
glow.addColorStop(0, "rgba(181,205,195,.10)");
glow.addColorStop(1, "rgba(10,25,32,0)");
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
for (const c of clouds) {
ctx.save();
ctx.globalAlpha = .07 + c.depth * .075;
ctx.fillStyle = storm > .6 ? "#a2b4b6" : "#d1d9cf";
ctx.beginPath();
ctx.ellipse(c.x, c.y, c.w * .55, c.h, 0, 0, TAU);
ctx.ellipse(c.x - c.w * .27, c.y + c.h * .12, c.w * .32, c.h * .72, 0, 0, TAU);
ctx.ellipse(c.x + c.w * .3, c.y + c.h * .18, c.w * .36, c.h * .62, 0, 0, TAU);
ctx.fill();
ctx.restore();
}
ctx.fillStyle = "rgba(7,24,30,.28)";
ctx.beginPath();
ctx.moveTo(0, groundY);
for (let x = 0; x <= W + 30; x += 30) {
const y = groundY - 36 - 24 * Math.sin(x * .004 + .7) -
13 * Math.sin(x * .011 + 2);
ctx.lineTo(x, y);
}
ctx.lineTo(W, groundY);
ctx.closePath();
ctx.fill();
if (flash > .01) {
ctx.fillStyle = `rgba(195,220,224,${flash * .13})`;
ctx.fillRect(0, 0, W, H);
}
}
function drawRain(front) {
ctx.save();
ctx.lineCap = "round";
for (const p of drops) {
if (p.front !== front) continue;
const vx = actualWind * PPM * .48 * p.depth;
const vy = p.speed * PPM * (.72 + .28 * p.depth);
const streak = front ? .032 : .022;
ctx.strokeStyle = `rgba(198,224,224,${(front ? .24 : .13) * p.depth})`;
ctx.lineWidth = (front ? 1.15 : .7) * p.depth;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - vx * streak, p.y - vy * streak);
ctx.stroke();
}
ctx.restore();
}
function drawGround() {
const gradient = ctx.createLinearGradient(0, groundY, 0, H);
gradient.addColorStop(0, "#263b3f");
gradient.addColorStop(1, "#0c191f");
ctx.fillStyle = gradient;
ctx.fillRect(0, groundY, W, H - groundY);
ctx.strokeStyle = "rgba(194,215,207,.16)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, groundY + .5);
ctx.lineTo(W, groundY + .5);
ctx.stroke();
for (let i = 0; i < 12; i++) {
const x = ((i * .173 + .07) % 1) * W;
const y = groundY + 15 + ((i * 37) % Math.max(25, H - groundY - 20));
ctx.fillStyle = "rgba(141,177,174,.055)";
ctx.beginPath();
ctx.ellipse(x, y, 28 + (i % 4) * 18, 2.5 + (i % 3), 0, 0, TAU);
ctx.fill();
}
ctx.fillStyle = `rgba(231,103,73,${sim.attached ? .065 : .018})`;
ctx.beginPath();
ctx.ellipse(personX + actualWind * 1.4, groundY + 24,
PPM * .55, PPM * .055, 0, 0, TAU);
ctx.fill();
for (const s of splashes) {
const alpha = (1 - s.age / .65) * .24;
ctx.strokeStyle = `rgba(190,220,215,${alpha})`;
ctx.lineWidth = .8;
ctx.beginPath();
ctx.ellipse(s.x, groundY + 2, s.age * 24 * s.size,
s.age * 5 * s.size, 0, 0, TAU);
ctx.stroke();
}
}
function worldLine(points, color, width) {
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 = width;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
function worldCircle(p, radius, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, TAU);
ctx.fill();
}
function drawUmbrella() {
const hub = sim.attached ? sim.hub : sim.freePos;
const screenX = personX + hub.x * PPM;
const screenY = groundY - hub.y * PPM;
if (screenX < -500 || screenX > W + 500 || screenY < -500 || screenY > H + 500) return;
const theta = sim.theta;
const c = Math.cos(theta);
const s = Math.sin(theta);
const toWorld = (x, y) => ({
x: hub.x + x * c + y * s,
y: hub.y - x * s + y * c
});
const bottom = toWorld(0, -1.27);
const tip = toWorld(0, .07);
const bend = Math.min(.11, sim.drag * .00028);
const control = {
x: (bottom.x + tip.x) * .5 + bend,
y: (bottom.y + tip.y) * .5
};
ctx.beginPath();
ctx.moveTo(bottom.x, bottom.y);
ctx.quadraticCurveTo(control.x, control.y, tip.x, tip.y);
ctx.strokeStyle = "#26363a";
ctx.lineWidth = .034;
ctx.lineCap = "round";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bottom.x - .006, bottom.y);
ctx.quadraticCurveTo(control.x - .006, control.y, tip.x - .006, tip.y);
ctx.strokeStyle = "rgba(211,224,213,.55)";
ctx.lineWidth = .009;
ctx.stroke();
const h1 = toWorld(0, -1.34);
const hc = toWorld(-.025, -1.43);
const h2 = toWorld(-.15, -1.37);
ctx.beginPath();
ctx.moveTo(bottom.x, bottom.y);
ctx.lineTo(h1.x, h1.y);
ctx.quadraticCurveTo(hc.x, hc.y, h2.x, h2.y);
ctx.strokeStyle = "#18272c";
ctx.lineWidth = .045;
ctx.stroke();
const points = sim.nodes.map(n => toWorld(n.x, n.y));
const underside = toWorld(0, lerp(-.13, -.38, sim.invertBlend));
function traceCanopy() {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length - 1; i++) {
const mx = (points[i].x + points[i + 1].x) * .5;
const my = (points[i].y + points[i + 1].y) * .5;
ctx.quadraticCurveTo(points[i].x, points[i].y, mx, my);
}
const last = points[points.length - 1];
ctx.lineTo(last.x, last.y);
ctx.quadraticCurveTo(underside.x, underside.y, points[0].x, points[0].y);
ctx.closePath();
}
const fabric = ctx.createLinearGradient(
hub.x, hub.y + .45, hub.x, hub.y - .4
);
fabric.addColorStop(0, "#ff9870");
fabric.addColorStop(.46, "#e66049");
fabric.addColorStop(1, "#a93739");
traceCanopy();
ctx.fillStyle = fabric;
ctx.fill();
ctx.strokeStyle = "#71343a";
ctx.lineWidth = .018;
ctx.stroke();
ctx.save();
traceCanopy();
ctx.clip();
const crown = toWorld(0, .015);
for (let i = 0; i < NODE_COUNT - 2; i += 4) {
ctx.beginPath();
ctx.moveTo(crown.x, crown.y);
ctx.lineTo(points[i].x, points[i].y);
for (let j = i + 1; j <= Math.min(i + 2, NODE_COUNT - 1); j++) {
ctx.lineTo(points[j].x, points[j].y);
}
ctx.closePath();
ctx.fillStyle = "rgba(255,209,164,.065)";
ctx.fill();
}
ctx.restore();
const ribStart = toWorld(0, -.065);
for (const i of [0, 4, 12, 16]) {
const n = sim.nodes[i];
const end = toWorld(n.x, n.y);
const ribControl = toWorld(n.x * .45, n.y * .15 - .07);
ctx.beginPath();
ctx.moveTo(ribStart.x, ribStart.y);
ctx.quadraticCurveTo(ribControl.x, ribControl.y, end.x, end.y);
ctx.strokeStyle = sim.canopy === "inverted"
? "rgba(49,45,47,.8)"
: "rgba(80,42,45,.52)";
ctx.lineWidth = .012;
ctx.stroke();
}
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 = "rgba(255,191,147,.58)";
ctx.lineWidth = .012;
ctx.stroke();
worldCircle(points[0], .022, "#3c4140");
worldCircle(points[points.length - 1], .022, "#3c4140");
worldCircle(toWorld(0, .045), .034, "#344347");
}
function personPose() {
const e = effort;
const hip = { x: -.05 * e, y: .89 - .055 * e };
const shoulder = { x: -.08 - .25 * e, y: 1.42 - .055 * e };
return {
hip,
shoulder,
shoulderBack: { x: shoulder.x - .075, y: shoulder.y - .005 },
shoulderFront: { x: shoulder.x + .07, y: shoulder.y - .025 },
head: { x: shoulder.x - .035, y: shoulder.y + .235 },
footLeft: { x: -.25 - .20 * e, y: .04 },
footRight: { x: .26 + .30 * e, y: .04 }
};
}
function drawArm(a, b, color) {
const elbow = {
x: (a.x + b.x) * .5 + .025,
y: (a.y + b.y) * .5 - .095
};
worldLine([a, elbow, b], color, .115);
worldLine([
{ x: a.x + .012, y: a.y },
{ x: elbow.x + .012, y: elbow.y },
{ x: b.x + .012, y: b.y }
], "rgba(255,229,163,.13)", .025);
}
function drawPerson() {
const p = personPose();
const e = effort;
let handFront, handBack;
if (sim.attached) {
const grip = getGrip();
const dir = { x: Math.sin(sim.theta), y: Math.cos(sim.theta) };
handFront = grip;
handBack = { x: grip.x - dir.x * .17, y: grip.y - dir.y * .17 };
} else {
const recoil = Math.exp(-sim.releaseAge * 2.2);
handFront = {
x: p.shoulder.x + .25 + .08 * recoil,
y: p.shoulder.y - .12 + .24 * recoil
};
handBack = {
x: p.shoulder.x + .10,
y: p.shoulder.y - .28 + .18 * recoil
};
}
const scarfStart = { x: p.shoulder.x + .015, y: p.shoulder.y + .15 };
const scarfLength = .20 + .50 * e;
const scarfWave = Math.sin(simTime * (5 + actualWind * .08)) * .045 * e;
ctx.beginPath();
ctx.moveTo(scarfStart.x, scarfStart.y);
ctx.quadraticCurveTo(
scarfStart.x + scarfLength * .45,
scarfStart.y - .13 * (1 - e) + scarfWave,
scarfStart.x + scarfLength,
scarfStart.y - .18 * (1 - e) - scarfWave
);
ctx.strokeStyle = "#c85643";
ctx.lineWidth = .052;
ctx.lineCap = "round";
ctx.stroke();
drawArm(p.shoulderBack, handBack, "#b7863f");
const kneeLeft = {
x: lerp(p.hip.x, p.footLeft.x, .48) - .05 * e,
y: .46 - .07 * e
};
const kneeRight = {
x: lerp(p.hip.x, p.footRight.x, .53) + .09 * e,
y: .48 - .09 * e
};
worldLine([p.hip, kneeRight, p.footRight], "#142831", .145);
worldLine([p.hip, kneeLeft, p.footLeft], "#203945", .145);
for (const foot of [p.footRight, p.footLeft]) {
worldLine([
{ x: foot.x - .15, y: .047 },
{ x: foot.x + .11, y: .047 }
], "#101d23", .095);
worldLine([
{ x: foot.x - .16, y: .008 },
{ x: foot.x + .12, y: .008 }
], "#091419", .025);
}
const flap = .08 + .18 * e + Math.sin(simTime * 8) * .025 * e;
ctx.beginPath();
ctx.moveTo(p.shoulder.x - .16, p.shoulder.y + .05);
ctx.quadraticCurveTo(
p.shoulder.x - .20, p.shoulder.y - .30,
p.hip.x - .24, p.hip.y - .15
);
ctx.quadraticCurveTo(
p.hip.x, p.hip.y - .25,
p.hip.x + .27 + flap, p.hip.y - .11 + flap * .15
);
ctx.lineTo(p.shoulder.x + .15, p.shoulder.y - .04);
ctx.quadraticCurveTo(
p.shoulder.x + .18, p.shoulder.y + .04,
p.shoulder.x + .10, p.shoulder.y + .10
);
ctx.closePath();
ctx.fillStyle = "#d6a34c";
ctx.fill();
ctx.beginPath();
ctx.moveTo(p.shoulder.x + .08, p.shoulder.y + .04);
ctx.lineTo(p.hip.x + .27 + flap, p.hip.y - .11 + flap * .15);
ctx.lineTo(p.hip.x + .05, p.hip.y - .18);
ctx.closePath();
ctx.fillStyle = "rgba(91,66,38,.18)";
ctx.fill();
worldLine([
{ x: p.hip.x - .20, y: p.hip.y - .045 },
{ x: p.hip.x + .25 + flap * .6, y: p.hip.y - .01 }
], "rgba(255,235,174,.55)", .024);
worldCircle(p.head, .157, "#bd8b40");
const face = { x: p.head.x - .058, y: p.head.y + .002 };
worldCircle(face, .105, "#d4a07a");
ctx.beginPath();
ctx.moveTo(face.x - .095, face.y + .025);
ctx.lineTo(face.x - .145, face.y - .005);
ctx.lineTo(face.x - .092, face.y - .025);
ctx.closePath();
ctx.fillStyle = "#d4a07a";
ctx.fill();
worldCircle({ x: face.x - .055, y: face.y + .035 }, .011, "#263237");
if (e > .5) {
worldLine([
{ x: face.x - .085, y: face.y - .047 },
{ x: face.x - .035, y: face.y - .052 }
], "#6e493c", .012);
}
ctx.beginPath();
ctx.arc(p.head.x, p.head.y, .158, Math.PI * .52, Math.PI * 1.55);
ctx.strokeStyle = "#e0ad54";
ctx.lineWidth = .035;
ctx.stroke();
worldLine([
{ x: scarfStart.x - .08, y: scarfStart.y - .015 },
{ x: scarfStart.x + .10, y: scarfStart.y - .035 }
], "#d45e47", .055);
drawArm(p.shoulderFront, handFront, "#dfae56");
worldCircle(handBack, .054, "#d7a27b");
worldCircle(handFront, .056, "#dfa982");
}
function drawScene() {
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.clearRect(0, 0, W, H);
drawSky();
drawRain(false);
drawGround();
ctx.fillStyle = "rgba(2,12,17,.28)";
ctx.beginPath();
ctx.ellipse(personX, groundY + 7, PPM * (.58 + .22 * effort),
PPM * .075, 0, 0, TAU);
ctx.fill();
ctx.save();
ctx.translate(personX, groundY);
ctx.scale(PPM, -PPM);
drawUmbrella();
drawPerson();
ctx.restore();
drawRain(true);
}
function windDescription(wind) {
if (wind < .5) return "Calm";
if (wind < 3.3) return "Light air";
if (wind < 8) return "Breeze";
if (wind < 13.8) return "Fresh breeze";
if (wind < 17.1) return "Near gale";
if (wind < 20.7) return "Gale · force 8";
if (wind < 24.4) return "Severe gale · force 9";
if (wind < 28.4) return "Storm · force 10";
if (wind < 32.6) return "Violent storm · force 11";
return "Hurricane force · 12";
}
function updateUI() {
if (!sim) return;
windSpeedEl.textContent = actualWind.toFixed(1);
windDescriptionEl.textContent =
`${windDescription(actualWind)} · ${Math.round(actualWind * 3.6)} km/h`;
windFill.style.width = `${clamp(actualWind / 45 * 100, 0, 100)}%`;
const severe = actualWind > 24;
windFill.style.background = severe ? "#ff946d" : "#9bd8d2";
windSpeedEl.style.color = severe ? "#ffd2b8" : "#edf3ee";
slider.value = baseWind.toFixed(1);
slider.style.setProperty("--p", `${baseWind / 45 * 100}%`);
slider.setAttribute("aria-valuetext", `${baseWind.toFixed(1)} metres per second`);
modeLabel.textContent = autoWind
? (baseWind >= 42 ? "Auto · holding" : "Auto · rising")
: "Manual control";
autoButton.setAttribute("aria-pressed", String(autoWind));
let text, key, color;
if (!sim.attached) {
text = "Umbrella away · R to reset";
key = "away";
color = "#ff8069";
} else if (sim.canopy === "inverted") {
text = sim.recovery > 0 ? "Coaxing the canopy back" : "Canopy inside out";
key = "inverted" + (sim.recovery > 0 ? "-recovery" : "");
color = "#ff946d";
} else if (sim.gripHealth < .58 || sim.load > sim.gripCap) {
text = "Grip slipping";
key = "slipping";
color = "#ffb06f";
} else if (sim.wrench > 0 || sim.theta > .85) {
text = "Wrenched sideways";
key = "wrench";
color = "#ffb06f";
} else if (sim.inversionCharge > .25) {
text = "Frame at its limit";
key = "limit";
color = "#ffb06f";
} else if (sim.load > 135) {
text = "Frame loading · stance braced";
key = "loading";
color = "#e6ca83";
} else {
text = "Two hands · canopy holding";
key = "holding";
color = "#9bd8d2";
}
if (key !== lastState) {
stateText.textContent = text;
stateDot.style.background = color;
stateDot.style.boxShadow = `0 0 12px ${color}88`;
lastState = key;
}
loadFill.style.width = `${clamp(sim.load / 300 * 100, 0, 100)}%`;
}
function setBaseWind(value) {
baseWind = clamp(value, 0, 45);
autoWind = false;
updateUI();
}
function doGust() {
gustPulse = Math.min(14, Math.max(gustPulse, 6.5 + baseWind * .18));
if (sim.attached) {
sim.omega += .25 + actualWind * .012;
sim.wrench = .5;
}
}
slider.addEventListener("input", () => setBaseWind(Number(slider.value)));
autoButton.addEventListener("click", () => {
autoWind = !autoWind;
updateUI();
});
gustButton.addEventListener("click", doGust);
resetButton.addEventListener("click", resetAll);
window.addEventListener("keydown", event => {
const key = event.key.toLowerCase();
const tag = event.target && event.target.tagName;
if (tag === "INPUT" && key.startsWith("arrow")) return;
if (tag === "BUTTON" && (key === " " || key === "enter")) return;
const amount = event.shiftKey ? 2 : .7;
if (key === "arrowup" || key === "arrowright" || key === "w") {
event.preventDefault();
setBaseWind(baseWind + amount);
} else if (key === "arrowdown" || key === "arrowleft" || key === "s") {
event.preventDefault();
setBaseWind(baseWind - amount);
} else if (key === "g" && !event.repeat) {
doGust();
} else if (key === "a" && !event.repeat) {
autoWind = !autoWind;
updateUI();
} else if (key === "r" && !event.repeat) {
resetAll();
}
});
window.addEventListener("resize", resize, { passive: true });
resize();
resetAll();
let previous = performance.now();
let accumulator = 0;
function frame(now) {
const elapsed = Math.min(.05, (now - previous) / 1000);
previous = now;
accumulator += elapsed;
while (accumulator >= FIXED_DT) {
step(FIXED_DT);
accumulator -= FIXED_DT;
}
drawScene();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The satisfying sequence is there: rain progressively lays over with the wind, the canopy shudders and changes section, the shaft bows under load, and the person leans in, widens their stance, and brings both hands into the struggle. Inversion unloads the canopy somewhat, so turning inside out is not automatically the same event as escape. Sustained overload then wears down the grip; release gives the umbrella its own tumble, drag, lift, and crude ground contact. Lowering an attached umbrella below 7 m/s for a few seconds lets the canopy recover.
The main cheat is dimensionality: this is a loaded 2-D cross-section with tuned thresholds, not a 3-D pressure simulation or a prediction of any real umbrella’s rating. The shaft’s bow is load-driven drawing rather than a separately solved flexible beam. Rain follows the wind but passes through the canopy and person, with splashes only on the ground. The person is a planted procedural puppet—feet quietly re-space instead of taking steps, and the arms can look rubbery at extreme angles. Once free, the umbrella’s flight is deliberately arcade-like and it has no collision with the person or scenery.