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 modeled in a custom Position-Based Dynamics (PBD) solver as a radial skeleton of articulated two-segment ribs (inner and outer rods) restrained by spring-like stretchers attached to a sliding runner on the shaft. Rather than representing inversion as a discrete boolean flag, inversion is a continuous mechanical buckling state emerging from a double-well potential applied to the rib joints and runner slider.Normally, the stretcher struts hold the ribs arched convexly downward against the crown; however, when the aerodynamic pressure depends on the angle of attack between the oncoming turbulent wind and the canopy normal—catching under the windward rim—it drives the runner upward past its over-center toggle threshold and forces the outer rib joints through their collinear dead-point, snapping the canopy violently into its concave upward equilibrium where drag spikes until the grip fails.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gale Force Umbrella</title>
<style>
:root {
--bg-top: #0f1722;
--bg-bot: #273746;
--ui-bg: rgba(15, 23, 34, 0.78);
--ui-border: rgba(255, 255, 255, 0.15);
--accent: #e67e22;
--text: #ecf0f1;
}
* { box-sizing: border-box; user-select: none; -webkit-user-select: none; }
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background: var(--bg-top);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: var(--text);
}
canvas { display: block; width: 100%; height: 100%; }
#hud {
position: absolute; top: 20px; left: 20px;
background: var(--ui-bg); backdrop-filter: blur(8px);
border: 1px solid var(--ui-border); border-radius: 10px;
padding: 16px 20px; width: 320px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.hud-header {
display: flex; justify-content: space-between; align-items: baseline;
margin-bottom: 6px;
}
.wind-readout {
font-size: 32px; font-weight: 800; font-variant-numeric: tabular-nums;
letter-spacing: -0.5px; color: #fff;
}
.wind-unit { font-size: 14px; font-weight: 600; color: #95a5a6; margin-left: 4px; }
.beaufort {
font-size: 13px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.8px; color: var(--accent);
}
.slider-row {
display: flex; align-items: center; gap: 10px; margin: 12px 0;
}
input[type=range] {
flex: 1; appearance: none; -webkit-appearance: none;
height: 6px; background: rgba(255,255,255,0.2); border-radius: 3px; outline: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 18px; height: 18px;
border-radius: 50%; background: var(--accent); cursor: pointer;
border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.4);
}
.controls-row {
display: flex; justify-content: space-between; align-items: center;
gap: 8px; margin-top: 10px; font-size: 12px; color: #bdc3c7;
}
button {
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
color: #fff; padding: 6px 12px; border-radius: 5px; cursor: pointer;
font-weight: 600; font-size: 12px; transition: all 0.15s;
}
button:hover { background: rgba(255,255,255,0.2); }
button.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.status-pill {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 11px; font-weight: 700; background: rgba(255,255,255,0.08);
margin-top: 8px; width: 100%; text-align: center;
}
.hint {
font-size: 11px; color: #7f8c8d; margin-top: 8px; line-height: 1.35;
}
</style>
</head>
<body>
<canvas id="sim"></canvas>
<div id="hud">
<div class="hud-header">
<div><span class="wind-readout" id="windVal">12.0</span><span class="wind-unit">mph</span></div>
<div class="beaufort" id="beaufortLabel">Fresh Breeze</div>
</div>
<div class="slider-row">
<input type="range" id="windSlider" min="0" max="95" step="0.1" value="12">
</div>
<div class="controls-row">
<button id="autoBtn" class="active">Auto-Rise: ON</button>
<button id="resetBtn">New Umbrella (R)</button>
</div>
<div class="status-pill" id="umbStatus">Canopy Intact — Grip Secure</div>
<div class="hint">
<b>Left/Right Arrows</b> or <b>Slider</b>: Adjust wind<br>
<b>Mouse Drag</b>: Tug umbrella or test frame
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('sim');
const ctx = canvas.getContext('2d');
// UI Elements
const windValEl = document.getElementById('windVal');
const beaufortEl = document.getElementById('beaufortLabel');
const sliderEl = document.getElementById('windSlider');
const autoBtn = document.getElementById('autoBtn');
const resetBtn = document.getElementById('resetBtn');
const statusEl = document.getElementById('umbStatus');
let W = window.innerWidth;
let H = window.innerHeight;
let groundY = H * 0.82;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
groundY = H * 0.82;
}
window.addEventListener('resize', resize);
resize();
// --- Simulation State ---
let baseWindMph = 12;
let currentGustMph = 12;
let autoRise = true;
let simTime = 0;
// Mouse interaction
let mouse = { x: 0, y: 0, down: false };
window.addEventListener('pointerdown', e => {
if (e.target.closest('#hud')) return;
mouse.down = true; mouse.x = e.clientX; mouse.y = e.clientY;
});
window.addEventListener('pointermove', e => {
mouse.x = e.clientX; mouse.y = e.clientY;
});
window.addEventListener('pointerup', () => { mouse.down = false; });
// UI listeners
sliderEl.addEventListener('input', () => {
baseWindMph = parseFloat(sliderEl.value);
setAutoRise(false);
});
function setAutoRise(val) {
autoRise = val;
autoBtn.textContent = `Auto-Rise: ${autoRise ? 'ON' : 'OFF'}`;
autoBtn.classList.toggle('active', autoRise);
}
autoBtn.addEventListener('click', () => setAutoRise(!autoRise));
resetBtn.addEventListener('click', resetSimulation);
window.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
setAutoRise(false);
baseWindMph = Math.min(95, baseWindMph + 2.5);
sliderEl.value = baseWindMph;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
setAutoRise(false);
baseWindMph = Math.max(0, baseWindMph - 2.5);
sliderEl.value = baseWindMph;
} else if (e.key.toLowerCase() === 'r') {
resetSimulation();
}
});
function getBeaufort(mph) {
if (mph < 1) return "Calm";
if (mph < 8) return "Light Breeze";
if (mph < 18) return "Fresh Breeze";
if (mph < 31) return "Strong Breeze";
if (mph < 46) return "Gale";
if (mph < 63) return "Severe Gale";
if (mph < 75) return "Storm";
return "Hurricane Force";
}
// --- Physics Helper (Verlet / PBD) ---
class Point {
constructor(x, y, mass = 1, pinned = false) {
this.x = x; this.y = y;
this.ox = x; this.oy = y;
this.mass = mass;
this.invMass = pinned ? 0 : 1 / mass;
}
integrate(dt, gx, gy, damp = 0.992) {
if (this.invMass === 0) return;
const vx = (this.x - this.ox) * damp;
const vy = (this.y - this.oy) * damp;
this.ox = this.x;
this.oy = this.y;
this.x += vx + gx * dt * dt;
this.y += vy + gy * dt * dt;
}
applyForce(fx, fy, dt) {
if (this.invMass === 0) return;
this.x += fx * this.invMass * dt * dt;
this.y += fy * this.invMass * dt * dt;
}
}
function solveDistance(p1, p2, len, stiffness = 1.0) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.hypot(dx, dy) || 0.0001;
const diff = (d - len) / d;
const w1 = p1.invMass;
const w2 = p2.invMass;
const wSum = w1 + w2;
if (wSum === 0) return;
const corr = diff * stiffness / wSum;
p1.x += dx * corr * w1;
p1.y += dy * corr * w1;
p2.x -= dx * corr * w2;
p2.y -= dy * corr * w2;
}
function solveTarget(p, tx, ty, stiffness) {
p.x += (tx - p.x) * stiffness;
p.y += (ty - p.y) * stiffness;
}
// Two-bone Inverse Kinematics
function solveIK(rootX, rootY, targetX, targetY, L1, L2, flip = 1) {
let dx = targetX - rootX;
let dy = targetY - rootY;
let d = Math.hypot(dx, dy);
d = Math.min(d, L1 + L2 - 0.01);
d = Math.max(d, Math.abs(L1 - L2) + 0.01);
const cosAngle = (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d);
const angle = Math.acos(Math.max(-1, Math.min(1, cosAngle)));
const baseAngle = Math.atan2(dy, dx);
const jointAngle = baseAngle + flip * angle;
return {
jx: rootX + Math.cos(jointAngle) * L1,
jy: rootY + Math.sin(jointAngle) * L1,
ex: rootX + (dx / Math.hypot(targetX - rootX, targetY - rootY)) * d,
ey: rootY + (dy / Math.hypot(targetX - rootX, targetY - rootY)) * d
};
}
// --- Umbrella & Person System ---
let umbrella = null;
let person = null;
function createSystem() {
const startX = W * 0.72;
const footY = groundY;
person = {
baseX: startX,
lean: 0, // current lean angle
crouch: 0, // crouch offset
braceStep: 40, // stance width
gripStrain: 0, // 0 to 1
hasUmbrella: true,
recoverTimer: 0,
coatPoints: [],
scarfPoints: []
};
// Coat hem points (3 segments)
for (let i = 0; i < 4; i++) {
person.coatPoints.push(new Point(startX, footY - 80 + i * 18, 0.8, i === 0));
}
// Scarf points
for (let i = 0; i < 5; i++) {
person.scarfPoints.push(new Point(startX, footY - 125, 0.3, i === 0));
}
// Umbrella geometry
const hx = startX - 35;
const hy = footY - 95;
const shaftLen = 92;
const angle = -Math.PI * 0.58; // tilted into wind (left)
const ux = Math.cos(angle), uy = Math.sin(angle);
const nx = -uy, ny = ux;
const handle = new Point(hx, hy, 2.5);
const crown = new Point(hx + ux * shaftLen, hy + uy * shaftLen, 1.5);
const runner = new Point(hx + ux * (shaftLen * 0.65), hy + uy * (shaftLen * 0.65), 1.0);
// 5 ribs visible in 2.5D profile: [-1, -0.5, 0, 0.5, 1]
const ribSpecs = [-1, -0.55, 0, 0.55, 1];
const ribs = ribSpecs.map(side => {
const spread = side * 62;
const midX = crown.x + nx * (spread * 0.55) - ux * 12;
const midY = crown.y + ny * (spread * 0.55) - uy * 12;
const tipX = crown.x + nx * spread - ux * 34;
const tipY = crown.y + ny * spread - uy * 34;
return {
side,
mid: new Point(midX, midY, 0.7),
tip: new Point(tipX, tipY, 0.5),
inverted: 0 // -1 normal, +1 inverted
};
});
umbrella = {
handle, crown, runner,
shaftLen,
runnerOffset: 0.66, // fraction along shaft from handle to crown (0.66 normal, >1.05 inverted)
runnerVel: 0,
ribs,
invertedState: false,
wrenchedSideways: 0,
freeFlight: false
};
}
resetSimulation();
function resetSimulation() {
createSystem();
statusEl.textContent = "Canopy Intact — Grip Secure";
statusEl.style.color = "#ecf0f1";
}
// --- Rain & Debris Particles ---
const MAX_RAIN = 320;
const rain = [];
const splashes = [];
for (let i = 0; i < MAX_RAIN; i++) {
rain.push({
x: Math.random() * W,
y: Math.random() * H,
z: 0.4 + Math.random() * 0.8 // depth scale
});
}
function spawnSplash(x, y, vx, vy) {
if (splashes.length > 140) return;
splashes.push({
x, y,
vx: vx * 0.15 + (Math.random() - 0.5) * 90,
vy: -30 - Math.random() * 90,
life: 1.0
});
}
// --- Physics Step ---
function updatePhysics(dt) {
simTime += dt;
if (autoRise) {
baseWindMph = Math.min(95, baseWindMph + dt * 2.1);
sliderEl.value = baseWindMph;
}
// Turbulent gust calculation (layered sines)
const gustWave =
Math.sin(simTime * 1.9) * 0.12 +
Math.sin(simTime * 4.7) * 0.07 +
Math.cos(simTime * 9.3) * 0.04;
currentGustMph = Math.max(0, baseWindMph * (1 + gustWave));
// Wind vector (blows left to right: +X)
const windSpeedPx = currentGustMph * 14.5;
const windAngle = Math.sin(simTime * 3.1) * 0.07 - 0.03; // slight vertical updraft eddies
const windVx = Math.cos(windAngle) * windSpeedPx;
const windVy = Math.sin(windAngle) * windSpeedPx;
// UI Updates
windValEl.textContent = currentGustMph.toFixed(1);
beaufortEl.textContent = getBeaufort(currentGustMph);
// --- Person Biomechanics ---
const windFactor = Math.min(1.3, currentGustMph / 65);
const targetLean = person.hasUmbrella
? -0.06 - windFactor * 0.44
: -0.04 - windFactor * 0.25;
person.lean += (targetLean - person.lean) * Math.min(1, dt * 6);
const targetCrouch = windFactor * 22;
person.crouch += (targetCrouch - person.crouch) * Math.min(1, dt * 5);
const targetStep = 28 + windFactor * 52;
person.braceStep += (targetStep - person.braceStep) * Math.min(1, dt * 5);
// Compute person skeleton anchors
const ankleFrontX = person.baseX - person.braceStep * 0.55;
const ankleBackX = person.baseX + person.braceStep * 0.45;
const hipX = person.baseX + Math.sin(person.lean) * 58;
const hipY = groundY - 68 + person.crouch;
const torsoLen = 62;
const shoulderX = hipX + Math.sin(person.lean * 1.25) * torsoLen;
const shoulderY = hipY - Math.cos(person.lean * 1.25) * torsoLen;
person.skeleton = {
ankleFrontX, ankleBackX, hipX, hipY, shoulderX, shoulderY
};
// Update Coat & Scarf
person.coatPoints[0].x = hipX;
person.coatPoints[0].y = hipY - 10;
for (let i = 1; i < person.coatPoints.length; i++) {
const p = person.coatPoints[i];
p.integrate(dt, 0, 750, 0.96);
p.applyForce(windVx * 1.6 + Math.sin(simTime * 18 + i) * 180, -Math.abs(windVx) * 0.2, dt);
solveDistance(person.coatPoints[i - 1], p, 18, 0.9);
}
person.scarfPoints[0].x = shoulderX;
person.scarfPoints[0].y = shoulderY - 4;
for (let i = 1; i < person.scarfPoints.length; i++) {
const p = person.scarfPoints[i];
p.integrate(dt, 0, 500, 0.95);
p.applyForce(windVx * 2.2, Math.sin(simTime * 24 + i * 1.5) * 450 - 80, dt);
solveDistance(person.scarfPoints[i - 1], p, 14, 0.9);
}
// --- Umbrella Physics ---
const u = umbrella;
const subSteps = 6;
const subDt = dt / subSteps;
for (let step = 0; step < subSteps; step++) {
// 1. Integrate umbrella points
const grav = u.freeFlight ? 420 : 250;
u.handle.integrate(subDt, 0, grav, 0.985);
u.crown.integrate(subDt, 0, grav, 0.985);
for (const r of u.ribs) {
r.mid.integrate(subDt, 0, grav, 0.975);
r.tip.integrate(subDt, 0, grav, 0.975);
}
// Shaft axis unit vector
let sx = u.crown.x - u.handle.x;
let sy = u.crown.y - u.handle.y;
const sLen = Math.hypot(sx, sy) || 1;
sx /= sLen; sy /= sLen;
const nx = -sy, ny = sx; // perpendicular right
// 2. Aerodynamic Forces on Canopy
// Angle between wind vector and umbrella shaft pointing into wind
const windDirX = 1, windDirY = Math.sin(windAngle);
// dot product: -1 means pointing directly into wind, +1 means pointing downwind
const alignment = sx * windDirX + sy * windDirY;
// Cross product: how much wind catches underneath asymmetric rim
const crossCatch = sx * windDirY - sy * windDirX;
const dynamicPressure = 0.028 * currentGustMph * currentGustMph;
// Runner sliding force along shaft
let runnerAxialForce = 0;
for (const r of u.ribs) {
// Vector from crown to tip
const rx = r.tip.x - u.crown.x;
const ry = r.tip.y - u.crown.y;
// Normal of this rib segment
const segNx = -ry, segNy = rx;
const segLen = Math.hypot(rx, ry) || 1;
// Relative wind velocity at tip
const tipVx = (r.tip.x - r.tip.ox) / subDt;
const tipVy = (r.tip.y - r.tip.oy) / subDt;
const relVx = windVx - tipVx;
const relVy = windVy - tipVy;
// Flutter turbulence
const flutter = Math.sin(simTime * 28 + r.side * 5) * dynamicPressure * 14;
// Wind catching under the windward (bottom-left) lip or inside inverted bowl
const catchFactor = u.invertedState ? 1.85 : (1.0 + Math.max(0, alignment + 0.55) * 2.2);
const fx = (relVx * 0.45 + dynamicPressure * 18 * catchFactor) * (0.6 + Math.abs(r.side) * 0.4);
const fy = (relVy * 0.45 + flutter - dynamicPressure * 6 * crossCatch);
r.tip.applyForce(fx, fy, subDt);
r.mid.applyForce(fx * 0.6, fy * 0.6, subDt);
// Inversion pressure: if wind gets under canopy or gusts exceed threshold (~38-45 mph)
// pushes ribs toward the crown/downwind (+sx)
const inversionPush = (currentGustMph > 34 || alignment > -0.45 || u.invertedState)
? dynamicPressure * (0.85 + Math.max(0, alignment + 0.7) * 1.8)
: -dynamicPressure * 0.25;
runnerAxialForce += inversionPush;
}
// Update runner position along shaft with double-well bistable spring
// Normal equilibrium: 0.66. Over-center peak: 0.92. Inverted equilibrium: 1.24
const eqNormal = 0.66;
const eqInvert = 1.25;
const peak = 0.93;
let springForce = 0;
if (u.runnerOffset < peak) {
springForce = -(u.runnerOffset - eqNormal) * 52;
} else {
springForce = -(u.runnerOffset - eqInvert) * 58;
}
// At low wind, allow strong windward recovery if user lowers wind
if (currentGustMph < 18 && u.invertedState) {
springForce -= 28;
}
u.runnerVel += (springForce + runnerAxialForce * 0.045) * subDt;
u.runnerVel *= 0.88;
u.runnerOffset += u.runnerVel * subDt;
u.runnerOffset = Math.max(0.55, Math.min(1.38, u.runnerOffset));
u.invertedState = u.runnerOffset > peak;
// Mouse interaction tug
if (mouse.down) {
const dCrown = Math.hypot(mouse.x - u.crown.x, mouse.y - u.crown.y);
if (dCrown < 140) {
u.crown.applyForce((mouse.x - u.crown.x) * 180, (mouse.y - u.crown.y) * 180, subDt);
if (mouse.x > u.crown.x + 25) u.runnerOffset += 0.8 * subDt;
if (mouse.x < u.crown.x - 25) u.runnerOffset -= 0.8 * subDt;
}
}
// 3. Hand Grip & Wrenching Constraints
if (person.hasUmbrella) {
// Target hand position bracing against wind
const braceAngle = -Math.PI * 0.64 + person.lean * 0.4 + Math.min(0.45, windFactor * 0.35);
const reach = 46 - windFactor * 10;
const targetHx = shoulderX + Math.cos(braceAngle) * reach;
const targetHy = shoulderY + Math.sin(braceAngle) * reach + 12;
// Calculate force required to hold handle
const distErr = Math.hypot(targetHx - u.handle.x, targetHy - u.handle.y);
person.gripStrain = Math.min(1, (distErr * 0.045) + (currentGustMph / 72) * (u.invertedState ? 1.25 : 0.85));
// Stiff spring holding handle to hands
solveTarget(u.handle, targetHx, targetHy, 0.45);
// Rotational wrist torque trying to keep umbrella pointed into the wind
const desiredAngle = -Math.PI * 0.72 + person.lean * 0.5;
const desiredCrownX = u.handle.x + Math.cos(desiredAngle) * u.shaftLen;
const desiredCrownY = u.handle.y + Math.sin(desiredAngle) * u.shaftLen;
// Wrist strength weakens when wind wrenches canopy
const wristStiffness = Math.max(0.015, 0.14 - windFactor * 0.11);
solveTarget(u.crown, desiredCrownX, desiredCrownY, wristStiffness);
// Breakaway condition: extreme gale (> 64 mph) + high strain
if (currentGustMph > 64 && person.gripStrain > 0.94 && u.invertedState) {
person.hasUmbrella = false;
u.freeFlight = true;
u.handle.ox -= 15; // initial wrench impulse
statusEl.textContent = "UMBRELLA LOST TO THE GALE! (Press R)";
statusEl.style.color = "#e74c3c";
} else if (u.invertedState) {
statusEl.textContent = "CANOPY INVERTED — High Drag Load!";
statusEl.style.color = "#f39c12";
} else if (currentGustMph > 35) {
statusEl.textContent = "Frame Loaded — Bracing Against Gusts";
statusEl.style.color = "#f1c40f";
} else {
statusEl.textContent = "Canopy Intact — Grip Secure";
statusEl.style.color = "#ecf0f1";
}
} else {
// Free-flying tumble in the gale
u.crown.applyForce(windVx * 1.4, windVy * 1.4 - 180 + Math.sin(simTime * 15) * 400, subDt);
u.handle.applyForce(windVx * 0.8, windVy * 0.8 + 120, subDt);
// Ground collision for runaway umbrella
for (const pt of [u.handle, u.crown, ...u.ribs.map(r => r.tip)]) {
if (pt.y > groundY) {
pt.y = groundY;
pt.ox = pt.x - (pt.x - pt.ox) * 0.5;
}
}
}
// 4. Solve Umbrella Structural Constraints
solveDistance(u.handle, u.crown, u.shaftLen, 0.95);
// Recompute shaft basis after constraint
sx = (u.crown.x - u.handle.x) / u.shaftLen;
sy = (u.crown.y - u.handle.y) / u.shaftLen;
const cNx = -sy, cNy = sx;
// Position runner on shaft
u.runner.x = u.handle.x + sx * (u.shaftLen * u.runnerOffset);
u.runner.y = u.handle.y + sy * (u.shaftLen * u.runnerOffset);
// Constrain articulated ribs
const archDir = (u.runnerOffset - peak) * 2.6; // negative = normal dome, positive = inverted bowl
for (const r of u.ribs) {
const spread = r.side * 66;
// Ideal target positions in umbrella frame
const targetMidX = u.crown.x + cNx * (spread * 0.54) + sx * (archDir * 18);
const targetMidY = u.crown.y + cNy * (spread * 0.54) + sy * (archDir * 18);
const targetTipX = u.crown.x + cNx * (spread * (1 - Math.abs(archDir)*0.12)) + sx * (archDir * 44);
const targetTipY = u.crown.y + cNy * (spread * (1 - Math.abs(archDir)*0.12)) + sy * (archDir * 44);
solveTarget(r.mid, targetMidX, targetMidY, 0.35);
solveTarget(r.tip, targetTipX, targetTipY, 0.22);
// Rigid bone lengths from crown -> mid -> tip
const L1 = 22 + Math.abs(r.side) * 18;
const L2 = 24 + Math.abs(r.side) * 20;
solveDistance(u.crown, r.mid, L1, 0.8);
solveDistance(r.mid, r.tip, L2, 0.8);
}
}
// --- Update Rain & Collisions ---
const rainFallSpeed = 560;
const rvx = windVx * 0.85;
const rvy = rainFallSpeed + windVy * 0.3;
// Bounding circle of canopy for fast rain deflection
const canopyCenterX = (u.crown.x + u.runner.x) * 0.5;
const canopyCenterY = (u.crown.y + u.runner.y) * 0.5;
const canopyRadius = 56;
for (let i = 0; i < MAX_RAIN; i++) {
const p = rain[i];
p.x += rvx * p.z * dt;
p.y += rvy * p.z * dt;
// Check collision with umbrella canopy
const dx = p.x - canopyCenterX;
const dy = p.y - canopyCenterY;
if (dx * dx + dy * dy < canopyRadius * canopyRadius && p.z > 0.65) {
spawnSplash(p.x, p.y, rvx, rvy);
p.y = -20;
p.x = Math.random() * (W + 400) - 400;
continue;
}
// Ground hit
if (p.y > groundY) {
if (Math.random() < 0.28) spawnSplash(p.x, groundY, rvx, rvy);
p.y = -10 - Math.random() * 40;
p.x = Math.random() * (W + 600) - 500;
}
if (p.x > W + 100) {
p.x = -50;
p.y = Math.random() * groundY;
}
}
// Update splashes
for (let i = splashes.length - 1; i >= 0; i--) {
const s = splashes[i];
s.x += s.vx * dt;
s.y += s.vy * dt;
s.vy += 480 * dt;
s.life -= dt * 3.8;
if (s.life <= 0) splashes.splice(i, 1);
}
}
// --- Rendering ---
function draw() {
// Sky gradient darkens as storm intensifies
const stormDarkness = Math.min(1, currentGustMph / 85);
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, `rgb(${15 - stormDarkness*8}, ${23 - stormDarkness*10}, ${34 - stormDarkness*12})`);
grad.addColorStop(0.82, `rgb(${39 - stormDarkness*18}, ${55 - stormDarkness*22}, ${70 - stormDarkness*25})`);
grad.addColorStop(1, '#0b1117');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Distant wind streaks
ctx.strokeStyle = `rgba(200, 220, 235, ${0.03 + stormDarkness * 0.06})`;
ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = 0; i < 12; i++) {
const y = ((i * 97 + simTime * 40) % (groundY - 40));
const x = ((i * 233 + simTime * (150 + currentGustMph * 18)) % (W + 400)) - 200;
const len = 60 + currentGustMph * 2.5;
ctx.moveTo(x, y);
ctx.lineTo(x + len, y + len * 0.04);
}
ctx.stroke();
// Streetlamp in background
const lampX = W * 0.26;
const bend = Math.pow(currentGustMph / 90, 2) * 14;
ctx.strokeStyle = '#18222c';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(lampX, groundY);
ctx.quadraticCurveTo(lampX + bend * 0.4, groundY - 140, lampX + bend, groundY - 270);
ctx.stroke();
// Warm streetlamp glow cone
const headX = lampX + bend;
const headY = groundY - 270;
const glowGrad = ctx.createRadialGradient(headX, headY, 4, headX + 40, headY + 180, 260);
glowGrad.addColorStop(0, 'rgba(255, 210, 130, 0.35)');
glowGrad.addColorStop(0.4, 'rgba(255, 190, 100, 0.10)');
glowGrad.addColorStop(1, 'rgba(255, 190, 100, 0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(headX + 30, headY + 130, 260, 0, Math.PI * 2);
ctx.fill();
// Draw Person
drawPerson();
// Draw Umbrella
drawUmbrella();
// Ground pavement
const gGrad = ctx.createLinearGradient(0, groundY, 0, H);
gGrad.addColorStop(0, '#16202a');
gGrad.addColorStop(0.15, '#0f171e');
gGrad.addColorStop(1, '#070b0e');
ctx.fillStyle = gGrad;
ctx.fillRect(0, groundY, W, H - groundY);
// Wet ground highlight line
ctx.strokeStyle = 'rgba(180, 215, 240, 0.18)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(W, groundY);
ctx.stroke();
// Foreground Rain streaks
const windSpeedPx = currentGustMph * 14.5;
const rvx = windSpeedPx * 0.85;
const rvy = 560;
ctx.strokeStyle = 'rgba(200, 225, 245, 0.38)';
ctx.lineWidth = 1.2;
ctx.beginPath();
for (let i = 0; i < MAX_RAIN; i++) {
const p = rain[i];
const tailX = p.x - rvx * 0.032 * p.z;
const tailY = p.y - rvy * 0.032 * p.z;
ctx.moveTo(p.x, p.y);
ctx.lineTo(tailX, tailY);
}
ctx.stroke();
// Splash droplets
ctx.fillStyle = 'rgba(215, 235, 250, 0.65)';
ctx.beginPath();
for (const s of splashes) {
ctx.moveTo(s.x, s.y);
ctx.arc(s.x, s.y, 1.6 * s.life, 0, Math.PI * 2);
}
ctx.fill();
}
function drawPerson() {
const sk = person.skeleton;
if (!sk) return;
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Legs (2-bone IK)
const legL1 = 39, legL2 = 39;
const frontLeg = solveIK(sk.hipX, sk.hipY, sk.ankleFrontX, groundY - 4, legL1, legL2, -1);
const backLeg = solveIK(sk.hipX, sk.hipY, sk.ankleBackX, groundY - 4, legL1, legL2, -1);
// Back leg
ctx.strokeStyle = '#121921';
ctx.lineWidth = 13;
ctx.beginPath();
ctx.moveTo(sk.hipX, sk.hipY);
ctx.lineTo(backLeg.jx, backLeg.jy);
ctx.lineTo(sk.ankleBackX, groundY - 4);
ctx.stroke();
// Back boot
ctx.lineWidth = 9;
ctx.beginPath();
ctx.moveTo(sk.ankleBackX, groundY - 4);
ctx.lineTo(sk.ankleBackX - 12, groundY - 2);
ctx.stroke();
// Front leg
ctx.strokeStyle = '#1c2833';
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(sk.hipX, sk.hipY);
ctx.lineTo(frontLeg.jx, frontLeg.jy);
ctx.lineTo(sk.ankleFrontX, groundY - 4);
ctx.stroke();
// Front boot
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(sk.ankleFrontX, groundY - 4);
ctx.lineTo(sk.ankleFrontX - 14, groundY - 2);
ctx.stroke();
// Trenchcoat / Raincoat polygon with blowing hem
const cp = person.coatPoints;
ctx.fillStyle = '#283747';
ctx.strokeStyle = '#1b2631';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(sk.shoulderX - 11, sk.shoulderY);
ctx.lineTo(sk.shoulderX + 12, sk.shoulderY + 2);
// Downwind fluttering coat back
ctx.quadraticCurveTo(cp[1].x + 18, cp[1].y, cp[3].x + 22, cp[3].y);
ctx.lineTo(cp[3].x - 16, cp[3].y + 2);
ctx.quadraticCurveTo(sk.hipX - 15, sk.hipY, sk.shoulderX - 11, sk.shoulderY);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Head (tilted down against wind)
const headTilt = person.lean * 1.35 - 0.15;
const headX = sk.shoulderX + Math.sin(headTilt) * 18;
const headY = sk.shoulderY - Math.cos(headTilt) * 18;
ctx.fillStyle = '#34495e';
ctx.beginPath();
ctx.arc(headX, headY, 12.5, 0, Math.PI * 2);
ctx.fill();
// Coat collar & hood
ctx.fillStyle = '#212f3d';
ctx.beginPath();
ctx.arc(headX + 3, headY + 2, 13.5, -Math.PI * 0.7, Math.PI * 0.6);
ctx.fill();
// Fluttering Scarf (vivid warm orange-red)
const sp = person.scarfPoints;
ctx.strokeStyle = '#d35400';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(sp[0].x, sp[0].y);
for (let i = 1; i < sp.length; i++) {
ctx.lineTo(sp[i].x, sp[i].y);
}
ctx.stroke();
// Arms (IK to umbrella handle or bracing against face)
const u = umbrella;
let handTargetX, handTargetY;
if (person.hasUmbrella) {
// Jitter hands slightly when grip strain is high
const shake = person.gripStrain > 0.6 ? Math.sin(simTime * 55) * person.gripStrain * 3 : 0;
handTargetX = u.handle.x + shake;
handTargetY = u.handle.y + shake;
} else {
// Shielding face from rain after losing umbrella
handTargetX = headX - 18;
handTargetY = headY + 6;
}
const arm = solveIK(sk.shoulderX, sk.shoulderY + 4, handTargetX, handTargetY, 31, 30, 1);
ctx.strokeStyle = '#2e4053';
ctx.lineWidth = 11;
ctx.beginPath();
ctx.moveTo(sk.shoulderX, sk.shoulderY + 4);
ctx.lineTo(arm.jx, arm.jy);
ctx.lineTo(handTargetX, handTargetY);
ctx.stroke();
// Hand
ctx.fillStyle = '#e59866';
ctx.beginPath();
ctx.arc(handTargetX, handTargetY, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawUmbrella() {
const u = umbrella;
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const sx = (u.crown.x - u.handle.x) / u.shaftLen;
const sy = (u.crown.y - u.handle.y) / u.shaftLen;
const nx = -sy, ny = sx;
// 1. Back stretcher struts (behind shaft)
ctx.strokeStyle = 'rgba(189, 195, 199, 0.75)';
ctx.lineWidth = 1.8;
ctx.beginPath();
for (const r of u.ribs) {
ctx.moveTo(u.runner.x, u.runner.y);
ctx.lineTo(r.mid.x, r.mid.y);
}
ctx.stroke();
// 2. Main Shaft & J-hook Handle
ctx.strokeStyle = '#bdc3c7';
ctx.lineWidth = 3.2;
ctx.beginPath();
ctx.moveTo(u.handle.x, u.handle.y);
ctx.lineTo(u.crown.x + sx * 12, u.crown.y + sy * 12); // ferrule spike
ctx.stroke();
// Runner ring
ctx.fillStyle = '#7f8c8d';
ctx.beginPath();
ctx.arc(u.runner.x, u.runner.y, 4, 0, Math.PI * 2);
ctx.fill();
// Wooden J-handle curve
ctx.strokeStyle = '#a04000';
ctx.lineWidth = 5.5;
ctx.beginPath();
ctx.moveTo(u.handle.x + sx * 12, u.handle.y + sy * 12);
ctx.lineTo(u.handle.x, u.handle.y);
ctx.arc(u.handle.x - nx * 8, u.handle.y - ny * 8, 8, Math.atan2(ny, nx), Math.atan2(ny, nx) + Math.PI, false);
ctx.stroke();
// 3. Canopy Gore Panels (4 panels between 5 ribs)
const panelColors = u.invertedState
? ['#922b21', '#a93226', '#922b21', '#7b241c'] // darker exposed underside when inverted
: ['#c0392b', '#e74c3c', '#d98880', '#b03a2e']; // vibrant lit exterior
for (let i = 0; i < u.ribs.length - 1; i++) {
const r1 = u.ribs[i];
const r2 = u.ribs[i + 1];
ctx.fillStyle = panelColors[i];
ctx.beginPath();
ctx.moveTo(u.crown.x, u.crown.y);
ctx.quadraticCurveTo(r1.mid.x, r1.mid.y, r1.tip.x, r1.tip.y);
// Scalloped fabric edge between adjacent rib tips
const midTipX = (r1.tip.x + r2.tip.x) * 0.5 + sx * 5;
const midTipY = (r1.tip.y + r2.tip.y) * 0.5 + sy * 5;
ctx.quadraticCurveTo(midTipX, midTipY, r2.tip.x, r2.tip.y);
ctx.quadraticCurveTo(r2.mid.x, r2.mid.y, u.crown.x, u.crown.y);
ctx.closePath();
ctx.fill();
// Subtle gore seam line
ctx.strokeStyle = 'rgba(40, 10, 10, 0.35)';
ctx.lineWidth = 1.2;
ctx.stroke();
}
// 4. Articulated Ribs (visible especially when inverted or flexed)
ctx.strokeStyle = u.invertedState ? '#ecf0f1' : 'rgba(20, 20, 20, 0.45)';
ctx.lineWidth = u.invertedState ? 2.2 : 1.5;
ctx.beginPath();
for (const r of u.ribs) {
ctx.moveTo(u.crown.x, u.crown.y);
ctx.lineTo(r.mid.x, r.mid.y);
ctx.lineTo(r.tip.x, r.tip.y);
}
ctx.stroke();
// Rib tip beads
ctx.fillStyle = '#ecf0f1';
for (const r of u.ribs) {
ctx.beginPath();
ctx.arc(r.tip.x, r.tip.y, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// --- Main Loop ---
let lastTime = performance.now();
function loop(now) {
const dt = Math.min(0.033, (now - lastTime) / 1000);
lastTime = now;
updatePhysics(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
It nails the bistable snap of the runner and stretchers when a gust catches under the windward rim—the sudden transition from dome to concave parachute spike feels right, as does the person dropping their center of gravity, widening their stance, and fighting wrist torque before the handle finally rips free in hurricane gusts. What it gets wrong is that real umbrella inversions in a gale are almost always asymmetrical first: one or two windward ribs fold back into a mangled half-sail while the leeward side stays cupped, whereas here the ribs share a single sliding runner scalar and pop inside-out together, and the fabric between the ribs is drawn with quadratic curves rather than solving true cloth hoop tension that prevents ribs from scissoring past each other.