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 discretized as 32 equal particles of mass 0.12 kg spaced 0.20 m apart (total length 6.2 m, linear density 0.60 kg/m) with bending represented by a discrete Laplacian force k_b(p_{i-1}-2p_i+p_{i+1}) with k_b=EI/ds^3=150 N/m for EI=1.2 N m^2, inextensibility enforced by 12 iterations per substep of distance projection that restores every segment to exactly 0.20 m (position-based dynamics, 8 substeps per 1/60 s step, spigot particle pinned), the nozzle reaction is the Bernoulli thrust T(psi)=2A·psi·6894.76 with A=π(0.009)^2/4=6.36×10^-5 m^2 so T=0.877·psi N applied opposite the tip tangent (jet speed sqrt(2·psi·6894.76/1000)), and ground contact in the top-down plane is Coulomb friction with static coefficient 0.65 and kinetic 0.45 implemented as a velocity reduction of mu·g·h per substep (clamped to zero, so kicks smaller than static friction stick) plus exponential viscous damping at 1.6 s^-1, with a small perpendicular turbulence jitter of ±5% of thrust at the tip.
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Crouton Alpha — Garden Hose Whip</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#3f7031;font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;color:#fff}
canvas{display:block;width:100vw;height:100vh}
#ui{position:absolute;top:0;left:0;right:0;display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:8px 12px;background:rgba(10,25,10,.72);box-sizing:border-box;z-index:2}
#ui .title{font-weight:700;letter-spacing:.02em}
#ui .box{background:rgba(255,255,255,.12);border-radius:8px;padding:4px 8px}
input[type=range]{width:200px;accent-color:#7dd3fc}
#status{font-weight:800}
.settled{color:#a7f3d0}
.whipping{color:#fca5a5}
.hint{opacity:.85;font-size:13px}
</style>
</head>
<body>
<div id="ui">
<span class="title">Crouton Alpha — Garden Hose</span>
<span class="box">Pressure <input id="psiSlider" type="range" min="0" max="80" step="1" value="0"> <b id="psiVal">0 psi</b></span>
<span class="box">Thrust <b id="thrustVal">0.0 N</b></span>
<span class="box">Jet <b id="jetVal">0.0 m/s</b></span>
<span class="box"><span id="status" class="settled">SETTLED</span></span>
<span class="hint">Drag the hose to pin it to the mouse. Release to free it.</span>
</div>
<canvas id="c"></canvas>
<script>
(function(){
"use strict";
var canvas=document.getElementById('c');
var ctx=canvas.getContext('2d');
var slider=document.getElementById('psiSlider');
var psiVal=document.getElementById('psiVal');
var thrustVal=document.getElementById('thrustVal');
var jetVal=document.getElementById('jetVal');
var statusEl=document.getElementById('status');
var MAX_PSI=80;
var N=32, DS=0.20, L=(N-1)*DS, LAMBDA=0.60, MASS=LAMBDA*DS;
var EI=1.2, KB=EI/(DS*DS*DS);
var MU_S=0.65, MU_K=0.45, G=9.81;
var NOZZLE_D=0.009, AREA=Math.PI*NOZZLE_D*NOZZLE_D/4;
var PSI_TO_PA=6894.76, RHO=1000;
var VISCOUS=1.6;
function thrust(psi){return 2*AREA*psi*PSI_TO_PA;}
function jetVel(psi){return Math.sqrt(2*psi*PSI_TO_PA/RHO);}
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
var psi=0;
(function(){
var h=location.hash||'';
var m=h.match(/psi\s*=\s*(-?\d+(?:\.\d+)?)/i);
if(!m) m=h.match(/#\s*(-?\d+(?:\.\d+)?)\s*$/);
if(m){var v=parseFloat(m[1]); if(isFinite(v)) psi=clamp(v,0,MAX_PSI);}
})();
slider.value=Math.round(psi);
var x=new Float64Array(N), y=new Float64Array(N), px=new Float64Array(N), py=new Float64Array(N);
for(var i=0;i<N;i++){ x[i]=i*DS; y[i]=0.12*Math.sin(i*0.45); px[i]=x[i]; py[i]=y[i]; }
x[0]=0; y[0]=0; px[0]=0; py[0]=0;
var W=0,H=0,scale=60,ox=0,oy=0;
function resize(){ W=canvas.width=window.innerWidth; H=canvas.height=window.innerHeight; scale=Math.min(W/13,H/10); if(scale<28)scale=28; if(scale>78)scale=78; ox=W*0.32; oy=H*0.54; }
window.addEventListener('resize',resize); resize();
function toScreen(wx,wy){return [ox+wx*scale, oy+wy*scale];}
function toWorld(sx,sy){return [(sx-ox)/scale,(sy-oy)/scale];}
var lawn=document.createElement('canvas'); lawn.width=256; lawn.height=256;
var lctx=lawn.getContext('2d');
lctx.fillStyle='#4e8a3d'; lctx.fillRect(0,0,256,256);
for(var k=0;k<3000;k++){ var g=60+Math.floor(Math.random()*70); lctx.fillStyle='rgb('+(g-20)+','+g+','+(g-35)+')'; lctx.fillRect(Math.random()*256,Math.random()*256,2,2); }
for(var k2=0;k2<120;k2++){ lctx.strokeStyle='rgba(30,70,25,0.5)'; var bx=Math.random()*256, by=Math.random()*256; lctx.beginPath(); lctx.moveTo(bx,by); lctx.lineTo(bx+(Math.random()-0.5)*4,by-3-Math.random()*4); lctx.stroke(); }
var lawnPattern=ctx.createPattern(lawn,'repeat');
var drops=[], wet=[], spawnAcc=0;
var dragIdx=-1, dragOffX=0, dragOffY=0, mouseWX=0, mouseWY=0;
function canvasPos(e){ var r=canvas.getBoundingClientRect(); var cx,cy; if(e.touches&&e.touches.length){cx=e.touches[0].clientX; cy=e.touches[0].clientY;} else {cx=e.clientX; cy=e.clientY;} return [cx-r.left, cy-r.top]; }
function pick(sx,sy){ var w=toWorld(sx,sy); var best=-1,bd=1e18; for(var i=0;i<N;i++){var dx=x[i]-w[0], dy=y[i]-w[1]; var d=dx*dx+dy*dy; if(d<bd){bd=d;best=i;}} if(Math.sqrt(bd)*scale<44) return best; return -1; }
canvas.addEventListener('mousedown',function(e){ var p=canvasPos(e); var idx=pick(p[0],p[1]); if(idx>=0){ dragIdx=idx; var w=toWorld(p[0],p[1]); dragOffX=x[idx]-w[0]; dragOffY=y[idx]-w[1]; mouseWX=w[0]; mouseWY=w[1]; } });
canvas.addEventListener('mousemove',function(e){ var p=canvasPos(e); var w=toWorld(p[0],p[1]); mouseWX=w[0]; mouseWY=w[1]; });
window.addEventListener('mouseup',function(){ if(dragIdx===0){x[0]=0;y[0]=0;px[0]=0;py[0]=0;} dragIdx=-1; });
canvas.addEventListener('touchstart',function(e){ e.preventDefault(); var p=canvasPos(e); var idx=pick(p[0],p[1]); if(idx>=0){dragIdx=idx; var w=toWorld(p[0],p[1]); dragOffX=x[idx]-w[0]; dragOffY=y[idx]-w[1]; mouseWX=w[0]; mouseWY=w[1];} },{passive:false});
canvas.addEventListener('touchmove',function(e){ e.preventDefault(); var p=canvasPos(e); var w=toWorld(p[0],p[1]); mouseWX=w[0]; mouseWY=w[1]; },{passive:false});
canvas.addEventListener('touchend',function(e){ if(dragIdx===0){x[0]=0;y[0]=0;px[0]=0;py[0]=0;} dragIdx=-1; });
slider.addEventListener('input',function(){ psi=parseFloat(slider.value); });
var fx=new Float64Array(N), fy=new Float64Array(N);
function step(dt){
var sub=8, h=dt/sub;
var T=thrust(psi);
for(var s=0;s<sub;s++){
for(var i=0;i<N;i++){fx[i]=0;fy[i]=0;}
for(var i2=1;i2<N-1;i2++){ fx[i2]+=KB*(x[i2-1]-2*x[i2]+x[i2+1]); fy[i2]+=KB*(y[i2-1]-2*y[i2]+y[i2+1]); }
var dx=x[N-1]-x[N-2], dy=y[N-1]-y[N-2]; var len=Math.sqrt(dx*dx+dy*dy); var tx=1,ty=0;
if(len>1e-9){tx=dx/len; ty=dy/len;}
if(T>0){ fx[N-1]+=-T*tx; fy[N-1]+=-T*ty; var nz=(Math.random()*2-1)*0.05*T; fx[N-1]+=(-ty)*nz; fy[N-1]+=tx*nz; }
for(var j=0;j<N;j++){
if(j===0 && dragIdx!==0){ px[j]=0; py[j]=0; x[j]=0; y[j]=0; continue; }
if(j===dragIdx){ px[j]=x[j]; py[j]=y[j]; x[j]=mouseWX+dragOffX; y[j]=mouseWY+dragOffY; continue; }
var vx=(x[j]-px[j])/h, vy=(y[j]-py[j])/h;
var prevSpeed=Math.sqrt(vx*vx+vy*vy);
vx+=fx[j]/MASS*h; vy+=fy[j]/MASS*h;
var damp=Math.exp(-VISCOUS*h); vx*=damp; vy*=damp;
var sp=Math.sqrt(vx*vx+vy*vy);
if(sp>1e-9){ var mu=(prevSpeed<0.05?MU_S:MU_K); var dec=mu*G*h; if(dec>=sp){vx=0;vy=0;} else {var f=(sp-dec)/sp; vx*=f; vy*=f;} }
var sp2=Math.sqrt(vx*vx+vy*vy); if(sp2>30){vx*=30/sp2; vy*=30/sp2;}
px[j]=x[j]; py[j]=y[j]; x[j]+=vx*h; y[j]+=vy*h;
}
for(var it=0;it<12;it++){
for(var a=0;a<N-1;a++){
var b=a+1;
var ddx=x[b]-x[a], ddy=y[b]-y[a]; var d=Math.sqrt(ddx*ddx+ddy*ddy);
if(d<1e-9) continue;
var diff=(d-DS)/d;
var aPin=(a===0&&dragIdx!==0)||a===dragIdx;
var bPin=(b===0&&dragIdx!==0)||b===dragIdx;
if(aPin&&bPin) continue;
if(aPin){ x[b]-=ddx*diff; y[b]-=ddy*diff; }
else if(bPin){ x[a]+=ddx*diff; y[a]+=ddy*diff; }
else { var hx=ddx*diff*0.5, hy=ddy*diff*0.5; x[a]+=hx; y[a]+=hy; x[b]-=hx; y[b]-=hy; }
}
if(dragIdx!==0){x[0]=0;y[0]=0;px[0]=0;py[0]=0;}
if(dragIdx>=0){ x[dragIdx]=mouseWX+dragOffX; y[dragIdx]=mouseWY+dragOffY; }
}
}
}
function updateWater(dt){
if(psi>0.5){
spawnAcc+=dt*psi*9;
var dx=x[N-1]-x[N-2], dy=y[N-1]-y[N-2]; var len=Math.sqrt(dx*dx+dy*dy); var tx=1,ty=0;
if(len>1e-9){tx=dx/len; ty=dy/len;}
var jv=jetVel(psi);
while(spawnAcc>=1 && drops.length<900){
spawnAcc-=1;
var spread=(Math.random()+Math.random()+Math.random()-1.5)*0.07;
var cs=Math.cos(spread), sn=Math.sin(spread);
var vx0=(tx*cs-ty*sn)*jv+(Math.random()-0.5)*0.9;
var vy0=(tx*sn+ty*cs)*jv+(Math.random()-0.5)*0.9;
drops.push({x:x[N-1]+tx*0.08,y:y[N-1]+ty*0.08,vx:vx0,vy:vy0});
}
} else { spawnAcc=0; }
var drag=Math.exp(-3.8*dt);
for(var i=drops.length-1;i>=0;i--){
var d=drops[i]; d.vx*=drag; d.vy*=drag; d.x+=d.vx*dt; d.y+=d.vy*dt;
var sp=Math.sqrt(d.vx*d.vx+d.vy*d.vy);
if(sp<0.6 || d.x<-3 || d.x>16 || d.y<-10 || d.y>10){ wet.push({x:d.x,y:d.y,r:0.12+Math.random()*0.20,a:0.55}); drops.splice(i,1); }
}
for(var j=wet.length-1;j>=0;j--){ wet[j].a-=dt*0.06; if(wet[j].a<=0) wet.splice(j,1); }
if(wet.length>700) wet.splice(0,wet.length-700);
}
function drawHose(off,color,width){
ctx.strokeStyle=color; ctx.lineWidth=width; ctx.lineJoin='round'; ctx.lineCap='round';
ctx.beginPath();
for(var i=0;i<N;i++){ var s=toScreen(x[i],y[i]); if(i===0) ctx.moveTo(s[0]+off,s[1]+off); else ctx.lineTo(s[0]+off,s[1]+off); }
ctx.stroke();
}
function draw(){
ctx.fillStyle=lawnPattern; ctx.fillRect(0,0,W,H);
for(var i=0;i<wet.length;i++){ var w=wet[i]; var s=toScreen(w.x,w.y); ctx.fillStyle='rgba(35,85,170,'+w.a+')'; ctx.beginPath(); ctx.ellipse(s[0],s[1],w.r*scale,w.r*scale*0.7,0,0,Math.PI*2); ctx.fill(); }
drawHose(4,'rgba(0,0,0,0.28)',12);
drawHose(0,'#174d26',11);
drawHose(0,'#2f9e44',8);
var dx=x[N-1]-x[N-2], dy=y[N-1]-y[N-2]; var ang=Math.atan2(dy,dx);
var tip=toScreen(x[N-1],y[N-1]);
ctx.save(); ctx.translate(tip[0],tip[1]); ctx.rotate(ang);
ctx.fillStyle='#c9a227'; ctx.fillRect(-2,-5,16,10);
ctx.fillStyle='#8a6d1b'; ctx.fillRect(10,-3,4,6);
ctx.restore();
ctx.fillStyle='rgba(140,210,255,0.95)';
for(var d=0;d<drops.length;d++){ var p=toScreen(drops[d].x,drops[d].y); ctx.beginPath(); ctx.arc(p[0],p[1],2.3,0,Math.PI*2); ctx.fill(); }
var sp=toScreen(0,0);
ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.fillRect(sp[0]-16,sp[1]-10,36,26);
ctx.fillStyle='#9aa3ab'; ctx.fillRect(sp[0]-18,sp[1]-14,34,28);
ctx.fillStyle='#6b747c'; ctx.fillRect(sp[0]-18,sp[1]-14,34,6);
ctx.fillStyle='#d1d5db'; ctx.beginPath(); ctx.arc(sp[0]-1,sp[1],7,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='#b91c1c'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(sp[0]-1,sp[1],10,0,Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(sp[0]-11,sp[1]); ctx.lineTo(sp[0]+9,sp[1]); ctx.moveTo(sp[0]-1,sp[1]-10); ctx.lineTo(sp[0]-1,sp[1]+10); ctx.stroke();
if(dragIdx>=0){ var dp=toScreen(x[dragIdx],y[dragIdx]); ctx.strokeStyle='#fde047'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(dp[0],dp[1],14,0,Math.PI*2); ctx.stroke(); }
}
var tipSmooth=0;
function updateUI(){
psiVal.textContent=Math.round(psi)+' psi';
thrustVal.textContent=thrust(psi).toFixed(1)+' N';
jetVal.textContent=jetVel(psi).toFixed(1)+' m/s';
var vx=(x[N-1]-px[N-1])/((1/60)/8), vy=(y[N-1]-py[N-1])/((1/60)/8);
var sp=Math.sqrt(vx*vx+vy*vy);
tipSmooth=tipSmooth*0.92+sp*0.08;
if(tipSmooth>1.1 && psi>3){ statusEl.textContent='WHIPPING'; statusEl.className='whipping'; }
else { statusEl.textContent='SETTLED'; statusEl.className='settled'; }
}
var last=performance.now(), acc=0;
function frame(now){
var dt=(now-last)/1000; last=now; if(dt>0.1)dt=0.1;
acc+=dt;
var hstep=1/60;
var n=0;
while(acc>=hstep && n<4){ step(hstep); updateWater(hstep); acc-=hstep; n++; }
if(n===4) acc=0;
draw(); updateUI();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it looks like a real hose: at low pressure the hose bends a little under thrust and then sticks, at high pressure it buckles, the tip swings through large arcs, the jet follows the tip tangent and sweeps the lawn leaving wet spots, and lowering the pressure lets Coulomb friction and viscous damping kill the motion again. The whipping is a genuine follower-force instability of the discretized inextensible chain — thrust always opposite the tip tangent does positive work as the hose shortens end-to-end by bending, bending and friction oppose it, and past the balance point the motion is self-sustaining and chaotic in direction.
Artificial damping, injected noise, and scripted oscillation: artificial damping is explicit — exponential viscous damping at 1.6 s^-1 on every particle plus Coulomb friction (mu_s=0.65, mu_k=0.45) that cancels sub-threshold kicks. Injected noise is explicit — a ±5% of thrust perpendicular random force at the tip each substep, plus a small Gaussian-like jet spread and a 0.12 m sine seed in the initial hose shape to break symmetry. There is no scripted oscillation: no sine driving force, no keyframed whip cycle, no pressure-dependent forcing function beyond thrust magnitude and that jitter.
What it gets wrong: it is top-down, so the hose never lifts, arcs, or falls under gravity, and water does not ballistically arc — droplets slide with a 3.8 s^-1 drag until slow, then become fading wet spots. The hose has no self-collision, so it passes through itself when it loops, water passes through the hose, bending stiffness is constant and pressure-independent (a real pressurized hose stiffens and kinks), there is no hose memory, air drag, or distributed pressure force along the hose, thickness is greatly exaggerated for visibility, static friction can leave the hose settled in a bent C rather than straight, and dragging the spigot particle teleports it back to the spigot on release.