27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 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
You build physics toys, and you have dropped a garden hose with the water on full.
Build me a single HTML page: a garden hose lying on a lawn, the spigot on, and the nozzle released.
#psi=40 sets the pressure to 40 psi with the nozzle free on load. Any number works.data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: how the hose discretization and inextensibility constraints are implemented, the formula for the nozzle reaction force as a function of psi, and ground friction handling.
The pressure in psi at which the hose transitions from stationary or settling to continuous whipping, as one number, derived from your model parameters.
The single fenced html block.
Where the motion looks like a real hose and where it does not. Name any artificial damping, injected noise or scripted oscillation. Everyone has watched a hose get away from somebody; say what yours gets wrong before I see it.
The hose is a 2D planar chain of 40 segments (2 m long) with 41 nodes lying in the lawn plane. The spigot is clamped: a fixed ghost node sits one segment behind node 0, so the spigot fixes both position and direction. Inextensibility is enforced by position-based distance projection: each segment is pulled back to length a = 0.05 m, mass-weighted, with 25 Gauss-Seidel iterations per 1/1200 s substep. Bending is a discrete elastic energy E = (K/2)Σθ² over the joints, with K = EI/a, EI = 0.02 N·m², and θ the signed turning angle at each joint. Its exact gradient gives explicit nodal forces, F = −Kθ∇θ. The nozzle reaction is a follower force along the current tip tangent, F_h = K_T·ψ with K_T = 0.0025 N/psi, pointing back along the hose. This is linear in ψ because ρAv² ∝ ψ when exit speed is v = 0.9√ψ m/s. The nozzle is tilted 25° up, so the reaction has a vertical part K_T·ψ·tan25° that presses the tip into the lawn and adds to its normal load. Ground friction is Coulomb-like, μ = 0.35, applied as a per-step displacement clamp of μ·N·dt² with N = m·g plus the nozzle's vertical load, before the constraint projection. Gravity acts only as the normal load, and the hose is restricted to the lawn plane.
≈ 40 psi. The model is a clamped-free column under a tangential follower load, the Beck column. Its undamped flutter bound is P_cr = 20.05·EI/L². Substituting the model parameters gives ψ_cr = 20.05·EI/(K_T·L²) = 20.05·0.02/(0.0025·4) ≈ 40.1 psi. This is an analytic estimate. I did not run the page. Friction and the added velocity damping will shift the observed onset.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hose on the lawn</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#27401e;font:13px/1.4 system-ui,sans-serif;color:#f2f2e6}
canvas{position:fixed;left:0;top:0;width:100%;height:100%;display:block;touch-action:none}
#bar{position:fixed;left:0;right:0;bottom:0;display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:10px 16px;background:rgba(10,18,8,.72)}
#bar input[type=range]{width:min(420px,60vw)}
#bar button{background:#3b6b2c;color:#fff;border:0;border-radius:6px;padding:6px 12px;cursor:pointer}
#psiOut{font-variant-numeric:tabular-nums;min-width:96px}
#pred{opacity:.8}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="bar">
<label>Pressure <input id="psi" type="range" min="0" max="60" step="0.5" value="0"></label>
<span id="psiOut">0.0 psi</span>
<button id="sweep">Sweep 0→60→0</button>
<button id="reset">Reset hose</button>
<span id="pred"></span>
<span>Click and drag the hose to pin a point. Release to free it.</span>
</div>
<script>
(() => {
// ---------- parameters ----------
const L = 2.0, N = 40, SEG = L / N; // hose length (m), segments, segment length
const EI = 0.02, K = EI / SEG; // bending stiffness (N m^2), joint stiffness
const MU_L = 0.05, MASS = MU_L * SEG; // linear density (kg/m), node mass
const GRAV = 9.8, MU_F = 0.35, CDAMP = 0.3; // gravity, ground friction, velocity damping (1/s)
const KT = 0.0025; // in-plane nozzle thrust per psi (N/psi)
const ALPHA = 25 * Math.PI / 180; // nozzle elevation
const CA = Math.cos(ALPHA), SA = Math.sin(ALPHA), TA = Math.tan(ALPHA);
const VCOEF = 0.9; // exit speed = VCOEF*sqrt(psi) m/s (visual/kinematic)
const NOZ_H = 0.03, RAD = 0.012; // nozzle height, hose radius
const DT = 1 / 1200, ITERS = 25;
const PSI_MAX = 60;
const PSI_PRED = 20.05 * EI / (KT * L * L); // Beck-column flutter bound
const GX = -SEG, GZ = 0; // ghost node clamping the spigot direction
// ---------- state ----------
const px = new Float64Array(N + 1), pz = new Float64Array(N + 1);
const qx = new Float64Array(N + 1), qz = new Float64Array(N + 1);
const fx = new Float64Array(N + 1), fz = new Float64Array(N + 1);
let pinIdx = -1, tgx = 0, tgz = 0;
let psi = 0, sweeping = false, sweepT = 0;
let TV = 0, nozUx = 1, nozUz = 0, tipEma = 0;
let particles = [], splashes = [], spawnAcc = 0;
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
const slider = document.getElementById('psi');
const psiOut = document.getElementById('psiOut');
const sweepBtn = document.getElementById('sweep');
document.getElementById('pred').textContent = 'model onset ≈ ' + PSI_PRED.toFixed(0) + ' psi';
let W = 0, H = 0, DPR = 1, S = 200, OX = 100, OY = 380, grass = null;
function resize() {
DPR = Math.min(2, window.devicePixelRatio || 1);
W = innerWidth; H = innerHeight;
cv.width = W * DPR; cv.height = H * DPR;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
S = Math.min(W * 0.2, H * 0.45);
OX = W * 0.1; OY = H * 0.62;
buildLawn();
}
function buildLawn() {
grass = document.createElement('canvas');
grass.width = W * DPR; grass.height = H * DPR;
const g = grass.getContext('2d');
g.setTransform(DPR, 0, 0, DPR, 0, 0);
const grd = g.createLinearGradient(0, 0, 0, H);
grd.addColorStop(0, '#3d6b2e'); grd.addColorStop(1, '#2a4f20');
g.fillStyle = grd; g.fillRect(0, 0, W, H);
for (let i = 0; i < 2600; i++) {
const x = Math.random() * W, y = Math.random() * H, len = 4 + Math.random() * 7;
g.strokeStyle = Math.random() < 0.5 ? '#5a9642' : '#213f19';
g.globalAlpha = 0.5; g.lineWidth = 1.2;
g.beginPath(); g.moveTo(x, y);
g.lineTo(x + (Math.random() - 0.5) * 4, y - len); g.stroke();
}
g.globalAlpha = 1;
}
// ---------- projection (lawn plane x,z; height y) ----------
function proj(x, y, z) { return [OX + (x + 0.3 * z) * S, OY + 0.5 * z * S - y * S]; }
function toWorld(cx, cy) {
const z = (cy - OY + RAD * S) / (0.5 * S);
const x = (cx - OX) / S - 0.3 * z;
return [x, z];
}
// ---------- hose init / reset ----------
function resetHose() {
for (let i = 0; i <= N; i++) {
px[i] = i * SEG; pz[i] = 0.05 * Math.sin(0.35 * i);
qx[i] = px[i]; qz[i] = pz[i];
}
pinIdx = -1;
}
// ---------- physics ----------
function step(dt, psiNow) {
if (pinIdx > 0) { px[pinIdx] = tgx; pz[pinIdx] = tgz; qx[pinIdx] = tgx; qz[pinIdx] = tgz; }
fx.fill(0); fz.fill(0);
// discrete bending, one term per interior joint
for (let i = 0; i < N; i++) {
const ax = i === 0 ? GX : px[i - 1], az = i === 0 ? GZ : pz[i - 1];
const bx = px[i], bz = pz[i], cx = px[i + 1], cz = pz[i + 1];
const e1x = bx - ax, e1z = bz - az, e2x = cx - bx, e2z = cz - bz;
const l1 = e1x * e1x + e1z * e1z, l2 = e2x * e2x + e2z * e2z;
const th = Math.atan2(e1x * e2z - e1z * e2x, e1x * e2x + e1z * e2z);
const c = -K * th;
const gAx = -e1z / l1, gAz = e1x / l1;
const gCx = -e2z / l2, gCz = e2x / l2;
const gBx = -(gAx + gCx), gBz = -(gAz + gCz);
if (i > 0) { fx[i - 1] += c * gAx; fz[i - 1] += c * gAz; }
fx[i] += c * gBx; fz[i] += c * gBz;
fx[i + 1] += c * gCx; fz[i + 1] += c * gCz;
}
// nozzle reaction: follower force along the tip tangent, pushing back toward the spigot
{
const tx = px[N] - px[N - 1], tz = pz[N] - pz[N - 1];
const tl = Math.hypot(tx, tz) || 1e-9;
nozUx = tx / tl; nozUz = tz / tl;
const T = KT * psiNow;
fx[N] -= T * nozUx; fz[N] -= T * nozUz;
TV = T * TA; // vertical part of the nozzle reaction, pressing into the lawn
}
// explicit Verlet with damping, plus Coulomb-like ground friction on the predicted displacement
const damp = 1 - CDAMP * dt, dt2 = dt * dt;
for (let i = 1; i <= N; i++) {
if (i === pinIdx) continue;
const dispX = (px[i] - qx[i]) * damp + fx[i] / MASS * dt2;
const dispZ = (pz[i] - qz[i]) * damp + fz[i] / MASS * dt2;
const fr = MU_F * (GRAV + (i === N ? TV / MASS : 0)) * dt2;
const m = Math.hypot(dispX, dispZ);
let nx, nz;
if (m <= fr) { nx = px[i]; nz = pz[i]; }
else { const s = (m - fr) / m; nx = px[i] + dispX * s; nz = pz[i] + dispZ * s; }
qx[i] = px[i]; qz[i] = pz[i];
px[i] = nx; pz[i] = nz;
}
// inextensibility: mass-weighted distance projection
for (let it = 0; it < ITERS; it++) {
for (let i = 0; i < N; i++) {
const wi = (i === 0 || i === pinIdx) ? 0 : 1;
const wj = (i + 1 === pinIdx) ? 0 : 1;
const sum = wi + wj;
if (sum === 0) continue;
const dx = px[i + 1] - px[i], dz = pz[i + 1] - pz[i];
const len = Math.hypot(dx, dz) || 1e-12;
const diff = (len - SEG) / len;
const cx = dx * diff, cz = dz * diff;
px[i] += cx * wi / sum; pz[i] += cz * wi / sum;
px[i + 1] -= cx * wj / sum; pz[i + 1] -= cz * wj / sum;
}
}
const sp = Math.hypot(px[N] - qx[N], pz[N] - qz[N]) / dt;
tipEma += (sp - tipEma) * Math.min(1, dt / 0.5);
}
// ---------- water ----------
function spawn(psiNow) {
const v = VCOEF * Math.sqrt(psiNow);
const nx = CA * nozUx, ny = SA, nz = CA * nozUz; // current nozzle vector
const j = () => (Math.random() - 0.5) * 0.08 * v;
particles.push({
x: px[N] + nozUx * 0.03, y: NOZ_H, z: pz[N] + nozUz * 0.03,
vx: v * nx + j(), vy: v * ny + j(), vz: v * nz + j(), age: 0
});
}
function updateWater(dt, psiNow) {
if (psiNow > 0.2) {
spawnAcc += (40 + 160 * Math.sqrt(psiNow / PSI_MAX)) * dt;
while (spawnAcc >= 1) { spawnAcc -= 1; spawn(psiNow); }
} else spawnAcc = 0;
for (const p of particles) {
p.vy -= GRAV * dt;
p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt; p.age += dt;
if (p.y < 0) { splashes.push({ x: p.x, z: p.z, t: 0 }); p.dead = true; }
}
particles = particles.filter(p => !p.dead && p.age < 3);
for (const s of splashes) s.t += dt;
splashes = splashes.filter(s => s.t < 0.5);
}
// ---------- drawing ----------
function drawHose() {
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const [sx, sy] = proj(px[i] + 0.012, 0, pz[i] + 0.01);
i ? ctx.lineTo(sx, sy) : ctx.moveTo(sx, sy);
}
ctx.strokeStyle = 'rgba(0,0,0,0.28)'; ctx.lineWidth = 12; ctx.stroke();
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const [sx, sy] = proj(px[i], RAD, pz[i]);
i ? ctx.lineTo(sx, sy) : ctx.moveTo(sx, sy);
}
ctx.strokeStyle = '#1d5a28'; ctx.lineWidth = 11; ctx.stroke();
ctx.strokeStyle = '#3f9a4c'; ctx.lineWidth = 5; ctx.stroke();
ctx.save(); ctx.translate(0, -2); ctx.strokeStyle = 'rgba(200,255,200,0.35)'; ctx.lineWidth = 2; ctx.stroke(); ctx.restore();
// nozzle head, oriented along the nozzle tangent
const [tx, ty] = proj(px[N], RAD, pz[N]);
const [ax, ay] = proj(px[N] + nozUx * 0.05, RAD, pz[N] + nozUz * 0.05);
const ang = Math.atan2(ay - ty, ax - tx);
ctx.save(); ctx.translate(tx, ty); ctx.rotate(ang);
ctx.fillStyle = '#c9a227'; ctx.strokeStyle = '#6b5510'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.roundRect(-4, -6, 14, 12, 3); ctx.fill(); ctx.stroke();
ctx.restore();
// pin marker
if (pinIdx > 0) {
const [mx, my] = proj(px[pinIdx], RAD, pz[pinIdx]);
ctx.beginPath(); ctx.arc(mx, my, 6, 0, 7);
ctx.fillStyle = 'rgba(255,80,60,0.85)'; ctx.fill();
}
}
function drawFaucet() {
const [b0x, b0y] = proj(-0.1, 0, 0), [b1x, b1y] = proj(-0.1, 0.2, 0);
ctx.lineCap = 'round';
ctx.strokeStyle = '#2b2f33'; ctx.lineWidth = 9;
const [hx, hy] = proj(0, RAD, 0), [ax, ay] = proj(-0.1, 0.03, 0);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(hx, hy); ctx.stroke();
ctx.strokeStyle = '#8a9096'; ctx.lineWidth = 14;
ctx.beginPath(); ctx.moveTo(b0x, b0y); ctx.lineTo(b1x, b1y); ctx.stroke();
const [h0x, h0y] = proj(-0.14, 0.2, 0), [h1x, h1y] = proj(-0.06, 0.2, 0);
ctx.strokeStyle = psi > 0.2 ? '#3fae5a' : '#b33'; ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(h0x, h0y); ctx.lineTo(h1x, h1y); ctx.stroke();
}
function drawWater() {
for (const p of particles) {
const [sx, sy] = proj(p.x, p.y, p.z);
ctx.beginPath();
ctx.fillStyle = `rgba(160,215,255,${0.85 * (1 - p.age / 3)})`;
ctx.arc(sx, sy, 2.2, 0, 7); ctx.fill();
}
for (const s of splashes) {
const [sx, sy] = proj(s.x, 0, s.z);
ctx.beginPath();
ctx.strokeStyle = `rgba(190,230,255,${0.5 * (1 - s.t / 0.5)})`;
ctx.lineWidth = 1.5;
ctx.ellipse(sx, sy, 4 + s.t * 30, (4 + s.t * 30) * 0.5, 0, 0, 7);
ctx.stroke();
}
}
function drawGauge() {
const cx = W - 80, cy = 84, r = 54;
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.beginPath(); ctx.arc(cx, cy, r + 10, 0, 7); ctx.fill();
ctx.strokeStyle = '#ddd'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(cx, cy, r, Math.PI * 0.75, Math.PI * 2.25); ctx.stroke();
const f = Math.min(psi, PSI_MAX) / PSI_MAX;
const a = Math.PI * 0.75 + f * Math.PI * 1.5;
ctx.strokeStyle = '#ff6a3d'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(a) * (r - 8), cy + Math.sin(a) * (r - 8)); ctx.stroke();
ctx.fillStyle = '#f2f2e6'; ctx.font = '600 16px system-ui';
ctx.textAlign = 'center'; ctx.fillText(psi.toFixed(1), cx, cy + 30);
ctx.font = '11px system-ui'; ctx.fillText('psi', cx, cy + 44);
ctx.textAlign = 'left';
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.drawImage(grass, 0, 0, W, H);
drawFaucet();
drawHose();
drawWater();
drawGauge();
ctx.font = '13px system-ui'; ctx.fillStyle = 'rgba(255,255,240,0.9)';
ctx.fillText(`exit ${(VCOEF * Math.sqrt(psi)).toFixed(1)} m/s reaction ${(KT * psi).toFixed(3)} N tip motion ${tipEma.toFixed(2)} m/s`, 16, 24);
}
// ---------- loop ----------
let last = performance.now(), acc = 0;
function frame(now) {
const frameDt = Math.min(1 / 30, (now - last) / 1000); last = now;
if (sweeping) {
sweepT += frameDt;
const ph = (sweepT / 40) % 2;
psi = PSI_MAX * (ph < 1 ? ph : 2 - ph);
slider.value = psi;
}
acc += frameDt;
while (acc >= DT) { step(DT, psi); acc -= DT; }
updateWater(frameDt, psi);
psiOut.textContent = psi.toFixed(1) + ' psi';
draw();
requestAnimationFrame(frame);
}
// ---------- input ----------
slider.addEventListener('input', () => {
psi = parseFloat(slider.value);
sweeping = false; sweepBtn.textContent = 'Sweep 0→60→0';
});
sweepBtn.addEventListener('click', () => {
sweeping = !sweeping;
if (sweeping) {
const ph = Math.min(1, psi / PSI_MAX);
sweepT = 40 * ph;
sweepBtn.textContent = 'Stop sweep';
} else sweepBtn.textContent = 'Sweep 0→60→0';
});
document.getElementById('reset').addEventListener('click', resetHose);
cv.addEventListener('pointerdown', e => {
let best = -1, bd = 26;
for (let i = 1; i <= N; i++) {
const [sx, sy] = proj(px[i], RAD, pz[i]);
const d = Math.hypot(sx - e.clientX, sy - e.clientY);
if (d < bd) { bd = d; best = i; }
}
if (best > 0) {
pinIdx = best;
[tgx, tgz] = toWorld(e.clientX, e.clientY);
cv.setPointerCapture(e.pointerId);
}
});
cv.addEventListener('pointermove', e => {
if (pinIdx > 0) [tgx, tgz] = toWorld(e.clientX, e.clientY);
});
const release = () => { pinIdx = -1; };
cv.addEventListener('pointerup', release);
cv.addEventListener('pointercancel', release);
function readHash() {
const m = /psi=([0-9.]+)/.exec(location.hash);
if (m) {
const v = parseFloat(m[1]);
if (isFinite(v) && v >= 0) { psi = v; slider.value = Math.min(PSI_MAX, v); }
}
}
window.addEventListener('hashchange', readHash);
window.addEventListener('resize', resize);
// ---------- start ----------
resize();
resetHose();
readHash();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
I have not executed this file. The 40 psi figure is analytic, and the behaviour below is what the code should do, not something I measured.
Where it should look like a hose:
Where it does not:
Math.random(), but it touches only the spray and never the hose.