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 discretized into N=100 nodes of mass m=0.15 kg connected by inextensible distance constraints of length ds=0.1 m, solved with 6 Gauss-Seidel iterations per substep. Bending resistance is a Laplacian smoothing constraint with stiffness k_bend=0.05, applied twice per iteration. The nozzle reaction force is F = k_psi · psi with k_psi=0.08 N/psi, directed opposite the nozzle exit vector and distributed over the last three nodes. Ground friction is Coulomb-style: each node’s velocity is reduced by mu·g·dt_sub with mu=0.8, and a small global damping (0.002) is applied. A tiny random jitter (±0.01 rad) is added to the thrust direction to seed the instability.
30 psi
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Garden Hose</title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: #1a2a1a;
font-family: system-ui, -apple-system, sans-serif;
touch-action: none;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
#ui {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(20, 30, 20, 0.85);
color: #e0f0e0;
padding: 12px 24px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,0.1);
user-select: none;
}
#ui label {
font-size: 14px;
letter-spacing: 0.5px;
text-transform: uppercase;
opacity: 0.8;
}
#psiValue {
font-size: 24px;
font-weight: 700;
min-width: 80px;
text-align: right;
font-variant-numeric: tabular-nums;
color: #8fd18f;
}
input[type="range"] {
width: 200px;
accent-color: #4caf50;
cursor: pointer;
}
#hint {
position: fixed;
top: 20px;
left: 20px;
color: rgba(255,255,255,0.7);
font-size: 14px;
background: rgba(0,0,0,0.3);
padding: 8px 12px;
border-radius: 8px;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hint">Drag the hose to pin it. Release to let go.</div>
<div id="ui">
<label for="psi">Pressure</label>
<input type="range" id="psi" min="0" max="80" step="1" value="60">
<span id="psiValue">60 psi</span>
</div>
<script>
// --- Utility ---
function bezier(a, b, c, d, t) {
const mt = 1 - t;
return mt*mt*mt*a + 3*mt*mt*t*b + 3*mt*t*t*c + t*t*t*d;
}
function makeRandom(seed) {
let s = seed;
return function() {
s = (s * 1664525 + 1013904223) % 4294967296;
return s / 4294967296;
};
}
// --- Canvas & World ---
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let cw, ch;
const worldW = 16;
const worldH = 9;
let scale, offsetX, offsetY;
function updateTransform() {
scale = Math.min(cw / worldW, ch / worldH) * 0.95;
offsetX = (cw - worldW * scale) / 2;
offsetY = (ch - worldH * scale) / 2;
}
function worldToScreen(x, y) {
return { x: offsetX + x * scale, y: offsetY + y * scale };
}
function screenToWorld(x, y) {
return { x: (x - offsetX) / scale, y: (y - offsetY) / scale };
}
function resize() {
cw = canvas.width = window.innerWidth;
ch = canvas.height = window.innerHeight;
updateTransform();
}
window.addEventListener('resize', resize);
// --- Background ---
const bgCanvas = document.createElement('canvas');
bgCanvas.width = 1600;
bgCanvas.height = 900;
const bgCtx = bgCanvas.getContext('2d');
function drawBackground() {
const w = bgCanvas.width;
const h = bgCanvas.height;
const grad = bgCtx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#2d5a2d');
grad.addColorStop(0.5, '#3a6b3a');
grad.addColorStop(1, '#2a4f2a');
bgCtx.fillStyle = grad;
bgCtx.fillRect(0, 0, w, h);
const rand = makeRandom(12345);
for (let i = 0; i < 4000; i++) {
const x = rand() * w;
const y = rand() * h;
const len = 4 + rand() * 12;
const angle = -Math.PI/2 + (rand() - 0.5) * 0.8;
const shade = 40 + rand() * 60;
bgCtx.strokeStyle = `rgb(${20 + shade*0.3}, ${60 + shade}, ${20 + shade*0.3})`;
bgCtx.lineWidth = 1 + rand() * 2;
bgCtx.beginPath();
bgCtx.moveTo(x, y);
bgCtx.lineTo(x + Math.cos(angle) * len, y + Math.sin(angle) * len);
bgCtx.stroke();
}
for (let i = 0; i < 50; i++) {
const x = rand() * w;
const y = rand() * h;
const r = 20 + rand() * 80;
const g = bgCtx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, 'rgba(0,0,0,0.1)');
g.addColorStop(1, 'rgba(0,0,0,0)');
bgCtx.fillStyle = g;
bgCtx.beginPath();
bgCtx.arc(x, y, r, 0, Math.PI*2);
bgCtx.fill();
}
}
drawBackground();
// --- Hose Physics ---
const N = 100;
const L = 10;
const ds = L / N;
const totalMass = 15;
const m = totalMass / N;
const kPsi = 0.08;
const mu = 0.8;
const g = 9.81;
const damping = 0.002;
const kBend = 0.05;
const substeps = 12;
const dt = 1/60;
const dtSub = dt / substeps;
let nodes = [];
let draggedNode = -1;
let mouseWorld = { x: 0, y: 0 };
function initHose() {
const p0 = { x: 1.5, y: 4.5 };
const p1 = { x: 4.0, y: 2.0 };
const p2 = { x: 8.0, y: 7.0 };
const p3 = { x: 11.5, y: 4.5 };
const steps = 2000;
const samples = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = bezier(p0.x, p1.x, p2.x, p3.x, t);
const y = bezier(p0.y, p1.y, p2.y, p3.y, t);
samples.push({ x, y });
}
let cum = [0];
for (let i = 1; i < samples.length; i++) {
const dx = samples[i].x - samples[i-1].x;
const dy = samples[i].y - samples[i-1].y;
cum.push(cum[i-1] + Math.hypot(dx, dy));
}
const totalLen = cum[cum.length - 1];
const scaleFactor = L / totalLen;
for (const s of samples) {
s.x = p0.x + (s.x - p0.x) * scaleFactor;
s.y = p0.y + (s.y - p0.y) * scaleFactor;
}
cum = [0];
for (let i = 1; i < samples.length; i++) {
const dx = samples[i].x - samples[i-1].x;
const dy = samples[i].y - samples[i-1].y;
cum.push(cum[i-1] + Math.hypot(dx, dy));
}
nodes = [];
for (let i = 0; i <= N; i++) {
const target = (i / N) * L;
let j = 0;
while (j < cum.length - 1 && cum[j+1] < target) j++;
const t = (target - cum[j]) / (cum[j+1] - cum[j]);
const x = samples[j].x + (samples[j+1].x - samples[j].x) * t;
const y = samples[j].y + (samples[j+1].y - samples[j].y) * t;
nodes.push({ x, y, px: x, py: y, pinned: i === 0 });
}
for (let iter = 0; iter < 50; iter++) {
applyDistanceConstraints();
}
}
function applyDistanceConstraints() {
for (let i = 0; i < N; i++) {
const a = nodes[i];
const b = nodes[i+1];
let dx = b.x - a.x;
let dy = b.y - a.y;
let dist = Math.hypot(dx, dy);
if (dist < 1e-6) continue;
const diff = (dist - ds) / dist;
const moveX = dx * diff;
const moveY = dy * diff;
const aFixed = a.pinned || i === draggedNode;
const bFixed = b.pinned || i+1 === draggedNode;
if (aFixed && bFixed) continue;
if (aFixed) {
b.x -= moveX;
b.y -= moveY;
} else if (bFixed) {
a.x += moveX;
a.y += moveY;
} else {
a.x += moveX * 0.5;
a.y += moveY * 0.5;
b.x -= moveX * 0.5;
b.y -= moveY * 0.5;
}
}
}
function applyBendingConstraints() {
for (let i = 1; i < N; i++) {
const a = nodes[i-1];
const b = nodes[i];
const c = nodes[i+1];
if (b.pinned || i === draggedNode) continue;
const mx = (a.x + c.x) * 0.5;
const my = (a.y + c.y) * 0.5;
b.x += (mx - b.x) * kBend;
b.y += (my - b.y) * kBend;
}
}
function physicsStep() {
const thrustMag = kPsi * currentPsi;
const n1 = nodes[N-1];
const n2 = nodes[N];
let dx = n2.x - n1.x;
let dy = n2.y - n1.y;
const len = Math.hypot(dx, dy) || 1;
const ux = dx / len;
const uy = dy / len;
const jitter = (Math.random() - 0.5) * 0.02;
const angle = Math.atan2(uy, ux) + jitter;
const fx = -Math.cos(angle) * thrustMag;
const fy = -Math.sin(angle) * thrustMag;
for (let sub = 0; sub < substeps; sub++) {
for (let i = 0; i <= N; i++) {
const node = nodes[i];
if (node.pinned || i === draggedNode) continue;
let vx = (node.x - node.px) * (1 - damping);
let vy = (node.y - node.py) * (1 - damping);
let ax = 0, ay = 0;
if (i === N) {
ax += fx / m;
ay += fy / m;
} else if (i === N-1) {
ax += fx * 0.5 / m;
ay += fy * 0.5 / m;
} else if (i === N-2) {
ax += fx * 0.25 / m;
ay += fy * 0.25 / m;
}
const newX = node.x + vx + ax * dtSub * dtSub;
const newY = node.y + vy + ay * dtSub * dtSub;
node.px = node.x;
node.py = node.y;
node.x = newX;
node.y = newY;
}
for (let i = 0; i <= N; i++) {
const node = nodes[i];
if (node.pinned || i === draggedNode) continue;
let vx = (node.x - node.px) / dtSub;
let vy = (node.y - node.py) / dtSub;
const speed = Math.hypot(vx, vy);
if (speed > 1e-6) {
const frictionDecel = mu * g * dtSub;
const newSpeed = Math.max(0, speed - frictionDecel);
const scale = newSpeed / speed;
vx *= scale;
vy *= scale;
node.px = node.x - vx * dtSub;
node.py = node.y - vy * dtSub;
} else {
node.px = node.x;
node.py = node.y;
}
}
if (draggedNode >= 0 && draggedNode <= N) {
const node = nodes[draggedNode];
node.x = mouseWorld.x;
node.y = mouseWorld.y;
node.px = node.x;
node.py = node.y;
}
for (let iter = 0; iter < 4; iter++) {
applyDistanceConstraints();
applyBendingConstraints();
}
for (let iter = 0; iter < 2; iter++) {
applyDistanceConstraints();
}
}
}
// --- Water Particles ---
let particles = [];
let spawnAccum = 0;
function spawnWater(dt) {
if (currentPsi < 1) return;
const rate = currentPsi * 0.8;
spawnAccum += rate * dt;
while (spawnAccum >= 1) {
spawnAccum -= 1;
const n1 = nodes[N-1];
const n2 = nodes[N];
let dx = n2.x - n1.x;
let dy = n2.y - n1.y;
const len = Math.hypot(dx, dy) || 1;
let ux = dx / len;
let uy = dy / len;
const angle = Math.atan2(uy, ux) + (Math.random() - 0.5) * 0.25;
const speed = 1.5 + currentPsi * 0.1;
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
particles.push({
x: n2.x, y: n2.y,
vx, vy,
life: 0.6 + Math.random() * 0.6,
maxLife: 1.2,
size: 0.02 + Math.random() * 0.03
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.98;
p.vy *= 0.98;
p.life -= dt;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
// --- Rendering ---
function drawHose(isShadow) {
if (nodes.length === 0) return;
ctx.save();
if (isShadow) {
ctx.translate(0.08 * scale, 0.08 * scale);
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = 0.18 * scale;
} else {
ctx.strokeStyle = '#1b4d1b';
ctx.lineWidth = 0.16 * scale;
}
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
const p0 = worldToScreen(nodes[0].x, nodes[0].y);
ctx.moveTo(p0.x, p0.y);
for (let i = 1; i <= N; i++) {
const p = worldToScreen(nodes[i].x, nodes[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
if (!isShadow) {
ctx.strokeStyle = '#2e7d32';
ctx.lineWidth = 0.12 * scale;
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
for (let i = 1; i <= N; i++) {
const p = worldToScreen(nodes[i].x, nodes[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(160, 220, 160, 0.4)';
ctx.lineWidth = 0.04 * scale;
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
for (let i = 1; i <= N; i++) {
const p = worldToScreen(nodes[i].x, nodes[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
}
ctx.restore();
}
function drawSpigot() {
const p = worldToScreen(nodes[0].x, nodes[0].y);
ctx.save();
ctx.translate(p.x, p.y);
const r = 0.25 * scale;
const grad = ctx.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r);
grad.addColorStop(0, '#e0e0e0');
grad.addColorStop(0.5, '#888');
grad.addColorStop(1, '#444');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 0.06 * scale;
ctx.beginPath();
ctx.moveTo(-r*0.8, 0);
ctx.lineTo(r*0.8, 0);
ctx.moveTo(0, -r*0.8);
ctx.lineTo(0, r*0.8);
ctx.stroke();
ctx.restore();
}
function drawNozzle() {
const n1 = nodes[N-1];
const n2 = nodes[N];
const p = worldToScreen(n2.x, n2.y);
const angle = Math.atan2(n2.y - n1.y, n2.x - n1.x);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(angle);
const len = 0.3 * scale;
const wid = 0.12 * scale;
ctx.fillStyle = '#555';
ctx.fillRect(0, -wid/2, len, wid);
ctx.fillStyle = '#888';
ctx.fillRect(0, -wid/2, len*0.3, wid);
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(len, 0, wid*0.4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
const sp = worldToScreen(p.x, p.y);
const alpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = `rgba(120, 200, 255, ${alpha * 0.8})`;
ctx.beginPath();
ctx.arc(sp.x, sp.y, p.size * scale, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = `rgba(120, 200, 255, ${alpha * 0.3})`;
ctx.lineWidth = p.size * scale * 0.8;
ctx.beginPath();
ctx.moveTo(sp.x, sp.y);
ctx.lineTo(sp.x - p.vx * 0.05 * scale, sp.y - p.vy * 0.05 * scale);
ctx.stroke();
}
}
function render() {
ctx.fillStyle = '#1a2a1a';
ctx.fillRect(0, 0, cw, ch);
ctx.drawImage(bgCanvas, offsetX, offsetY, worldW * scale, worldH * scale);
drawHose(true);
drawHose(false);
drawSpigot();
drawNozzle();
drawParticles();
}
// --- Interaction ---
canvas.addEventListener('pointerdown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const w = screenToWorld(x, y);
let minDist = 0.8;
let closest = -1;
for (let i = 0; i <= N; i++) {
const dx = nodes[i].x - w.x;
const dy = nodes[i].y - w.y;
const d = Math.hypot(dx, dy);
if (d < minDist) {
minDist = d;
closest = i;
}
}
if (closest >= 0) {
draggedNode = closest;
mouseWorld.x = w.x;
mouseWorld.y = w.y;
canvas.setPointerCapture(e.pointerId);
}
});
canvas.addEventListener('pointermove', (e) => {
if (draggedNode < 0) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const w = screenToWorld(x, y);
mouseWorld.x = w.x;
mouseWorld.y = w.y;
});
canvas.addEventListener('pointerup', (e) => {
if (draggedNode >= 0) {
draggedNode = -1;
canvas.releasePointerCapture(e.pointerId);
}
});
canvas.addEventListener('pointercancel', (e) => {
if (draggedNode >= 0) {
draggedNode = -1;
canvas.releasePointerCapture(e.pointerId);
}
});
// --- Pressure UI ---
const slider = document.getElementById('psi');
const psiValue = document.getElementById('psiValue');
let initialPsi = 60;
if (location.hash.startsWith('#psi=')) {
const val = parseFloat(location.hash.slice(5));
if (!isNaN(val)) {
initialPsi = Math.max(0, Math.min(80, val));
}
}
slider.value = initialPsi;
let targetPsi = initialPsi;
let currentPsi = initialPsi;
psiValue.textContent = Math.round(currentPsi) + ' psi';
slider.addEventListener('input', () => {
targetPsi = parseFloat(slider.value);
});
// --- Main Loop ---
let lastTime = performance.now();
function loop(now) {
const delta = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
currentPsi += (targetPsi - currentPsi) * 0.08;
if (Math.abs(currentPsi - targetPsi) < 0.05) currentPsi = targetPsi;
psiValue.textContent = Math.round(currentPsi) + ' psi';
physicsStep();
spawnWater(delta);
updateParticles(delta);
render();
requestAnimationFrame(loop);
}
// --- Start ---
resize();
initHose();
requestAnimationFrame(loop);
</script>
</body>
</html>
The motion looks like a real hose when it is settling: the chain sags into a curve, friction holds it, and small pressure changes cause slow, believable adjustments. At high pressure the nozzle kicks and the hose begins to flail, with the water jet visibly following the nozzle direction. The spigot stays pinned and the hose does not stretch noticeably.
It gets several things wrong. The hose is a 2D top-down chain, so it cannot lift off the ground or twist out of plane. The bending constraint is a simple Laplacian smoothing, not a physical angular spring, so the stiffness is not quantitatively accurate and the whipping is more chaotic and elastic than a real hose. Friction is applied as a velocity-level Coulomb damping rather than a proper static/kinetic model with normal forces, so the threshold is approximate and can vary with frame rate. I inject a small random jitter into the thrust direction to seed the instability; without it the motion can be too symmetric. The water particles are purely visual and do not affect the hose dynamics. The distance constraints are iterative, so under high tension the hose can stretch slightly. Finally, the whipping often looks like a flailing snake rather than the coherent, heavy whip of a real garden hose.