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 26 point masses (0.28 kg each) joined by 25 inextensible 0.30 m segments, integrated with Verlet integration at 240 Hz; after each step, 12 Gauss–Seidel iterations project every neighbour pair back to exactly 0.30 m (point 0 is hard-pinned at the spigot, and a dragged point is hard-pinned to the pointer), while a Laplacian bending force (k_b(p_{i-1}-2p_i+p_{i+1})) with (k_b=120\ \mathrm{N,m^{-1}}) resists kinking. The nozzle reaction is the momentum-flux thrust (F = \dot m v = 2 A P), with (P = \mathrm{psi}\times 6895\ \mathrm{Pa}) and an effective nozzle area (A = 1.3\times10^{-5}\ \mathrm{m^2}), giving (F \approx 0.179\ \mathrm{N/psi}) (7.2 N at 40 psi), applied to the last two masses opposite the current nozzle tangent. Ground friction is Coulomb plus viscous: each moving mass loses (\mu g,\Delta t) of speed per step with (\mu = 0.55), multiplied by (e^{-1.0\Delta t}), so motion decays to rest once thrust can no longer sustain buckling against friction and bending stiffness.
36 psi
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Runaway Garden Hose</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#2e5d23;
font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
#c{position:absolute;inset:0;display:block;touch-action:none;cursor:crosshair;}
#panel{position:absolute;top:14px;left:14px;width:308px;max-width:calc(100vw - 28px);
background:rgba(12,26,10,.82);color:#eef7e6;border:1px solid rgba(255,255,255,.18);
border-radius:14px;padding:14px 16px 12px;box-shadow:0 10px 30px rgba(0,0,0,.45);
backdrop-filter:blur(4px);user-select:none;-webkit-user-select:none;}
#panel h1{font-size:17px;margin:0 0 2px;letter-spacing:.3px;}
#panel .sub{font-size:12px;color:#bcd6a8;margin:0 0 10px;line-height:1.45;}
.row{display:flex;align-items:baseline;justify-content:space-between;margin:6px 0 2px;}
#psiVal{font-size:30px;font-weight:700;color:#fff;font-variant-numeric:tabular-nums;}
#psiVal small{font-size:13px;color:#bcd6a8;font-weight:400;}
#thrust{font-size:12px;color:#9fd0ff;font-variant-numeric:tabular-nums;}
input[type=range]{width:100%;margin:8px 0 2px;accent-color:#57b6ff;height:28px;cursor:pointer;}
.ticks{display:flex;justify-content:space-between;font-size:10px;color:#8fae7d;margin-bottom:8px;}
#status{display:inline-block;font-size:12px;font-weight:700;padding:3px 10px;border-radius:20px;
background:#3c6e2f;color:#dff2cf;letter-spacing:.4px;}
#status.whip{background:#b3372a;color:#ffe9e4;}
#status.flow{background:#2b5f8a;color:#dcefff;}
.btns{margin-top:10px;display:flex;gap:8px;}
button{flex:1;background:#41703a;color:#eef7e6;border:1px solid rgba(255,255,255,.25);
border-radius:8px;padding:7px 4px;font-size:12px;cursor:pointer;}
button:hover{background:#4f8546;}
.hint{font-size:11px;color:#9db98c;margin-top:9px;line-height:1.5;}
#tip{position:absolute;right:14px;bottom:12px;color:rgba(255,255,255,.75);font-size:11px;
background:rgba(0,0,0,.35);padding:5px 10px;border-radius:8px;pointer-events:none;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="panel">
<h1>🚿 The Hose Got Away</h1>
<p class="sub">Spigot is on, nozzle is loose on the lawn. Crank the pressure and physics does the rest — no scripted wiggling.</p>
<div class="row">
<span id="psiVal">0 <small>psi</small></span>
<span id="thrust">thrust 0.0 N</span>
</div>
<input id="psi" type="range" min="0" max="80" step="0.5" value="0">
<div class="ticks"><span>0</span><span>20</span><span>40</span><span>60</span><span>80 psi</span></div>
<span id="status">SETTLED</span>
<div class="btns">
<button id="reset" type="button">↺ Re-lay hose on lawn</button>
<button id="nudge" type="button">👢 Nudge the nozzle</button>
</div>
<p class="hint"><b>Drag any point of the hose</b> to pin it to your pointer; let go to free it.
Try 15 psi (calm), then 55 psi (chaos), then back down (it settles). Load with <b>#psi=40</b> to start at 40.</p>
</div>
<div id="tip">top-down lawn · water lands where the rings splash</div>
<script>
"use strict";
/* ============================== setup ============================== */
var canvas = document.getElementById('c'), ctx = canvas.getContext('2d');
var W = 0, H = 0, DPR = 1;
function resize(){
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
}
window.addEventListener('resize', resize); resize();
/* world: metres, top-down. x east, y south. */
var WORLD_W = 11.5, WORLD_H = 7.6;
function view(){
var s = Math.min(W / WORLD_W, H / WORLD_H) * 0.99;
return { s: s, ox: (W - WORLD_W * s) / 2, oy: (H - WORLD_H * s) / 2 };
}
function toScreen(x, y){ var v = view(); return [v.ox + x * v.s, v.oy + y * v.s]; }
function toWorld(px, py){ var v = view(); return [(px - v.ox) / v.s, (py - v.oy) / v.s]; }
/* deterministic pseudo-random for grass texture */
var seed = 1234567;
function rnd(){ seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }
var grass = [];
for (var gi = 0; gi < 900; gi++) grass.push([rnd() * WORLD_W, rnd() * WORLD_H, rnd() * 6.28, 0.5 + rnd()]);
var speck = [];
for (var si = 0; si < 500; si++) speck.push([rnd() * WORLD_W, rnd() * WORLD_H, rnd()]);
/* ============================== hose model ============================== */
var N = 26, SEG = 0.30, MASS = 0.28;
var MU = 0.55, GRAV = 9.81, VISC = 1.0, KBEND = 120;
var A_NOZ = 1.3e-5; /* m^2, effective nozzle area */
var N_PER_PSI = 2 * A_NOZ * 6895; /* F = 2*A*P -> ~0.179 N per psi */
var ANCHOR = { x: 0.95, y: 3.8 };
var px = [], py = [], ox = [], oy = []; /* position, previous position */
function layHose(){
px = []; py = []; ox = []; oy = [];
for (var i = 0; i < N; i++){
var x = ANCHOR.x + i * SEG * 0.97;
var y = ANCHOR.y + Math.sin(i * 0.42) * 0.38 + i * 0.012;
px.push(x); py.push(y); ox.push(x); oy.push(y);
}
}
layHose();
var psi = 0, simT = 0;
var dragIdx = -1, dragWX = 0, dragWY = 0;
function thrustNewtons(){ return N_PER_PSI * psi; }
function stepHose(dt){
simT += dt;
var i, j;
/* --- forces -> accelerations --- */
var ax = new Array(N), ay = new Array(N);
for (i = 0; i < N; i++){ ax[i] = 0; ay[i] = 0; }
/* bending: Laplacian spring */
var kb = KBEND / MASS;
for (i = 1; i < N - 1; i++){
ax[i] += kb * (px[i-1] - 2*px[i] + px[i+1]);
ay[i] += kb * (py[i-1] - 2*py[i] + py[i+1]);
}
/* nozzle reaction: F = 2*A*P opposite the exit tangent (a follower force) */
var F = thrustNewtons();
if (F > 0.001){
var tx = px[N-1] - px[N-2], ty = py[N-1] - py[N-2];
var tl = Math.hypot(tx, ty) || 1; tx /= tl; ty /= tl;
/* tiny seeded imperfection so a perfectly straight hose can buckle */
var na = 0.045 * Math.sin(simT * 11.3) + 0.03 * Math.sin(simT * 23.7 + 1.7);
var ca = Math.cos(na), sa = Math.sin(na);
var fx = (-tx * ca - (-ty) * sa), fy = (-tx * sa + (-ty) * ca);
/* note: fx,fy = nozzle tangent rotated by na, negated */
fx = -(tx * ca - ty * sa); fy = -(tx * sa + ty * ca);
ax[N-1] += 0.7 * F / MASS * fx; ay[N-1] += 0.7 * F / MASS * fy;
ax[N-2] += 0.3 * F / MASS * fx; ay[N-2] += 0.3 * F / MASS * fy;
}
/* --- Verlet integration with Coulomb + viscous ground friction --- */
var damp = Math.exp(-VISC * dt), coul = MU * GRAV * dt;
for (i = 1; i < N; i++){
if (i === dragIdx) continue;
var vx = (px[i] - ox[i]) / dt, vy = (py[i] - oy[i]) / dt;
vx *= damp; vy *= damp;
var sp = Math.hypot(vx, vy);
if (sp > 1e-9){
var ns = Math.max(0, sp - coul), sc = ns / sp;
vx *= sc; vy *= sc;
}
if (sp > 14){ vx *= 14 / sp; vy *= 14 / sp; } /* safety cap */
ox[i] = px[i]; oy[i] = py[i];
px[i] += vx * dt + ax[i] * dt * dt;
py[i] += vy * dt + ay[i] * dt * dt;
}
/* --- pins --- */
px[0] = ANCHOR.x; py[0] = ANCHOR.y; ox[0] = ANCHOR.x; oy[0] = ANCHOR.y;
if (dragIdx > 0){ px[dragIdx] = dragWX; py[dragIdx] = dragWY; ox[dragIdx] = dragWX; oy[dragIdx] = dragWY; }
/* --- inextensibility: distance-constraint projection --- */
for (var it = 0; it < 12; it++){
px[0] = ANCHOR.x; py[0] = ANCHOR.y;
if (dragIdx > 0){ px[dragIdx] = dragWX; py[dragIdx] = dragWY; }
for (j = 0; j < N - 1; j++){
var dx = px[j+1] - px[j], dy = py[j+1] - py[j];
var d = Math.hypot(dx, dy) || 1e-6;
var diff = (d - SEG) / d;
var p0fixed = (j === 0) || (j === dragIdx);
var p1fixed = (j + 1 === dragIdx);
if (p0fixed && p1fixed) continue;
if (p0fixed){ px[j+1] -= dx * diff; py[j+1] -= dy * diff; }
else if (p1fixed){ px[j] += dx * diff; py[j] += dy * diff; }
else {
px[j] += dx * diff * 0.5; py[j] += dy * diff * 0.5;
px[j+1] -= dx * diff * 0.5; py[j+1] -= dy * diff * 0.5;
}
}
}
/* --- stay on the lawn --- */
for (i = 1; i < N; i++){
if (px[i] < 0.35){ px[i] = 0.35; ox[i] = px[i]; }
if (px[i] > WORLD_W - 0.2){ px[i] = WORLD_W - 0.2; ox[i] = px[i]; }
if (py[i] < 0.2){ py[i] = 0.2; oy[i] = py[i]; }
if (py[i] > WORLD_H - 0.2){ py[i] = WORLD_H - 0.2; oy[i] = py[i]; }
}
}
/* ============================== water particles ============================== */
var parts = [], splashes = [], wet = [], spawnAcc = 0;
function nozzleDir(){
var tx = px[N-1] - px[N-2], ty = py[N-1] - py[N-2];
var l = Math.hypot(tx, ty) || 1; return [tx / l, ty / l];
}
function updateWater(dt){
if (psi > 0.5){
spawnAcc += dt * psi * 1.15;
var dir = nozzleDir();
var vExit = Math.sqrt(2 * psi * 6895 / 1000) * 0.35; /* visual scale */
while (spawnAcc >= 1){
spawnAcc -= 1;
var spread = (Math.random() - 0.5) * 0.13;
var cs = Math.cos(spread), sn = Math.sin(spread);
var dx = dir[0] * cs - dir[1] * sn, dy = dir[0] * sn + dir[1] * cs;
var sp = vExit * (0.9 + Math.random() * 0.2);
parts.push({ x: px[N-1] + dx * 0.12, y: py[N-1] + dy * 0.12, z: 0.12,
vx: dx * sp, vy: dy * sp, vz: 1.7 + Math.random() * 0.7 });
}
}
for (var i = parts.length - 1; i >= 0; i--){
var p = parts[i];
p.vz -= 9.81 * dt;
var dr = Math.exp(-0.25 * dt); p.vx *= dr; p.vy *= dr;
p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
if (p.z <= 0){
splashes.push({ x: p.x, y: p.y, r: 0.03, a: 0.85 });
if (wet.length < 140) wet.push({ x: p.x, y: p.y, r: 0.10 + Math.random() * 0.16, a: 0.20 });
parts.splice(i, 1);
} else if (p.x < -1 || p.x > WORLD_W + 1 || p.y < -1 || p.y > WORLD_H + 1) parts.splice(i, 1);
}
for (var a = splashes.length - 1; a >= 0; a--){
splashes[a].r += dt * 0.55; splashes[a].a -= dt * 1.5;
if (splashes[a].a <= 0) splashes.splice(a, 1);
}
for (var b = wet.length - 1; b >= 0; b--){ wet[b].a -= dt * 0.012; if (wet[b].a <= 0) wet.splice(b, 1); }
}
/* ============================== drawing ============================== */
function draw(){
var v = view(), s = v.s;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
/* lawn with mowing stripes */
for (var k = 0; k < 8; k++){
ctx.fillStyle = (k % 2) ? '#3c7a2e' : '#448734';
ctx.fillRect(v.ox + k * WORLD_W / 8 * s, 0, WORLD_W / 8 * s + 1, H);
}
ctx.fillStyle = '#356f27'; ctx.fillRect(0, 0, W, v.oy);
ctx.fillRect(0, v.oy + WORLD_H * s, W, H);
/* speckles + grass blades */
ctx.fillStyle = 'rgba(0,0,0,0.10)';
for (var i = 0; i < speck.length; i++){
var q = toScreen(speck[i][0], speck[i][1]);
ctx.fillRect(q[0], q[1], 2, 2);
}
ctx.strokeStyle = 'rgba(20,60,15,0.35)'; ctx.lineWidth = 1;
ctx.beginPath();
for (var g = 0; g < grass.length; g++){
var gp = toScreen(grass[g][0], grass[g][1]);
ctx.moveTo(gp[0], gp[1]);
ctx.lineTo(gp[0] + Math.cos(grass[g][2]) * 3 * grass[g][3], gp[1] - 3.5 * grass[g][3]);
}
ctx.stroke();
/* patio slab + house wall at the spigot side */
var patio = toScreen(0.78, 0);
ctx.fillStyle = '#9a958a'; ctx.fillRect(0, 0, patio[0], H);
ctx.fillStyle = '#8a857a'; ctx.fillRect(0, 0, Math.max(0, patio[0] - 8), H);
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
for (var ly = 0; ly < WORLD_H; ly += 0.95){
var lp = toScreen(0, ly); ctx.beginPath(); ctx.moveTo(0, lp[1]); ctx.lineTo(patio[0], lp[1]); ctx.stroke();
}
ctx.fillStyle = '#7c4a2d'; ctx.fillRect(0, 0, 10, H); /* house edge */
/* wet patches where water has landed */
for (var wI = 0; wI < wet.length; wI++){
var wp = toScreen(wet[wI].x, wet[wI].y);
ctx.fillStyle = 'rgba(18,50,40,' + wet[wI].a.toFixed(3) + ')';
ctx.beginPath(); ctx.ellipse(wp[0], wp[1], wet[wI].r * s, wet[wI].r * s * 0.7, 0, 0, 6.29); ctx.fill();
}
/* splash rings */
for (var sI = 0; sI < splashes.length; sI++){
var sp2 = toScreen(splashes[sI].x, splashes[sI].y);
ctx.strokeStyle = 'rgba(190,230,255,' + Math.max(0, splashes[sI].a).toFixed(3) + ')';
ctx.lineWidth = 1.6;
ctx.beginPath(); ctx.ellipse(sp2[0], sp2[1], splashes[sI].r * s, splashes[sI].r * s * 0.65, 0, 0, 6.29); ctx.stroke();
}
/* hose shadow */
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
function hosePath(off){
ctx.beginPath();
for (var i2 = 0; i2 < N; i2++){
var p = toScreen(px[i2], py[i2]);
if (i2 === 0) ctx.moveTo(p[0] + off, p[1] + off); else ctx.lineTo(p[0] + off, p[1] + off);
}
}
ctx.strokeStyle = 'rgba(0,0,0,0.28)'; ctx.lineWidth = 0.17 * s; hosePath(0.05 * s); ctx.stroke();
/* hose body: dark casing, green body, highlight */
ctx.strokeStyle = '#17351a'; ctx.lineWidth = 0.155 * s; hosePath(0); ctx.stroke();
ctx.strokeStyle = '#2c6e35'; ctx.lineWidth = 0.125 * s; hosePath(0); ctx.stroke();
ctx.strokeStyle = 'rgba(140,220,150,0.5)'; ctx.lineWidth = 0.04 * s;
ctx.beginPath();
for (var i3 = 0; i3 < N; i3++){
var p3 = toScreen(px[i3], py[i3]);
if (i3 === 0) ctx.moveTo(p3[0] - 0.03 * s, p3[1] - 0.035 * s); else ctx.lineTo(p3[0] - 0.03 * s, p3[1] - 0.035 * s);
}
ctx.stroke();
/* coupling rings */
ctx.fillStyle = '#c8ccd2';
for (var ci = 4; ci < N - 1; ci += 5){
var cp = toScreen(px[ci], py[ci]);
ctx.beginPath(); ctx.arc(cp[0], cp[1], 0.055 * s, 0, 6.29); ctx.fill();
}
/* dragged point marker */
if (dragIdx > 0){
var dp = toScreen(px[dragIdx], py[dragIdx]);
ctx.strokeStyle = '#ffd94d'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.arc(dp[0], dp[1], 0.16 * s, 0, 6.29); ctx.stroke();
ctx.fillStyle = '#ffd94d';
ctx.beginPath(); ctx.arc(dp[0], dp[1], 3.5, 0, 6.29); ctx.fill();
}
/* nozzle: brass cone along last segment */
var dir = nozzleDir(), tip = toScreen(px[N-1], py[N-1]);
var ang = Math.atan2(dir[1], dir[0]);
ctx.save(); ctx.translate(tip[0], tip[1]); ctx.rotate(ang);
ctx.fillStyle = '#8a8f96'; ctx.fillRect(-0.10 * s, -0.045 * s, 0.10 * s, 0.09 * s);
var grad = ctx.createLinearGradient(0, -0.05 * s, 0, 0.05 * s);
grad.addColorStop(0, '#f2c14e'); grad.addColorStop(0.5, '#c9962e'); grad.addColorStop(1, '#8a5f16');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, -0.052 * s); ctx.lineTo(0.20 * s, -0.026 * s);
ctx.lineTo(0.20 * s, 0.026 * s); ctx.lineTo(0, 0.052 * s); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#5e4008'; ctx.fillRect(0.185 * s, -0.027 * s, 0.018 * s, 0.054 * s);
ctx.restore();
/* spigot fixture */
var ap = toScreen(ANCHOR.x, ANCHOR.y);
ctx.strokeStyle = '#6f757d'; ctx.lineWidth = 0.09 * s;
ctx.beginPath(); ctx.moveTo(0.12 * s, ap[1]); ctx.lineTo(ap[0], ap[1]); ctx.stroke();
ctx.fillStyle = '#aeb4bc'; ctx.beginPath(); ctx.arc(ap[0], ap[1], 0.075 * s, 0, 6.29); ctx.fill();
ctx.strokeStyle = '#c23b2e'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(ap[0] - 0.16 * s, ap[1] - 0.13 * s, 0.085 * s, 0, 6.29); ctx.stroke();
ctx.beginPath();
ctx.moveTo(ap[0] - 0.16 * s - 0.085 * s, ap[1] - 0.13 * s); ctx.lineTo(ap[0] - 0.16 * s + 0.085 * s, ap[1] - 0.13 * s);
ctx.moveTo(ap[0] - 0.16 * s, ap[1] - 0.13 * s - 0.085 * s); ctx.lineTo(ap[0] - 0.16 * s, ap[1] - 0.13 * s + 0.085 * s);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = '600 11px system-ui,sans-serif'; ctx.textAlign = 'center';
ctx.fillText('SPIGOT', ap[0] - 0.1 * s, ap[1] + 0.42 * s);
/* airborne water (with little ground shadows) */
ctx.fillStyle = 'rgba(0,0,0,0.18)';
for (var pI = 0; pI < parts.length; pI++){
var sh = toScreen(parts[pI].x, parts[pI].y);
ctx.beginPath(); ctx.arc(sh[0], sh[1], 2.2, 0, 6.29); ctx.fill();
}
for (var pJ = 0; pJ < parts.length; pJ++){
var pp2 = toScreen(parts[pJ].x, parts[pJ].y - parts[pJ].z * 0.35);
ctx.fillStyle = 'rgba(165,220,255,0.9)';
ctx.beginPath(); ctx.arc(pp2[0], pp2[1], 2.4, 0, 6.29); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.beginPath(); ctx.arc(pp2[0] - 0.7, pp2[1] - 0.7, 0.9, 0, 6.29); ctx.fill();
}
}
/* ============================== UI / input ============================== */
var slider = document.getElementById('psi'), psiVal = document.getElementById('psiVal'),
thrustEl = document.getElementById('thrust'), statusEl = document.getElementById('status');
function setPsi(val, fromSlider){
if (!isFinite(val)) val = 0;
psi = Math.max(0, Math.min(80, val));
if (!fromSlider) slider.value = psi;
psiVal.innerHTML = (Math.round(psi * 10) / 10) + ' <small>psi</small>';
thrustEl.textContent = 'thrust ' + thrustNewtons().toFixed(1) + ' N';
try { history.replaceState(null, '', '#psi=' + (Math.round(psi * 10) / 10)); } catch (e) {}
}
slider.addEventListener('input', function(){ setPsi(parseFloat(slider.value), true); });
function psiFromHash(){
var m = /psi=([^&]+)/.exec(location.hash || '');
if (m){ var v = parseFloat(decodeURIComponent(m[1])); if (isFinite(v)) return v; }
return null;
}
var h0 = psiFromHash();
setPsi(h0 === null ? 22 : h0, false); /* nozzle starts free either way */
window.addEventListener('hashchange', function(){
var v = psiFromHash(); if (v !== null) setPsi(v, false);
});
document.getElementById('reset').addEventListener('click', function(){ layHose(); parts = []; splashes = []; });
document.getElementById('nudge').addEventListener('click', function(){
ox[N-1] -= 0.02; oy[N-1] += 0.015; ox[N-2] -= 0.012;
});
function pointerPos(e){ return [e.clientX, e.clientY]; }
canvas.addEventListener('pointerdown', function(e){
var mp = pointerPos(e), best = -1, bestD = 55;
for (var i = 1; i < N; i++){
var sp3 = toScreen(px[i], py[i]);
var d = Math.hypot(sp3[0] - mp[0], sp3[1] - mp[1]);
if (d < bestD){ bestD = d; best = i; }
}
if (best > 0){
dragIdx = best;
var w = toWorld(mp[0], mp[1]); dragWX = w[0]; dragWY = w[1];
canvas.setPointerCapture(e.pointerId);
}
});
canvas.addEventListener('pointermove', function(e){
if (dragIdx > 0){
var mp = pointerPos(e), w = toWorld(mp[0], mp[1]);
dragWX = Math.max(0.3, Math.min(WORLD_W - 0.15, w[0]));
dragWY = Math.max(0.15, Math.min(WORLD_H - 0.15, w[1]));
}
});
function endDrag(){ dragIdx = -1; }
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
/* ============================== main loop ============================== */
var last = performance.now(), acc = 0, tipSpeedSm = 0, statusTick = 0;
function frame(now){
var dtF = Math.min(0.05, (now - last) / 1000); last = now;
acc += dtF;
var h = 1 / 240, guard = 0;
while (acc >= h && guard < 40){ stepHose(h); acc -= h; guard++; }
updateWater(dtF);
var tvx = (px[N-1] - ox[N-1]) / h, tvy = (py[N-1] - oy[N-1]) / h;
tipSpeedSm += (Math.hypot(tvx, tvy) - tipSpeedSm) * Math.min(1, dtF * 4);
if (++statusTick % 8 === 0){
if (psi < 1){ statusEl.textContent = 'OFF · SETTLED'; statusEl.className = ''; statusEl.id = 'status'; }
else if (tipSpeedSm > 1.6){ statusEl.textContent = '⚠ WHIPPING'; statusEl.className = 'whip'; }
else { statusEl.textContent = 'FLOWING · SETTLED'; statusEl.className = 'flow'; }
}
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
Where it looks right: at low pressure the hose creeps, takes a lazy S-curve, and friction parks it, which is what a real hose does on grass; past the transition the tip starts to travel, the bend chases it, and the whole thing begins flailing in widening, irregular arcs, snapping round when a loop tightens — and dropping the slider back down genuinely re-settles it, because nothing is animating it. Water leaves along the actual nozzle direction, arcs, and leaves splash rings and dark wet patches roughly where a jet would land. Where it is wrong: it is a top-down 2D world, so there is no gravity sag, no hose lifting off the ground, no rolling or twisting about its own axis, and no kinking flat like a real rubber hose — the Laplacian bending spring makes it bend more like a stiff rope or a pool noodle. There is no self-collision, so the hose happily passes through itself instead of piling up and shoving itself around, which is a big part of real hose chaos. The water is cosmetic: particles carry no momentum back into the hose beyond the single tip thrust, there is no pressure wave, no flow straightening of upstream bends, and no puddle feedback on friction. Named cheats, as requested: artificial damping is the viscous term (e^{-1.0\Delta t}) on every mass on top of the Coulomb friction, plus a 14 m/s velocity safety cap; injected noise is a small deterministic wobble (±0.075 rad, from two sine waves at 11.3 and 23.7 rad/s) applied to the thrust direction so a perfectly straight hose can buckle at all — real hoses get this from ground bumps and manufacturing curl. There is no scripted oscillation, no pre-baked path, and no hidden "whip mode": the whipping is just the follower force (F=2AP) chasing its own tangent, and if you pin the nozzle with the mouse at 70 psi it stops dead, exactly as it should.