Aquarium breach · qwen3.8-2.4t-a95b
Create a large glass aquarium whose side panel develops a visible crack and then bursts. The simulation must include: - Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains - A curved water jet affected by gravity - A spreading puddle that collides with the room boundaries - Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current - Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions - Fish attempting to swim against the current before being swept through the breach - Glass fragments with angular velocity, collisions, and water resistance - A visible waterline that lowers continuously rather than disappearing all at once Let the user drag the crack vertically before triggering the failure. A lower crack should initially produce a stronger jet than a higher crack. Give me 1 html file
transcript · 12 messages
Create a large glass aquarium whose side panel develops a visible crack and then bursts. The simulation must include: - Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains - A curved water jet affected by gravity - A spreading puddle that collides with the room boundaries - Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current - Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions - Fish attempting to swim against the current before being swept through the breach - Glass fragments with angular velocity, collisions, and water resistance - A visible waterline that lowers continuously rather than disappearing all at once Let the user drag the crack vertically before triggering the failure. A lower crack should initially produce a stronger jet than a higher crack. Give me 1 html file
thinking
The user wants a single-file HTML simulation of an aquarium that cracks and bursts. Let me build a solid canvas-based physics simulation.
Plan:
- Canvas fills the entire screen, room with a floor.
- Aquarium: a glass tank sitting on a stand or floor, positioned at the left/center of the room.
- Water inside the tank drawn as a rectangle from the bottom up to the water surface. The water surface drops over time as water drains out.
- Crack position: user drags the crack vertically along the side panel (right panel). A "Burst" button triggers it (or automatic after cracking? The requirement says: "allow the user to drag the crack vertically before triggering the rupture" — so there's a trigger button, or a spacebar/click).
- Jet: particles ejected from the hole. Velocity based on Torricelli: v = sqrt(2g * (waterSurface - holeY)). Decreases as the tank drains. When the water surface drops below the hole, flow stops (or just a trickle until the water level reaches the hole height).
- Jet curves under gravity — particle simulation.
- Particles hit the floor and spread into a puddle. The puddle grows (drawn as a rising water region on the floor, spreading until it hits the walls of the room). Model puddle volume, width spreads to walls, then depth rises.
- Fish: underwater, swims toward the crack / struggles against the current (a current that pulls toward the hole), then gets swept out through the breach, becomes airborne, falls, collides with the floor (flapping). Rocks sink, plants sway and stay rooted (maybe float away after rupture?), floating toys bob on the water surface. After water drains, objects transition to airborne, then to floor.
- Glass shards when bursting: shards with angular velocity, affected by gravity, collide with floor, water drag while submerged.
- Reset button.
Implementation details:
Tank geometry: tank x0, x1, yBottom (floor), height. Glass walls. Water level waterY.
Crack: position on right wall, crackY, drawn as a jagged line. Draggable vertically within the range of the tank wall. Before burst, a small leak? "Develops visible cracks, then bursts" — maybe a small seep at the crack before bursting. Keep it simple: crack is visible. On burst, the wall section near the crack is blown out into shards.
Flow: while waterY < crackY, holeQ = C * sqrt(2g*(waterY - holeY)). Particle emission rate proportional. Particles exit with velocity vx = flow velocity, vy small; gravity applied; collide with floor → convert to puddle volume and despawn.
Water level drops based on outflow: waterLevel -= outflowRate*dt (area-based).
Puddle: track volume. Puddle spreads from base of tank right wall across the floor, bounded by room walls. Simple model: puddle is a region on the floor; width increases until it hits walls, then depth rises. Draw as a blue ellipse/rectangle with some waves. Objects in puddle region float if water deep enough.
Fish behavior: underwater, fish swims; after burst, current field pulls toward hole (velocity toward hole proportional to flow and 1/distance). Fish tries to swim away (thrust in opposite direction), but when current > fish's max swim speed near hole, it gets swept out; when outside the tank and airborne, gravity, drag; floor: flop (random small hops), eventually rests.
Rocks: high density, sink, stay put. Strong current might roll them slightly? Mostly stay put.
Plants: rooted to bottom, sway with current. When water drains below the top of the plant, plant flops over (falls to side), lies on floor.
Toy (floating duck/ball): floats on water surface (buoyancy). Follows surface; when level drops, toy settles on floor. When water flows out — toy might drift toward hole but probably too big to pass through; pressed against wall.
Glass shards: polygons generated along the wall section on burst; given outward velocity + angular velocity; while in water (y below interior water level?) get water drag... Actually shards fly outward, might pass through the jet region. Simple: in puddle (below puddle surface, outside tank) or inside remaining water gets water drag. Collide with floor and bounce with restitution, rotation.
Rendering: canvas 2D. Background room: wall color, floor, maybe a window. Draw the tank glass, water with gradient, water surface line highlight, objects, jet particles, puddle, shards.
UI: instruction overlay, Burst button, Reset button. Crack drag: mouse/touch near the crack point.
Physics units: pixels, gravity ~1400 px/s². dt fixed 1/60, with clamp.
Let me define the layout: canvas 960x600 logical size, scaled to window. Floor at y=540. Tank: x 140..460, top y=120, bottom y=540 (sitting on floor). Water starts at waterY=150.
Right wall x1=460. Crack on right wall between waterY+10 and floor-20. On burst, the hole opens around crackY with height ~40px; wall segment above the hole? Actually "bursts" — the panel breaks: shards = entire lower portion of right wall up to some height? Let's say the wall shatters from crackY-30 up to the floor... Hmm, better: crack grows, and the panel bursts over a region: from crackY - 40 to crackY + 40? A stronger burst: shards fly off from the whole section of the wall. Then water flows out through the opening. The opening's bottom is at the floor (y=540) and top at burstTop = crackY - 60? Then water drains until waterY reaches the opening's top... Actually if the wall breaks from the crack downward to the floor, water drains until the water surface reaches the crack height? No — if the wall is missing from crackY to the floor, water flows over... Water can only stay if the wall is intact below the water surface. If the wall is missing from crackY down to floor, all water above the floor flows out — water drains until the interior water surface equals the floor level (i.e., fully drains to puddle, minus what the puddle covers: water equalizes between interior and puddle). Nice detail: final equilibrium — water level inside tank equals puddle depth. Can implement: total volume is conserved between interior water (above tank floor) and puddle; flow driven by interior level and puddle level head difference. This gives a nice continuous drop in water surface and puddle growth.
Simpler robust model:
- interiorLevel: water height above tank floor (in px, measured inside tank).
- Puddle: volume V_p. Puddle depth d depends on width: width grows, up to available floor (right of tank to right wall, plus wraps to left too?). Let puddle only be on right side of tank: from x=460 to min(460 + w, roomRight=940). Depth d = V_p / w. When reaching wall, depth rises. Also puddle water can flow back into the tank through the broken opening? Equilibrium: once the interior level drops to the puddle level, flow stops. Model flow rate Q = k * sqrt(2g*max(0, interiorLevel - puddleDepth)) * openingArea. Opening height = min(interiorLevel, floor - burstTop)...
Let me define burst opening: bottom of opening = tank floor (y=540), top = burstTopY = crackY - 50 (so opening extends from crack region down to floor). Effective outflow height h_open = clamp(interiorLevel - 0, 0, floorY - burstTopY), in px of water.
Head for velocity: head = interiorLevel - puddleDepth. Jet velocity v = sqrt(2*g*head) (Torricelli), horizontal direction.
Volume conservation: totalWater = interior water + jet in flight (negligible) + puddle. When jet particles land, add their volume to puddle. Interior level decreases by Q*dt/tankWidth.
Equilibrium: interiorLevel approaches puddleDepth → head→0 → flow stops. Puddle depth at equilibrium: totalW = W*tankWidth… total area: interior width 320, puddle width up to 480. Final level L = totalVol/(320+480). Initial interior water height ~390px → final ~390*320/800 = 156 px depth. That's a big puddle but fine, dramatic. Maybe limit puddle to spread to both sides of tank to reduce depth. Let's allow puddle to spread on both sides: right of tank, and after it wraps? Simpler: puddle spreads on right side to wall, then left side, then depth rises. That's a hassle. Alternative: puddle depth caps visually; that's fine — 150px puddle, objects float in it. Fish flop in the puddle. Fun! Let's go with that; puddle spreads across right region 460..940, width 480. Final level ≈ 156px. Hmm, high, but fish/toy can float in it. OK.
Actually maybe reduce initial water height, or have some water "lost" (splash out). Could add splash particles that don't return. Or have puddle spread across the entire room floor (both sides of tank). Tank sits on floor; water flowing out on the right would wrap around the front, but in a 2D side view, the base of the tank blocks. Draw the puddle as both sides, as if water went around — simpler physics: puddleWidth is total available = room width minus tank base. Let's do this: puddle occupies the right side first (closer), then when it hits right wall, also appears on left side (spills around behind). Even simpler: one puddle region across the whole floor except tank footprint, with depth uniform. Growth: starts from base of right wall, extends right; when it reaches right wall, extends left; then depth rises. Implement with two segments: rightSeg [460, rx], leftSeg [lx, 140]. Draw both at same depth. Good.
Volume: interior water width 320, total floor width (960-320)=640. Final depth = 390*320/640 = 195. Hmm. If initial water height 300 (waterY=240), final 150. OK let's just accept; dramatic flooding. Alternatively, lose 40% of jet volume to splashes/evaporation off-screen to keep puddle smaller. Let me add a splash loss factor: particles that land contribute 100% but some particles fly out as pure splash droplets that disappear. Actually simplest: when a particle lands, add volume * 0.8, and spawn splash droplets that don't get recycled (visual only). Good.
Fish: N=5. Each: position, velocity, tail phase, size, color. States: 'swim' (underwater), 'swept' (near hole/outside), 'air', 'floor' (flop/rest), 'puddleFloat' (swim in puddle if depth > fish size). Behavior underwater before burst: gentle wander inside tank. After burst: current field toward hole. Current strength at a point: proportional to Q, direction toward hole center, magnitude ~ k*Q/(dist+50). Fish thrust: tries to swim away from hole (opposite of current), with max speed; if currentMag > thrust*1.2 or fish enters hole region → gets swept: state 'ejected': velocity = current + hole velocity, then airborne physics (gravity, air drag), floor collision: bounce with restitution 0.3, flop animation (impulse to vy, rotation oscillation) while energy timer lasts, then rest. If lands inside puddle with depth > fish height: state 'puddle': buoyancy swims near puddle surface, limited.
Rocks: 4-6, static on tank floor; when water drains, just sit there; maybe get nudged slightly by strong current near hole. Rest on floor/puddle.
Plants: 3-4, rooted on tank floor, drawn as swaying fronds (sine wave with time and current strength). When water level drops below plant top, plant droops: animate bendAngle to 90°, lie on floor.
Toy: beach ball or rubber duck floating on surface: y = waterY - radius*(fraction submerged). Drifts toward current; when level drops below hole, the toy settles on tank floor (if water shallow) → rests. Ball too big to exit through hole; pressed against right wall near the hole, bobbing.
Glass shards: on burst, create ~14 shards along the wall from burstTopY to floorY: each shard is a triangle/quad with random vertices, position on the wall, velocity outward-right (vx 100-400) and some vy, angular velocity. Physics: gravity; while in water (inside puddle depth region or inside tank water — but shards are outside) → if y > puddleSurfaceY within puddle x range: drag coefficient 0.9^..., buoyancy slightly up if submerged. Floor collision: y + r > floorY: bounce vy*=-0.35, vx*=0.7, omega*=0.7; when velocity small → rest. Some shards might fly into jet path — that's fine.
Crack drag: pointer events; hit test near crack marker on right wall (within 20px). Drag updates crackY between topLimit (waterY+20? Allow anywhere in wall, 140..520) and floor. Lower crack → greater head (waterY is fixed) → stronger jet. ✓
Pre-burst: also show a small seep droplet at crack? "Develops visible cracks then bursts" — crack appears with animation (draw jagged line growing over 1s at start or after a short delay), plus a slight drip. Add a slight seep particle at crack with small flow — nice touch, plus a slow pre-leak that decreases interior level slightly? Simple: just a few drip particles, negligible volume.
Water surface line: draw water with gradient, bright line at surface, slight waves. Continuous drop ✓.
Also water in tank has a hole: when wall is broken below interior level, water flows out; interior water drawn as rectangle up to waterY from tank floor, clipped to interior of tank. Broken section of wall: don't draw glass there, draw jagged edge.
Jet particles: emit from hole center (x=460, y between max(waterY, ...) and floor). Actually opening spans y ∈ [burstTop..floor] but water only exists below waterY. Emit from random y between max(burstTop, waterY) and floorY, vx = v0*(0.8+0.4*rand), v0 = sqrt(2g*head)*scale; vy small random. Gravity. When y>floorY: add volume to puddle, spawn splash (2-3 small short-lived droplets), remove. Particle is a circle radius 2-4, blue. Max ~800 particles; emit rate proportional to Q.
Also add streamlines: to make the jet look coherent, can also draw a quadratic curve from hole to landing point. Particles are enough.
Numbers: g=1500 px/s². Head up to ~350px → v=sqrt(2*1500*350)=1024 px/s. Horizontal travel before hitting floor: hole at y=500, fall 40px: t=sqrt(2*40/1500)=0.23s → 235px. Good, reaches floor within room (460+235=695 < 940). If crack is high (y=250), fall 290px t=0.62 → but head small? If crack is high, waterY=200, head=50 → v=387, distance 240. OK.
Current in tank pulls fish toward hole ✓.
Code structure:
```js
const state = { phase:'intact'|'cracking'|'burst', crackY, ... }
```
Timeline: on load, after 1.2s, crack grows over ~1s (crackProgress 0→1). User drags crack. "Burst" button (or Enter/double-click on crack). On burst: create shards, opening exists.
Reset: reload state (reinitialize without page reload; simplest: full reset of state, keep canvas).
Let me write the code carefully but compactly. Use requestAnimationFrame, fixed dt substeps (clamp dt to 1/30).
Key variables:
```
const W=960,H=600,FLOOR=545;
tank = {x0:150,x1:470,top:110,bot:FLOOR};
water = {level: height above floor; initial level = FLOOR-160 → waterY=... }
```
Let me set the top of water at y=170 → level = 545-170=375.
Crack constraint: between y=200 and y=530.
burstTop = crackY - 55.
Opening: from burstTop to floor; effective outflow height = clamp(level, 0, FLOOR-burstTop).
puddle = {vol:0, rx:tank.x1, lx:tank.x0, depth: computed}.
depth calc: width = (rx - x1) + (x0 - lx); if vol==0 depth 0. Grow: first expand rx from x1 to WALL_R=950 at depth cap? Better: depth grows with vol: target shape: as vol increases, spread with a shallow leading edge. Simple approach: maintain depth d, width w, vol = d*w. Growth rule: if w < maxW (950-x1 + x0-10=860… total available: right: 950-470=480; left: 150-10=140; total 620) then increase w such that d is ≤ dTarget…
Simplest stable: d = vol / w with w = min(maxW, vol/ minDepthSpread?) Hmm. Let me do: d = sqrt(vol*α), limited: w = min(maxW, vol/d). Iterate: choose d = min( maxDepth, cbrt-ish).
Let me do: d = Math.min(200, Math.sqrt(vol*0.15)); w = Math.min(maxW, vol/d). When vol=30000: d=67, w=448. When vol=100000: d=122, w=620 (capped). Total volume initial = 375*320=120000. Final: vol≈110000 (some loss) → d≈128, w=620. Inside final level ≈ puddleDepth+little. Equilibrium: level ≈ d. Total = 320*level + 620*d = 120000, level=d → 940d=120000 → d=128. So puddle depth ~128px — fish swim in it, toy might float out? Toy is inside tank, water stays at depth 128 inside tank — toy floats there at 128 depth. Nice, consistent.
Hmm wait — if water level stays 128px inside tank, hole region from burstTop..floor: if burstTop < FLOOR-128, then the opening is above water level; equilibrium is fine.
Flow rate Q: Q = Cd * openingEff * sqrt(2g*head) / something → convert to level drop: dLevel/dt = -Q / tankWidth, where Q in px²/s = v * openingEff * 0.35 (contraction coefficient). With v=1000, opening 300: Q=105000 px²/s → level drops 328px/s — too fast (drains in 1 second). Scale: use v*0.08 factor → Q ~ 24000 → 75px/s, drains in ~3s to equilibrium. Reasonable drama. Tune Cd=0.10.
Particle emission: emit = Q * dt / volPerParticle; volPerParticle = 60 → 400/s at start. Too many; cap 700 particles, volPerParticle adjusts: actually particle volume each = 120 → 200/s. Cap 600. OK.
Particle volume added to puddle on landing: *0.85 (15% splash loss).
Current field for fish underwater in tank:
```
function currentAt(x,y){
if(phase!=='burst') return {x:0,y:0};
const holeCY = clamp(...);
dx = holeX - x, dy = holeCY - y; dist;
const s = flowV * 900/(dist+60) ...
}
```
flowV = sqrt(2g*head). Current magnitude = flowV * 140/(dist+80), cap flowV. Direction toward hole. Plus a slight downward near hole.
Fish thrust: maxSpeed ~ 90 normally wander; flee: thrust away from hole 140. If currentMag > 170, fish loses fight and gets swept. "Attempts to swim against the current before being washed out" ✓ — fish visibly strains, gets dragged slowly, then sucked in.
Ejection: when fish x > x1-14 and y in opening range → state 'air', vx = flowV*0.9 (+rand), keep vy.
Air physics: vy += g*dt; drag vx*=pow(0.5,dt)… light air drag.
Floor: fish half-height ~6: if y>FLOOR-6: y=FLOOR-6; if |vy|>80 bounce vy*=-0.35 else flopTimer: if flopping: occasionally vy=-rand*200, vx=rand±, omega. If inside puddle region (x in puddle range) and puddleDepth>14: state 'puddle': buoyancy: target y = FLOOR - puddleDepth + 8; gentle swim, limited x within puddle.
Rocks: static; drawn as gray ellipse/polygon.
Plants: rooted; sway amplitude based on current at their position; when level < plantTop: droopProgress→1; draw bent.
Toy: duck/ball radius 16, density 0.3: floats: submerged fraction 0.3. Follows surface: y = waterSurfaceY + r*(submerged-... center y = surfaceY + r*(2*frac-1)? center is frac*2r - r below surface. During burst, drift x toward hole with current*0.5; blocked by wall x1-r. When level low: settle on floor. If puddle depth in tank at equilibrium > 2*r*frac? water level 128, still floats. Good.
Glass shards water resistance: in puddle (x within puddle range and y > FLOOR-puddleDepth): buoyancy & drag: vy -= (submergedFrac)*buoy*dt; vel *= drag.
Shards also collide with tank? Shards fly right; tank is to left; ignore. Might land in puddle → float partially? Glass density 2.5 > water: sinks, but with drag.
Crack drawing: jagged polyline from (x1, crackY) with small branches up-left. Pre-burst, grow with progress. Also a white glint. Drag handle: small circle indicator on outer wall at crack position, "drag me" hint.
Pre-burst seep: emit slow drip particles at crack: vx 30, gravity; volume negligible.
Also on burst, screen shake briefly.
HUD: title text, buttons Burst 💥 / Reset ↺, and status (head, flow). Maybe show a flow meter. Keep minimal.
Water rendering: interior: gradient fill rect from waterY to FLOOR between x0+gl..x1-gl. Water surface line highlight. Puddle: fill from FLOOR-depth to FLOOR across puddle segments with slight alpha and animated top edge ripple (sin wave). Above floor: also a darker line for floor.
Room: wall background gradient, floor strip, skirting, maybe a window and a picture frame for atmosphere (drawn with code).
Fish rendering: ellipse body, tail triangle animated, eye. Rotation = velocity angle when moving; on floor, side-lying (rot 90? draw tilted).
Airborne vs underwater transition: fish in air, above surface in tank? Fish stays underwater (if fish y < surfaceY, clamp to y >= surfaceY+... — fish can't go above surface except via ejected hole). Swept fish exits through hole (hole is below surface) → outside, then air if y < puddleSurface/floor.
Also when water drains, fish inside tank: if water level drops, fish follow downward; equilibrium depth 128 — fish survive in tank and puddle. Nice story.
Also splashes hitting room walls: particles when x>950 → collide: vx*=-0.3. "Puddle that spreads and collides with room boundaries" ✓ (puddle stops at walls).
Now let me write the file. Aim for ~600-700 lines of tidy code. Fixed canvas 960x600, scaled to window with CSS (keep aspect).
Pointer handling: convert client coords to canvas coords with scale.
Let me code.
Details for drawing broken wall: after burst, right wall is only drawn from top to burstTopY, with jagged edge at burstTopY, no wall below. Also leave some stuck shards near the top.
Jet emission y range: from yTop = max(waterY, burstTop)+4 to FLOOR-4? Water only occupies below waterY. Emit weighted toward bottom? Random uniform is fine, and velocity scales with depth of that y: v(y) = sqrt(2g*(y - waterY))… actually head at emission point = (waterY→that point)? Torricelli at depth d below surface: v=sqrt(2g*d). d = y - waterY. Plus the overall head for flow is relative to puddle. Use v = sqrt(2g*max(0,y - waterY) + ...) hmm. Standard: v=sqrt(2g*(y - waterY)). Lower hole → larger d → stronger jet ✓. And as water surface drops, d shrinks → weaker ✓. But the net outflow should also consider puddle back-pressure: use v = sqrt(2g*max(0, y - max(waterY, FLOOR-puddleDepth)))? Simpler: effective surface = max(waterY, FLOOR - puddleDepth). Let's do that.
Level update: total Q = Cd * integral ≈ Cd * openingEff * sqrt(2g*head_avg), head_avg = level - puddleDepth. Use Q = Cd*openEff*sqrt(2g*max(0,head)). level -= Q/tankInnerW*dt. Also add landed particle volumes to puddle — but that double-counts: level drops by Q and particles carry Q worth of volume to puddle: consistent ✓ (particles transport it). To keep balance approximately, each particle has volume Q*dt/nEmitted. Compute emission: nAcc += Q*dt/PARTVOL; while nAcc>1 && particles<cap spawn.
PARTVOL: pick 100.
Puddle volume added on landing: p.vol += PARTVOL*0.85.
Also seep drops before burst add trivial volume.
Splash particles: purely visual array, short life.
Fish count 5, sizes vary.
OK, let me code. Write with moderate comments.
Also instruction overlay: "Drag the crack ↕ along the right panel, then press BURST". Buttons in top-right.
Screen shake: canvas translate offset with shake magnitude damped.
Edge cases: crack dragged after burst? Disallow (crack locked on burst).
Let me write JS:
```js
(() => {
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const W=960,H=600,FLOOR=545,G=1500;
const TANK={x0:150,x1:470,top:115,gl:8}; // gl glass thickness
const innerW = TANK.x1-TANK.x0-2*TANK.gl; // ~304
const WALL_L=8, WALL_R=952;
let state;
function init(){
state={
phase:'intact', // intact -> cracked -> burst
t:0, crackY:430, crackProg:0, crackSeed: Math.random()*1000,
level: FLOOR-175, // water surface y? let's keep level = height above floor
...
};
}
```
Store level as height above floor: level0 = FLOOR-175 → height 370. waterY = FLOOR - level.
Puddle: {vol:0, rx:TANK.x1, lx:TANK.x0}. Compute depth & segments:
```
function puddleGeom(){
const maxR = WALL_R - TANK.x1; // 482
const maxL = TANK.x0 - WALL_L; // 142
const vol = state.puddle.vol;
if(vol<=0) return {d:0,segs:[]};
// depth grows as sqrt
let d = Math.min(170, Math.sqrt(vol*0.16));
let w = Math.min(maxR+maxL, vol/d);
// fill right first
const wr = Math.min(w, maxR);
const wl = w - wr;
segs = [[TANK.x1, TANK.x1+wr]];
if(wl>0) segs.push([TANK.x0-wl, TANK.x0]);
return {d, segs};
}
```
Equilibrium: total volume ~ 370*304=112480 minus losses ~95000 → d = sqrt(95000*.16)=123, w=772 <624? maxR+maxL=624 → w capped at 624, then d=vol/624=152. Hmm d formula mismatch: when w capped, d should = vol/maxW = 152 > 123 from sqrt. Fix: d = max(sqrt(vol*0.16), vol/maxW): d=Math.max(Math.min(170,Math.sqrt(vol*0.16)), vol/(maxW)). Simpler: d = vol/maxW when vol>…, let me do: w grows with vol keeping a minimum depth 6: w = min(maxW, vol/6). Then d = vol/w. Early: vol=300 → w=50, d=6. vol=3700 → w=616≈max, d=6. After: d=vol/624. Final d≈150. Puddle depth 150 — tank interior final level also ~150 (equilibrium). Total: 304*150 + 624*150 = 139200 > 112480 → equilibrium d = 112480*0.85/(304+624)= 103. OK ~103px puddle. Good.
But min depth 6 px spread 600px quickly — visually fast sheet. Fine.
Flow/head: head = level - d (d is puddle depth). Opening: burstTop = crackY-55; openH = min(level, FLOOR-burstTop); Q = 0.10 * openH * sqrt(2*G*max(head,0)). level -= Q/innerW*dt… wait Q units px³/s per unit depth (2D). Level drop rate = Q/innerW.
Verify: level 370, crack 430 → burstTop=375, openH=min(370, 170)=170, head=370, v= sqrt(2*1500*370)=1053 → Q=0.1*170*1053=17900 → drop 59px/s. Drains 220px in ~4s until head small. Good.
Particle emission y: between yMin = max(waterY, burstTop) and FLOOR-3. v at y: sqrt(2*G*max(2, y - effSurface)), effSurface = max(waterY, FLOOR-d). vx = v*(0.85+0.3r), vy = (r-0.5)*40.
Also when head<… stop emitting when openH<=0 or head<=2.
Jet particle update: vy+=G*dt; x+=vx; if x>WALL_R-3: x=WALL_R-3, vx*=-0.25 (wall collision); if y>FLOOR-3: landing → puddle.vol += PARTVOL*0.85; spawn splash; remove. If puddle depth>0 and y > FLOOR-d: inside puddle → drag vx*= (1-3*dt), small buoy? Just drag and sink to floor, deposit volume on floor contact.
PARTVOL: emit count: nAcc += Q*dt/PARTVOL, PARTVOL=90, cap 650.
Seep (cracked phase): every 0.4s spawn drop at (TANK.x1+2, crackY), vx 25+r30, small. On landing, puddle.vol+=8.
Shards: {x,y,vx,vy,a,va,pts[],r}. Update: gravity, if in puddle (x within segs & y > FLOOR-d): submerged fraction → vy -= 600*frac*dt (buoyancy, but glass sinks: buoyancy less than gravity: net still down but slower), drag: v *= (1-2.5dt). Floor: y+r*0.5 > FLOOR: bounce. Also left wall of tank? Shards fly right; tank is to left; ignore. Rest when small.
Fish: class-like objects {x,y,vx,vy,size,hue,tail,state,flopT,rot}.
Update per phase:
- Underwater test: inTank = x in [x0+gl, x1-gl], y > waterY && y < FLOOR; if phase intact/cracked: wander: steering: random target + wall avoidance; keep below surfaceY+8.
- If burst && inTank: current c = currentAt(x,y). Fish tries: desired = away from hole with thrust 150; integrate vx += (thrust - pull). Effectively: apply current acceleration and thrust:
```
const c = currentAt(x,y);
// fish thrust away
const mag = Math.hypot(c.x,c.y);
if(mag>20){ dir = normalize(-c); vx += (dir.x*thrust + c.x)*dt*?
```
Simpler: vx += c.x*dt*k; then thrust: vx -= c.x*dt*k*fight, fight ramps up but fatigues: fish.stamina decreases; when stamina low, fight drops → swept. This gives "attempts to swim against current then swept" ✓.
Per-fish implementation: stamina 1, decreases 0.15/s when mag>60, regenerate 0.05/s when calm. fight = 0.4+0.9*stamina. Net accel = c*(1-fight)… if fight>1 fish advances slowly away. Near hole (x > x1-30 and y in opening y-range), if c is strong: suction: strong multiplier. If x > x1-gl: ejected: state='air', vx = max(vx, flowV*0.8).
Clamp fish inside tank walls except through hole.
- Air: vy+=G*dt*0.9; drag; rot = atan2(vy,vx) partly; floor: if y>FLOOR-size: if within puddle & d> size*1.5: state='puddle' (swim); else bounce/flop: vy*=-0.3, flopT=2+rand; during flopT: random impulses 3/s: vy=-150r, vx=±120r, tail fast. Then rest state='down' (rot = 90° sideways, eye X? draw normally but still, occasional gill twitch).
- puddle: swim: target depth center; keep within seg bounds; gentle wander; buoyancy: vy += (targetY - y)*4*dt - vy*2*dt.
Rocks: {x,y,r,pts,color} static; maybe shift: during burst, if rock near hole and small, nudge vx. Keep static for simplicity, but add a slight shift for small pebbles: if current>250, vx → move, clamp inside tank, settle when flow weak. Implement small physics for pebbles: high density: accel = c*0.15 - friction.
Plants: {x, h, blades, phase}. Sway: angle = sin(t*2+phase)*0.15 + currentFactor*0.5 toward hole. Droop: if waterY > FLOOR-h (level below top): droop += dt → rotate whole plant toward floor, lying flat.
Toy: ball {x,y,r=15, density 0.35}. Underwater: floats to surface: vy += ((FLOOR-level + r*(1-2*0.35)?)... target center y = waterY + r*(0.3*2 -1)… submerged frac f=0.35: center is r*(2f-1) below? Ball with fraction f submerged: center depth below surface = r*(2f-1)… if f=0.5, center at surface. If f=0.35, center is above surface by r*0.3. center = waterY - r*(1-2f)= waterY - 0.3r. Bob with spring: vy += ((targetY - y)*30 - vy*4)*dt. Drift: vx += current*0.3*dt - vx*1.5dt; blocked by walls. When level drops, eventually ball rests on floor: if level < 2r*f? if waterY > FLOOR-2r: rest on floor y=FLOOR-r. In equilibrium level 103 > 2r=30 → keeps floating inside tank.
Also toy too big to exit: clamp x ≤ x1-gl-r near hole.
Water surface: draw with slight sin ripple, stronger during burst.
Also render falling stream from hole as a continuous shape? Particles are enough + semi-transparent jet cone: draw quadratic curve with low alpha from hole to impact point. Compute impact: from hole center with v0: t=sqrt(2*(FLOOR-hy)/G), xhit = hx+vx0*t. Draw a gradient band. Nice.
HUD text: instructions, head (m), flow L/s (arbitrary units). Buttons.
Shake: on burst, shake=8, decays; ctx.translate(rand*shake...).
Crack growth: phase 'intact' → after t>1.0 set phase='cracking', crackProg→1 over 0.9s, with tick sound? No audio needed (could add a small WebAudio crack sound — optional, skip or add a simple oscillator thump). Let me add a small WebAudio for burst (white noise) — safe offline. Keep simple, guard with try/catch, init on first pointer interaction.
Buttons: BURST enabled when phase==='cracking'. RESET always.
Drag: pointerdown near (TANK.x1+? crack drawn at wall x1) within 24px & phase==='cracking' → dragging; pointermove sets crackY clamp [TANK.top+60, FLOOR-18]; cursor style.
Also allow drag during 'intact'? Crack not there yet. Only after crack forms.
Let me now write the final code. Also make fish avoid the top: clamp y ≥ waterY+10 when underwater.
currentAt:
```
function currentAt(x,y){
if(state.phase!=='burst'||state.flowV<=0) return {x:0,y:0};
const hx=TANK.x1, hy=state.holeCY;
let dx=hx-x, dy=hy-y, d=Math.hypot(dx,dy)+0.001;
const s=state.flowV*160/(d+70);
return {x:dx/d*s, y:dy/d*s};
}
```
flowV = sqrt(2G*head) saved each frame; holeCY = (max(waterY,burstTop)+FLOOR)/2.
Fish underwater update during burst:
```
const c=currentAt(f.x,f.y);
const mag=hypot;
if(mag>25){ f.stamina=max(0,f.stamina-0.12*dt*(mag/150)); } else f.stamina=min(1,f.stamina+0.2*dt);
const fight=(0.5+1.1*f.stamina);
f.vx += (c.x*(1-fight))*dt*3?
```
Hmm units: treat c as velocity field (px/s). Fish: desired velocity = -c_norm * swimSpeed (swimSpeed ~ 90+80*stamina). Blend: f.vx += (desx - f.vx)*2*dt + wander. If |c| > swimSpeed possible, fish net drifts toward hole. At start flowV~1000, near hole c big → definitely swept; far side c small, fish can hold for a while, stamina drains → swept one by one.
desx = -c.x/mag*swim (away). Net vx approaches des + c? Physically: water carries fish at c, fish swims at des relative to water: total = c + des. So f.vx += ((c.x+desx) - f.vx)*3*dt. ✓ nice: if swim < |c|, swept.
Suction capture: if f.x > TANK.x1-26 && f.y>burstTop && f.y<FLOOR: add extra acceleration toward hole: vx += 900*dt.
Once f.x > TANK.x1-6 && y in opening: state='air'.
Clamp fish inside tank: x < x1-gl-… except if in capture zone, x≥…, only allow passage if phase burst && y in [burstTop, FLOOR]. Else clamp.
Fish in tank if water drains below fish size? Level equilibrium ~100, fine.
Air fish: rot toward atan2. Landing in puddle if depth>16, x in puddle range & y>FLOOR-d: transition to 'puddle'.
Fish draw: save, translate, rotate (rot), body ellipse len=size*2, tail sin(tailPhase), fins, eye; if state 'down': draw with rot fixed ~0.1, add X eye? Draw with white eye. Good.
Plants drawn with quadratic curves per frond: base x, control point sways.
Rocks drawn as polygons.
Glass tank draw: thick base, side walls, right wall with missing section after burst: draw from top to burstTop (with jagged), no wall below. Also leave few stuck shards near top.
Crack draw: generate polyline once from seed: from (x1, crackY) with segments going left-up with branches. Actually crack is on the glass: draw with slight offset in wall thickness, white/dark jagged. Use seeded random for deterministic shape per crackY change → regenerate on drag end? Regenerate each time crackY changes (cheap, fixed length array). Let me regenerate with seeded rand.
Water rendering: interior: gradient fill rect x0+gl..x1-gl, from waterY to FLOOR. Water surface line highlight. Puddle: fill rect from FLOOR-d to FLOOR across segments, slight alpha and animated top ripple (sin wave). Above floor: also darker line for floor.
Jet cone: draw a semi-transparent path from hole spanning the vertical opening, curving to impact — approximate with 3 curves at top/mid/bottom emission points, filled between top and bottom curves: compute ballistic points sampled. Good visual. Do it: for yE at [yTop, yBot], path of x(t)=hx+v(yE)*t… sample 12 points each, build polygon: top curve forward, bottom curve backward. v depends on yE and effSurface.
Also water inside tank near hole shows swirl: skip.
Text/status: show "Head: X px • Flow: Y".
OK let me code, aim for ~600-700 lines. Fixed canvas 960x600, scaled to window via CSS (keep aspect).
Pointer handling: convert client coords to canvas coords with scale.
Let me write JS:
Also handle canvas scaling: style width 100vw height 100vh object-fit contain? Simpler: fit canvas preserving aspect via CSS: display block margin auto; compute with JS resize to fit.
Audio: create on first interaction, function: playCrack (short noise burst lowpass), playBurst (larger noise), playSplash small. Guard with try/catch.
Let me just write it all out.
Fish colors: orange/gold angelfish-ish.
One more thing: "objects transitioning correctly from underwater motion to airborne motion, then to floor collision" — fish swept through hole: underwater → air → floor ✓. Shards also in water → air? Shards start at wall, may pass through jet/puddle → drag ✓. Toy & plants also transition with level.
Puddle collision with room boundaries: jet particle x clamped at WALL_R ✓, puddle width capped at walls ✓.
Let me write the file.
```html
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aquarium Burst</title>
<style>...</style></head>
<body>
<div id="ui"><button id="burst">💥 BURST</button><button id="reset">↺ Reset</button></div>
<div id="hint">...</div>
<canvas id="cv" width="960" height="600"></canvas>
<script>...</script>
</body></html>
```
Write the JS fully. Be careful with all function definitions.
Rough code (I'll write final directly in the write call):
Key globals: state object with fields: phase, t, crackY, crackProg, crackPts (generated), level, puddle{vol,rx,lx}, jets[], splashes[], shards[], fish[], rocks[], plants[], toy, shake, flowV, head, openH, burstTop, emitAcc, seepT, holeTop/holeBot.
step(dt):
- t+=dt
- if phase intact && t>1: phase='cracking', crackProg=0, crack sound.
- if cracking: crackProg+=dt/0.9; ≥1 → phase='cracked'; generate crackPts each frame from seed while progressing.
- burst button → doBurst(): phase='burst', create shards, shake=10, sound.
- Compute waterY=FLOOR-level; geom: burstTop=crackY-55; if phase burst: head=level-puD; openH=clamp(min(level, FLOOR-burstTop),0,∞); flowV=head>0? sqrt(2G head):0; Q=0.10*openH*flowV; level=max(0, level - Q/innerW*dt); emit particles.
- Update jets, splashes, shards, fish, plants, toy, pebbles.
- Puddle depth from geom each frame.
Fish wander (intact): simple: target points change every few seconds; steer; clamp within tank, below waterY+10; tail speed ∝ speed.
Alright let me write the final answer file. Also don't forget instruction: end with index.html present. Verify with ls after write, maybe node quick syntax check? Use `node -e` to parse script? Can extract script and run node --check. Let me do that: extract between <script> and </script> and `node --check`. Good sanity check.
Let me write code now in my head carefully.
Crack generation details: function genCrack(): seeded rand; main path starting (TANK.x1, crackY), step 5-7 segments going left: x-=3..8, y+=rand±9; branches: 2 branches from random node. Points saved as arrays; draw with progress: reveal first k points * crackProg.
Seeded rand: mulberry32.
Burst jagged edge at burstTop: generate zigzag points across wall thickness… draw wall remnant with jagged bottom: polygon top..burstTop with zigzag bottom. Also leave few stuck shards near top.
Draw water in opening: during burst, water in tank at hole region — water rectangle drawn up to x1-gl; at hole, jet emerges; fine.
Jet emission: only if openH>2 && head>1.
yE rand between holeTopY=max(waterY, burstTop) and FLOOR-2. v=sqrt(2G*max(3, yE - effSurfY)), effSurfY = max(waterY, FLOOR-d). vx=v*(0.8+0.35r), vy=(r-.5)*50.
Particle radius 2.5+r2. Color rgba light blue.
Landing: y >= FLOOR-2: puddle.vol += PARTVOL*0.85; spawn 1-2 splash particles (vy=-rand*250, vx±, life .5). Delete. But if puddle depth>0: y >= FLOOR-d: particle enters puddle: vx damps quickly, add volume when reaches floor, or add immediately and convert to splash. Simpler: if y >= FLOOR - d (d>4): add vol*0.85, spawn splash, remove. ✓
Wall: if x>WALL_R-2: x=WALL_R-2; vx*=-0.3; (some may go past? no.)
Tank left wall outside too? Jets only go right.
Shards generation: N=16: for y spanning burstTop..FLOOR: shard size 6-16, pts: triangle-ish around r. x=TANK.x1+2, vx=120+r*380, vy=-200..150 (biased up at top? explosion: vy = (y - mid)/h*150 + rand). va = ±10.
Shard update:
```
vy+=G*dt;
puddle: const pd=puddleD; if(pd>2 && y>FLOOR-pd && (x within segs)){ sub=min(1,(y-(FLOOR-pd))/(r*2)); vy-=G*1.6*sub*dt?
```
Buoyancy: glass density 2.5 → net downward when submerged = G*(1-1/2.5)=0.6G; plus drag: vx*=1-3*dt*sub; vy*=1-3dt*sub; va*=1-2dt*sub. Implementation: if submerged frac>0: vy += G*dt*(1-0.6*frac) else full G? Cleaner:
```
vy+=G*dt; if(sub>0){ const dr=Math.min(1,3*sub*dt); vx-=vx*dr; vy-=vy*dr; va-=va*dr; vy-=400*sub*dt; }
```
Floor: if y>FLOOR-r*0.4: y=FLOOR-r*0.4; if vy>60: vy*=-0.3, vx*=0.75, va*=0.7, tiny splash; else vy=0, vx*= (1-6dt) friction, va*=… rest when slow.
Also shards may hit right wall: x>WALL_R-4: vx*=-0.4.
Fish floor flop: state 'down': if energy>0: timer random impulses: vy=-(60+140r), vx=(r-.5)*160, tail fast; energy decays. Rot: while flopping rot oscillates; when at rest → rot=0.05.
Fish in puddle: swim target: y between FLOOR-pd+10 and FLOOR-10; wander x within seg; if pd < 14 → back to 'down'.
Toy: as above; also small bobbing wave on surface.
Rocks: static list, maybe 2 pebbles dynamic {x,y,r,vx} with simple physics: c=currentAt; vx += c.x*0.1*dt… high density → small response; friction; clamp inside tank; after burst if water shallow, rest.
Plants: {x, h, blades, phase}. Sway: angle = sin(t*2+phase)*0.15 + currentFactor*0.5 toward hole. Droop: if waterY > FLOOR-h (level below top): droop += dt → rotate whole plant toward floor, lying flat.
Draw order: room bg → back of tank (glass tint) → water interior → plants, rocks, fish (clipped in water? fish underwater are in water; clipped to water rect) → toy → glass walls/front glint → crack → jet cone + particles → puddle → fish/shards outside → splashes → HUD.
Clip: use ctx.save; clip rect interior below waterY for underwater objects. Fish drawn under clip. Airborne fish drawn after restore.
Water surface line: stroke with brighter color and a few sin waves.
Puddle draw: for each segment: path along top edge with sin ripple, fill rgba(40,120,200,0.55); reflection glint.
HUD hint div updated per phase.
OK let me write it. Also `flowV` display in liters: show Q.
Now let me write the final file.
Potential pitfall: fish clamp logic for hole passage. Implementation:
```
// clamp inside tank unless exiting via hole
const L=TANK.x0+TANK.gl+f.size, R=TANK.x1-TANK.gl-f.size;
const canExit = state.phase==='burst' && f.y>state.burstTop && f.y<FLOOR;
if(f.x<L){f.x=L;f.vx=Math.abs(f.vx)*0.5;}
if(f.x>R){ if(canExit){ if(f.x>TANK.x1+6){f.state='air';} } else {f.x=R;f.vx=-Math.abs(f.vx)*0.5;} }
```
When fish passes through x1+6 → air: set vx=max(vx, state.flowV*0.7), burst sound.
Top clamp: f.y < waterY+8 → f.y=waterY+8, vy=abs. Bottom: FLOOR-f.size.
Fish state: 'tank' (all underwater), 'air', 'down', 'puddle'.
In 'tank' and phase burst: current physics as above.
If level becomes < f.size*2+8 in tank… equilibrium 100 fine.
Airborne fish update:
```
f.vy+=G*dt; f.vx*= (1-0.15*dt);
f.x+=, f.y+=;
f.rot = lerp toward atan2(f.vy, f.vx)*0.6;
// puddle catch
if(pd>16 && inPuddleX(f.x) && f.y> FLOOR-pd+6){ f.state='puddle'; f.vy*=0.3; splash }
else if(f.y> FLOOR-f.size*0.7){ f.y=FLOOR-f.size*0.7; if(Math.abs(f.vy)>120){f.vy*=-0.35; f.vx*=0.8;} else { f.state='down'; f.energy=1.5+rand; f.vy=0;} }
// walls
if(f.x>WALL_R-8){f.x=WALL_R-8;f.vx*=-0.4;}
if(f.x<WALL_L+8){...}
```
'down': energy-=dt; if energy>0: flopT-=dt; if flopT<0: flopT=0.25+0.4r; vy=-(80+160r); vx=(r-.5)*140; else gravity & floor contact, friction; tail phase advances fast while energy>0.
'puddle': swim: f.vx += ((rand drift))… use wander target like intact but bounded to puddle seg containing fish (find seg where x in range; else nearest). Target y = FLOOR-pd*0.5. Steering: vx += (tx-f.x)*1.5*dt clamp speed 60; buoyancy: vy += ((FLOOR-pd*0.55) - f.y)*6*dt - f.vy*3*dt. rot → small.
Wander (intact): f.tx,f.ty retarget every 2-4s inside tank water region. Accel toward target, capped speed 70; plus sin bobbing.
Tail phase advance: f.tail += dt*(4+speed*0.05) or 20 when flopping.
Fish draw function drawFish(f): translate/rotate; body: ellipse size: len=f.size*2, h=f.size*0.9; tail: triangle at back with flap = sin(f.tail)*0.5; dorsal fin; eye circle; if 'down' draw eye with 'x'? Just leave.
Plants draw: each frond: quadratic from base (x,FLOOR) to tip: tip offset = sway. sway = sin(t*1.5+ph)*8 + currentPush (current at plant * 0.05 toward hole, capped 40) + droop rotation: if droop>0: rotate fronds around base by droop*1.4 rad toward hole side? Droop direction: fall away from hole? Toward left (as water recedes plants slump randomly). Use sign per plant.
Fronds as tapered stroke with green gradient: use strokes with width 4→1: draw with quadraticCurveTo, lineWidth 3, color '#2e8b57' etc. 5-7 fronds per plant, heights vary.
Droop condition: level < h → droopTarget=1; droop += (target-droop)*dt*1.5.
Draw drooped fronds: interpolate tip position from vertical to horizontal lying on floor: tip = base + dir*h, dir rotating from up to sideways by droop. Compute angle = -PI/2 + droop*sign*(PI/2 - 0.15), then frond curve.
Sway applies to tip additionally when underwater.
Toy ball draw: circle with two-color hemispheres + stripe, rotated with small angle from drift.
Rocks: predefine shapes: array of points around an ellipse; fill gray gradient; static positions at tank bottom; also 2 pebbles dynamic: {x,y,r,vx} simple physics: c=currentAt; vx += c.x*0.1*dt… high density → small response; friction; clamp inside tank; after burst if water shallow, rest.
Jet cone draw:
```
if(phase burst && openH>2 && head>1){
const yTop=Math.max(waterY,burstTop), yBot=FLOOR-2;
const es=Math.max(waterY,FLOOR-pd);
function path(yE){ const v=Math.sqrt(2*G*Math.max(3,yE-es))*0.95; pts=[]; for(t=0;;t+=0.03){ x=TANK.x1+v*t; y=yE+0.5*G*t*t*0.95; pts.push; if(y>FLOOR-pd? FLOOR) break; if(x>WALL_R)break } }
polygon between top path and bottom path reversed, fill 'rgba(120,190,255,0.28)'.
}
```
Watch time step: fall from yE to FLOOR: t=sqrt(2(FLOOR-yE)/G). Use parametric.
Good.
Splash particles: {x,y,vx,vy,life}: gravity, life-=dt, draw small circle with alpha=life.
Seep: when phase==='cracked': seepT-=dt; if <0: seepT=0.5+r0.5; spawn jet particle with small vx 40, tiny at (TANK.x1+1,crackY), volume small: on landing puddle.vol += 6. Mark p.small.
Also hint crack drip stain? Skip.
Buttons: #burst disabled unless phase==='cracked'. On burst, disable. Reset reinitializes.
Shake: on burst shake=8, decays; ctx.translate(rand*shake…).
Now HUD numbers: head (px→cm label), flow: show Q in "px³/s"? Show as L/s: L = Q/5000 arbitrary. Cute: display "Pressure head: 2.3 m" scaled: head/100.
Time to write. Write file, extract JS and node --check.
Canvas resize: JS sets canvas style width/height preserving 8:5 ratio fitting window.
Pointer coords: rect scale mapping.
Also add a subtle grid/wallpaper pattern? Simple gradient + skirting + window frame for ambience.
Let me write the final code text.
One concern: fish 'tank' state during 'cracked' phase (before burst) — just wander. After burst, current engages. ✓
Also during burst, water surface drops continuously ✓; fish follow surface down.
Puddle inside-x check: function puddleInfoAt(x): returns depth if x within a seg else 0.
Finalizing code:
Also don't forget initial crackY default 430 (lowerish). Clamp range [TANK.top+70, FLOOR-15].
Show drag handle: when phase 'cracked', small arrow circle on outer wall at crackY: draw '↕' glyph.
Write everything now. Also add a small "water volume" conservation display? Skip.
Edge: burstTop must be ≥ TANK.top+ something; crackY min top+70 → burstTop = top+15 fine.
openH = min(level, FLOOR-burstTop): FLOOR-burstTop = 545-(crackY-55)=600-crackY; if crackY=430 → 170 ✓ lower crack (480) → 120? Wait FLOOR-burstTop gets smaller for lower crack? crackY=480 → burstTop=425 → FLOOR-burstTop=120, and crackY=300 → burstTop=245 → 300. Hmm — higher crack → larger opening height, but head (level-d) is same… jet strength comes from v at emission points: deeper emission points have higher v. Lower crack means bottom of hole is deeper? Bottom is always floor. Difference: lower crack → burstTop lower → opening shorter but head same… so lower crack = weaker?? Requirement: "lower crack should initially produce a stronger jet". Physics: jet speed depends on depth below surface — the emission velocity at bottom of opening is same either way. But if opening extends from burstTop to floor in both cases, lower crack just removes less wall. To satisfy requirement: make opening = region around crack that breaks, e.g., burstTop=crackY-90, and hole doesn't extend to floor? Then water drains to crack level… but "bursts" usually fails at bottom.
Alternative: opening from burstTop down to floor, but jet velocity computed from crack depth (y=crackY), and flow area ∝ openH… to make lower crack produce stronger jet: use head = (waterY... crackY - waterY), i.e., Torricelli at crack depth, and Q ∝ that v. Physically if the panel fails below crack… honestly: treat as the failure initiates at crack and the bottom section blows out; initial jet velocity determined by pressure at crack → v0 = sqrt(2g*(crackY - waterY)). Lower crack → bigger v0 ✓. Flow decreases as water surface approaches crackY ✓ (when waterY reaches crackY, pressure at crack is zero → flow stops? But opening extends to floor — water above floor still flows… well). Compromise: head_flow = max(0, crackY - effSurfaceY… define surface-based head = (crackY - waterY). Q = Cd*openH*sqrt(2G*max(0, crackY-waterY))? And water drains until waterY=crackY, leaving water below crack level — but opening extends down to floor, so physically that water would flow out too. Simplify: opening = crackY-40 down to crackY+50 (window around crack), not to floor. Water drains until surface reaches top of opening… if bottom of opening is above floor, water below bottom of opening stays in tank. Then equilibrium: water in tank stays at bottom of opening (crackY+50), flows into puddle until puddle depth equals interior level. Lower crack → bottom of opening lower → more water escapes, and initial head bigger ✓✓. And "initially a stronger jet" clearly satisfied: v=sqrt(2g*(crackY-waterY)).
So opening: holeTop=crackY-40, holeBot=crackY+50 (clamped ≤ FLOOR-4). openH = clamp(min(level_height_above… in y coords: water surface y = FLOOR-level. Emitting if waterY < holeBot: emission y range [max(waterY,holeTop), holeBot]. head = max(0, crackY - max(waterY, FLOOR-pd))?? Use head = max(0, (crackY) - max(waterY, FLOOR-pd))? Hmm back-pressure: head = max(0, (crackY - waterY) - … simpler: headY = max(waterY, FLOOR-pd); head = max(0, holeBot? use crackY - headY + 20?). Let's just use head = max(0, holeBot - headY)?? For crackY=430: holeBot=480, waterY=175: head=305; v=sqrt(2*1500*305)=957 ✓. When waterY→480 head→0, stops ✓. Higher crack (crackY=250): holeBot=300, head=125 → v=612 weaker ✓ matches requirement.
Q = Cd * openH_eff * v, openH_eff = min(holeBot, FLOOR) - max(holeTop, waterY) (wetted opening height). level -= Q/innerW dt. Cd=0.10.
Water below holeBot stays in tank: final interior level height = FLOOR-holeBot = 545-480=65px (for crack 430). Fish survive in 65px? A bit shallow; fish half-size ~8 fine, fish 'tank' state OK in shallow: clamp works. Toy ball r=15: needs 30px? 65 fine, floats.
Puddle vol ≈ (370-65)*304*0.85 ≈ 79k → d = 79k/624 ≈ 127? w max 624 → d=127px. Hmm deep puddle; but interior final 65 < 127?? Then puddle deeper than tank water — back pressure: my head uses FLOOR-pd: if FLOOR-pd (=418) < waterY? head = holeBot - max(waterY, FLOOR-pd). If puddle rises above holeBot (FLOOR-pd=418 < holeBot=480 → puddle surface y=418 is above hole bottom 480? y smaller = higher. puddle surface y = FLOOR-d = 418; hole bottom y=480 → puddle surface is above bottom of hole → back pressure. head = holeBot - max(waterY, FLOOR-pd) = 480-418=62 still positive → keeps flowing until interior waterY reaches 418, i.e., interior level = pd. Consistent equilibrium: interior level = pd = (total escaped)/624… self-consistent solve: interior final level L (height), escaped = (370-L)*304*0.85 = pd*624, and L=pd → (370-L)*258.4 = 624L → L = 370*258.4/(882)=108. But hole bottom height 65px < 108 → final surface above bottom of hole, consistent with back-pressure equilibrium ✓. So final depth ~108px everywhere.
Then fish in tank in 108px, puddle 108 — fish swim either. Toy floats. Plants partly submerged.
Verify lower crack strength: crackY=480: holeBot=530, initial v: head=530-175=355, v=1032 vs higher crack 250: head=300-175=125 v=612 ✓.
Emission y range: [max(waterY,holeTop), min(holeBot,FLOOR-3)]. At final stages, range small, OK.
Jet from high crack: holeBot=300, falls from y~300 to floor/puddle: nice arc ✓. Lower crack holeBot=530: nearly horizontal short jet → puddle at base. Realistic ✓.
Also shards region: wall from holeTop to holeBot blows out (~90px tall window) + maybe cracks extend further: shards from holeTop-10..holeBot+10. Wall drawn: full except [holeTop,holeBot] replaced with jagged edge. Water below holeBot stays — but wall missing there! The opening spans holeTop..holeBot and wall intact below holeBot. OK good: water 65→108px stays below holeBot=480 (height 65..). Wait interior final level height 108 → surface y=437; holeBot=480 → water surface above hole bottom… water level y=437 is above y=480 (smaller y = higher). Water occupies 437..545, opening 390..480: water overlaps opening region 437..480 — but equilibrium head zero so no flow. Visually: water is in contact with opening but static, puddle same height — consistent ✓.
Good. Jagged edge: generate zigzag along holeTop and holeBot.
Now shard count ~ 12.
Alright, let me write everything.
Status text: phase message.
Writing the file now (~700 lines). Then node --check extracted JS.
Careful with template: avoid `</script>` inside strings.
Let me write.
function rand(a,b), seeded mulberry for crack.
Crack points: generate when crackY changes or phase starts:
```
function genCrack(){
const r=mulberry32(Math.floor(state.crackY*7.13)+42);
const pts=[[TANK.x1+4,state.crackY]];
let x=TANK.x1+4?
```
Crack on glass, at x ≈ x1 (wall thickness 8 spans x1..x1+8? Let me put wall rect at x=x1, w=8). Crack drawn on top of that: main line goes down-left/up-right? Jagged vertical-ish: from (x1+4, crackY-26) to (x1+4, crackY+26) with jitter, plus 3 branches radiating. Points in relative; scale reveal by crackProg.
Branches: from center point at angles.
Simplification: main: 7 points from y-28..y+28, x jitter ±5. Branches: from 2 random main points, 3 segments outward with random angles. Save as arrays of arrays.
drawCrack: stroke width 1.5 'rgba(255,255,255,0.9)' + dark shadow offset. Reveal: for each polyline draw floor(n*prog) points.
Also during 'cracking', emit small glass dust? Skip.
Burst: convert crack to hole; draw remnant jagged edges of hole boundary: precompute zigzag arrays for top/bottom edges at burst time: edgeTop: points across wall thickness… wall drawn as vertical rect x1..x1+8, from top to holeTop; jagged bottom edge: few teeth. Similarly wall below from holeBot to FLOOR with jagged top (wall below hole remains! Yes since opening is a window). So right wall = 2 segments with jagged facing edges. Good.
Draw walls: rgba glass color with alpha.
OK writing now. Final answer should include just the file and brief note.
Code:
```js
'use strict';
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
const W=960,H=600,FLOOR=545,G=1500;
const TANK={x0:150,x1:470,top:115,th:8};
const INNER_L=TANK.x0+TANK.th, INNER_R=TANK.x1-TANK.th, innerW=INNER_R-INNER_L;
const WALL_L=10,WALL_R=950;
const CD=0.10, PARTVOL=90, MAXPARTS=700;
let S, last=0, audio=null;
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const rnd=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
```
init():
```js
function init(){
S={phase:'intact',t:0,crackY:430,crackProg:0,crack:null,
level:370, emitAcc:0, seepT:0.4,
puddleVol:0,
jets:[],splashes:[],shards:[],fish:[],plants:[],rocks:[],pebbles:[],
holeTop:0,holeBot:0,flowV:0,head:0,Q:0,shake:0,pd:0,segs:[],
burstEdges:null, msg:''};
// fish
const hues=[18,35,48,200,330];
for(let i=0;i<5;i++)S.fish.push({x:rnd(INNER_L+30,INNER_R-40),y:rnd(250,FLOOR-40),vx:0,vy:0,size:rnd(8,13),hue:hues[i],tail:rnd(0,6),state:'tank',rot:0,stamina:1,energy:0,flopT:0,tx:0,ty:0,retarget:0});
// rocks
...
}
```
Rocks: place along bottom:
```
const rx=[190,240,300,360,420]; sizes vary; each {x,y=FLOOR-r*0.6,r,seed}
Pebbles: 2 {x,y,r:4,vx:0}
```
plants: {x:[200,330,440? keep away from hole], h:rnd(90,160), phase, sign, droop:0, blades:5..7}
toy: {x:300,y:0,r:15,vx:0,vy:0,rest:false,spin:0}
Geometry helpers:
```
function waterY(){return FLOOR-S.level}
function puddleGeom(){
const vol=S.puddleVol, maxR=WALL_R-TANK.x1, maxL=TANK.x0-WALL_L, maxW=maxR+maxL;
if(vol<20){S.pd=0;S.segs=[];return}
let w=Math.min(maxW,vol/5), d=vol/w;
const wr=Math.min(w,maxR), wl=w-wr;
S.pd=d; S.segs=[[TANK.x1,TANK.x1+wr]];
if(wl>1)S.segs.push([TANK.x0-wl,TANK.x0]);
}
function puddleAt(x){ for(const s of S.segs) if(x>=s[0]&&x<=s[1])return S.pd; return 0 }
```
Physics step:
```
function step(dt){
S.t+=dt;
if(S.phase==='intact'&&S.t>1){S.phase='cracking';S.crackProg=0;genCrack();snd('crack')}
if(S.phase==='cracking'){S.crackProg=Math.min(1,S.crackProg+dt/0.9);if(S.crackProg>=1)S.phase='cracked'}
const wy=waterY();
// flow
S.head=0;S.Q=0;S.flowV=0;
if(S.phase==='burst'){
const backSurf=Math.max(wy,FLOOR-S.pd);
S.head=Math.max(0,S.holeBot-backSurf);
const wetTop=Math.max(wy,S.holeTop), wetBot=Math.min(S.holeBot,FLOOR-2);
const openH=Math.max(0,wetBot-wetTop);
if(S.head>1&&openH>1){
S.flowV=Math.sqrt(2*G*S.head);
S.Q=CD*openH*S.flowV;
S.level=Math.max(0,S.level-S.Q/innerW*dt);
S.emitAcc+=S.Q*dt/PARTVOL;
while(S.emitAcc>=1&&S.jets.length<MAXPARTS){S.emitAcc-=1;
const yE=rnd(wetTop,wetBot);
const v=Math.sqrt(2*G*Math.max(4,yE-backSurf));
S.jets.push({x:TANK.x1+6,y:yE,vx:v*rnd(0.8,1.1),vy:rnd(-25,25),r:rnd(2,3.8),vol:PARTVOL});
}
}
}
...
}
```
Wait: v at emission yE is relative depth yE-backSurf — but yE-backSurf ≤ holeBot-backSurf = head, so OK.
Seep when cracked:
```
if(S.phase==='cracked'){S.seepT-=dt;if(S.seepT<=0){S.seepT=rnd(0.3,0.8);S.jets.push({x:TANK.x1+6,y:S.crackY,vx:rnd(30,70),vy:0,r:1.8,vol:5})}}
```
Jets update:
```
for(i--){p; p.vy+=G*dt; p.x+=vx*dt; p.y+=vy*dt;
if(p.x>WALL_R-3){p.x=WALL_R-3;p.vx*=-0.25}
const pd=puddleAt(p.x);
if(pd>3&&p.y>FLOOR-pd){S.puddleVol+=p.vol*0.85;splash(p.x,FLOOR-pd,1);del}
else if(p.y>FLOOR-2){S.puddleVol+=p.vol*0.85;splash(p.x,FLOOR-2,2);del}
}
```
Splash: {x,y,vx,vy,life}. Update with gravity, life.
Shards update as planned.
Fish update — big function.
Toy:
```
const t=S.toy, wy=waterY();
const surfaceInTank = wy; // interior
if(S.level> t.r*1.2){ floating: targetY=wy - t.r*0.3; bob: t.vy+=((targetY-t.y)*25 - t.vy*4)*dt;
drift: current at toy pos *0.35
clamp x in [INNER_L+r, INNER_R-r] always (can't exit)
if level low: if targetY > FLOOR-r: rest: targetY=FLOOR-r, fall with gravity
}
```
Simplify: if level > 26: float toward surface; else: gravity, floor collision, rest.
currentAt only meaningful during burst.
Plants update: droop target level<h? Use S.level < p.h*0.85 → droop→1.
Fish code:
```
function updFish(f,dt){
const wy=waterY();
f.tail+=dt*(5+Math.hypot(f.vx,f.vy)*0.04);
if(f.state==='tank'){
const c = S.phase==='burst'?currentAt(f.x,f.y):{x:0,y:0};
const mag=Math.hypot(c.x,c.y);
if(mag>25){f.stamina=Math.max(0,f.stamina-dt*0.10*(mag/120));}else f.stamina=Math.min(1,f.stamina+dt*0.25);
let des={x:0,y:0};
if(mag>15){ const swim=60+90*f.stamina; des.x=-c.x/mag*swim; des.y=-c.y/mag*swim; f.fight=true }
else {
f.fight=false;
f.retarget-=dt;
if(f.retarget<=0){f.retarget=rnd(1.5,3.5);f.tx=rnd(INNER_L+25,INNER_R-25);f.ty=rnd(wy+30,FLOOR-20)}
des.x=(f.tx-f.x)*1.2; des.y=(f.ty-f.y)*1.2;
const m=Math.hypot(des.x,des.y); if(m>70){des.x*=70/m;des.y*=70/m}
}
f.vx+=((c.x+des.x)-f.vx)*Math.min(1,3*dt);
f.vy+=((c.y+des.y)-f.vy)*Math.min(1,3*dt);
// suction near hole
if(S.phase==='burst'&&f.x>S.holeTopX? ... f.x>INNER_R-30&&f.y>S.holeTop&&f.y<S.holeBot){f.vx+=1200*dt;f.vy+= (S.crackY-f.y)*3*dt;}
f.x+=f.vx*dt; f.y+=f.vy*dt;
// clamp
if(f.y<wy+f.size){f.y=wy+f.size;f.vy=Math.max(f.vy,0)}
if(f.y>FLOOR-f.size*0.6){f.y=FLOOR-f.size*0.6;f.vy=Math.min(f.vy,0)}
if(f.x<INNER_L+f.size){f.x=INNER_L+f.size;f.vx=Math.abs(f.vx)*0.4}
const canExit=S.phase==='burst'&&f.y>S.holeTop-4&&f.y<S.holeBot+4;
if(f.x>INNER_R-f.size){ if(canExit){ if(f.x>TANK.x1+10){f.state='air';f.vx=Math.max(f.vx,S.flowV*0.75);snd('plop')} } else {f.x=INNER_R-f.size;f.vx=-Math.abs(f.vx)*0.4} }
f.rot=Math.atan2(f.vy,Math.abs(f.vx)+20)*0.7*(f.vx>=0?1:1); // face direction
f.dir = f.vx<0?-1:1...
```
Facing: draw fish facing sign of vx; flip scale. rot = atan2(vy, |vx|)*sign… let me store f.face = vx<-5?-1:(vx>5?1:f.face).
Air/down/puddle as planned.
currentAt:
```
function currentAt(x,y){
if(S.phase!=='burst'||S.flowV<=0)return{x:0,y:0};
const hx=TANK.x1, hy=clamp(y,S.holeTop,S.holeBot)? use hole center: hy=(S.holeTop+S.holeBot)/2;
let dx=hx-x, dy=hy-y, d=Math.hypot(dx,dy)+1e-3;
const s=S.flowV*150/(d+60);
return {x:dx/d*Math.min(s,S.flowV), y:dy/d*Math.min(s,S.flowV)};
}
```
doBurst():
```
S.phase='burst';
S.holeTop=S.crackY-40; S.holeBot=Math.min(S.crackY+50,FLOOR-3);
genBurstEdges();
Shards: n=14: y=rnd(holeTop,holeBot), size rnd(5,15); vx=rnd(140,520), vy=rnd(-260,140)+(y-mid)*2, va=rnd(-12,12), pts generated.
S.shake=10; snd('burst');
```
Rendering: lots of drawing code. Write carefully.
Also status messages:
- intact: "Something's not right…"
- cracking: "*CRACK*"
- cracked: "Drag the crack ↕ up/down, then BURST. Lower crack = stronger jet."
- burst: "Head: …, Flow: …"
- done (Q<1): "The tank has emptied. Puddle depth Xpx."
Burst button disabled attribute toggled.
Sound via WebAudio:
```
function snd(kind){ try{ audio=audio||new (window.AudioContext||window.webkitAudioContext)();
const a=audio,t=a.currentTime;
const buf=(dur)=>{const b=a.createBuffer(1,a.sampleRate*dur,a.sampleRate);const d=b.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2);return b};
const src=a.createBufferSource();src.buffer= kind==='burst'?buf(0.8):buf(0.15);
const g=a.createGain(); g.gain.value= kind==='burst'?0.5:0.2;
const f=a.createBiquadFilter(); f.type='lowpass'; f.frequency.value=kind==='burst'?900:2500;
src.connect(f).connect(g).connect(a.destination); src.start(t);}catch(e){} }
```
AudioContext needs user gesture — burst is a click ✓; crack happens automatically before gesture → may be blocked, OK with try/catch.
Rendering code — write now. Also draw a small table/stand? Tank sits directly on floor.
Ambience: wall gradient #dfe8ee→#c8d4dc; floor: wood color #b98a5a with plank lines; skirting. Window frame on left? Tank at x150-470; window at x 560-820 y 60-260? Sunlight? Simple frame + sky gradient. Shelf? Just keep window + frame.
Water interior: vertical gradient rgba(30,120,200,0.75)→(20,80,160,0.85); sand bottom strip inside tank (10px tan). Surface: animated sin line stroke, white alpha .8. Caustics light streaks: skip or few.
Glass: fill walls rgba(180,220,235,0.5), highlight lines.
Draw right wall with hole (during burst): seg1 top..holeTop, seg2 holeBot..FLOOR; jagged edges: precomputed zig arrays:
```
genBurstEdges: S.edgeT=[]: for x 0..th step: y jitter; similarly edgeB.
```
Draw wall rect and teeth: polygon for upper part: (x1, top)→(x1+th, top)→(x1+th, holeTop+jag…)→(x1, holeTop+jag…). Simple: rect + triangle teeth drawn with same fill.
Also draw some residual shards stuck at edges? Skip.
Jet cone: as planned with paths from yTop..yBot emission. Compute:
```
function jetPath(yE){
const backSurf=Math.max(waterY(),FLOOR-S.pd);
const v=Math.sqrt(2*G*Math.max(4,yE-backSurf))*0.92;
const pts=[]; let t=0;
while(true){ const x=TANK.x1+6+v*t, y=yE+0.5*G*t*t+... (+initial vy 0); pts.push([x,y]); t+=0.035;
if(y>FLOOR-(puddleAt(x)>3?S.pd:0)-1||x>WALL_R-4||t>2)break; }
return pts;
}
```
Draw cone: top=jetPath(wetTop), bot=jetPath(wetBot): path top then reverse bot, fill rgba(140,200,255,0.30). Only if wetBot-wetTop>4.
Jet particle draw: circles with alpha.
Puddle: fill each segment: path along top with ripple: x step 12: y=FLOOR-S.pd + sin(x*0.05+t*3)*1.5; lineTo down to floor. Fill 'rgba(50,130,210,0.55)'; stroke top brighter.
Fish draw:
```
function drawFish(f){
ctx.save();ctx.translate(f.x,f.y);ctx.scale(f.face,1);ctx.rotate(f.rot*f.face? ...
```
Simplify: rot applied before flip? Use ctx.scale(face,1) and rotate(f.rot*face)? Hmm: for fish facing left, we flip and want to pitch correctly: rot is computed as atan2(vy, |vx|). Apply scale(face,1) and rotate(f.rot*face)? Let me just do: translate; rotate(atan2(vy,vx)) when moving right; if face=-1: scale(-1,1) and rotate(-atan2(vy,|vx|))? Simpler approach: always rotate by angle=atan2(f.vy, f.vx), then if |angle|>PI/2 (moving left) flip vertically so fish not upside down: standard trick: rotate(angle); if face<0 scale(1,-1). Do that.
Body: ellipse rx=size, ry=size*0.55; tail: triangle from (-size,0) to (-size*1.6, ±size*0.5 rotated by tailFlap=sin(tail)*0.6): path moveTo(-size*0.8,0) lineTo(-size*1.7, -size*0.5+flap?) use rotate for tail: translate(-size*0.9,0) rotate(sin(tail)*0.5) triangle. Dorsal fin arc. Eye at (size*0.5,-size*0.15) white + black. If state 'down': draw eye as 'x' line and body pale (alpha .9). Color: hsl(hue,80%,55%).
Plant draw:
```
function drawPlant(p){
const submerged = S.level> 10;
const sway = Math.sin(S.t*1.6+p.phase)* (submerged?6:1) + (S.phase==='burst'? currentAt(p.x,FLOOR-20).x*0.03:0);
for blade i: frac=i/(p.blades-1)-0.5; base=p.x+frac*14;
const ang=-Math.PI/2 + frac*0.5 + p.droop*p.sign*(Math.PI/2-0.2) ;
tip = base + cos(ang)*len, FLOOR + sin(ang)*len (len=p.h*(0.7+0.3r_i)) plus sway offset to tip x.
quadraticCurveTo with control point in middle offset by sway*0.5.
stroke green, width 3.5, cap round, slight hue variation.
}
```
Droop update: target = S.level < p.h*0.9 ?1:0; p.droop += (target-p.droop)*Math.min(1,dt*1.2).
Rocks: precomputed jittered polygon via seeded rand; fill #777 gradient; static positions at tank bottom; 2 pebbles dynamic: {x,y,r,vx} simple physics.
Toy ball draw: circle: red top half, white bottom? Duck? Ball easier: two-tone with stripe, small rotation angle from drift.
HUD: DOM element: #hint bottom-left, #stats top-left, buttons top-right.
Drag: pointerdown: if phase 'cracked': check if |mx-(TANK.x1+4)|<30 && |my-S.crackY|<34 → dragging=true. pointermove: if dragging: S.crackY=clamp(my,TANK.top+70,FLOOR-14); genCrack(). Cursor 'ns-resize' on hover.
Also allow drag during 'cracking'? Only after formed ('cracked').
Burst button click → doBurst. Also space key.
Reset: init(); button states.
Loop: rAF, dt=clamp((now-last)/1000, 0, 0.033); substep: run step(dt) once (fixed-ish OK) — maybe 2 substeps for stability: step(dt/2) twice. Fish physics not stiff so OK with 1, but jet velocities are high: particle motion 1000px/s * 0.016=16px OK.
Shake: S.shake*=Math.pow(0.02,dt) style decay; offset=rnd(-1,1)*S.shake.
Draw order as mentioned; also clip underwater objects:
```
ctx.save(); ctx.beginPath(); ctx.rect(INNER_L, wy, innerW, FLOOR-wy); ctx.clip();
drawSand, plants, rocks, pebbles, fish(state==='tank'), toy(if in tank water)
ctx.restore();
```
Fish that are 'tank' always inside tank. Toy always in tank.
Fish air/down/puddle, shards, jets, splashes drawn after/outside.
Also water surface line drawn after clip restore.
If level==0: skip water rect.
Also draw subtle water tint on submerged parts outside (puddle covers it).
Status text update.
Alright, let me write the final HTML. Keep tidy. Estimate ~550 lines. Here we go.
One more check — fish 'tank' clamp when water very shallow: wy+f.size may exceed FLOOR-f.size*0.6 → order clamp: y in [min(wy+size, FLOOR-..), ...]: compute yMin=Math.min(wy+f.size, FLOOR-f.size*0.6). OK.
Pebble physics:
```
for pb: if burst: c=currentAt(pb.x,pb.y); pb.vx+=c.x*0.25*dt; pb.vx*=(1-4*dt); pb.x+=vx*dt; clamp inside; if level<pb.r*2, rest: nothing.
```
Also rocks inside tank when water drains stay; OK.
Toy current: c=currentAt(toy) *0.4 to vx. Clamp near wall INNER_R-r (can't exit through hole, since hole is smaller than ball? Hole height ~90 > ball 30… hmm ball could exit! Requirement: floating toy reacts — better if toy gets pressed against opening and stays (slightly bigger? hole 90px vs ball diameter 30 — it could escape). Choice: let toy escape too? "Objects transition from underwater to air to floor" — toy floating at surface wouldn't reach hole unless level drops near hole. Toy r=20 (diameter 40); allow escape if waterY is within hole range and toy near wall: fun — duck swims out, arcs down, lands in puddle. Let's allow: clamp logic: canExitT = phase burst && toyY between holeTop+r and holeBot-r; when level drops and surface enters hole range, current pushes toy to wall → exits. But then toy leaves tank, and final tank water remains below holeBot… toy out. Both outcomes fine; implement like fish: if x>INNER_R-r and canExit: allow passage; once x>TANK.x1+r: state 'air' → gravity, floor bounce with restitution 0.4, roll, rest in puddle (float if puddleAt>r). Toy states: 'float', 'air', 'floor'.
Floating target: surface wy: targetY = wy - r*0.3 only when toy in tank and level enough. If level small (<r), gravity to floor.
Handle:
```
if(toy.state==='float'){
const canExit=...
targetY=wy-r*0.3; if(targetY>FLOOR-r){ // water too shallow
}
vy spring to targetY; vx current drift; clamp; if canExit&&x>INNER_R-r*0.5: state='air'; vx=flowV*0.6
}
air: gravity, wall, floor: if puddleAt(x)>r*0.8: back to 'float' in puddle: bob to puddle surface: targetY=FLOOR-pd-r*0.3… generalize float: surfaceY = if in tank ? wy : FLOOR-pd.
```
Simplify: float target surface detection: if x in TANK inside: surfY=wy else if puddleAt(x)>2: surfY=FLOOR-pd else no float → floor.
OK implement generically.
Now write it. Toy spin: rotation based on vx.
Also don't forget initial hint text.
Time to write the file.
After writing, extract script and node --check.
Let me write the complete code carefully now.
Also stats: display head cm and flow L/s: head/100 → m: "2.1 m". Flow: Q/60000 → L/s-ish arbitrary. OK.
Also show water level %: level/370.
Let me write.
drawRoom:
```
Wall: gradient #e8eef2→#cfd9e0 fill 0..FLOOR
Window: x 600..840, y 70..280: sky gradient, frame stroke, cross bars
Wall art: small frame at x 60..120
Floor: fill #a8... rect FLOOR..H with plank lines every 60px, plus skirting line.
```
Tank stand: none, tank on floor.
Also draw shadow under tank.
Alright, final code:
Let me write out write call content. Need to be careful about length but OK.
Edge case: puddleAt during jet particle update uses S.segs from previous frame's puddleGeom — call puddleGeom at start of step before physics.
Also cone drawing uses puddleAt — OK.
genCrack uses seeded rand; store crack polylines: S.crack={main:[[x,y]...],branches:[ [...] ]}. Points computed absolute.
```
function genCrack(){
const r=mulberry32(Math.floor(S.crackY*97.7)+13);
const main=[]; let x=TANK.x1+TANK.th*0.5, y=S.crackY-30;
for(let i=0;i<=6;i++){ main.push([x+(r()-0.5)*7, y]); y+=10; }
const br=[];
for(let b=0;b<3;b++){ const st=main[1+Math.floor(r()*4)]; const a=r()*Math.PI*2; let bx=st[0],by=st[1]; const p=[[bx,by]]; for(let i=0;i<3;i++){bx+=Math.cos(a)*6+(r()-0.5)*4;by+=Math.sin(a)*6+(r()-0.5)*4;p.push([bx,by])} br.push(p);}
S.crack={main,br};
}
```
Drawing with reveal frac.
Burst edges:
```
function genEdges(){
const r=mulberry32(777);
const mk=(y)=>{const p=[];for(let i=0;i<=4;i++)p.push([TANK.x1+ (i/4)*TANK.th, y+(r()-0.5)*10]);return p};
S.edgeT=mk(S.holeTop); S.edgeB=mk(S.holeBot);
}
```
Draw wall segments: upper: rect(TANK.x1, TANK.top, th, holeTop-TANK.top) + teeth polygon from edgeT; simpler: draw a path: start (x1,TANK.top) → (x1+th,TANK.top) → (x1+th, edgeT last..) use edge points: descending from (x1+th, edgeT[4].y)… just draw rect + small triangle teeth: acceptable: path (x1, holeTop+3) zigzag through edgeT points to (x1+th, ...) and rect up. Let me do a polygon:
```
ctx.beginPath();ctx.moveTo(TANK.x1,TANK.top);ctx.lineTo(TANK.x1+th,TANK.top);
for(let i=S.edgeT.length-1;i>=0;i--)ctx.lineTo(S.edgeT[i][0],S.edgeT[i][1]);
ctx.closePath()
```
edgeT[i].x spans x1..x1+th so reverse iterate to draw bottom edge right→left ✓.
Lower:
```
moveTo(x1,holeBot..) forward through edgeB, then down to FLOOR:
ctx.moveTo(TANK.x1,S.edgeB[0][1]); edgeB forward → lineTo(x1+th,edgeB last y) → lineTo(x1+th,FLOOR) → lineTo(x1,FLOOR) close.
```
Glass fill 'rgba(190,225,240,0.6)', stroke lighter.
Left wall rect, bottom rect, right wall normally (before burst) too drawn.
Front glint: diagonal white translucent stripe across tank.
OK — writing now. Also `#hint` styling.
Also fish initial y in water: wy=175, fish y rnd(210, 520) ✓.
Final: check node --check.
/tmp/ws/index.html (28974 bytes)Successfully wrote 28974 bytes to /tmp/ws/index.html
thinking
I'll extract the script and run `node --check` to verify the JS syntax.
{"command":"cd /tmp/ws && python3 -c \"\nimport re\nhtml=open('index.html').read()\nm=re.search(r'<script>(.*)</script>',html,re.S)\nopen('//bin/bash: line 1: python3: command not found Command exited with code 127
thinking
python3 is not available. Will use node itself.
{"command":"cd /tmp/ws && node -e \"\nconst fs=require('fs');\nconst html=fs.readFileSync('index.html','utf8');\nconst m=html.match(/<scriptSYNTAX_OK -rw-r--r--. 1 node node 28974 Aug 13 01:33 index.html
thinking
Syntax is OK. Let me double-check some potential logic issues:
1. Inside `updToy`, there's a weird leftover line: `const canExit=...?false:true;` — an unused variable, harmless but ugly. Also references `inTank` in a strange way. It's not used, so functionally fine. Could remove for cleanliness. Let's remove it.
2. Fish `f.rot` when swimming left: scale(face,1) then rotate(f.rot*face) — inside drawFish I wrote `ctx.scale(f.face,1);ctx.rotate(f.rot*f.face);`. rot = atan2(vy,|vx|+25)*0.8. When face=-1, the flip mirrors it, and multiplying by face and rotating gives the correct pitch. Seems fine.
3. `S.done` is set when head<=1 after burst && S.t>3. However, the hint uses S.done. OK.
4. Fish getting sucked out: canExit is checked with `f.x>INNER_R-f.size`, and when `f.x>TANK.x1+10` state becomes air. Between INNER_R (462) and 480, the fish is inside the hole region; there's no wall there so it's fine.
5. Toy when it's in the puddle (x >= TANK.x1... actually the toy's x could be between TANK.x1 and INNER_R? INNER_R=462 < TANK.x1=470. The toy is drawn with clipping if x<TANK.x1. When the toy is floating in the puddle to the right of the tank, x>=470, drawn without clipping. Good. But when the toy exits through the hole, it passes through x range 462..490 — drawn without clipping, that's fine.
Wait — there's a bug in the float logic in updToy: `const inTank=t.x<TANK.x1;` when floating in the puddle, pd=puddleAt(t.x). If the toy is at x between TANK.x1 and the start of the puddle... the puddle starts at TANK.x1, so OK.
6. Jet emission `wetTop` uses the current frame's wy; no problem.
7. Equilibrium: interior water surface eventually becomes pd (about 100px). The fish inside the tank in a 100px depth is fine. Plants with h up to 165 droop (level < h*0.9). Rocks visible. Toy floats inside the tank (level 100 > r*0.9=18) ✓.
8. Puddle growth `vol/5`: minimum depth 5px, width = vol/5. At vol 25 → width 5. No problem.
9. `drawJet` and jetPath use S.pd — set in the previous frame's puddleGeom; drawing happens after the step, and puddleGeom was called during the step. ✓
10. One issue: fish getting ejected play snd('crack') — a crack sound for each fish ejection; a bit odd but tolerable. Change to 'plop'? snd only has two kinds; leave it.
11. Potential issue: inside updFish 'tank', when phase is 'burst' but water has drained to equilibrium and fish are above the water surface? Fish y is clamped below wy+size, so always underwater. ✓
12. `drawWater` sand gets drawn even without water — no problem.
13. burstBtn.disabled gets set twice (hud + doBurst), no problem.
Remove the unused `canExit` line for cleanliness. Also, one subtle issue inside updToy's float branch: if inTank and exitOK, the wall clamp `if(t.x>INNER_R-t.r&&!exitOK)` is only applied when not exitOK — good.
Also checking the `step` order: puddleGeom is called before jet update ✓ (yes, it's called before flow calculation).
One more: fish 'tank' state when phase is not burst and level is normal — wander target ty is set with rnd(wy+30, FLOOR-20), OK.
Edge case: drag during 'cracking' isn't allowed (only 'cracked') ✓.
Space key triggers doBurst only when cracked ✓.
Let me clean up the dead code line, re-check syntax, and finish.
Syntax checks out. Let me remove one leftover dead-code line in the toy logic for cleanliness:
{"path":"/tmp/ws/index.html","edits":[{"oldText":" t.spin+=t.vx*0.02*dt;\n const canExit=S.phase==='burst'&&t.y>S.holeTop+t.r*0.5&Successfully replaced 1 block(s) in /tmp/ws/index.html.
thinking
The `inTank` variable is still being used in other places. Let me check if it's still defined.
{"command":"cd /tmp/ws && node -e \"\nconst fs=require('fs');\nconst html=fs.readFileSync('index.html','utf8');\nconst m=html.match(/<scriptSYNTAX_OK
thinking
inTank is defined at the top of the float branch (`const inTank=t.x<TANK.x1;`), so it's still being used. No problem. Done. Let me wrap it up.
Done — `index.html` (≈29 KB, fully self-contained, zero network dependencies) is in `/tmp/ws`. Syntax verified with `node --check`. **How it plays:** 1. After ~1 s a jagged crack propagates across the right panel (with a cracking sound). 2. **Drag the crack ↕** via the yellow handle — lower = deeper = more pressure. 3. Hit **💥 BURST** (or Space). Reset with the button or `R`. **Physics implemented:** - **Torricelli outflow** — jet speed `v = √(2g·head)` where head is the hole depth below the surface, so a lower crack bursts harder and the flow decays continuously as the tank drains. Puddle depth acts as back-pressure; the tank drains to equilibrium with the puddle. - **Curved ballistic jet** — particle stream + a translucent envelope curve, gravity-bent, splashing on impact. - **Spreading puddle** — volume-conserved; spreads right, wraps left around the tank, stops at the room walls, then deepens. - **Fish** — wander peacefully, then *fight the current* (stamina drains); when overwhelmed they're sucked through the breach → airborne arc → floor flopping → some survive swimming in the puddle (dead ones get X-eyes). - **Density/buoyancy variety** — sinking rocks, current-nudged pebbles, rooted plants that sway with the current and collapse when the waterline drops below them, and a floating beach ball that rides the falling surface and can even get swept out. - **Glass shards** — 15 angular fragments with spin, floor bounces, and water drag/buoyancy when they land in the puddle. - **Continuous waterline** — animated surface line lowers smoothly; live HUD shows water %, pressure head, jet speed, flow rate, and puddle depth.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Aquarium with fish, crack, and glass shards on floor, but no visible burst jet or spreading puddle collision effects shown.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.