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 3.0 m long with mass 0.45 kg/m (rubber plus the water inside) and bending stiffness EI = 2.0 N·m². It is split into N = 30 rigid links of l = 0.1 m. The generalized coordinates are the absolute link angles θᵢ, so the hose cannot stretch by construction; there is no constraint solver to drift or leak energy. Masses are lumped at the link ends, with half mass at the tip. Lagrange's equations give a dense mass matrix M_ij = l²·S_max(i,j)·cos(θᵢ−θⱼ), where S_j is the mass beyond link j. Each RK4 stage solves M·θ̈ = Q − Σⱼ l²S_max(i,j) sin(θᵢ−θⱼ) θ̇ⱼ² by Cholesky, with a timestep of at most 1/1500 s. Bending is a rotational spring EI/l at every joint. The spigot is a clamp: position and outgoing direction are fixed, with a spring EI/l against the fixed spout angle. "Pinned at the spigot" therefore means clamped, which is how a hose threaded onto a bib behaves.
The nozzle is a 4 mm orifice. With Bernoulli exit speed v = √(2p/ρ) and an ideal jet, the reaction is
F = ρAv² = 2·p·A = 2·(psi·6894.76 Pa)·π(0.002 m)² ≈ 0.1733 N per psi.
That force is applied at the tip node opposite the current tip tangent. It is a follower load, so the model is Beck's column.
Ground friction is not Coulomb. It is an isotropic linear viscous drag of −γ·m·v on every node, with γ = 1.2 s⁻¹. Because it is mass-proportional, it reduces exactly to −γθ̇ in the angle coordinates. There is no stiction and no static friction cone.
27.5 psi
Derivation:
The expected onset frequency is about 11/t₀ = 2.6 rad/s, a period of roughly 2.4 s.
The page computes this same number live from its own parameters and shows it in the panel. Discretization at N = 30 should shift it by a percent or two; I have not measured that.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dropped garden hose</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#3d6a24;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;user-select:none;-webkit-user-select:none}
#c{position:fixed;left:0;top:0;width:100%;height:100%;touch-action:none;cursor:grab}
#c.grabbing{cursor:grabbing}
#panel{position:fixed;left:12px;top:12px;width:262px;background:rgba(18,26,16,.84);color:#e9f1e2;border-radius:10px;padding:11px 13px 10px;font-size:12px;box-shadow:0 4px 18px rgba(0,0,0,.35);line-height:1.35}
#panel h1{font-size:13px;margin:0 0 4px;font-weight:650;letter-spacing:.02em;color:#cfe8b8}
.psirow{display:flex;align-items:baseline;gap:6px}
#psiBig{font-size:34px;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}
.u{font-size:14px;color:#a9c79a}
#psi{width:100%;margin:4px 0 6px;accent-color:#e07b2a}
.btns{display:flex;gap:4px;align-items:center}
button{background:#33462b;color:#eaf5df;border:1px solid #577a45;border-radius:5px;padding:3px 7px;font-size:12px;cursor:pointer}
button:hover{background:#46603a}
#psiNum{width:62px;background:#1d2a19;color:#fff;border:1px solid #577a45;border-radius:5px;padding:2px 4px;font-size:12px}
table{width:100%;border-collapse:collapse;margin:7px 0 5px}
td{padding:1px 0}
td:last-child{text-align:right;font-variant-numeric:tabular-nums;color:#fff}
#status{font-weight:700}
#chart{width:236px;height:64px;display:block;margin:3px 0 5px;background:#10180d;border-radius:4px}
label{display:block;margin:3px 0;color:#c9dcbc}
.hint{color:#9fb893;margin:5px 0 0;font-size:11px}
#slow{color:#ffb35c;display:none}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="panel">
<h1>Garden hose, dropped, water on</h1>
<div class="psirow"><span id="psiBig">0</span><span class="u">psi</span><span id="slow">slow-mo</span></div>
<input id="psi" type="range" min="0" max="100" step="0.5" value="0">
<div class="btns">
<button data-d="off">Off</button><button data-d="-5">−5</button><button data-d="5">+5</button><button data-d="full">Full</button>
<input id="psiNum" type="number" min="0" step="0.5" value="0">
</div>
<table>
<tr><td>jet reaction F</td><td id="rF">0</td></tr>
<tr><td>jet speed / flow</td><td id="rV">0</td></tr>
<tr><td>load F·L²/EI</td><td id="rP">0</td></tr>
<tr><td>predicted onset</td><td id="rPred">–</td></tr>
<tr><td>hose kinetic energy</td><td id="rKE">0</td></tr>
<tr><td>status</td><td id="status">–</td></tr>
</table>
<canvas id="chart"></canvas>
<label><input type="checkbox" id="noise" checked> flow-turbulence noise (2% side force)</label>
<button id="relay">Re-lay hose</button>
<p class="hint">Drag any point of the hose to hold it there; release to let go. ↑/↓ = ±1 psi. Chart: orange = psi (dashed = predicted onset), cyan = log KE.</p>
</div>
<script>
'use strict';
(function(){
const PSI_PA=6894.757;
const P={L:3.0,N:30,m:0.45,EI:2.0,dNoz:0.004,rho:1000,gamma:1.2,thetaBase:0.35,
kPin:800,cPin:30,full:100,noiseFrac:0.02,noiseTau:0.05};
const N=P.N,l=P.L/N,l2=l*l,kb=P.EI/l,A=Math.PI*(P.dNoz/2)*(P.dNoz/2);
// lumped node masses (node k = end of link k-1), tip half mass
const mk=new Float64Array(N+1);
for(let k=1;k<=N;k++) mk[k]=P.m*l*(k===N?0.5:1);
const S=new Float64Array(N);
for(let j=0;j<N;j++){let s=0;for(let k=j+1;k<=N;k++)s+=mk[k];S[j]=s;}
const th=new Float64Array(N),om=new Float64Array(N);
const Cc=new Float64Array(N),Ss=new Float64Array(N),M=new Float64Array(N*N),R=new Float64Array(N),Q=new Float64Array(N);
const a1=new Float64Array(N),a2=new Float64Array(N),a3=new Float64Array(N),a4=new Float64Array(N);
const t2=new Float64Array(N),t3=new Float64Array(N),t4=new Float64Array(N);
const w2=new Float64Array(N),w3=new Float64Array(N),w4=new Float64Array(N);
let psi=0,Fjet=0,Fn=0,grab=-1,gx=0,gy=0,noiseOn=true;
function accel(t,w,out){
for(let i=0;i<N;i++){Cc[i]=Math.cos(t[i]);Ss[i]=Math.sin(t[i]);}
const cN=Cc[N-1],sN=Ss[N-1];
let d=t[0]-P.thetaBase;
for(let i=0;i<N;i++){
const dn=i<N-1?t[i+1]-t[i]:0;
let q=-kb*d+kb*dn; // bending
q-=l*Fjet*(sN*Cc[i]-cN*Ss[i]); // follower jet reaction, -F*tangent at tip
q+=l*Fn*(cN*Cc[i]+sN*Ss[i]); // lateral turbulence force at tip
Q[i]=q;d=dn;
}
if(grab>0){
let x=0,y=0,vx=0,vy=0;
for(let i=0;i<grab;i++){x+=l*Cc[i];y+=l*Ss[i];vx-=l*w[i]*Ss[i];vy+=l*w[i]*Cc[i];}
const fx=P.kPin*(gx-x)-P.cPin*vx,fy=P.kPin*(gy-y)-P.cPin*vy;
for(let i=0;i<grab;i++)Q[i]+=l*(-Ss[i]*fx+Cc[i]*fy);
}
for(let i=0;i<N;i++){
let h=0;const ci=Cc[i],si=Ss[i],row=i*N;
for(let j=0;j<N;j++){
const wij=l2*S[i>j?i:j];
M[row+j]=wij*(ci*Cc[j]+si*Ss[j]);
h+=wij*(si*Cc[j]-ci*Ss[j])*w[j]*w[j];
}
R[i]=Q[i]-h;
}
// Cholesky (lower, in place)
for(let j=0;j<N;j++){
let s=M[j*N+j];
for(let k=0;k<j;k++)s-=M[j*N+k]*M[j*N+k];
const dd=Math.sqrt(s);M[j*N+j]=dd;
for(let i=j+1;i<N;i++){
let v=M[i*N+j];
for(let k=0;k<j;k++)v-=M[i*N+k]*M[j*N+k];
M[i*N+j]=v/dd;
}
}
for(let i=0;i<N;i++){let s=R[i];for(let k=0;k<i;k++)s-=M[i*N+k]*out[k];out[i]=s/M[i*N+i];}
for(let i=N-1;i>=0;i--){let s=out[i];for(let k=i+1;k<N;k++)s-=M[k*N+i]*out[k];out[i]=s/M[i*N+i];}
for(let i=0;i<N;i++)out[i]-=P.gamma*w[i]; // mass-proportional ground drag
}
function step(h){
accel(th,om,a1);
for(let i=0;i<N;i++){t2[i]=th[i]+0.5*h*om[i];w2[i]=om[i]+0.5*h*a1[i];}
accel(t2,w2,a2);
for(let i=0;i<N;i++){t3[i]=th[i]+0.5*h*w2[i];w3[i]=om[i]+0.5*h*a2[i];}
accel(t3,w3,a3);
for(let i=0;i<N;i++){t4[i]=th[i]+h*w3[i];w4[i]=om[i]+h*a3[i];}
accel(t4,w4,a4);
for(let i=0;i<N;i++){
th[i]+=h/6*(om[i]+2*w2[i]+2*w3[i]+w4[i]);
om[i]+=h/6*(a1[i]+2*a2[i]+2*a3[i]+a4[i]);
}
}
function relay(){
for(let i=0;i<N;i++){th[i]=P.thetaBase+0.9*Math.sin(1.25*Math.PI*i/(N-1));om[i]=0;}
Fn=0;
}
function gauss(){const u=1-Math.random(),v=Math.random();return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);}
// ---- prediction from the same parameters (2-mode Galerkin + damping, shifted to exact Beck 20.05)
function predict(){
const L4=[Math.pow(1.8751041,4),Math.pow(4.6940911,4)];
const B=[[0.8582,1.8738],[-11.7432,-13.2943]];
const t0=P.L*P.L*Math.sqrt(P.m/P.EI),c=P.gamma*t0;
function unstable(p,cc){
const k11=L4[0]+p*B[0][0],k12=p*B[0][1],k21=p*B[1][0],k22=L4[1]+p*B[1][1];
const tr=k11+k22,det=k11*k22-k12*k21,disc=tr*tr-4*det;
if(disc>=0)return (tr-Math.sqrt(disc))/2<0;
return (-disc/4)>(tr/2)*cc*cc;
}
function crit(cc){let p=0;while(p<300&&!unstable(p,cc))p+=0.05;let lo=Math.max(0,p-0.05),hi=p;
for(let i=0;i<50;i++){const mm=(lo+hi)/2;if(unstable(mm,cc))hi=mm;else lo=mm;}return hi;}
const pnd=crit(c)-(crit(0)-20.05);
return {pnd,psi:pnd*P.EI/(P.L*P.L)/(2*A)/PSI_PA,c,t0};
}
const pred=predict();
// ---- particles
const MAXP=6000;
const pX=new Float32Array(MAXP),pY=new Float32Array(MAXP),pZ=new Float32Array(MAXP),
pVX=new Float32Array(MAXP),pVY=new Float32Array(MAXP),pVZ=new Float32Array(MAXP),pK=new Float32Array(MAXP);
let np=0,emitAcc=0;
function tipState(){
let x=0,y=0,vx=0,vy=0;
for(let i=0;i<N;i++){const c=Math.cos(th[i]),s=Math.sin(th[i]);x+=l*c;y+=l*s;vx-=l*om[i]*s;vy+=l*om[i]*c;}
return [x,y,vx,vy,Math.cos(th[N-1]),Math.sin(th[N-1])];
}
function emit(tb,ts,vj){
if(np>=MAXP)return;
const el=0.12+Math.random()*0.08,az=(Math.random()-0.5)*0.05;
const ca=Math.cos(az),sa=Math.sin(az);
const dx=ts[4]*ca-ts[5]*sa,dy=ts[5]*ca+ts[4]*sa;
const v=vj*(0.86+0.14*Math.random()),vh=v*Math.cos(el);
const vx=dx*vh+ts[2],vy=dy*vh+ts[3],vz=v*Math.sin(el);
pX[np]=ts[0]-vx*tb;pY[np]=ts[1]-vy*tb;pZ[np]=0.035-vz*tb;
pVX[np]=vx;pVY[np]=vy;pVZ[np]=vz;pK[np]=0.15+Math.random()*0.55;np++;
}
// ---- canvas / view
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const lawn=document.createElement('canvas'),wet=document.createElement('canvas'),wctx=wet.getContext('2d');
let W=0,H=0,dpr=1,scale=100,ox=0,oy=0;
const X=x=>ox+x*scale,Y=y=>oy+y*scale;
function resize(){
dpr=Math.min(2,window.devicePixelRatio||1);W=innerWidth;H=innerHeight;
cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr);
scale=Math.min(W/10.5,H/7.4);ox=W>760?W*0.57:W*0.5;oy=H*0.47;
makeLawn();
wet.width=cv.width;wet.height=cv.height;wctx.setTransform(dpr,0,0,dpr,0,0);
}
function makeLawn(){
lawn.width=cv.width;lawn.height=cv.height;
const g=lawn.getContext('2d');g.setTransform(dpr,0,0,dpr,0,0);
const grd=g.createRadialGradient(ox,oy,40,ox,oy,Math.max(W,H)*0.85);
grd.addColorStop(0,'#5d9135');grd.addColorStop(1,'#3a6522');
g.fillStyle=grd;g.fillRect(0,0,W,H);
g.save();g.translate(W/2,H/2);g.rotate(-0.25);
const band=Math.max(40,scale*0.6),Rr=Math.hypot(W,H);
let i=0;for(let x=-Rr;x<Rr;x+=band,i++){g.fillStyle=i%2?'rgba(255,255,220,0.05)':'rgba(0,25,0,0.05)';g.fillRect(x,-Rr,band,2*Rr);}
g.restore();
let seed=12345;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296;};
const cols=['#2f5a1b','#3b6b22','#4a7d2b','#5a8f35','#6aa03e','#7bb048','#34601f','#86b850'];
const count=Math.floor(W*H/55);
g.lineWidth=1;g.globalAlpha=0.55;
for(let c=0;c<cols.length;c++){
g.strokeStyle=cols[c];g.beginPath();
for(let n=0;n<count/cols.length;n++){
const x=rnd()*W,y=rnd()*H,len=2+rnd()*5,a=-Math.PI/2+(rnd()-0.5)*1.3;
g.moveTo(x,y);g.lineTo(x+Math.cos(a)*len,y+Math.sin(a)*len);
}
g.stroke();
}
g.globalAlpha=1;
for(let n=0;n<W*H/9000;n++){
const x=rnd()*W,y=rnd()*H;
if(rnd()<0.3){g.fillStyle='rgba(250,215,50,0.9)';g.beginPath();g.arc(x,y,1.7,0,7);g.fill();}
else{g.fillStyle='rgba(125,175,85,0.55)';for(let k=0;k<3;k++){g.beginPath();g.arc(x+Math.cos(k*2.1)*2,y+Math.sin(k*2.1)*2,1.8,0,7);g.fill();}}
}
}
// ---- UI
const slider=document.getElementById('psi'),num=document.getElementById('psiNum'),big=document.getElementById('psiBig');
const rF=document.getElementById('rF'),rV=document.getElementById('rV'),rP=document.getElementById('rP'),
rPred=document.getElementById('rPred'),rKE=document.getElementById('rKE'),rSt=document.getElementById('status'),
slowEl=document.getElementById('slow');
rPred.textContent=pred.psi.toFixed(1)+' psi';
function setPsi(v){
v=Number(v);if(!isFinite(v))v=0;psi=Math.max(0,v);
if(psi>Number(slider.max))slider.max=String(Math.ceil(psi));
slider.value=String(psi);num.value=String(Math.round(psi*10)/10);
big.textContent=psi<100?psi.toFixed(1):psi.toFixed(0);
}
slider.addEventListener('input',()=>setPsi(slider.value));
num.addEventListener('change',()=>setPsi(num.value));
document.querySelectorAll('button[data-d]').forEach(b=>b.addEventListener('click',()=>{
const d=b.getAttribute('data-d');
if(d==='off')setPsi(0);else if(d==='full')setPsi(P.full);else setPsi(psi+Number(d));
}));
document.getElementById('noise').addEventListener('change',e=>{noiseOn=e.target.checked;});
document.getElementById('relay').addEventListener('click',relay);
addEventListener('keydown',e=>{
if(e.target&&e.target.tagName==='INPUT'&&e.target.type==='number')return;
if(e.key==='ArrowUp'){setPsi(psi+1);e.preventDefault();}
else if(e.key==='ArrowDown'){setPsi(psi-1);e.preventDefault();}
});
function readHash(){
const m=/psi\s*=\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)/i.exec(location.hash||'');
if(m)setPsi(parseFloat(m[1]));
}
addEventListener('hashchange',readHash);
// pointer: pin nearest node to mouse
let mouseX=0,mouseY=0;
function worldOf(e){const r=cv.getBoundingClientRect();return [(e.clientX-r.left-ox)/scale,(e.clientY-r.top-oy)/scale];}
cv.addEventListener('pointerdown',e=>{
const [wx,wy]=worldOf(e);let x=0,y=0,best=-1,bd=Infinity;
for(let k=1;k<=N;k++){x+=l*Math.cos(th[k-1]);y+=l*Math.sin(th[k-1]);const d=Math.hypot(x-wx,y-wy);if(d<bd){bd=d;best=k;}}
if(bd<Math.max(0.3,26/scale)){grab=best;gx=wx;gy=wy;mouseX=wx;mouseY=wy;cv.setPointerCapture(e.pointerId);cv.classList.add('grabbing');}
});
cv.addEventListener('pointermove',e=>{if(grab>0){const [wx,wy]=worldOf(e);gx=wx;gy=wy;mouseX=wx;mouseY=wy;}});
function release(){grab=-1;cv.classList.remove('grabbing');}
cv.addEventListener('pointerup',release);cv.addEventListener('pointercancel',release);
// chart
const chart=document.getElementById('chart'),cctx=chart.getContext('2d');
chart.width=236*dpr;chart.height=64*dpr;cctx.setTransform(dpr,0,0,dpr,0,0);
const HN=400,hKE=new Float32Array(HN).fill(-6),hPsi=new Float32Array(HN);let hHead=0,hAcc=0;
function drawChart(){
const w=236,h=64,g=cctx;g.clearRect(0,0,w,h);
const pmax=Math.max(100,Number(slider.max));
g.strokeStyle='rgba(255,160,60,0.55)';g.setLineDash([3,3]);g.beginPath();
const yp=h-2-(pred.psi/pmax)*(h-4);g.moveTo(0,yp);g.lineTo(w,yp);g.stroke();g.setLineDash([]);
g.lineWidth=1.3;
g.strokeStyle='#ff9a3c';g.beginPath();
for(let i=0;i<HN;i++){const v=hPsi[(hHead+i)%HN],x=i/(HN-1)*w,y=h-2-(v/pmax)*(h-4);i?g.lineTo(x,y):g.moveTo(x,y);}g.stroke();
g.strokeStyle='#5fe0ff';g.beginPath();
for(let i=0;i<HN;i++){const v=hKE[(hHead+i)%HN],x=i/(HN-1)*w,y=h-2-((v+6)/8)*(h-4);i?g.lineTo(x,y):g.moveTo(x,y);}g.stroke();
}
// ---- drawing
const sx=new Float64Array(N+1),sy=new Float64Array(N+1),wxN=new Float64Array(N+1),wyN=new Float64Array(N+1);
function hosePath(dx,dy){
ctx.beginPath();ctx.moveTo(sx[0]+dx,sy[0]+dy);
for(let k=1;k<N;k++)ctx.quadraticCurveTo(sx[k]+dx,sy[k]+dy,(sx[k]+sx[k+1])/2+dx,(sy[k]+sy[k+1])/2+dy);
ctx.lineTo(sx[N]+dx,sy[N]+dy);
}
let wetFrame=0,splats=[];
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.drawImage(lawn,0,0,W,H);
ctx.drawImage(wet,0,0,W,H);
wxN[0]=0;wyN[0]=0;
for(let k=0;k<N;k++){wxN[k+1]=wxN[k]+l*Math.cos(th[k]);wyN[k+1]=wyN[k]+l*Math.sin(th[k]);}
for(let k=0;k<=N;k++){sx[k]=X(wxN[k]);sy[k]=Y(wyN[k]);}
const hw=Math.max(6,0.045*scale);
const cb=Math.cos(P.thetaBase),sb=Math.sin(P.thetaBase);
const rx=X(-cb*0.13),ry=Y(-sb*0.13);
// riser shadow + spout
ctx.fillStyle='rgba(0,0,0,0.3)';ctx.beginPath();ctx.ellipse(rx+5,ry+6,0.07*scale+3,0.06*scale+3,0,0,7);ctx.fill();
ctx.lineCap='round';
ctx.strokeStyle='#7d8288';ctx.lineWidth=hw*0.85;ctx.beginPath();ctx.moveTo(rx,ry);ctx.lineTo(sx[0],sy[0]);ctx.stroke();
// hose
ctx.lineJoin='round';
ctx.strokeStyle='rgba(0,0,0,0.28)';ctx.lineWidth=hw+3;hosePath(3,4);ctx.stroke();
ctx.strokeStyle='#1f5e22';ctx.lineWidth=hw+1.5;hosePath(0,0);ctx.stroke();
ctx.strokeStyle='#2f8a33';ctx.lineWidth=hw;hosePath(0,0);ctx.stroke();
ctx.strokeStyle='rgba(230,215,70,0.85)';ctx.lineWidth=Math.max(1,hw*0.13);hosePath(hw*0.12,hw*0.12);ctx.stroke();
ctx.strokeStyle='rgba(255,255,255,0.28)';ctx.lineWidth=Math.max(1,hw*0.28);hosePath(-hw*0.2,-hw*0.22);ctx.stroke();
// nozzle (last 16 cm drawn over the hose)
const tc=Math.cos(th[N-1]),ts=Math.sin(th[N-1]);
const nl=0.16*scale,bxn=sx[N]-tc*nl,byn=sy[N]-ts*nl;
ctx.lineCap='butt';
ctx.strokeStyle='rgba(0,0,0,0.3)';ctx.lineWidth=hw*1.35;ctx.beginPath();ctx.moveTo(bxn+3,byn+4);ctx.lineTo(sx[N]+3,sy[N]+4);ctx.stroke();
ctx.strokeStyle='#9c7420';ctx.lineWidth=hw*1.35;ctx.beginPath();ctx.moveTo(bxn,byn);ctx.lineTo(sx[N],sy[N]);ctx.stroke();
ctx.strokeStyle='#e2b94c';ctx.lineWidth=hw*0.55;ctx.beginPath();ctx.moveTo(bxn-ts*hw*0.2,byn+tc*hw*0.2);ctx.lineTo(sx[N]-ts*hw*0.2,sy[N]+tc*hw*0.2);ctx.stroke();
ctx.strokeStyle='#2a2a2a';ctx.lineWidth=hw*1.45;ctx.beginPath();ctx.moveTo(bxn,byn);ctx.lineTo(bxn+tc*hw*0.35,byn+ts*hw*0.35);ctx.stroke();
ctx.strokeStyle='#c99b33';ctx.lineWidth=hw*0.8;ctx.beginPath();ctx.moveTo(sx[N],sy[N]);ctx.lineTo(sx[N]+tc*hw*0.5,sy[N]+ts*hw*0.5);ctx.stroke();
// coupling + riser + handle
ctx.fillStyle='#c9a13c';ctx.beginPath();ctx.arc(sx[0],sy[0],hw*0.75,0,7);ctx.fill();
ctx.fillStyle='#8f9499';ctx.beginPath();ctx.arc(rx,ry,0.055*scale,0,7);ctx.fill();
ctx.fillStyle='#b9bec3';ctx.beginPath();ctx.arc(rx-1.5,ry-1.5,0.035*scale,0,7);ctx.fill();
const ha=psi/P.full*4.5,hr=0.085*scale;
ctx.strokeStyle='#b8231d';ctx.lineWidth=Math.max(2.5,0.018*scale);
ctx.beginPath();ctx.arc(rx,ry,hr,0,7);ctx.stroke();
ctx.beginPath();for(let k=0;k<4;k++){const a=ha+k*Math.PI/2;ctx.moveTo(rx,ry);ctx.lineTo(rx+Math.cos(a)*hr,ry+Math.sin(a)*hr);}ctx.stroke();
ctx.fillStyle='#7a1410';ctx.beginPath();ctx.arc(rx,ry,0.02*scale,0,7);ctx.fill();
// water drops: shadows then elevated streaks
if(np){
ctx.fillStyle='rgba(0,20,10,0.13)';ctx.beginPath();
for(let i=0;i<np;i++){ctx.rect(X(pX[i])-1,Y(pY[i])-1,2,2);}
ctx.fill();
ctx.strokeStyle='rgba(215,238,255,0.78)';ctx.lineWidth=Math.max(1.2,scale*0.011);ctx.lineCap='round';ctx.beginPath();
const zf=0.8*scale;
for(let i=0;i<np;i++){
const x=X(pX[i]),y=Y(pY[i])-pZ[i]*zf;
ctx.moveTo(x,y);ctx.lineTo(x-pVX[i]*0.018*scale,y-pVY[i]*0.018*scale+pVZ[i]*0.018*zf);
}
ctx.stroke();
}
// grab indicator
if(grab>0){
ctx.strokeStyle='rgba(255,255,255,0.8)';ctx.lineWidth=1.5;ctx.setLineDash([4,3]);
ctx.beginPath();ctx.moveTo(sx[grab],sy[grab]);ctx.lineTo(X(mouseX),Y(mouseY));ctx.stroke();ctx.setLineDash([]);
ctx.fillStyle='rgba(255,230,200,0.85)';ctx.beginPath();ctx.arc(X(mouseX),Y(mouseY),7,0,7);ctx.fill();
ctx.strokeStyle='rgba(80,40,20,0.8)';ctx.stroke();
}
// scale bar
ctx.fillStyle='rgba(255,255,255,0.85)';ctx.fillRect(W-20-scale,H-24,scale,3);
ctx.font='11px system-ui,sans-serif';ctx.fillText('1 m',W-20-scale,H-30);
}
// ---- main loop
let last=performance.now(),keSm=0,hud=0;
function frame(now){
let dt=(now-last)/1000;last=now;if(!(dt>0))dt=0;dt=Math.min(dt,1/30);
Fjet=2*psi*PSI_PA*A;
const vj=Math.sqrt(2*psi*PSI_PA/P.rho),flow=A*vj;
const wEst=Math.sqrt((16*kb+4*l*Fjet)/(P.m*l*l2));
const hMax=Math.min(1/1500,1.2/wEst);
let n=Math.max(1,Math.ceil(dt/hMax)),simDt=dt,slow=false;
if(n>300){n=300;simDt=n*hMax;slow=true;}
const h=simDt/n,rate=Math.min(3000,flow*3.0e6);
for(let s=0;s<n;s++){
if(noiseOn&&Fjet>0){Fn+=-Fn*h/P.noiseTau+P.noiseFrac*Fjet*Math.sqrt(2*h/P.noiseTau)*gauss();}else Fn=0;
step(h);
emitAcc+=rate*h;
if(emitAcc>=1){const tsx=tipState(),tb=(s+1)*h;while(emitAcc>=1){emitAcc-=1;emit(tb,tsx,vj);}}
}
if(!isFinite(th[N-1])||!isFinite(om[N-1]))relay();
slowEl.style.display=slow?'inline':'none';
// particles
const dtp=simDt;splats.length=0;
for(let i=0;i<np;){
let vx=pVX[i],vy=pVY[i],vz=pVZ[i];
const sp=Math.sqrt(vx*vx+vy*vy+vz*vz),f=1/(1+pK[i]*sp*dtp);
vx*=f;vy*=f;vz=vz*f-9.81*dtp;
const x=pX[i]+vx*dtp,y=pY[i]+vy*dtp,z=pZ[i]+vz*dtp;
if(z<=0||Math.abs(x)>40||Math.abs(y)>40){
if(z<=0)splats.push(x,y);
np--;pX[i]=pX[np];pY[i]=pY[np];pZ[i]=pZ[np];pVX[i]=pVX[np];pVY[i]=pVY[np];pVZ[i]=pVZ[np];pK[i]=pK[np];continue;
}
pX[i]=x;pY[i]=y;pZ[i]=z;pVX[i]=vx;pVY[i]=vy;pVZ[i]=vz;i++;
}
if(splats.length){
wctx.fillStyle='rgba(18,45,28,0.16)';wctx.beginPath();
const r0=Math.max(1,scale*0.012);
for(let i=0;i<splats.length;i+=2){const x=X(splats[i]),y=Y(splats[i+1]),r=r0*(0.7+Math.random()*1.1);wctx.moveTo(x+r,y);wctx.arc(x,y,r,0,7);}
wctx.fill();
}
if(++wetFrame%20===0){wctx.globalCompositeOperation='destination-out';wctx.fillStyle='rgba(0,0,0,0.04)';wctx.fillRect(0,0,W,H);wctx.globalCompositeOperation='source-over';}
// kinetic energy
let ke=0,vx=0,vy=0;
for(let k=1;k<=N;k++){const i=k-1;vx-=l*om[i]*Math.sin(th[i]);vy+=l*om[i]*Math.cos(th[i]);ke+=0.5*mk[k]*(vx*vx+vy*vy);}
keSm+=(ke-keSm)*Math.min(1,simDt/0.7);
hAcc+=simDt;while(hAcc>=0.1){hAcc-=0.1;hKE[hHead]=Math.log10(ke+1e-6);hPsi[hHead]=psi;hHead=(hHead+1)%HN;}
render();
if(++hud%4===0){
rF.textContent=Fjet.toFixed(2)+' N';
rV.textContent=vj.toFixed(1)+' m/s · '+(flow*60000).toFixed(1)+' L/min';
rP.textContent=(Fjet*P.L*P.L/P.EI).toFixed(1)+' (crit '+pred.pnd.toFixed(1)+')';
rKE.textContent=ke<1e-3?(ke*1000).toFixed(2)+' mJ':ke.toFixed(3)+' J';
let st,col;
if(grab>0){st='held by you';col='#ffd27a';}
else if(keSm<5e-4){st='settled';col='#9fe39a';}
else if(keSm>0.05){st='whipping';col='#ff7a5c';}
else{st='moving / settling';col='#ffe08a';}
rSt.textContent=st;rSt.style.color=col;
drawChart();
}
requestAnimationFrame(frame);
}
resize();addEventListener('resize',resize);
relay();readHash();
requestAnimationFrame(t=>{last=t;requestAnimationFrame(frame);});
})();
</script>
</body>
</html>
What I expect from the math (I have not timed it against the 27.5 psi prediction):
What is artificial:
What it gets wrong, compared with the hose that got away from you:
#psi= values work, but the simulation falls into slow motion (flagged on screen) to stay stable.