16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You build physics toys, and you have held an umbrella in a gale.
Build me a single HTML page: a person holding an umbrella in wind that keeps getting stronger.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: what the canopy is made of in your code, what inversion is as a state change, and what decides when it happens.
The single fenced html block.
Where it looks right and where it looks wrong. Everyone who has held an umbrella knows what one does in wind, so say what yours gets wrong before I find it.
The umbrella canopy is modeled as a bistable elastic arch supported by flexible ribs flanking a rigid central shaft. Aerodynamic load is computed along the canopy arc by sampling differential wind pressure ($q \propto (\mathbf{v}{\text{wind}} \cdot \mathbf{\hat{n}})|\mathbf{v}{\text{wind}} \cdot \mathbf{\hat{n}}|$), resolving into net drag, lift, and an overturning moment about the runner. Inversion is a snap-through buckling transition: the canopy maintains a dome equilibrium via simulated rib pre-tension; when positive pressure on the concave under-surface or extreme aerodynamic moment exceeds the rib yield threshold, the spring equilibrium snaps over-center into a reversed, flared stable state. In this inverted state, rib stiffness degrades, flutter amplitude multiplies, and aerodynamic drag surges. If the resultant force on the handle exceeds the person's dynamic grip limit—accelerated by sudden gust shears—the handle slips from the hand, transitioning the umbrella into an unconstrained 3-DOF tumbling rigid body swept downstream by drag and gravity.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gale & Umbrella</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
body {
background: #0f1318;
color: #d0d7de;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
#canvas-container {
flex: 1;
position: relative;
width: 100%;
height: 100%;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#ui {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(18, 24, 33, 0.82);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
padding: 14px 20px;
display: flex;
gap: 20px;
align-items: center;
backdrop-filter: blur(8px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
z-index: 10;
}
.ui-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.label-row {
display: flex;
justify-content: space-between;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #8b949e;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 180px;
height: 5px;
background: #2a3340;
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: #58a6ff;
cursor: pointer;
box-shadow: 0 0 8px rgba(88, 166, 255, 0.6);
}
.readout {
font-size: 20px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #f0f6fc;
min-width: 90px;
}
.unit {
font-size: 11px;
font-weight: 400;
color: #8b949e;
margin-left: 2px;
}
button {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 8px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
button:hover {
background: #30363d;
border-color: #8b949e;
color: #fff;
}
button.active {
background: #1f6feb;
border-color: #58a6ff;
color: #fff;
}
#status-banner {
position: absolute;
top: 24px;
left: 24px;
font-size: 13px;
line-height: 1.5;
color: #8b949e;
pointer-events: none;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
#status-banner strong {
color: #f0f6fc;
font-size: 15px;
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas id="c"></canvas>
<div id="status-banner">
<div><strong id="beaufort-desc">Moderate Breeze</strong></div>
<div id="state-desc">Holding ground</div>
</div>
<div id="ui">
<div class="ui-group">
<div class="label-row">
<span>Wind Speed</span>
<span id="auto-tag">Auto: ON</span>
</div>
<input type="range" id="windSlider" min="0" max="80" step="0.5" value="18">
</div>
<div class="readout">
<span id="speedVal">18.0</span><span class="unit">mph</span>
</div>
<button id="btnToggleAuto" class="active">Auto Wind</button>
<button id="btnReset">Reset Umbrella</button>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let width = 0;
let height = 0;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// --- Simulation State ---
let autoWind = true;
let baseWindMph = 18.0;
let currentWindMph = 18.0;
let gust = 0;
let gustTimer = 0;
const slider = document.getElementById('windSlider');
const speedVal = document.getElementById('speedVal');
const autoTag = document.getElementById('auto-tag');
const btnToggleAuto = document.getElementById('btnToggleAuto');
const btnReset = document.getElementById('btnReset');
const beaufortDesc = document.getElementById('beaufort-desc');
const stateDesc = document.getElementById('state-desc');
slider.addEventListener('input', (e) => {
baseWindMph = parseFloat(e.target.value);
autoWind = false;
btnToggleAuto.classList.remove('active');
autoTag.textContent = "Auto: OFF";
});
btnToggleAuto.addEventListener('click', () => {
autoWind = !autoWind;
btnToggleAuto.classList.toggle('active', autoWind);
autoTag.textContent = autoWind ? "Auto: ON" : "Auto: OFF";
});
btnReset.addEventListener('click', () => resetUmbrella());
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === 'd') {
baseWindMph = Math.min(80, baseWindMph + 2);
slider.value = baseWindMph;
autoWind = false;
btnToggleAuto.classList.remove('active');
autoTag.textContent = "Auto: OFF";
} else if (e.key === 'ArrowLeft' || e.key === 'a') {
baseWindMph = Math.max(0, baseWindMph - 2);
slider.value = baseWindMph;
autoWind = false;
btnToggleAuto.classList.remove('active');
autoTag.textContent = "Auto: OFF";
} else if (e.key === 'r') {
resetUmbrella();
} else if (e.key === ' ') {
autoWind = !autoWind;
btnToggleAuto.classList.toggle('active', autoWind);
autoTag.textContent = autoWind ? "Auto: ON" : "Auto: OFF";
}
});
// Rain particles
const RAIN_COUNT = 450;
const rain = [];
for (let i = 0; i < RAIN_COUNT; i++) {
rain.push({
x: Math.random() * 2000 - 500,
y: Math.random() * 1000,
z: 0.5 + Math.random() * 0.5,
len: 12 + Math.random() * 10
});
}
// Splashes
const splashes = [];
function createSplash(x, y, vx, vy) {
if (splashes.length > 120) splashes.shift();
splashes.push({
x, y,
vx: (Math.random() - 0.5) * 40 + vx * 0.2,
vy: -Math.random() * 50 - 20,
life: 1.0
});
}
// Person & Umbrella Rig
const person = {
x: 0,
y: 0,
torsoAngle: 0,
leanTarget: 0,
handX: 0,
handY: 0,
braced: false,
gripStrength: 1.0
};
const umbrella = {
attached: true,
inverted: false,
inversionProgress: 0, // 0 = normal, 1 = inverted
shaftAngle: -0.35, // radians relative to vertical (pointing up-left)
shaftAngularVel: 0,
flex: 0, // canopy tip backward displacement
flexVel: 0,
flutter: 0,
// Free flight kinematics when ripped away
freeX: 0,
freeY: 0,
freeVx: 0,
freeVy: 0,
freeRot: 0,
freeVrot: 0,
length: 82,
canopyRadius: 52
};
function resetUmbrella() {
umbrella.attached = true;
umbrella.inverted = false;
umbrella.inversionProgress = 0;
umbrella.shaftAngle = -0.35;
umbrella.shaftAngularVel = 0;
umbrella.flex = 0;
umbrella.flexVel = 0;
person.gripStrength = 1.0;
}
function getBeaufort(mph) {
if (mph < 1) return "Calm";
if (mph < 4) return "Light Air";
if (mph < 8) return "Light Breeze";
if (mph < 13) return "Gentle Breeze";
if (mph < 19) return "Moderate Breeze";
if (mph < 25) return "Fresh Breeze";
if (mph < 32) return "Strong Breeze";
if (mph < 39) return "Near Gale";
if (mph < 47) return "Gale";
if (mph < 55) return "Strong Gale";
if (mph < 64) return "Storm";
if (mph < 73) return "Violent Storm";
return "Hurricane Force";
}
let simTime = 0;
let lastTimestamp = performance.now();
function update(dt) {
simTime += dt;
// Wind Progression (auto mode slowly ramps up and introduces gust dynamics)
if (autoWind) {
// Slow ramping progression towards gale, then fluctuates
baseWindMph = 16 + (Math.sin(simTime * 0.08) * 0.5 + 0.5) * 52;
slider.value = baseWindMph.toFixed(1);
}
// Gust noise model
gustTimer += dt;
const gustNoise = Math.sin(gustTimer * 2.4) * 0.5 + Math.sin(gustTimer * 5.1) * 0.25;
const gustEnvelope = Math.max(0, Math.sin(gustTimer * 0.45));
gust = gustNoise * (baseWindMph * 0.4) * gustEnvelope;
currentWindMph = Math.max(0, baseWindMph + gust);
const windSpeedMps = currentWindMph * 0.44704; // m/s
const windPx = currentWindMph * 24; // Visual horizontal speed px/s
// Person layout anchor
const groundY = height * 0.82;
person.x = width * 0.38;
person.y = groundY;
// Umbrella normal / apex reference
let shaftDirX, shaftDirY, apexX, apexY;
if (umbrella.attached) {
// Hand tracking & dynamic body compensation
// Person leans into wind depending on aerodynamic load
const dragFactor = (currentWindMph / 50);
person.leanTarget = Math.max(-0.65, -0.08 - dragFactor * 0.45);
person.torsoAngle += (person.leanTarget - person.torsoAngle) * (4.0 * dt);
// Person hip / shoulder / hand kinematics
const hipX = person.x;
const hipY = person.y - 74;
const shoulderX = hipX + Math.sin(person.torsoAngle) * 62;
const shoulderY = hipY - Math.cos(person.torsoAngle) * 62;
// Arm reaches forward & up to hold shaft
// If wind is fierce, two hands lock tight and raise closer to body
const armReach = 48;
const armAngle = person.torsoAngle - 0.65 - (currentWindMph > 35 ? 0.2 : 0);
person.handX = shoulderX + Math.sin(armAngle) * armReach;
person.handY = shoulderY - Math.cos(armAngle) * armReach + 4;
// Shaft Physics (Torque from aerodynamic pressure)
// Wind acts mostly horizontally from left to right: (+windPx, down slight downdraft)
shaftDirX = Math.sin(umbrella.shaftAngle);
shaftDirY = -Math.cos(umbrella.shaftAngle);
apexX = person.handX + shaftDirX * umbrella.length;
apexY = person.handY + shaftDirY * umbrella.length;
// Canopy normal vector (perpendicular to canopy base chord)
// Base orientation points along shaft
const canopyNormalX = shaftDirX;
const canopyNormalY = shaftDirY;
// Relative angle between wind vector (1, 0.15) and shaft
// Wind pushing on front/back of umbrella
const windDotNormal = 1.0 * canopyNormalX + 0.15 * canopyNormalY;
const aeroDynamicDynamicPressure = 0.5 * 1.2 * (windSpeedMps * windSpeedMps);
// Moment of wind on the shaft about hand:
// Airflow catches the rim or face, rotating umbrella downwind (positive angle)
const catchProfile = umbrella.inverted ? 1.6 : (windDotNormal > 0 ? 0.85 : 1.3);
const windTorque = (windSpeedMps * 0.32) * (1.1 - shaftDirX) * catchProfile;
// Restoring muscle torque: person tries to brace umbrella facing into wind (-45 deg = -0.78 rad)
const targetHoldingAngle = -0.68 + (currentWindMph > 40 ? 0.25 : 0.0);
const muscleStiffness = currentWindMph > 45 ? 65.0 : 45.0;
const muscleDamping = 12.0;
const angleErr = targetHoldingAngle - umbrella.shaftAngle;
const muscleTorque = angleErr * muscleStiffness;
// Angular acceleration of shaft
const netTorque = muscleTorque + windTorque * 30.0;
const shaftInertia = 2.4;
umbrella.shaftAngularVel += (netTorque / shaftInertia) * dt;
umbrella.shaftAngularVel -= umbrella.shaftAngularVel * muscleDamping * dt;
umbrella.shaftAngle += umbrella.shaftAngularVel * dt;
// Canopy flex and Inversion logic
// Under high wind, ribs flex backward. If wind catches underside (windDotNormal > -0.15)
// and speed exceeds 34 mph, rib buckle threshold is breached.
const ribFlexForce = aeroDynamicDynamicPressure * (umbrella.inverted ? 0.04 : 0.025);
const ribSpringK = umbrella.inverted ? 28.0 : 90.0;
umbrella.flexVel += (ribFlexForce - umbrella.flex * ribSpringK) * dt;
umbrella.flexVel *= Math.exp(-8.0 * dt);
umbrella.flex += umbrella.flexVel * dt;
// Inversion snap trigger
const catchUnderside = (canopyNormalX > -0.2); // Shaft blown back or tilted wrong
const inversionThreshold = 38.0; // mph base threshold
if (!umbrella.inverted) {
if (currentWindMph > inversionThreshold && (catchUnderside || currentWindMph > 52)) {
umbrella.inverted = true;
// violent snap jerk
umbrella.shaftAngularVel += 8.5;
umbrella.flex = 1.4;
}
}
// Smooth inversion visual transition
const targetInvProg = umbrella.inverted ? 1.0 : 0.0;
umbrella.inversionProgress += (targetInvProg - umbrella.inversionProgress) * Math.min(1.0, 16.0 * dt);
// Flutter oscillation
const flutterFreq = umbrella.inverted ? 32.0 : 18.0;
const flutterAmp = (currentWindMph / 40.0) * (umbrella.inverted ? 8.0 : 2.5);
umbrella.flutter = Math.sin(simTime * flutterFreq) * flutterAmp;
// Grip detachment check
// Net drag force pulling on the hand
const totalPullForce = aeroDynamicDynamicPressure * (umbrella.inverted ? 1.8 : 1.1) + Math.abs(windTorque) * 4.0;
const maxGrip = 420.0; // Max tensile grip in simulation units
if (totalPullForce > maxGrip || (currentWindMph > 62 && Math.random() < 0.015)) {
// Rip away from hand!
umbrella.attached = false;
umbrella.freeX = person.handX;
umbrella.freeY = person.handY;
umbrella.freeVx = windPx * 0.45 + (Math.random() - 0.5) * 80;
umbrella.freeVy = -80 - Math.random() * 80;
umbrella.freeRot = umbrella.shaftAngle;
umbrella.freeVrot = umbrella.shaftAngularVel * 1.5 + (Math.random() * 6 + 4);
}
} else {
// Free flying umbrella kinematics
umbrella.freeVy += 380 * dt; // gravity
// Drag pushes it toward wind velocity
umbrella.freeVx += (windPx - umbrella.freeVx) * 1.8 * dt;
umbrella.freeX += umbrella.freeVx * dt;
umbrella.freeY += umbrella.freeVy * dt;
umbrella.freeRot += umbrella.freeVrot * dt;
umbrella.freeVrot *= Math.exp(-0.4 * dt);
// Person recovers stance when burden is gone
person.torsoAngle += (-0.08 - person.torsoAngle) * (3.0 * dt);
}
// Rain simulation & collisions
for (let i = 0; i < rain.length; i++) {
const p = rain[i];
const fallSpeed = 550 * p.z;
const driftSpeed = windPx * (0.85 + p.z * 0.25);
p.x += driftSpeed * dt;
p.y += fallSpeed * dt;
// Check collision against umbrella canopy if umbrella is still near
if (umbrella.attached && p.y > apexY - 30 && p.y < apexY + 50 && p.x > apexX - 60 && p.x < apexX + 60) {
// Approximate canopy shield line
const dx = p.x - apexX;
const dy = p.y - apexY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < umbrella.canopyRadius) {
createSplash(p.x, p.y, driftSpeed * 0.2, -50);
p.y = -20;
p.x = Math.random() * (width + 400) - 400;
}
}
// Ground splash
if (p.y > groundY + 12) {
if (Math.random() < 0.25) createSplash(p.x, groundY + 10, driftSpeed * 0.15, -40);
p.y = -20;
p.x = Math.random() * (width + 600) - 400;
}
// Recycle horizontal wrap
if (p.x > width + 200) {
p.x = -200;
p.y = Math.random() * groundY;
}
}
// Update splash particles
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 += 450 * dt;
s.life -= dt * 2.8;
if (s.life <= 0) splashes.splice(i, 1);
}
// UI Updates
speedVal.textContent = currentWindMph.toFixed(1);
beaufortDesc.textContent = getBeaufort(currentWindMph);
if (!umbrella.attached) {
stateDesc.innerHTML = `<span style="color:#ff7b72">Umbrella torn away! (Press R or Reset)</span>`;
} else if (umbrella.inverted) {
stateDesc.innerHTML = `<span style="color:#f0883e">Canopy inside out! Extreme drag</span>`;
} else if (currentWindMph > 35) {
stateDesc.innerHTML = `Bracing hard against gale force`;
} else {
stateDesc.innerHTML = `Shielding against rain`;
}
}
// --- Rendering ---
function draw() {
ctx.clearRect(0, 0, width, height);
// Moody Storm Gradient Background
const sky = ctx.createLinearGradient(0, 0, 0, height);
const darkness = Math.min(0.4, currentWindMph / 120);
sky.addColorStop(0, `rgb(${18 - darkness * 20}, ${23 - darkness * 20}, ${32 - darkness * 30})`);
sky.addColorStop(0.7, `rgb(${28 - darkness * 20}, ${36 - darkness * 25}, ${46 - darkness * 35})`);
sky.addColorStop(1, '#0b0e13');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, width, height);
// Wind streaks in background
ctx.strokeStyle = `rgba(180, 215, 255, ${Math.min(0.25, currentWindMph * 0.0035)})`;
ctx.lineWidth = 1;
const streakCount = Math.floor(currentWindMph * 0.4);
for (let i = 0; i < streakCount; i++) {
const sx = ((simTime * 900 + i * 163) % (width + 500)) - 250;
const sy = (i * 47) % (height * 0.75);
const slen = 40 + (currentWindMph * 1.8);
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + slen, sy + slen * 0.12);
ctx.stroke();
}
const groundY = height * 0.82;
// Ground wet tarmac with reflection glow
const gGrad = ctx.createLinearGradient(0, groundY, 0, height);
gGrad.addColorStop(0, '#151b24');
gGrad.addColorStop(0.12, '#0f141b');
gGrad.addColorStop(1, '#07090d');
ctx.fillStyle = gGrad;
ctx.fillRect(0, groundY, width, height - groundY);
// Wet reflective sheen line
ctx.strokeStyle = 'rgba(110, 140, 180, 0.28)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, groundY + 1);
ctx.lineTo(width, groundY + 1);
ctx.stroke();
// Render Person
drawPerson(ctx, groundY);
// Render Umbrella
drawUmbrella(ctx);
// Render Rain
ctx.lineWidth = 1.5;
for (let i = 0; i < rain.length; i++) {
const p = rain[i];
const windAngle = Math.atan2(550 * p.z, currentWindMph * 24 * (0.85 + p.z * 0.25));
const rlen = p.len * (1 + currentWindMph * 0.02);
const rx1 = p.x;
const ry1 = p.y;
const rx2 = p.x - Math.cos(windAngle) * rlen;
const ry2 = p.y - Math.sin(windAngle) * rlen;
ctx.strokeStyle = `rgba(175, 205, 240, ${0.18 + p.z * 0.25})`;
ctx.beginPath();
ctx.moveTo(rx1, ry1);
ctx.lineTo(rx2, ry2);
ctx.stroke();
}
// Render Splashes
ctx.fillStyle = 'rgba(185, 215, 255, 0.65)';
for (let i = 0; i < splashes.length; i++) {
const s = splashes[i];
ctx.beginPath();
ctx.arc(s.x, s.y, Math.max(0.8, 2.2 * s.life), 0, Math.PI * 2);
ctx.fill();
}
}
function drawPerson(ctx, groundY) {
ctx.save();
const px = person.x;
const py = person.y;
const lean = person.torsoAngle;
// Head, torso, legs coords
const ankleLX = px - 18;
const ankleRX = px + 14;
const footY = py;
const hipX = px + Math.sin(lean) * 12;
const hipY = py - 72;
const shoulderX = hipX + Math.sin(lean) * 58;
const shoulderY = hipY - Math.cos(lean) * 58;
const headX = shoulderX + Math.sin(lean * 1.1) * 20;
const headY = shoulderY - Math.cos(lean * 1.1) * 20;
// Rear / Left Leg (bracing backward against wind)
ctx.strokeStyle = '#1b222c';
ctx.lineWidth = 10;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(hipX, hipY);
ctx.lineTo(ankleLX - 12, py - 36); // knee
ctx.lineTo(ankleLX, footY); // back foot planted
ctx.stroke();
// Front / Right Leg (bent forward, absorbing load)
ctx.strokeStyle = '#222b37';
ctx.lineWidth = 11;
ctx.beginPath();
ctx.moveTo(hipX, hipY);
ctx.lineTo(ankleRX + 8, py - 38); // forward knee
ctx.lineTo(ankleRX + 10, footY); // front foot
ctx.stroke();
// Shoes
ctx.strokeStyle = '#0a0d11';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(ankleLX - 8, footY);
ctx.lineTo(ankleLX + 12, footY);
ctx.moveTo(ankleRX + 2, footY);
ctx.lineTo(ankleRX + 24, footY);
ctx.stroke();
// Flapping Coat Tails (wind-reactive geometry)
const coatTailWave = Math.sin(simTime * (14 + currentWindMph * 0.35)) * (currentWindMph * 0.4);
ctx.fillStyle = '#1c2633';
ctx.beginPath();
ctx.moveTo(shoulderX - 4, shoulderY + 8);
ctx.lineTo(hipX - 8, hipY);
// Coat billowing downstream (+x)
ctx.lineTo(hipX + 38 + coatTailWave * 1.2, hipY + 34 + coatTailWave * 0.4);
ctx.lineTo(hipX + 24, hipY + 44);
ctx.lineTo(hipX, hipY + 12);
ctx.closePath();
ctx.fill();
// Torso (Raincoat)
ctx.strokeStyle = '#2a3749';
ctx.lineWidth = 18;
ctx.beginPath();
ctx.moveTo(hipX, hipY);
ctx.lineTo(shoulderX, shoulderY);
ctx.stroke();
// Head / Hood
ctx.fillStyle = '#202a37';
ctx.beginPath();
ctx.arc(headX, headY, 13, 0, Math.PI * 2);
ctx.fill();
// Face / Shadow inside hood
ctx.fillStyle = '#e0b59b';
ctx.beginPath();
// Leaning head tilts down against the storm
ctx.arc(headX + Math.sin(lean) * 3 - 2, headY + 3, 6, 0, Math.PI * 2);
ctx.fill();
// Hood rim shadow
ctx.fillStyle = '#18212c';
ctx.beginPath();
ctx.arc(headX - 1, headY - 1, 13.5, 0.4, Math.PI * 1.4, true);
ctx.fill();
// Arms holding umbrella
if (umbrella.attached) {
const hx = person.handX;
const hy = person.handY;
// Back arm
ctx.strokeStyle = '#1d2735';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(shoulderX, shoulderY);
ctx.lineTo((shoulderX + hx) * 0.5 - 6, (shoulderY + hy) * 0.5 + 10);
ctx.lineTo(hx, hy);
ctx.stroke();
// Front arm (straining)
ctx.strokeStyle = '#2c3b50';
ctx.lineWidth = 9;
ctx.beginPath();
ctx.moveTo(shoulderX + 4, shoulderY + 2);
ctx.lineTo((shoulderX + hx) * 0.5 + 4, (shoulderY + hy) * 0.5 + 6);
ctx.lineTo(hx, hy);
ctx.stroke();
// Hands clamped on handle
ctx.fillStyle = '#d49b78';
ctx.beginPath();
ctx.arc(hx, hy, 5.5, 0, Math.PI * 2);
ctx.fill();
} else {
// Reaching / reeling arms after losing umbrella
ctx.strokeStyle = '#2c3b50';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(shoulderX, shoulderY);
ctx.lineTo(shoulderX + 28, shoulderY - 18);
ctx.stroke();
}
ctx.restore();
}
function drawUmbrella(ctx) {
ctx.save();
let rootX, rootY, angle, flex;
if (umbrella.attached) {
rootX = person.handX;
rootY = person.handY;
angle = umbrella.shaftAngle;
flex = umbrella.flex;
} else {
rootX = umbrella.freeX;
rootY = umbrella.freeY;
angle = umbrella.freeRot;
flex = umbrella.inverted ? 1.3 : 0.2;
}
ctx.translate(rootX, rootY);
ctx.rotate(angle);
const len = umbrella.length;
const inv = umbrella.inversionProgress; // 0 = normal, 1 = inverted
const flutter = umbrella.flutter;
// Curved handle (J-hook at hand)
ctx.strokeStyle = '#15191e';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(-5, 6, 6, 0, Math.PI, false);
ctx.stroke();
// Central Metal Shaft
ctx.strokeStyle = '#b0b8c2';
ctx.lineWidth = 3.5;
ctx.beginPath();
ctx.moveTo(0, 6);
ctx.lineTo(0, -len);
ctx.stroke();
// Runner & Ferrule (metallic details)
ctx.fillStyle = '#7a8593';
ctx.fillRect(-3, -len * 0.52, 6, 6);
ctx.fillStyle = '#cbd5e1';
ctx.fillRect(-2, -len - 4, 4, 5); // top ferrule cap
// Canopy Rib and Fabric Geometry
// Apex is at (0, -len)
const apexY = -len;
const canopySpan = umbrella.canopyRadius;
// Dynamic dome height:
// When normal: dome bows upward/forward (negative relative Y)
// When inverted: dome flares violently backward/downward (positive relative Y)
const normalDomeH = 26;
const invertedDomeH = -34;
const currentDomeH = normalDomeH * (1 - inv) + invertedDomeH * inv;
// Left / Right tip coordinates relative to apex
// Flex pulls tips downwind (+X relative or angled)
const flexX = flex * 12;
const flexY = flex * 22;
const tipLeftX = -canopySpan + flexX * 0.4;
const tipLeftY = apexY + currentDomeH + (inv * 18) + flutter * 0.8;
const tipRightX = canopySpan + flexX;
const tipRightY = apexY + currentDomeH + (inv * -12) - flutter * 0.6;
// Mid ribs for 3D curved canopy appearance
const ribMidLX = tipLeftX * 0.52;
const ribMidLY = apexY + currentDomeH * 0.85 + flutter * 0.4;
const ribMidRX = tipRightX * 0.52;
const ribMidRY = apexY + currentDomeH * 0.85 - flutter * 0.3;
// Stretchers (struts connecting runner to ribs)
const runnerY = -len * 0.52;
ctx.strokeStyle = '#636e7b';
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(0, runnerY);
ctx.lineTo(ribMidLX, ribMidLY + 4);
ctx.moveTo(0, runnerY);
ctx.lineTo(ribMidRX, ribMidRY + 4);
ctx.stroke();
// Fabric Panels
// Distinct shaded panels give depth and convey tension / inversion state
const fabricBackDark = umbrella.inverted ? '#922020' : '#14304f';
const fabricMid = umbrella.inverted ? '#c0392b' : '#1f4e79';
const fabricFrontLight = umbrella.inverted ? '#e74c3c' : '#296ca8';
// Back Panel (Left)
ctx.fillStyle = fabricBackDark;
ctx.beginPath();
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(ribMidLX * 0.7, apexY - 4 * (1 - inv), ribMidLX, ribMidLY);
ctx.quadraticCurveTo((ribMidLX + tipLeftX) * 0.5, ribMidLY + 8 * (1 - inv), tipLeftX, tipLeftY);
ctx.quadraticCurveTo(tipLeftX * 0.4, apexY + 14 * (1 - inv), 0, apexY);
ctx.fill();
// Center Main Panel
ctx.fillStyle = fabricMid;
ctx.beginPath();
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(ribMidLX * 0.5, apexY - 6 * (1 - inv), ribMidLX, ribMidLY);
ctx.quadraticCurveTo(0, apexY + currentDomeH * 1.05, ribMidRX, ribMidRY);
ctx.quadraticCurveTo(ribMidRX * 0.5, apexY - 6 * (1 - inv), 0, apexY);
ctx.fill();
// Front Panel (Right / Downwind side)
ctx.fillStyle = fabricFrontLight;
ctx.beginPath();
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(ribMidRX * 0.7, apexY - 4 * (1 - inv), ribMidRX, ribMidRY);
ctx.quadraticCurveTo((ribMidRX + tipRightX) * 0.5, ribMidRY + 8 * (1 - inv), tipRightX, tipRightY);
ctx.quadraticCurveTo(tipRightX * 0.4, apexY + 14 * (1 - inv), 0, apexY);
ctx.fill();
// Flexible Carbon/Steel Rib Edges (drawn on top of fabric)
ctx.strokeStyle = '#d1d8e0';
ctx.lineWidth = 1.4;
ctx.beginPath();
// Left Rib
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(ribMidLX * 0.6, apexY - 2 * (1 - inv), tipLeftX, tipLeftY);
// Center Rib
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(0, apexY + 4 * (1 - inv), (ribMidLX + ribMidRX) * 0.5, (ribMidLY + ribMidRY) * 0.5);
// Right Rib
ctx.moveTo(0, apexY);
ctx.quadraticCurveTo(ribMidRX * 0.6, apexY - 2 * (1 - inv), tipRightX, tipRightY);
ctx.stroke();
// Scalloped Rim Outline
ctx.strokeStyle = '#0e1724';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(tipLeftX, tipLeftY);
ctx.quadraticCurveTo((tipLeftX + ribMidLX) * 0.5, tipLeftY - 5 * (1 - inv), ribMidLX, ribMidLY);
ctx.quadraticCurveTo((ribMidLX + ribMidRX) * 0.5, ribMidLY - 6 * (1 - inv), ribMidRX, ribMidRY);
ctx.quadraticCurveTo((ribMidRX + tipRightX) * 0.5, ribMidRY - 5 * (1 - inv), tipRightX, tipRightY);
ctx.stroke();
// Rib tip balls
ctx.fillStyle = '#cbd5e1';
[ [tipLeftX, tipLeftY], [tipRightX, tipRightY], [ribMidLX, ribMidLY], [ribMidRX, ribMidRY] ].forEach(([bx, by]) => {
ctx.beginPath();
ctx.arc(bx, by, 2, 0, Math.PI * 2);
ctx.fill();
});
ctx.restore();
}
// Main Loop
function loop(timestamp) {
const dt = Math.min(0.05, (timestamp - lastTimestamp) / 1000);
lastTimestamp = timestamp;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Where it looks right:
Where it looks wrong: