Aquarium breach · qwen3.8-flash
Output saved · Stopped by an API error Cost / run $0.0434 Runtime 879.3s
A model request failed and its retries ran out. The output is the HTML saved before that.
Prompt for this run
Build an interactive 2D aquarium-breach simulation on a canvas. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Initially show an intact glass tank containing water, fish, rocks, plants, and a floating toy. Let the user drag a visible crack vertically on one side below the initial waterline, then press Breach. Provide Reset to restore the same initial scene. - On Breach, show the panel breaking around the selected opening. Water exits in a gravity-curved jet, and the waterline falls continuously. Outflow weakens as the water level approaches the opening and stops when water no longer reaches it. - Escaped water forms a spreading puddle constrained by the floor and room walls. - Model distinct responses to buoyancy, drag, and current: rocks sink, the toy floats, plants bend or drift, and fish initially swim against the current before some are swept through the breach. Movable objects that fit the opening can be carried out by the flow. - Objects leaving the water transition to gravity-driven motion and collide with the floor. Glass fragments rotate, collide, and experience resistance in water. - Simplified, internally consistent physics is sufficient; a full computational-fluid-dynamics solver is not required. Verify: Position the crack, trigger a breach, and sample the early outflow. Check that the jet curves downward, the waterline begins to fall, and nearby objects respond to the current. Use Reset and check that the tank returns to its intact initial state.
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
METRICS
Transcript · 35 messages
Build an interactive 2D aquarium-breach simulation on a canvas. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Initially show an intact glass tank containing water, fish, rocks, plants, and a floating toy. Let the user drag a visible crack vertically on one side below the initial waterline, then press Breach. Provide Reset to restore the same initial scene. - On Breach, show the panel breaking around the selected opening. Water exits in a gravity-curved jet, and the waterline falls continuously. Outflow weakens as the water level approaches the opening and stops when water no longer reaches it. - Escaped water forms a spreading puddle constrained by the floor and room walls. - Model distinct responses to buoyancy, drag, and current: rocks sink, the toy floats, plants bend or drift, and fish initially swim against the current before some are swept through the breach. Movable objects that fit the opening can be carried out by the flow. - Objects leaving the water transition to gravity-driven motion and collide with the floor. Glass fragments rotate, collide, and experience resistance in water. - Simplified, internally consistent physics is sufficient; a full computational-fluid-dynamics solver is not required. Verify: Position the crack, trigger a breach, and sample the early outflow. Check that the jet curves downward, the waterline begins to fall, and nearby objects respond to the current. Use Reset and check that the tank returns to its intact initial state.
thinking
Let me start by exploring the working directory and then build the simulation.
Let me plan the physics simulation:
- Canvas single page app.
- Tank: glass walls on left, right, bottom. Water filled to waterline.
- Crack: draggable vertically on one side (say the right wall) below initial waterline.
- Breach button: creates hole in the panel around selected opening — panel breaks into glass fragments.
- Water exits in gravity-curved jet: particle system for water droplets with gravity + initial horizontal velocity from pressure (Torricelli: v = sqrt(2*g*h) where h = depth below waterline). Waterline falls continuously: dH/dt = -flowRate / tankArea.
- Puddle: escaped water accumulates on floor, spreading horizontally constrained by room walls. Could model as a puddle width/height that spreads, or as a height-field array of columns on the floor.
- Buoyancy/drag/current: rocks sink (density high), toy floats, plants bend or drift (verlet strands), fish swim against current then get swept out.
- Objects leaving water → gravity + floor collision.
- Glass fragments rotate, collide, water resistance.
- Reset restores initial scene.
Implementation approach: a particle-based approach for water inside? That's expensive. Simpler: analytic water level for the tank, and free-surface jet particles for the escaping water. Puddle height-field with columns for volume conservation.
Let me design:
World coordinates = canvas pixels (with y downward). Use a physics scale: 1 m = 100 px, gravity 9.81 m/s² → 981 px/s².
Tank:
- x from 140 to 700, floor y = 560 (room floor at y=560). Tank bottom at 555 maybe. Tank interior.
- water level y_water (initial 200). Room floor at y = 560.
Crack on right wall at y = crackY (draggable between waterline+20 and tank bottom-20).
Breach: hole height = 46 px centered at crackY (opening). Water jet emerges horizontally from right wall with velocity v = sqrt(2*g*depth) where depth = crackCenterY - waterY... Actually use head h = (holeY - waterY) in px → convert to meters. Outflow flux = holeArea * v, but the tank has width; water level drop rate = Q / tankCrossSection. Simplify: dh/dt = -k * sqrt(max(0, holeY - h_water)) with k tuned. Stops when waterY >= holeY (water no longer reaches opening) — i.e., when waterline falls to the hole bottom edge? Real behavior: as waterline approaches the opening, outflow weakens; when the waterline drops below the top of the opening, flow becomes partial; stops when below the bottom of the opening. Let's use: head = (holeBottomY - waterY)... Hmm y downward. Let's define depthBelowSurface = holeCenterY - waterY (positive when hole is below waterline). Flow rate Q = clamp(head)... Use effective head = max(0, (holeBottom - waterY))? Let's do: submerge = holeCenterY - waterY. When submerge > 0 (waterline above hole center), v = sqrt(2 g (submerge)). As waterline approaches hole center, outflow → weaker. When waterline below hole center, remaining outflow from the portion of hole still below waterline: fraction f = clamp((holeBottom - waterY)/holeH, 0, 1); v = sqrt(2 g max(0, (holeCenterBelow?))). Simpler:
- effHead = clamp((holeBottomY - waterY) ... hmm.
Let me just do: levelY = waterY (surface). holeTop = crackY - holeH/2, holeBottom = crackY + holeH/2.
wetFrac = clamp((holeBottom - max(levelY, holeTop)) / holeH, 0, 1) — fraction of hole height below surface.
head = clamp(crackY - levelY, 0, ...) but when surface is below hole center, head should be measured from surface to hole centroid of wet part.
Use: head = (wetCentroidY - levelY) where wetCentroidY = midpoint of the wet portion (from max(levelY,holeTop) to holeBottom).
v = sqrt(2 g head). Q = wetFrac * holeH * v. level falls: dLevel = Q / tankWidth (scaled). That naturally weakens as level approaches holeBottom (wetFrac→0, head→0). Stops when levelY >= holeBottom.
Jet particles: spawn at hole with vx = v * 0.9, vy small upward? Actually the jet emits horizontally and curves down under gravity. Spawn a bunch of droplet particles each frame proportional to Q, at (holeX, wetCentroidY) with vx = v, vy = small random. Then gravity curves them. When they hit the floor, they add volume to the puddle height-field near their x, and the particle dies.
Puddle height field: array of columns across room floor from roomLeft to roomRight, cell width ~8px. Water flows: simple relaxation - each step move excess from high columns to neighbors, and cap max height. Also puddle constrained by room walls (edges reflect). Volume conservation: total escaped volume = sum of column heights * cellW.
Also puddle should perhaps have slight flow-back? No. Keep simple: spreading with a max speed.
Objects:
- Rocks (3): circles/ellipse polygons, density 2.5 → sink. Rest on tank bottom. In flow they barely move (heavy, high friction with floor). Rocks don't fit through the opening? Some could. Requirement: "Movable objects that fit the opening can be carried out by the flow." So rocks mostly too big/heavy; toy floats and may get carried out; fish get swept out.
- Toy: floating rubber duck-ish — a small block that floats at waterline with buoyancy; bobs. When breach current appears, toy drifts toward hole, and if it fits opening (it does), it gets carried out, lands on floor with gravity.
- Plants (2-3): rooted at bottom, verlet chain of segments; they bend with current, drift sway.
- Fish (5): boids-ish swimmers, steering; they swim against the current (toward the left/away from hole) with some effort; when current force exceeds their max swim speed (near hole), they get swept toward the hole and out through it, then fall to the floor (or flop). Some fish escape, some manage to hold on. Glass fragments: when breach triggered, generate shards from the right wall panel region around the opening: polygons with position, velocity, angle, angular velocity; water resistance (drag proportional to relative velocity), gravity, collide with floor and tank bottom and walls.
Water inside tank: the region is filled with water color up to level; but as level falls, objects below are in air → they fall.
Determine "inWater" for objects: object y > levelY (below surface) and inside tank x-range (or in puddle → puddle depth too). Buoyancy from water level: tank water or puddle water.
Rendering: draw room background, floor, tank glass with reflections, water with gradient + surface wobble line, objects, jet particles, shards, puddle with surface highlight, HUD text.
Crack dragging: before breach, show a visible crack (zigzag line) on right wall at crackY; user drags it vertically (mouse down near crack → drag; clamp between initial waterline + 12 and hole bottom limit). Show a dashed guide line and label "drag the crack". Cursor changes.
Buttons: Breach, Reset. Plus maybe a slider for hole size — no, keep focused.
Also add a play/pause? Not required. Keep it clean but add small "Reset" always available.
Let's set fixed canvas size, e.g., 1200x700? Use device pixel ratio scaling and CSS to fit width. Let's make canvas logical size 1000x620 and scale with CSS max-width 100%.
Coordinate layout:
- Room: floor at y = 560. Left wall x = 20, right wall x = 980.
- Tank: left glass x=200, right glass x=760, bottom y=548 (tank floor thickness), top rim y=140. Interior from 200..760, bottom interior y = 540.
- Initial waterline y = 200.
- Crack range: y from 240 to 500.
- Hole height 44.
Water depth initial = 200 → level y=200; tank interior height 140..540.
Scale: PIXELS_PER_METER = 220 (so tank 560px wide ≈ 2.5m — too big). Doesn't matter much for visuals; tune for feel. Let's use g = 1600 px/s² for a snappy jet? Let's think: head = 300 px ≈ ~1.36 m with PPM=220 → v = sqrt(2*9.81*1.36)= 5.2 m/s → 1140 px/s. Jet crosses 220px to the right wall quickly, then falls. Over 300px of horizontal travel at 1140px/s takes 0.26s, drop = 0.5*1600*0.26² = 54px. Hmm decent curve. Actually the hole is at the tank right wall x=760, and room right wall at 980: 220 px. If crack at y=480, head = 480-200=280px→1.27m→ v=4.99m/s=1100px/s. Time to fall from 480 to 560 = sqrt(2*80/1600)=0.32s → horizontal travel 350px > 220 so it hits the wall. Fine—maybe push splash off the wall. Let's reduce g_effective or PPM. Let's use PPM = 400 (tank width 560px = 1.4m, plausible aquarium), g = 9.81*400 = 3924 px/s². head 280px = 0.7m → v = sqrt(2*9.81*0.7)=3.7m/s=1480px/s. Falls 80px in sqrt(2*80/3924)=0.2s → 296px horizontal. Still hits wall. That's fine actually: water hitting the right wall splashes and runs down into the puddle. Or make tank narrower/shifted left so jet lands in open floor. Let's put tank left: tank from x=120 to x=620, room walls at 20 and 980. Then hole at 620 and 360px of space to the right wall — jet lands on floor around x=620+300=920. Hmm near wall. Let's instead reduce v by a discharge coefficient 0.65 (realistic!) → v=960 px/s, horizontal 195px, lands at x≈815.
Use dt-based simulation with fixed timestep 1/60 (subdivide for stability). Let's implement with accumulator.
Water level fall rate: dLevel = Q / tankWidth. Q = wetFrac*holeH*v (px²/s per unit depth). Actually per unit tank depth (into screen), so level drop = Q/tankWidth. With holeH=44, v=960, wetFrac=1: Q=42240; /tankWidth(500) = 84 px/s. So the level falls 84px/s initially, slowing. Full 340px depth → several seconds. Good—maybe a bit fast; add coefficient 0.6 → 50px/s. Nice, ~8s to drain to hole.
Puddle: total escaped area = integral of Q dt. Spread into columns: each column cellW=10, room interior width 960 → 96 columns. Volume added to puddle = Q*dt (px²). Distribute at impact x. Then spread algorithm.
Now objects physics:
Generic rigid body circles for simplicity: rocks (r=14..22, density 2.6), toy (r=18 box, density 0.3), fish (small ellipse, active). Use circle collisions with floor and tank walls (interior left/right/bottom) and among each other (simple separation impulse). Glass shards: polygon-ish (draw as triangles) with rotation, collide with floor/tank.
Buoyancy: if body center below water surface in tank x-range: F_b = -rho_water * V * g (up), weight = rho_body * V * g. Drag: F = -c * v_rel ... use relative velocity to water velocity field. Water velocity field inside tank: near the hole, flow converges to hole with speed ~ v * (holeArea/dist) — simplified: current = flowDir toward hole with magnitude proportional to Q/(distance to hole) capped. So objects near the hole get pushed toward it. Also add surface wave orbital motion minor.
Fish behavior: each fish has a heading and swim speed; desired: swim against current (i.e., away from hole, to the left) and wander vertically. thrust = max swim force. If current force on fish > fish thrust capacity (near hole), it gets swept. Implement as forces: fish applies force opposite to local current, magnitude up to Fmax. Net force includes drag from current. As current near the hole exceeds threshold, drag wins → fish accelerates toward hole → passes into hole (if it fits: fish width < hole height) → outside tank, gravity+no water → falls to floor, flops and gets stunned, then maybe leaps back? Keep: fish lands on puddle floor, wiggles, and can flop. That's fine visually.
Plants: verlet chains of 6 nodes, rooted at base at tank bottom. Each node: gravity slight (they're floppy, tend upward? real plants bend up in still water due to buoyancy) — simpler: rest shape is a gentle curve upward; add "buoyancy" restoring force toward rest offset, plus drag from water current bends them downstream. When water level drops below plant top, it droops (gravity wins). Also some loose drifting particles (leaves detaching? maybe "plants bend or drift" — small floating leaf bits that drift toward the hole and out).
Let me add a few "leaf/debris" floaters that drift with the current and exit through the breach.
Water surface rendering: polyline across tank interior with small ripple based on level fall speed.
Puddle rendering: height field → fill path with polygon, top edge slightly smoothed.
Also spawn splash particles when jet hits floor.
Let me also handle objects floating down with the falling level: toy follows surface (buoyancy naturally does that). Fish swim in the water; when the level drops below their depth, they fall/flip? Fish should try to stay submerged: steer to remain below the surface and away from hole; when water gets shallow, fish get stranded on the tank bottom and flop (gravity + no buoyancy).
Some fish escape through the hole and land in the puddle.
Rocks: sink, sit on bottom; high friction; current can't move them much (maybe nudge). "Rocks sink" — verify: they rest at the bottom, and if water drains they stay. Also a rock placed on top? Rocks fall to the bottom quickly at start (spawned higher) to demonstrate sinking. Yes—spawn rocks initially partially lifted and they sink to the bottom, demonstrating sinking. Reset regenerates them at the same initial state so they always sink the same way (deterministic-ish). Requirement "Provide Reset to restore the same initial scene" — so reset re-seeds with the same deterministic initial positions (rock spawn slightly above resting, they settle). Use seeded RNG for reproducibility.
Implement seeded RNG (mulberry32) with fixed seed for reset.
UI layout: title bar with buttons Breach / Reset, a hint text, maybe readouts (water level %, escaped volume, fish escaped count). Keep tasteful dark room aesthetic.
Also a crack drag with keyboard? Provide arrow-key support maybe. Mouse + touch.
Let me write the code carefully.
Key structures:
```js
const W=1000, H=620;
const ROOM={left:16,right:984,floor:560};
const TANK={left:120,right:620,top:150,bottom:540,wall:8};
const INIT_LEVEL=210;
const HOLE_H=46;
```
Wait, tank right at 620, room right at 984: 364 px of clearance. Jet lands at ~620+195=815. Fine.
Actually let me widen the tank a bit for looks: left 110, right 640. OK.
Physics constants:
PPM=400; G=9.81*PPM.
C_D = 0.62 discharge.
Simulation state:
```js
state = {
breached:false, level:INIT_LEVEL, crackY: 430 (default within range),
hole: {top,bottom,cx},
jet: [], splash: [], shards: [], bodies:[], fish:[], plants:[], debris:[],
puddle: Float64Array,
}
```
Puddle: array of n cells across room interior; height h[i] px. Flow: for each cell, compute slope to neighbors, move volume with speed limited. Simple shallow-water-ish:
```js
for iter: flux between i and i+1 = k*(h[i]-h[i+1]) clamped by max transfer and dt
```
Use n=110 cells, cellW=(ROOM.right-ROOM.left)/n.
Add "dryness" threshold: min layer 0.6px; also water spreads with a max front speed.
Also if a body is in the puddle, buoyancy applies: puddle depth at body x → if body center y > floor - puddleDepth... puddle is shallow, only tiny buoyancy. Fish in puddle flop. Keep buoyancy from puddle minimal (they're mostly out of water). Let's just check depth>2px for partial buoyancy.
Bodies update:
```js
function waterInfoAt(x,y){ // returns {level, vx, vy, depth}
if inside tank x-range and level<y: tank water: depth = y - level, velocity = current field
else if outside tank and y > floor - puddleH(x): shallow puddle
}
```
Current field inside tank: target velocity = direction from body to hole center * speed where speed = Q * C / (r + 60) roughly (2D source flow: v = Q/(2π r) → use Q / (r) * scale). Also apply only within tank; near the hole the flow accelerates through it. Cap speed at jet speed. Outside the tank in the jet region, use jet momentum? Simpler: objects in the hole region get pushed by jet velocity field.
Also, before breach, water is still (small ambient swirl from fish).
Drag: F = -0.5 * rho * Cd * A * |v_rel| v_rel → in 2D per unit depth: A = 2r. Use kDrag = 0.5*rho_water*Cd*(2r)*|vrel| → tune with damping.
Buoyancy: mass = rho_body * π r² (2D). Weight = m*G. Buoyancy = rho_water * submergedFraction * π r² * G upward. Rock rho 2.4 → sinks. Toy rho 0.35 → floats high. Fish rho 1.02 → near-neutral, so they must swim. Debris leaves rho 0.9 → float at surface.
Semi-implicit Euler with substeps (4 per frame at dt 1/60 → 0.0042 s). Add linear damping for stability.
Collisions:
- Tank interior walls: x in [TANK.left+r, TANK.right-r] when y > TANK.top... but there's a hole in the right wall: if the body is within hole vertical band (y between holeTop..holeBottom) and beyond, it can pass. Implement: right wall collision unless (breached && y within hole band + margin). Also wall only exists below TANK.top (above the rim, no walls, open top).
- Tank floor: y <= TANK.bottom - r when x within tank.
- Outside: room floor: y <= ROOM.floor - r. Room walls: x in [ROOM.left+r, ROOM.right-r].
- Tank glass outside face at TANK.right..TANK.right+wall. Bodies pushed against the outside of the wall stop at x = TANK.right - ... hmm, they'd be falling past the tank's outer right face — that's fine, no collision with outer face needed except tank base/stand. Add tank stand: a base block under tank from x TANK.left..TANK.right, top y = TANK.bottom+... Let's simplify: tank bottom slab from y=540 to 552 (thickness) — objects landing near it bounce on it. Let's include: if body overlaps the slab rectangle → resolve. Add a simple AABB for the tank base and the walls as rectangles for shard collisions. Keep it simple: for shards and bodies, collide with a set of static rectangles (tank bottom slab, left wall panel (only if not broken? left panel intact), right wall panel below/above the hole region — after breach, the panels around the opening are gone (that's where the hole is) plus shards removed).
Let me define static collider rectangles list generated at breach time:
- before breach: right panel is a full rect (x TANK.right..TANK.right+wall, y TANK.top..TANK.bottom). Bodies inside tank collide with its left face.
- after breach: right panel replaced by two rects: above hole (TANK.top..holeTop) and below hole (holeBottom..TANK.bottom) — as separate colliders (both faces) — plus glass shards flying.
For simplicity, do circle-vs-AABB resolution for all bodies/shards against a list of rects (walls, bottom, floor, room walls). Shards as circles for collision but drawn as polygons (approximate radius). That's acceptable: "Glass fragments rotate, collide, and experience resistance in water." Collide with each other too? Add simple pairwise circle collisions among shards for tumbling pile on floor. Cheap with ~30 shards.
Puddle rendering must be behind the tank stand etc.
Let's also render a "wet patch" glow on the floor.
Now jet particles: droplets with pos, vel, r, life. Each frame, spawn count = clamp(Q*dt*0.02, ...). Cap total ~1200. Update: vel.y += G*dt; pos += vel*dt; drag minor; if pos.y >= ROOM.floor - 1 → deposit volume: puddle.add(pos.x, dropVol) → then remove. Also if hits room right wall → deposit. Also spawn splash particles when hitting floor (small droplets with random upward vel, which also deposit). To keep puddle volume consistent: total volume of all drops = Q*dt each. So each drop has volume = Q*dt/nSpawn.
Puddle max capacity: room floor area limited; if the puddle reaches the walls, level rises. Total escaped volume: at most tank area 500*340=170,000 px² spread over 968px → 176px deep... that's a big puddle. Actually only the water below... all water drains eventually? Flow stops when level reaches hole bottom. If hole at y=430, water from 210 to 430 drains = 220px depth * 500 = 110,000 px² → over 968 width = 113px deep puddle. Hmm that's deep for a "puddle" but physically consistent (a lot of water). Visually, a 100px deep puddle covering the floor is fine — looks like a flooded floor. Cap puddle depth at like 40px, with excess going... let it just rise, it's consistent. Let's cap at ~70px and let the level show. Actually with max hole at y=500: drain 290px*500 = 145,000 → 150px deep puddle, which would nearly reach the tank top... no, floor at 560, 150 deep → surface at 410. That's a flooded room. Hmm. Reduce: make the tank narrower (500→350?) or the room wider? Or the puddle confined to the right side region. Let's make the tank narrower: interior 400px wide, and the room floor 968 wide → 400*340/968 = 140 max. Still. Alternatively let water spread also under the tank stand and out of view — fine.
Practical approach: make it look right: allow max puddle depth ~60px by having the room floor "absorb" beyond a capacity (say water flows to a drain). Not physical but fine. Hmm, better: shrink tank interior to 320 px (x 300..620) and keep room wide. Then worst case drain 350*320=112,000/968=116px. Still high.
I think a visually acceptable compromise: the puddle spreads and rises but the waterline never gets above ~90px because the tank holds only ~112k px² and the puddle spreads to a max with the walls containing... it does rise. Let's just accept — deep puddle at the far end. Actually the water spreads over the whole room floor width 968, so 112k/968 = 116px deep average, i.e., surface at y=560-116=444 — the tank (bottom 540) would be submerged up to 444? That means water above the tank base by 96px. Hmm that would look like the tank is submerged. And buoyancy on the tank? Not modeled.
Better: reduce water volume: make initial water depth smaller relative to room. Options: raise room/floor line? Make the tank sit on a stand/pedestal of height 60 and floor area wide. Or make the puddle deeper but limited to a realistic "sheet flow" thin layer: in reality a 1.4m×0.9m tank of water spread over 4 m² of floor gives ~10-15cm deep puddle. Our scale: PPM 400 → tank 320px wide = 0.8m, water depth 340px=0.85m, per unit depth into screen 0.5m → volume 0.8*0.85*0.5 = 0.34 m³. Room 968px = 2.4m wide × 0.5m deep = 1.2 m² → 0.28m depth = 113px. So it IS physically 28cm deep puddle. Physically consistent, and honestly a 28cm flooded floor is plausible for a burst aquarium. Fine — the requirement says "Escaped water forms a spreading puddle constrained by the floor and room walls". So deep-ish puddle is fine and consistent. But visually we should show the puddle level rising and the tank standing in water. It's fine and demonstrates volume conservation. I'll add a readout "floor water depth: X cm". Nice touch.
But then the puddle submerges the lower part of the tank... The tank glass is impermeable, so puddle water outside the tank doesn't interact. Objects that fall into the puddle (fish, toy) get buoyancy from the puddle. Good.
Also the jet should hit the floor and puddle surface — deposit at the puddle surface level (y = floor - h(x)), which reduces fall distance. Handle: check drop against puddle surface: if pos.y >= floorSurface(x) → deposit.
OK. Let me also cap puddle: no cap needed since volume limited.
Let me now consider hole placement: "drag a visible crack vertically on one side below the initial waterline". Right wall. crackY in [INIT_LEVEL+30, TANK.bottom-40].
On Breach:
- generate shards from the right panel region: for the broken zone (hole band ± some), create 14-20 shards with velocities outward-left? Actually the glass blows outward (right) slightly, and fragments fall. Give vx positive small, random, vy from gravity; rotate.
- Also a spider-crack pattern drawn on the wall above/below the hole edges briefly (the remaining panels get crack lines).
- Set breached=true, hole band.
Also add subtle screen shake on breach. Nice.
Now fish logic:
```js
fish: {x,y,r, vx,vy, angle, swim:{}, energy, escaped:false, flopping:false}
```
Behavior each step:
- compute local current velocity (from source flow toward hole when breached) → cur.
- desired direction: away from hole (leftwards) plus vertical wander; if not breached: patrol with slight noise.
- apply thrust force F = m * a_max in the desired direction (a_max ~ 900 px/s² scaled by "effort") but only while submerged and "alive" (until swept out).
- Drag toward current: F = k*rho*(cur - v)*|cur-v| ... standard quadratic drag.
- Buoyancy ≈ neutral.
- If fish passes through the hole to outside: it becomes "airborne": no thrust, gravity, lands in puddle/floor → flops (apply impulses, small hops) — keep flopping in place with occasional wiggle. Then it stays there.
- Some fish (say 2 of 5, the "strong" ones with higher a_max) hold position, others get swept. Since the current scales with 1/r near the hole, fish that wander close get swept. Give each fish different strength → deterministic variety.
Rocks: rho 2.5; they sink to tank bottom; also they're too big to fit through the hole (r > holeH/2? holeH=46, so r=25 wouldn't fit). Requirement says "rocks sink" and "movable objects that fit the opening can be carried out" — so use rocks with r 12-16 (fit) but heavy so they just roll toward the hole and get stuck / tumble in the hole and half-exit? Better: keep two big rocks (r 20-26) that sit on the bottom and don't fit; and small pebbles (r 7) that get dragged along the bottom toward the breach and washed out. That demonstrates both. Nice.
Plants: 3 verlet strands rooted to the bottom at x 360, 420, 500 with heights ~110, 130, 90. In still water: gentle sway (rest shape + sinusoidal ambient). Under current: bend toward hole. When the water level drops below the plant top: gravity droop (nodes above surface get gravity, no buoyancy/drag from water). "Plants bend or drift" — also add a couple of detached leaf particles that drift with the current and wash out.
Also a decoration castle? skip.
Toy: a "floating toy" — a rubber duck made of circles (body + head) but as a single circular body with a drawn duck. Density 0.3 → floats; it will bob at the surface, and near the breach gets drawn to the current and washed out through the hole (it fits, r=16 < holeH 46/... hole half=23, ok). Then it falls and lands on floor. Good demo.
Rocks (3): circles/ellipse polygons, density 2.6, rest on tank bottom.
Glass panel break: draw remaining shards stuck? Fine.
Let me write the code. Also add HUD: water level %, time, escaped fraction, fish washed out count. Buttons: Breach (disabled after breach), Reset, and pause/step? Add "Pause" maybe. Also "hole size"? Keep 2 buttons + maybe checkbox "show vectors"? Skip.
Interaction detail: draggable crack. Draw crack zigzag with a highlighted grip circle and arrow hints. On pointerdown within 30px of crack center → dragging. On move → clamp. Also keyboard: focus canvas, ArrowUp/Down moves crack. Also touch support via pointer events.
Also allow dragging before breach only; after breach, the crack is gone (broken).
Time budget: implement carefully but not overly long. Let's write it.
Detailed physics numbers:
```js
const PPM=400, G=9.81*PPM; // 3924 px/s^2
```
Water density rho_w = 1.0 (in "mass per px²" arbitrary units). Use consistent: mass = rho * π r².
For a rock r=22: m = 2.5*π*484=3800. Weight = 3800*3924 = 1.5e7 — huge numbers; forces are fine as long as consistent, accelerations are what matter. Use accelerations directly to avoid overflow:
- net accel = G*(rhoBody - rhoWater*submergedFrac)/rhoBody downward... Actually a = (W - B)/m = G - rho_w*V/(rho_b*V)*G = G*(1 - rho_w/rho_b) when fully submerged.
- Drag accel = -0.5*Cd*rho_w*|vrel|*vrel*(area/m) = -0.5*Cd*rho_w*|vrel|*vrel*(2r/(rho_b π r²)) = -(Cd*rho_w*|vrel|*vrel)/(rho_b*π*r).
Compute per substep. With Cd=1.0 and r=10, rho_b=1: drag accel = |vrel|*vrel/(π*10) = for vrel=100: 10000/31.4=318 px/s² — mild. For a fish being swept at vrel=800: 800²/31.4 = 20k px/s² → strong, good (it means it rapidly matches current). Time scale: with a=20000, reaching 800 px/s takes 0.04s. That's plausible for heavy objects; but for tiny debris it's very stiff; substep dt=1/240 → dv = 83 px/s per step, stable-ish. Cap the drag accel to avoid instability: limit dv per step to 0.5*|vrel|. Good — implement as a velocity-blending (implicit-ish) drag:
```
k = 1 - exp(-beta*dt) where beta = |vrel| * areaFactor / m
v += (vwater - v) * k * shapeFactor
```
That's stable for any dt. Use beta = (rho_w * Cd * 2r / m) * |vrel| ; with m = rho_b π r²: beta = (2 Cd |vrel|)/(rho_b π r). For r=10, rho_b=1, |vrel|=100 → beta=6.4/s → per 1/240 s, k=0.026. Fine. For a rock r=22, rho_b=2.5, |vrel|=100: beta = 200/(2.5*π*22)=1.16 → slow → rock barely moves.
Add floor friction: when resting on a surface, tangential velocity damped by factor.
Buoyancy for partially submerged: submerged fraction by circle geometry approx: use clamp((y_surface - (y - r))/(2r),0,1) where y_surface is water level. Simplify with linear fraction.
Float equilibrium: rho_b=0.35 → submergedFrac=0.35 → center sits at level + r*0.7. Good, mostly above.
Water surface vertical velocity: as the level falls, the surface moves down; add small vy to water velocity field = levelVel.
Current field (breached): treat the hole as a 2D sink at (holeX, wetCenterY) with strength S = Q (px²/s per unit depth). v_radial = -Q/(2π d) toward the sink, clamped to a max of jet speed. Inside the tank only (x < TANK.right). Outside: if inside the hole band and x between panel faces → v = (vjet, small). Beyond → use the jet droplets, not a field. Objects outside just fall.
Before breach, ambient current ~ tiny noise.
Fish thrust: desired dir = normalize(-cur) mostly away from the hole, plus wander. Apply accel = a_max*dir. a_max per fish: [500, 900, 1400] variety. Current near 100px from hole with Q=42000: v = 42000/(2π*100)=67 px/s. Hmm too weak to sweep a fish. Real aquaria: flow near a drain is fast. Let's use v = Q/(2π d) but with d measured and clamped: at d=30: 223 px/s; at d=10: 668. And the hole "capture" region: any object within the hole band and x > panel - 10 gets pushed out. To make fish swept, the drag accel from vrel: fish r=12, rho_b=1.02 → beta = 2*|vrel|/(1.02*π*12)=0.052*|vrel|; at vrel=300 → beta=15.6/s → dv/dt = 15.6*300=4700 px/s². Fish max thrust 1400 → swept. At vrel=60: beta=3.1, accel=188 → fish holds. So the "capture" radius where |vrel| > thrust/beta: solve for the current speed threshold. Fish with a_max=500: threshold |cur| ~ sqrt(...)? a_drag ≈ beta*vrel = 0.052*vrel² → vrel_crit = sqrt(500/0.052)=98 px/s → d = Q/(2π*98) = 42000/616 = 68px. So within 68px of the hole, weak fish get pulled in. Good — fish that wander near the hole get swept, others hold off. Nice dynamic. Also as the level drops, Q drops, so fish survive.
For toy: rho 0.35, r=16 → beta = 2*v/(0.35*π*16)=0.113 v; drag accel 0.113v² ; but toy also has buoyancy at surface; the horizontal current at surface pushes it. Also direct pressure on a floating body → it drifts to the hole. Should work; also add extra: floating objects get carried by the surface flow which converges to the hole. Good.
Pebbles r=6-8, rho 2.6: beta = 2v/(2.6*π*7)=0.035 v → at v=200: accel=1400 px/s² — they slide along the bottom toward the hole and get washed out. But drag alone might not overcome friction; use low floor friction for pebbles. Also as the level drops, the remaining water near the hole is shallow and fast, sweeping pebbles. Might be too slow — tune: pebble r=5, rho 2.2. Add "traction" 0.6 damping when on floor. Let's test empirically in the browser and tune.
Also I should make sure the flow drags objects toward the hole even far away (gentle drift), so it's visually clear.
Also water velocity should be nonzero in the puddle where the jet lands (small churn) — skip.
Let me write code now. Then run in the browser and verify.
Also "Provide Reset to restore the same initial scene" — reset should also reset the crack to the default? "restore the same initial scene" — yes, restore water level, objects, shards, puddle, and crack to the initial default position. I'll reset crack to the default position (initial scene) — but then the user's chosen position is lost. Hmm: "Position the crack, trigger a breach ... Use Reset and check that the tank returns to its intact initial state." Intact = no hole, full water. I'll keep the user's crack position (as the pre-breach initial scene includes crack position) — safer: reset the crack to the default too? I'll keep the current crack position but make it draggable again (that seems most useful) and note it. Hmm "restore the same initial scene" — deterministic same scene. I'll reset everything including the crack to the default position, and mention in the help text that Reset restores the pristine initial setup. Actually keeping the user's crack choice is convenient for re-running. Compromise: Reset restores the scene intact with the crack at the last user position... Let me just restore the crack to its default position to satisfy "the same initial scene" literally. I'll do that.
Let's write the file.
Then run browser: screenshot, check console, test drag, breach, sample early outflow, verify the jet curves, level falls, objects respond, reset works.
Let me write the code.
I'll write carefully to avoid bugs.
```js
// ---------- state ----------
const state = {
breached:false, breachTime:0, time:0, running:true,
level:INIT_LEVEL, crackY:CRACK_MIN+...,
holeTop, holeBottom, holeX,
Q:0, jetV:0, wetCenter:0,
jet:[], splash:[], drops:[], shards:[], rocks:[], pebbles:[], fish:[], floaters:[], leaves:[], plants:[],
puddle:null, ...
}
```
Let me write the body type with generic fields: {kind, x,y,r, vx,vy, rho, angle, av, onFloor, escaped, ...}.
Water query:
```js
function waterAt(x,y){
// returns {level, vx, vy, inTank}
}
```
Implementation of tank water: surface y = level; if y > level and x in interior → submerged, depth = y-level.
Now write the actual code. I'll produce ~900 lines. Let's go.
Key functions:
1. `resetScene()`
2. `breach()`
3. `stepPhysics(dt)`
- `updateFlow(dt)`: compute wetFrac, head, v, Q; integrate level; spawn jet drops; deposit into puddle; update puddle spread.
- `updateCurrent()`: compute sink strength.
- `bodies`: rocks, pebbles, toy, fish, leaves, debris.
- `shards`
- `plants`
- `droplets` (jet) and splash
4. `draw()`
Collision helper:
```js
function rectsFor(b){
const L=[];
L.push(ROOM_FLOOR_RECT, ROOM_LEFT_RECT, ROOM_RIGHT_RECT, TANK_BOTTOM_RECT);
L.push(TANK_LEFT_RECT);
if(!state.breached){ L.push(TANK_RIGHT_FULL) } else { L.push(TANK_RIGHT_ABOVE, TANK_RIGHT_BELOW) }
return L;
}
```
Hmm, the right wall panels: the piece above the hole remains (from top to holeTop) and below (holeBottom to bottom). Shards from the broken part fly away. Also I should make the broken band edges slightly jagged for visuals.
Circle-vs-AABB resolution should handle both inside and outside. But if a rock inside the tank ends up overlapping the right-above panel (possible if the level drop causes nothing weird) fine.
One risk: when the breach happens, the fish/toy near the hole could be inside the wall band? No—hole band removed.
Another risk: A body outside the tank (x > TANK.right) falling could collide with the tank bottom rect extended? The tank bottom rect spans the interior, x from TANK.left-wall to TANK.right+wall → so a falling body outside the tank at x in [TANK.right, TANK.right+wall] would land on the tank bottom slab top. The bottom slab top is at y=540, and the wall goes 540..552. That's outside the tank at y 540 — fine, that's the tank's outer base edge; a body landing there can then roll right and fall to floor. OK, but it could catch objects. Only in a narrow 8px strip. Fine.
Also the objects outside the tank must not enter the tank interior through the hole from outside; not a concern.
TANK_TOP rim: bodies can fly above the tank? Toy exiting the hole won't go up. Fine.
Now the current field:
```js
function currentAt(x,y){
if(!state.breached) return {vx: ambient..., vy:0}
const dx = holeCX - x, dy = holeCY - y; d = hypot...
if(x < TANK.right + 4) { // inside or at the wall
const s = state.Q * CUR_SCALE / (d + 26);
return {vx: dx/dir... }
}
}
```
Use v = Q/(2π (d + 20)) clamped to 1400. With Q=42000: at d=100: 42000/(6.28*120)=557 px/s. Hmm that's strong — everything gets swept quickly. Reduce CUR_SCALE to 0.35: at d=100 → 195 px/s; at d=300 (far left) → 42000/(6.28*320)=21. Good gradient. Let's use scale 0.4.
Also the sink only applies within the tank; and flow converges from all directions but the left wall blocks — fine.
Vertical component: flow toward the hole pulls objects down if above the hole center and up if below — for surface floating objects, the horizontal dominates. Fine.
Also to prevent objects circling the sink forever, they need to pass through the hole: near x >= TANK.right - 20 and |y - holeCY| <= HOLE_H/2 - r*0.3 → allow escape. If an object arrives at the right wall outside the hole band, it collides with the panel and slides along it toward the hole band (the current has a vertical component guiding it).
Fish that exit: set escaped=true; keep simulating (no thrust). Actually if a fish exits, then the level keeps dropping... fine.
Now plants. Verlet:
```js
node {x,y,px,py, restDx, restDy (offset from parent in rest shape), pinned}
```
Constraints: distance to parent = segLen (with the parent possibly also moving). Standard verlet with 3 iterations.
Forces: gravity (only if above the water surface; and reduced by buoyancy if submerged → submerged nodes get a slight upward "up-buoyancy" making them stand up), drag toward the water velocity (blend node velocity to water velocity strongly when submerged), and stiffness restoring toward the rest position relative to the parent (apply as a rotation-toward-rest of the chain, i.e., bend constraint). Simplify: apply a "shape matching" step: for each node i>0, compute desired position = parent + restVec rotated by small sway; lerp by stiffness*k*dt. That gives plants that stand up in still water and bend with the current (because drag pushes them, and stiffness pulls them back proportional to flow → equilibrium bend).
Implement:
```
// after verlet integration
for i=1..n:
desired = {nodes[i-1].x + restVec.x(rot sway), ...}
// apply stiffness toward desired as position correction weighted
```
Hmm, verlet with distance constraints already handles the length; the bend stiffness can be applied by moving node i toward the rest-offset-from-parent with a factor s (0.02..0.1). That's an angular spring. Good enough.
For drooping when unsubmerged: for nodes above the waterline, apply full gravity and much lower stiffness (they lose buoyant support) → they droop. That satisfies "plants bend or drift".
Also "drift": one plant may become uprooted when the water rushes out — give plant #3 a "loose" flag: it detaches at breach and drifts with the current, eventually washing out through the hole (fits? plant segments are small; let it drift and pile). Nice touch: it can go out through the breach. I'll implement detaching: base node becomes free after the current exceeds a threshold, and the plant then behaves as a free-body chain advected by drag (nodes pinned=false). It'd likely jam in the puddle. Let's allow that; if it's tedious, keep it simpler: leaves detach and drift out (already planned) — that satisfies "drift". I'll do both: leaves drift out for sure, and the middle plant is "loose-rooted": it uproots after ~1.5s of strong flow and drifts (mostly stays in the tank as the level drops, ends up flopping on the bottom). Good.
Draw plants as tapered polygons.
Let's write the code. Also I need to compute the water volume stats: escapedVol += Q*dt.
Also I'll clamp level to >= TANK.bottom (all drained to the bottom? no—flow stops when the level hits the hole bottom, leaving water below the hole in the tank). So the level stops at holeBottom (residual water below the opening stays). Actually the level stops exactly at holeBottom (when wetFrac=0). Good, physically correct: water remains below the breach. Show that in HUD: "residual below the breach".
Also: what if the user drags the crack very low? Then most water drains → big puddle. Good.
Now writing.
Rendering details for the jet: draw droplets as circles with alpha, plus a "solid stream" polygon near the hole: from the hole edges, a tapered ribbon following the ballistic path of the center stream: sample the trajectory analytically: p(t) = p0 + (v*t, 0.5 g t²) for t up to hitting floor. Draw as a filled path with width tapering from HOLE_H*0.8 to HOLE_H*0.45, with a wavy edge. Combined with droplets = convincing.
I'll compute the ribbon each frame from the current hole position and jet velocity.
Also: as the level falls, the emission point moves up? No—the hole is fixed; the wet center moves up as the level drops... wetCentroid = midpoint of (max(level, holeTop) .. holeBottom). When the level is above holeTop → centroid = holeCY. As level drops below holeTop, wet part shrinks from the top, so the centroid moves down toward holeBottom. So the emission point drifts downward as the level falls — realistic. And the emission velocity head = centroid - level.
Q = Cd * wetHeight * v where wetHeight = holeBottom - max(level, holeTop).
Then dLevel/dt = -Q / TANK.width, and escapedVol += Q*dt (px² per unit depth).
Also the level drop should slow near the end. Good.
Also handle: the level must never rise. And when level >= holeBottom → flow = 0, but a thin film continues... stop.
Also I want the level drop visible in the puddle rise. Good.
Jet drop spawn: emit at hole x = TANK.right + wall*0.5, y = centroid, vx = v*0.95, vy = -? The vena contracta is ~0.6-0.8 of the height at the center. Spawn drops distributed across the wet height * 0.7 centered at the centroid, with vx = v*(0.9+rand*0.2), vy = 10*rand.
Number spawned: proportional to Q*dt/volPerDrop. Let each drop have volume vol = π r_drop² and set r = 3..6. Then n = Q*dt/(π*16) ≈ Q*dt/50. With Q=40000, dt=1/60 → 800/50=16 drops per frame. Good. Cap drops at 900.
Deposited volume: when a drop dies (hits puddle/floor/wall), add its vol to the puddle at its x (with a small spread). Also on impact spawn 1-3 splash droplets with volume accounting ignored (they add to the puddle when they land too — simpler: splash droplets also have vol and deposit). Total puddle volume ≈ escapedVol. Good.
Let's also remove drops that leave the room (clamp x within room).
Alright — writing the file now.
For the fish rendering: body ellipse + tail triangle + eye; rotate to velocity heading with a wagging tail (sin(time*10)).
For the toy: rubber duck: yellow circle body, small head circle, orange beak triangle, eye dot. Draw rotated by body angle.
Rocks: irregular polygon (seeded), gray with highlight.
Pebbles: small circles.
Leaves: small elongated ellipse green.
Glass: light blue-white with alpha.
Now, code.
Also HUD: show "Water depth: xx cm", "Escaped: xx%", "Crack height: y", plus a small legend of what to do.
Time check: 15 minutes budget. Let me write it efficiently but completely.
I'll write the file now in one `write` call, then run the browser and iterate.
Careful with the puddle: implement as Float64Array of heights, cells across the room floor.
```js
const PUD_N=140; const cellW=(ROOM.right-ROOM.left)/PUD_N;
function puddleAdd(x, vol){ const i=clamp(floor((x-ROOM.left)/cellW),0,N-1); pud[i]+=vol/cellW; }
function spreadPuddle(dt){
const k=0.45; // diffusion-ish, but needs to be front-limited
for(let pass=0;pass<2;pass++){
for(let i=0;i<N-1;i++){
const h0=pud[i], h1=pud[i+1];
let dh=h0-h1;
let f=dh*0.5;
// limit transfer
const maxT=Math.min(h0,h1)*0.25+0.05;
f=clamp(f,-maxT,maxT);
// only move if there's a slope (dry film threshold)
pud[i]-=f; pud[i+1]+=f;
}
// reverse direction pass for symmetry
}
}
```
Better: do a symmetric loop accumulating fluxes into an array then apply. And to spread quickly to far walls, diffusion with dt: f = -D*(dh/dx)*dt*h (like a lubrication approximation): flux ∝ h * dh/dx. Use D=8.
```
for i in 0..N-2: q = D*(pud[i]-pud[i+1]) * dt * avgH clamped
```
This gives fast spreading where deep, slow at thin fronts — realistic-ish. Let's set D such that a front moves ~200px/s. Test empirically.
Also clamp min height to 0 and remove tiny "dry" films: if pud[i] < 0.15 → set to 0 (and lose that volume? better to keep: merge into neighbors). I'll just do: below 0.2 treated as 0 for rendering but still tracked (volume conserved). Simpler: don't zero.
Also the tank occupies floor space; puddle under the tank: the tank bottom sits on the floor so water can't pass under it. Realistic: water flows around/over the stand. Let's make the tank stand (a cabinet) so water can't flow beneath x in [TANK.left-6, TANK.right+6] — i.e., mark those puddle cells as blocked (height 0, water reflects). That means the puddle only on the right side (and left of the tank). "constrained by the floor and room walls" — the stand adds a nice constraint: the puddle on the right can't go past the tank. I'll implement blocking cells for the stand region and treat water at the boundary as reflecting. That's good: the escaped water spreads right, hits the right wall, and pools. Nice.
But careful: the stand blocks water but the jet flies over it. The stand region cells are set to 0 and treated as walls in the flux computation (q=0 across those boundaries).
Let's implement pud with `blocked[]` flags.
Now write the file.
Also the tank stand visual: rect from TANK.left-10 to TANK.right+10, y from TANK.bottom(540) to 560 (20 tall) — the tank base slab. But if the stand is 20px tall, the tank bottom is at 540 which is above the floor 560. Let's set TANK.bottom=536 and the stand from 536 to 560 (24 tall). Water can't flow under the stand. Good.
Then the tank interior: y from TANK.top(150) to TANK.bottom(536). Interior floor at 536.
INIT_LEVEL=205 → depth 331px ≈ 83cm. Max hole y: 500 → holeBottom 523.
Crack range: [INIT_LEVEL+28=233, 500].
Puddle max depth: worst case level drops 523-205=318 over 504 wide = 160k px² over the free floor: right of the stand x from TANK.right+10=630 to 984 = 354 wide → 160k/354=450px deep!! Way too much. That's a problem: the stand blocks spreading, and the room isn't that wide on the right.
Hmm. So don't block, or only partially. Alternatively, reduce tank width and add more room. Or make the puddle "drain away" past the edge of the visible room (i.e., water flows out of frame under a doorway). Let's make the room wider visually and have the puddle spread over the whole floor, and set a soft cap: when the total exceeds capacity, allow the water to spread over a virtual larger area? Not consistent.
Simplest consistent fix: reduce the tank size (it's a desktop aquarium): interior width 320 (x 300..620), and let the water spread over the entire room floor (both sides, no stand blocking; the tank sits on a stand that spans 290..630, but water can flow around it and also to the left of the stand). Total free width = 968 - 350 (blocked by stand) = 618 → volume max: 320*318 = 102k → 102k/618 = 165px deep. Still deep. Hmm, the ratio: the tank cross-section (320 wide × 318 tall) vs the floor width 618: the puddle depth = 320*318/618 = 165. Because the tank holds a column of water of height ~1.6× the available floor width. Physically: a 0.8m×0.8m tank emptied into a room 1.5m of free floor width → 0.43m deep. That's realistic actually! A burst 800L aquarium does flood a room to ankle height. But visually in a 2D side view, the "floor width" isn't the real constraint — the room depth (into screen) matters, which we can't see. So in a 2D side view, a proper 2D simulation gives an exaggerated puddle depth.
Solution: treat it as a 2D slice and just accept the puddle depth. I'll allow the puddle surface to rise, and if it rises above the tank stand top, then the stand region cells also fill (the water overtops the stand!). That's the fix: the stand is 24px tall, so water above 24px deep flows over the stand. Then the free spreading width becomes the full 968. Puddle depth max = 102k/968=105px, overtopping the stand. And the tank glass is impermeable, so water inside the tank doesn't mix; but the puddle outside can rise up the tank's outer walls. If the puddle rises to y=430 (130 deep) it's higher than the crack at 500... then water flows INTO the breach! That's physically right (if the room floods above the crack, water would flow back in) but confusing. Max puddle 105px deep (surface at 455) vs crack at 500 — the puddle surface would be ABOVE the crack, causing backflow. Hmm.
To avoid weirdness: make the tank taller/narrower and keep the puddle shallow by using a wider room (or make the room visually wide relative to the tank). Let's set:
- TANK interior width 300 (x 330..630), height from 150 to 536 (depth 386, water 331).
- ROOM width from 16 to 1184 with canvas W=1200 → floor width 1168.
Then max escaped = 300*331 = 99,300 / 1168 = 85px deep → surface at 475. Still above the crack at 500? The crack at y=500 with holeBottom 523: puddle surface at 475 is above the hole top (477) → backflow possible. Marginal. If the crack is high (y=250), escaped = 300*(45) = 13.5k → 11px deep puddle. Fine.
To be safe: make the canvas 1280 wide with a wide room, and tank narrow-ish (300px). And add: when the puddle surface is above the hole, reverse flow (flow into the room → actually water flows back INTO the tank). Implementing backflow adds complexity. Alternative: raise the room floor's capacity by making the puddle spread thin — with the aspect: canvas 1280x720. Let me use W=1280, H=720, floor at y=660, tank interior x 380..720 (340 wide), tank top 190, tank bottom 640 (stand 640..660 = 20 tall). Water level 250 → depth 390. Hmm.
Max escaped = 340*390 = 132,600 over 1250 free width = 106px.
The ratio is what it is: puddle depth ≈ tankWidth*drainHeight/roomWidth ≈ (340/1250)*390 = 0.27*390 = 106. To keep the puddle below, say, 60px with a full drain of 390: need roomWidth ≥ 340*390/60 = 2210. Not possible on screen.
So: accept a deep puddle and just don't simulate backflow (or handle it: if puddleSurface > holeTop, reverse: flow into the tank... that would be a cool touch but complicated and could look weird). I'll add a simple guard: the puddle can't rise above the crack level — excess volume is "lost to the room" (drains away). Implement: when a cell's height would put its surface above (crackY - HOLE_H/2), clamp cell height so surface ≥ holeTop... Actually simplest: clamp the puddle surface to be at most 20px above the room floor? No.
Better idea: Let a thin layer of water flow off-frame at both room edges (the room extends beyond view). Implement: cells at the extreme ends (i<3 or i>N-4) drain out of the puddle (volume lost, representing water spreading into the next room / under the door). Then the puddle stays shallow-ish, and we can display "water escaped the room: X L". This is a practical, visually clean solution. Combined with a wide room, the depth stays moderate.
Also we can make the tank shallower: initial depth ~ 300px, tank width 300 → volume 90k. With edge drainage, the equilibrium is dynamic.
I'll do: room floor from 16 to W-16; the puddle drains at both ends (a "drain" marker drawn as a floor grate at the left edge? Actually a doorway at the right edge). Let's add a doorway at the far right (x > 1230) where water flows out of the room, and note "water drains away at the room edges". Simplest: any cell within 2 cells of the ends loses volume proportional to its height (like a spillway). I'll draw subtle arrows at both edges "out of frame". Fine, and note the drained liters.
Actually — cleaner narrative: keep the constraint as required ("spreading puddle constrained by the floor and room walls"), and just let it rise. It IS constrained by walls. Max depth ~100px means the surface at y=560 which is above the crack at y=500 in the deep case → the tank would be standing in water up to its mid-height. That's visually OK for a flooded-room sim (and the tank is behind the water; drawn with translucent water over the glass). And it stops rising because the level in the tank reaches the hole. But then the equilibrium: water in the tank above the puddle surface stays. Fine.
Backflow: if the puddle surface is above the hole bottom, physically water flows out until... hmm, actually if the puddle is above the hole, the tank gains water back. Since the flow direction is determined by head = wetCentroid - level vs the puddle... Let me implement the simple physical check:
driveHead = (level above hole bottom stuff) → outflow; if puddleSurface > hole bottom level and puddleSurface above the tank level... The tank level will always be ≥ the puddle surface in this scenario? Let's see: the tank drains until level = holeBottom. The puddle surface ends up at 560-100=460 in px terms (i.e., higher up). The tank level = holeBottom ≈ 523. So the tank level (523) is BELOW the puddle surface (460)? "Below" in height = the tank's water surface at y=523 is lower than the puddle surface at y=460. So yes, the puddle is higher than the remaining tank water → water would flow back into the tank through the hole until equalized. That's real physics but visually would mean the tank refills. Confusing/undesirable for the demo (the required behavior is "outflow ... stops when water no longer reaches it").
So the pragmatic choice: cap the puddle depth via edge drainage so the puddle surface stays below the crack. With a wide room + edge drainage, the puddle surface stays around 30-60px deep, i.e., y=600..630 in a floor-660 layout — below the crack (max crack y = 500ish) → no backflow. Good. And it's still "constrained by the floor and room walls" (it piles against the walls, and excess spills away at the room thresholds). I'll implement drainage when a cell's height exceeds a "spill capacity": total depth beyond ~55px spills out of the room (drawn as water running down the wall / out of frame). Hmm, that's a bit arbitrary. Alternative honest approach: "the room's floor extends beyond the view; water that reaches the edges drains away" — that's fine and I'll label it.
Decision: W=1280, floor y=660. Room x 16..1264. Tank interior x 380..760 (380 wide), top 170, bottom 640, stand 640..660. Initial level 230 (depth to bottom 410; water depth 410... a tall tank). Hmm PPM 400 → 410px = 1.03m tall tank of water. That's a big aquarium but fine (600L+).
Let me reduce: level 250, tank bottom 640 → water 390px.
Max escaped (crack at 580, holeBottom 603): 380*353=134k px². Free floor width 1248 minus stand 400 = ~850 (if the stand blocks) or 1248 (if it doesn't). Puddle depth ≈ 108. Spill threshold: keep it simple — allow the puddle to rise up to a max depth cap of 60px per cell, and the excess "seeps away" (drained volume counted in HUD). With 60px cap: 1248*60 = 75k capacity, less than 134k → a lot drains. Hmm, then the puddle looks full and the rest is off-screen. Acceptable: with the wide room, 60px deep over the whole floor looks like a genuinely flooded floor (60px = 15cm) — that's plenty dramatic.
But cells near the jet impact would spike high before spreading; the cap handles that.
OK: cap = 58px, overflow → mark as "overflow/drain" and add to a drained counter; visually show a thin film running down the wall.
Actually the simplest, most consistent: no cap, no drain, and no stand blocking → puddle depth = 134k/1248 = 107px max, surface at y=553 with the tank bottom at 640 and the crack max at 580 (holeBottom 603). Puddle surface (553) vs hole bottom (603): the surface is ABOVE the hole (smaller y = higher). Backflow again when the crack is set low. If the crack range is limited to y ≤ 520 then holeBottom ≤ 543 vs the puddle surface at ~553 → the puddle surface is above holeBottom by 10px → marginal backflow. Ugh.
Fine: I'll implement backflow! It's ~5 lines: if puddleSurfaceAt(holeX) > wetCentroid... Actually flow direction = sign of (headOut - headIn):
- driving head from inside: H_in = wetCentroid - level (if the level above holeTop... )
- back head from outside: H_out = max(0, puddleSurfaceNear - holeBottom)??
Let's do a clean version:
outsideLevel = floorY - pudHeightAt(holeX region).
If inside (tank level) surface is above the outside level → outflow with head = wetCentroid - level, as before, but reduced by the outside head: netHead = (wetCentroid - level) - max(0, wetCentroid - outsideLevel). Hmm, Torricelli between two levels: v = sqrt(2g*Δh) where Δh = level_in_above_out - head.
Simplification: Δh_eff = (puddleSurfaceY vs tankLevelY): if tankLevel < puddleSurface in y-terms (tank surface higher up = smaller y) → outflow. Use head = max(0, (holeBottom - min? )).
OK let's define:
```
levelY = tank surface y (smaller = higher)
surfOutY = outside puddle surface y near the hole
wetTop = max(levelY, holeTop) // top of the wet opening from inside
if (levelY < surfOutY) { // tank surface higher than puddle surface => outflow
wetH = clamp(holeBottom - wetTop, 0)
head = clamp(( (wetTop+holeBottom)/2 ... )
}
```
Getting complicated. Since flow only happens when the tank level is above holeBottom (from inside) and inflow happens when the outside surface is above holeBottom... wait no: inflow happens when the puddle surface is above holeTop-ish.
Practical: allow both, with:
- outflow: if levelY < holeBottom: Q_out = Cd*HOLEwet*(sqrt(2g*headOut) - sqrt(2g*headBack)) ...
I'll implement it as a single signed flow through the opening driven by the difference of the two surfaces, with the wetted height determined by the higher side:
```
dY = surfOutY - levelY // positive if the puddle surface is lower (in y) i.e. puddle is shallower/higher-up?
```
Ugh signs. Let y decrease upward. The tank surface is at levelY; the puddle surface at surfOutY. Water flows from the smaller y to the bigger y. If levelY < surfOutY → the tank water is higher → outflow. Head = difference? Not exactly, the classic orifice between two reservoirs: v = sqrt(2 g Δh) where Δh = difference of surface levels, and the flow stops when the surfaces equalize. But our outflow should also stop when the tank surface drops to the hole bottom — with the orifice model, Δh → 0 at equalization; but the hole is at a fixed height, so once the tank surface drops below the hole, the model breaks (no more siphon). So: v = sqrt(2 g Δh_eff) with Δh_eff based on the wetted opening.
I'll do this:
```
const out = (levelY < surfOutY) && (levelY < holeBottom);
if(out){ wetTop = max(levelY, holeTop); wetH = holeBottom - wetTop; centroid=(wetTop+holeBottom)/2; head = max(0, centroid - levelY); ... v=sqrt(2 g head); Q = Cd*wetH*v }
else if(!out && surfOutY < holeBottom){ // puddle above the opening's top → inflow
wetTopOut = max(surfOutY, holeTop); head = max(0, (holeBottom + wetTopOut)/2 - surfOutY); v=sqrt(2g*head); Q = Cd*(holeBottom-wetTopOut)*v; // into the tank
}
else Q=0
```
Then level += |Q|/tankWidth (for inflow) or -= (outflow), and the puddle loses/gains correspondingly. And for inflow, the puddle loses volume at the hole's x. This handles flooding back and adds realism ("outflow weakens... and stops when water no longer reaches it" — still satisfied: with a low puddle, the outflow stops when the level hits holeBottom).
Given the edge-drain-free design with a deep puddle, backflow would refill the tank up to the puddle level and then oscillate/level out. That might look odd (tank refilling). But with a wide room, the puddle level falls as it spreads... Let's just include the backflow logic; it's physically consistent and self-limiting (it equalizes). It also means the HUD could show net flow. I think it's a nice touch but risks confusing the required behavior check "waterline falls continuously". Backflow only starts after the tank level drops below the hole, at which point the puddle may be higher than the hole. In that case, the tank refills up to the puddle surface, which then drains the puddle... and eventually equalizes at (totalV)/(roomWidth) — leaving both at a common level. Hmm, that ends the visual: the tank partially refilled. A reviewer might find that confusing but it's correct given the 2D slice.
To keep it simple and match the spec's expectations, I'll PREVENT deep puddles by capping max cell height at 46px (a thin sheet, "water runs to the room's drains/floor edges") and count the excess as "drained away" in the HUD. Then surfOutY = floor-46 = 614 vs holeBottom max 603 (for a crack at 580) — the puddle surface (614) is BELOW the hole bottom (603)? In y terms 614 > 603 means the puddle surface is lower than the hole bottom → no backflow.
So the cap 46px guarantees no backflow if the crack's lowest position gives holeBottom ≤ floor-46-2 = 612. With the crack range max = 560 and HOLE_H=44 → holeBottom=582 ≤ 612. Good. Then I don't need backflow logic at all. I'll skip backflow (with a comment) and use the cap + "drains away at the room edges" visual (water thin-film running to the edges). Actually with the cap of 46 and the free floor width ~1250, capacity = 57.5k px², but the tank can deliver 134k → half "drains". The puddle would be full-depth everywhere = a flooded floor 46px deep across the room, and the HUD notes "excess water runs off to the floor drain at the room edges". Add a drain grate visual at the bottom-right/left edges where water disappears. That's clean, understandable, and avoids weird physics.
Alternatively shrink the tank so its volume < capacity: tank interior width 250 × water 390 = 97k > 57k. Raise the cap to 70 → 87k capacity. Hmm.
Decision: cap 70px per cell, room width 1248 → capacity 87k. Tank interior width 300 (x 400..700), water depth from level 250 to holeBottom. If the crack is at 560 → drain height 332 → volume 99.6k, slightly over capacity → a bit drains. Acceptable. If the crack is at the default 430: drain = 180*300=54k → depth 43px average. Good.
Let me set: canvas 1280x720. Room: left 20, right 1260, floor 660. Tank: interior left 400, right 700, top 170, bottom 640, wall thickness 9, stand from 640 to 660 spanning 388..712 (blocking flow under). Hmm, the stand blocking means the free floor width = 1248-324 = 924, capacity 924*70=64k. Then a full drain of 99k would flood past the stand top (20 tall) and spill over — with the cap it "drains". Let's make the stand low (14px: from 646 to 660) and tank bottom at 646.
Simplify: tank bottom interior = 644; the stand/base block from 644 to 660 spanning [391, 709] (i.e., the tank walls' footprint), height 16 → blocks the puddle; water overtops it if depth > 16 at that spot (allowed: cells in the stand region have a floor 16 higher). I'll implement per-cell floor offsets! floorY[i] = 660 normally, 644 in the stand region. Then the height cap applies per column relative to its own floor, and water can flow over the stand when the surface exceeds its top. That's elegant: cells with a raised floor. Flux between cells uses the surface elevation (floorY - h) difference rather than h difference.
Implement: surface_i = floorY_i - h_i. Flux from i to i+1 = D * hAvg * (surface_i - surface_{i+1})... sign: water flows from higher surface (smaller y) to lower.
```
dh = (floorY[i]-h[i]) - (floorY[i+1]-h[i+1]); // negative if i's surface is higher (smaller y)
q = -D*havg*dh*dt → hmm sign
```
Let surf[i] = floorY[i] - h[i]. Water flows from smaller surf to bigger surf (downward in y). flux q (from i to i+1, positive) = D * havg * (surf[i] - surf[i+1]) * dt / cellW. If surf[i] < surf[i+1] (i is higher up... wait smaller y = higher elevation). If surf[i] is smaller in y, the water at i is HIGHER (up), so it flows right → q positive requires (surf[i+1] - surf[i]) > 0? No: if surf[i]=600 and surf[i+1]=620, then i's surface is higher up (y=600) and i+1's is at 620 (lower). Water flows from i to i+1 → q>0. (surf[i+1]-surf[i]) = 20 > 0 → q = D*havg*(surf[i+1]-surf[i])*dt. So q positive ⟺ moving right.
Then h[i] -= q/cellW... careful with units: h is a depth in px; flux in px²/s; per-cell transfer = q*dt/cellW... let's just use: transfer = clamp(D * havg * (surf[i+1]-surf[i]) * dt, 0, ...) with D tuned so the numbers come out as px of depth. Let me define directly: `t = k*(surf[i+1]-surf[i]) * min(havg,1)` in px per step, k≈0.25 at dt=1/60 → plus a stability limit t ≤ 0.25*|dh|. Fine, tune empirically.
Good, per-cell floor offsets also let me place a "drain grate" region at the right edge where floorY drops (water flows into it and is removed). Nice: cells in the last 3 columns have floorY = 660+ and act as a drain: h there is zeroed with volume counted as "drained". Also maybe a drain at the left edge. Let's put a floor drain grate at x≈1230 (right edge) with a visual, so the excess is explained. But if the puddle only drains from the far right, the jet lands mid-right and the puddle fills mostly on the right. Fine.
Simpler: cap per column at maxH=70, and any excess is spilled to the neighbor/removed with a "drained" counter. And draw a thin wet sheen near the base of the walls to suggest it spreading out of frame. I'll go with: overflow removal at the two end cells + a per-cell cap of 80 with excess moved to neighbors, and if a neighbor is at the cap, keep passing. Actually just implement the cap + diffusion which naturally distributes; the last 4 cells are drains (h forced to 0, volume counted). Good.
Let me finalize the layout numbers:
```
W=1280, H=720
ROOM = {left:20, right:1260, floor:660}
TANK = {left:400, right:700, top:160, bottom:644, wall:9}
INIT_LEVEL = 232 // water depth = 412 px ≈ 1.03 m
CRACK_MIN = INIT_LEVEL + 26 = 258
CRACK_MAX = 560
HOLE_H = 46
```
Default crack y = 430.
Max drain: from 232 to 583 = 351 × 300 = 105k px². Puddle free width ≈ 1240 → 85px avg if uniform... exceeds cap 80 → some drains. OK.
Stand: base from 644 to 660, x 391..709 → cells above the stand have floorY = 644.
Jet reach: hole at x=709 (outer face), v ≈ Cd*sqrt(2*3924*head). With head=198 (default: centroid 430, level 232): v_theoretical = sqrt(2*3924*198)=1245 px/s; ×0.62 = 772 px/s. Fall from 430 to 660 = 230px → t=sqrt(2*230/3924)=0.34s → x travel = 263 → lands at x≈970. Room right wall at 1260 → good, lands in the open floor.
With a low crack (560): head = 560-232=328 → v=1010 → fall 100px → t=0.226 → x travel 228 → lands at 950. Good.
With a high crack (258): head=26 → v=283; fall from 258 to 660=402 → t=0.45 → travel 128 → lands x=840. Good. Nice: higher crack → lands closer... wait travel 128 vs 263 — hmm, higher cracks have less horizontal speed but more fall time. Lands around 840 vs 970. Fine, all within the room.
Tank top 160, so the jet from the hole flies out horizontally. Objects falling out of the hole land on the floor to the right of the stand.
Now write it.
Fish: 5 of them, r≈11-14. Tank interior 300 wide × 412 deep — roomy.
Also add "swim against current" visualization: maybe show small arrows? Skip.
Let me write the code.
```js
// ---------- helpers ----------
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
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}}
```
Main loop with fixed dt=1/120 for physics, 2 substeps per frame; droplets/shards/puddle at dt.
Order per substep: flow update, then objects.
I'll write the code and then test.
Let me write it now for real.
For the water current field function:
```js
function currentAt(x,y){
if(!S.breached){
return {vx: Math.cos(y*0.02 + S.time*0.7)*6, vy: 0};
}
if(x < TANK.right + TANK.wall){
const hx = TANK.right + TANK.wall*0.5, hy = S.wetCentroid;
const dx = hx-x, dy=hy-y, d=Math.hypot(dx,dy)||1;
const sp = Math.min(S.Q*CUR/(d+24), 1600); // CUR ~ 0.4/(2π)
return {vx: dx/d*sp, vy: dy/d*sp*0.8};
}
// outside: in the jet zone, flow follows the falling water (small)
return {vx:0, vy:0};
}
```
Careful: inside the tank, flow toward the hole; the hole is on the wall so the horizontal component dominates near it. But objects inside must converge to the hole's vertical position. That works with the sink model.
Also: the vertical component near the wall could push objects through the wall above/below the hole — they'll collide with the panel and slide along it.
Now the water-at query:
```js
function waterAt(x,y){
if(x>TANK.left && x<TANK.right && y>S.level){
return {level:S.level, depth:y-S.level, inTank:true};
}
if(y > ROOM.floor - pudH(x) - ...) ...
}
```
For the puddle: pudH(x) → depth at x; surface y = floorAt(x) - h.
Buoyancy for bodies: submerged fraction f from the circle vs the two possible surfaces; take the deepest applicable surface. Since a body can't be both in the tank and in the puddle (the tank wall separates), determine by x: inside tank x-range → use the tank level (if inside the tank and not escaped); outside → use the puddle.
Actually a body inside the tank near the hole at the moment of passing out... treat x < TANK.right as inside, else outside.
Body "inTank" state should be tracked as a flag (to avoid ambiguity at the boundary). Let's compute per step: inside = (x > TANK.left && x < TANK.right). If a body is in the wall band (TANK.right..TANK.right+wall) and outside the hole band → the collision resolution pushes it in or out.
OK, writing the code. Enough design.
One more: "Provide Reset to restore the same initial scene" — implemented via init() with a fixed seed.
Also add a Pause/Play and a speed control? Add a Pause button — cheap and useful. And a "Slow-mo" toggle for inspection? Keep: Breach, Reset, Pause. Plus a checkbox "show physics vectors" (velocity/current arrows) that helps demonstrate the modeling. Nice extra for verification. Let's include a toggle for "flow vectors + labels" — modest cost. I'll add it (draws the current field arrows, jet centerline, and object velocity arrows). Good for the verification requirement.
Let me write.
Also keyboard: Space = breach? Might interfere with button focus. Use "B" for breach, "R" for reset, arrow keys for the crack.
Let me write the file now.
Code sketch for drawing order:
1. background wall gradient, floor line
2. puddle (behind the tank? The puddle is in front of the tank base... The tank stands on the floor; the puddle water surrounds it. Draw the puddle, then the tank stand over it? Realistically the water is in front of and behind. Draw the puddle with alpha over the stand base but under the tank. Simpler: draw floor, then puddle, then the stand and tank. But then water in front of the stand's right face would be hidden by the tank. It's fine: draw the puddle after the stand so the water appears in front. I'll draw the tank stand + tank body first, then... no, the tank must be drawn with the water inside it, and the puddle outside. Order: background, floor, stand, tank glass back, tank water, in-tank objects, jet drops, puddle, shards, out-of-tank objects, glass front highlights, crack UI, HUD. Hmm, objects outside should be drawn above the puddle partially (fish floating in the puddle) — draw the puddle first then the external objects. But the puddle surface should overlay the submerged part of the fish — with alpha it's OK.
Final order: bg → floor → stand → tank water (with the interior back) → plants → in-tank bodies (rocks, fish, toy, leaves) → jet droplets → puddle → external bodies (fish/toy/pebbles outside, shards) → tank glass panes (translucent, drawn over the water) → front wall of the tank rim → crack/hole UI → HUD.
Wait: shards should be drawn over the glass area; fine.
Let me now write the code.
I'll write the whole file in one go. Then test with the browser tool: check console errors, take a screenshot, simulate the drag and breach via JS-evaluated pointer events (the browser tool only supports click/drag by coordinates — good, I can use `drag` from the crack to a new y, then `click` the Breach button, screenshot after a moment, etc.). The browser tool actions: reload, inspect, screenshot, motion, click, drag, key, type, scroll. `motion` presumably for animation frames sampling. Let's use it.
To sample values (level, etc.), I can read the HUD text in the screenshot or use `inspect` (DOM). Maybe add a hidden debug element with JSON state so I can inspect programmatically. I'll add `<pre id="dbg" aria-hidden>` updated each frame with key numbers — actually useful as a telemetry panel in the UI: show level, Q, head, escaped %, puddle max, fish out. I'll make it a visible telemetry readout (looks technical/nice) so `inspect` can read it.
Let's write.
Time to write the code — being careful about correctness.
```js
const cvs=document.getElementById('c'), ctx=cvs.getContext('2d');
const DPR=Math.min(2,window.devicePixelRatio||1);
cvs.width=W*DPR; cvs.height=H*DPR; ctx.scale(DPR,DPR);
```
CSS: canvas width:100%; aspect-ratio: 1280/720.
Pointer mapping:
```js
function ptr(e){const r=cvs.getBoundingClientRect(); return {x:(e.clientX-r.left)*(W/r.width), y:(e.clientY-r.top)*(H/r.height)};}
```
Init:
```js
function init(){
rnd=mulberry32(20240607);
S = { time:0, breached:false, paused:false, shake:0, level:INIT_LEVEL, crackY:430, dragging:false,
Q:0, wetCentroid:0, jetV:0, escapedVol:0, drainedVol:0, fishOut:0, objectsOut:0 }
... build bodies, plants, shards=[], drops=[], splash=[], puddle
}
```
Bodies list: I'll keep one array `objs` with `type` in {rock, pebble, toy, fish, leaf, plantFree}. Update in a unified loop with type-specific forces.
Fish extra fields: dir, phase, strength, escaped, flopT, alive.
Let me write the update for each type:
```js
function integrateBody(o,dt){
const w = waterInfo(o.x,o.y,o); // {surfaceY or null, cur:{vx,vy}}
const sub = o.surface ? o.r/(o.r+ (o.r-...)) ...
```
Compute submerged fraction: f = clamp((surfY==null?0: ((o.y+o.r) - surfY)/(2*o.r)),0,1) where surfY is the water surface y at the body.
Forces:
- accel_y += G*(1 - f*(RHO_W/o.rho)) [gravity minus buoyancy/mass]
- drag: blend toward the water velocity if submerged (or partial):
```
if(f>0){ const vx0=o.vx-w.vx, vy0=o.vy-w.vy, sp=Math.hypot(vx0,vy0);
const beta = 2*CD*max(w.rho_w,RHO_W*0.5+puddle?)*f*sp/(o.rho*Math.PI*o.r);
const k=1-Math.exp(-beta*dt); o.vx-=vx0*k; o.vy-=vy0*k; }
```
Hmm mass per unit depth = rho*π r²; the projected area = 2r (times depth 1); drag coefficient includes shape. Use beta = (CD * rho_w * 2r * sp)/(rho_b*π r²) = 2*CD*sp/(π*r*rhoRel).
- fish thrust, etc.
- floor contact & friction
- wall collisions
Let's write and test. Also angular: shards & rocks rotate with av damped; rocks' av gets set from tangential sliding. Simple: av *= exp(-3*dt); av += (-vx*0.002) for rolling. Roll constraint: for a rolling circle on the floor, av = -vx/r. Let's set that when grounded for rocks/pebbles → they visibly roll toward the hole.
OK writing now — final.
Also: the fish angle should follow swimming direction (not rotation from rolling).
Also "the toy floats" — when the level drops, the toy descends with it; near the bottom it may get out through the hole if aligned. Good.
I need to make sure the hole band collision uses the body's r vs the band: allow passage only if (y - r >= holeTop + 1 && y + r <= holeBottom - 1)? That's restrictive (r=16 → needs a 32px clearance in a 46 band → ok, y must be within [holeTop+16+1, holeBottom-16-1] = a 12px window). Too tight → the toy would slam into the panel edge. Use: the collision with the panel applies unless the body's center is within [holeTop - r*0.4, holeBottom + r*0.4]... Actually the panel above/below are separate AABBs and the circle-vs-AABB resolution handles the geometry properly: a body near the hole edge gets pushed away by whichever rect it overlaps. That's the correct behavior. So just use rect colliders for the two remaining panels. Bodies that fit through the gap pass; bodies too big get caught at the opening (perfect demonstration!).
Rocks with r=22 → diameter 44 ≈ hole 46 → it can just barely squeeze; make one big rock r=28 (56 > 46) → it gets stuck in the opening partially blocking it (cool, but then flow around it... doesn't reduce flow in my model). Actually that would look neat: a rock jammed in the breach. Hmm, but it might block the fish/toy too. Let's keep: 2 decorative rocks (r 20, 26) at the bottom that can't reach the hole because the hole bottom (min holeBottom = 258+23=281) is far above the bottom (644) — they stay. Pebbles (r 5-7) get washed out. The toy (r 16) floats at the surface, and as the level drops below the hole it slides down the wall into the opening and out. Fish (r 12) get swept out.
Wait: if the crack is high (y=258) and the toy floats at the surface around level 232-258, the toy will be right at the hole and get sucked out immediately. Great.
For the "objects too big" case, add a big rock r=24 and a low crack: the rock sits at the bottom, and if the hole is near the bottom (crack 560, holeBottom 583, tank bottom 644 — 60px above the bottom), a rock of r=24 resting on the bottom has its top at 644-48=596 > holeBottom → it can't pass; the current pins it against the wall below the opening. Fine, shows "rocks sink and stay".
OK, write it!
For the plant drift: after uprooting, the plant's base becomes a body of sorts. I'll implement the loose plant's base node as draggable by the water flow: pinned=false, and the base node also collides with the tank floor. Then the whole chain advects with the flow. Good — verlet nodes have mass ~1 with high drag → they follow the water velocity. The chain will be carried to the hole and out. It might jam in the opening — fine and fun.
I'll write the code with per-node water sampling.
Let me write it out.
Also: draw order — I'll write the render code carefully.
Let me produce the file.
Final consideration on `wetCentroid` used for the sink position; before breach, set it to crackY.
Write now.
For the HUD telemetry element:
```html
<div id="tele"> ... </div>
```
Update every ~100ms via textContent.
Let me write the code.
Also: screen shake on breach.
I'll write the code.
Also the "initial" state: rocks spawn slightly above their rest position so the user sees them sink at load (nice demo of buoyancy/density). Also the toy starts submerged? "a floating toy" — start it held under and released → it rises and pops up. Let's spawn the toy at y=level+120 and let it rise to the surface: demonstrates floatation. Similarly, the leaves start mid-water and rise. Good opening demo.
Let's write.
Puddle sampling function pudAt(x) → height; floorYAt(x) → the per-cell floor.
I'll write the code and test.
Also note: bodies outside the tank should collide with the puddle? No, the puddle is water; bodies sink to the floor.
Write.
Also to keep the file from being too long, I'll be economical.
Let me go.I'll build this in one file, then run and test it in the browser.
{"command":"cd /tmp/job-l9pof4 && ls -la && node -v 2>/dev/null; echo \"---\"; pwd"}{"command":"ls /app/pipeline/node_modules/@earendil-works/pi-coding-agent/docs 2>/dev/null | head -30"}total 12 drwx------ 3 node node 4096 Oct 14 02:29 . drwxrwxrwt 6 root root 4096 Oct 14 02:29 .. drwxr-xr-x 3 node node 4096 Oct 14 02:29 .home v24.18.0 --- /tmp/job-l9pof4
cli-integration.md cli.md compaction.md configuration.md containerization.md custom-provider.md docs.json environment-variables.md extensions.md how-pi-works.md images index.md json.md keybindings.md llama-cpp.md message-types.md models.md packages.md prompt-templates.md providers.md quickstart.md rpc-commands.md rpc-extension-ui.md rpc.md sdk.md security.md session-format.md sessions.md settings.md shell-aliases.md
thinking
Let me write index.html now. I'll write a complete, careful implementation.
Now writing the simulation.
/tmp/job-l9pof4/index.html (49116 bytes)Successfully wrote to /tmp/job-l9pof4/index.html
10 minutes have elapsed. At most 5 minutes remain. Finalize the output as soon as possible: save index.html now, fix only essential issues, and finish.
thinking
I have a bug: `mulberry32(0xAQUA_SAFE || 1337)` — invalid JS syntax (`0xAQUA_SAFE` is not valid hex... actually `0xAQUA_SAFE` would be parsed as a numeric literal error). Let me fix immediately and run the browser to check for errors. Also `TANK_WIDTH` is used in updateFlow before its const declaration — const hoisting means TDZ error at runtime when updateFlow runs (but it's declared at module level before any call... updateFlow is called from physics which runs after the const is initialized, since the script fully evaluates first). Fine. Also `btnPause` is referenced in doReset before declaration — doReset is called after script eval, and btnPause is a const declared later in the script but at top level; by the time it's called it exists. Fine. Let me fix the seed and test quickly.
{"path":"/tmp/job-l9pof4/index.html","edits":[{"oldText":" rnd = mulberry32(0xAQUA_SAFE || 1337);","newText":" rnd = mulberry32(1337);"}]}Successfully replaced 1 block(s) in /tmp/job-l9pof4/index.html.
thinking
Also there's a `free` variable name in stepPuddle — `const free = ...` — "free" isn't reserved in JS non-strict... it's fine. Also `const r2` etc. Let me quickly syntax-check with node and then run in browser.
{"command":"cd /tmp/job-l9pof4 && node -e \"\nconst fs=require('fs');const h=fs.readFileSync('index.html','utf8');\nconst m=h.match(/<scriptSYNTAX ERR missing ) after argument list
thinking
Need to find the syntax error. Let me use node to get a better error by writing to a file and checking.
{"command":"cd /tmp/job-l9pof4 && node -e \"\nconst fs=require('fs');const h=fs.readFileSync('index.html','utf8');\nconst m=h.match(/<script/tmp/chk.js:909 const volTotal = TANK_WIDTH*(S.holeBot?Math.min(S.breached?S.holeBot:TANK.bottom:TANK.bottom)-INIT_LEVEL:0); ^^^^^^ SyntaxError: missing ) after argument list at wrapSafe (node:internal/modules/cjs/loader:1804:18) at checkSyntax (node:internal/main/check_syntax:76:3) Node.js v24.18.0
thinking
That line has unbalanced ternary. Just simplify it — remove unused variable.
{"edits":[{"newText":" const volOut = S.escaped;","oldText":" const volOut = S.escaped;\n const volTotal = TANK_WIDTH*(S.holeBot?Math.minSuccessfully replaced 2 block(s) in /tmp/job-l9pof4/index.html.
thinking
Wait, the drain detection: floorOf for drain columns = ROOM.floor+240 → floorOf[i]-ROOM.floor+240 = 480 > 200 → true. For normal columns: 0+240=240 > 200 → also true! Bug — that would zero out all puddle. Fix: check floorOf[i] > ROOM.floor + 100.
{"command":"cd /tmp/job-l9pof4 && node -e \"\nconst fs=require('fs');const h=fs.readFileSync('index.html','utf8');\nconst m=h.match(/<scriptSYNTAX OK
thinking
The file is saved and syntax-clean. I have very limited time — run the browser once to check console and initial render, and fix the puddle drain bug I identified plus verify quickly.
{"command":"cd /tmp/job-l9pof4 && python3 - <<'EOF'\nimport re\np='index.html'; s=open(p).read()\nold = \"\"\" const drainedCol = floorOf/bin/bash: line 1: python3: command not found Command exited with code 127
thinking
The file is saved; I only need one edit.
{"path":"/tmp/job-l9pof4/index.html","edits":[{"oldText":" const drainedCol = floorOf[i]-ROOM.floor+240; // marker for drain columnsSuccessfully replaced 1 block(s) in /tmp/job-l9pof4/index.html.
thinking
Now run the app in the browser and do the required verification quickly: load, drag crack, breach, check the jet curves / waterline falls / objects respond, then reset.
{"action":"reload"}Frame 1, +0ms
[Browser image supplied to model]
{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.","error: Cannot set properties of undefined (setting 'length')","error: Cannot read properties of undefined (reading 'length')"],"dom":"AQUARIUM BREACH LAB\nBREACH\nRESET\nPAUSE\nFLOW VECTORS\nRUNNING\nHOW TO USE\n1. Drag the cracked star-point on the right glass panel up/down (it stays below the waterline).\n2. Press Breach (B) — the panel blows out, a curved jet forms, the waterline falls.\n3. Reset (R) restores the identical intact scene. ↑↓ nudge the crack, Space pause.\nTELEMETRY","images":"attached"}
thinking
Error: "Cannot set properties of undefined (setting 'length')" — in init(), `drops.length=0` but drops is declared `let S, objs, plants, shards, drops, splash, rnd;` — arrays are never created! I declared them with `let` but never initialized as arrays. Fix: initialize them.
{"edits":[{"newText":"let S, objs, plants, shards=[], drops=[], splash=[], rnd;","oldText":"let S, objs, plants, shards, drops, splash, rnd;Successfully replaced 1 block(s) in /tmp/job-l9pof4/index.html.
thinking
The file is saved. I'm nearly out of time (5 min was the cap and I've used most of it). I'll do one quick reload+screenshot to confirm it renders, then finish and report honestly.
{"action":"reload"}Frame 1, +0ms
[Browser image supplied to model]
{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"AQUARIUM BREACH LAB\nBREACH\nRESET\nPAUSE\nFLOW VECTORS\nRUNNING\nHOW TO USE\n1. Drag the cracked star-point on the right glass panel up/down (it stays below the waterline).\n2. Press Breach (B) — the panel blows out, a curved jet forms, the waterline falls.\n3. Reset (R) restores the identical intact scene. ↑↓ nudge the crack, Space pause.\nTELEMETRY\nwaterline y: 232.0 px\ndepth over breach: 0.0 cm\njet speed: 0.00 m/s\noutflow Q: 0.0k px²/s\nwater level: 100 % of initial\nescaped volume: 0k px²\npuddle depth: 0.0 px (0 cm)\npuddle span: —\nfish washed out: 0 / 5\nobjects out: 0\nshards: 0\ndroplets: 0\nScene intact. Drag the crack, then press Breach.","images":"attached"}
thinking
Renders. Bugs visible: one plant is outside the tank (bottom-left, at x~390 which is < TANK.left=400 → plant at 424? no, the stray plant at left is plant at x=424? Actually the one drawn outside at bottom-left near x=390,y=600 is the uprooted/loose plant drifting left already? It says "drifting" label on the middle plant inside. The left one outside the tank is a plant whose nodes fell outside — likely plant 1 (x=424) collapsed and slid left out of the tank? Its base is pinned at x=424 bottom... Hmm the stray one is at x~390 y~590-640, outside the glass. Possibly plant at 424 with nodes drooping and no wall collision for plant nodes → they pass through the glass. Need to clamp plant nodes inside the tank. Quick fix: in stepPlants, clamp node x to [TANK.left+2, TANK.right-2] and y <= TANK.bottom. Also the loose plant shows "drifting" already at t=0.2 — it shouldn't be uprooted before breach. Because `pl.rooted=!loose` → loose plant starts unrooted. Change: loose plants start rooted and uproot under flow. Fix: rooted:true for all, loose flag enables uprooting. Also the waterline dashes are fine. Let me apply both fixes and do a quick breach test.
{"edits":[{"newText":" return {x, h, seg, L, nodes:n, rest, loose, rooted:true, uprootT:0, seed:rnd()*TAU()};","oldText":" return {x, h, sSuccessfully replaced 2 block(s) in /tmp/job-l9pof4/index.html.
thinking
Now test the breach quickly: drag crack to ~480 then press Breach, wait, screenshot, check telemetry (level falling, jet, fish out, puddle). The crack handle is at canvas coords (TANK.right+wall/2+16, crackY) = (700+4.5+16=720.5, 432) in logical 1280x720 space. The browser tool viewport is 1200x800 — screenshots are in viewport coords, and the canvas is scaled to container width (max 1320-32=1288 → but viewport 1200 → container ~1168 wide, canvas height ~657). Mapping: canvas displayed at x from 16 to ~1184 (1168 wide) → scale 1168/1280=0.9125. So logical (720,432) → screen (16+720*0.9125, headerHeight + 432*0.9125). The stage top appears around y=48 in the screenshot... In the previous screenshot (1200x800 scaled?), the canvas top edge was ~48 and the crack handle appeared at ~(680,452). So drag from (680,452) to (680,500) to move the crack down. Then press Breach button at (759,30) and take a screenshot after ~2s. The tool doesn't have a wait; motion action gives frames. I'll just do: drag, click, then screenshot (each action takes time anyway).
{"action":"drag","x":680,"y":452,"toX":680,"toY":505}Frame 1, +0ms
[Browser image supplied to model]
{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing."],"dom":"AQUARIUM BREACH LAB\nBREACH\nRESET\nPAUSE\nFLOW VECTORS\nRUNNING\nHOW TO USE\n1. Drag the cracked star-point on the right glass panel up/down (it stays below the waterline).\n2. Press Breach (B) — the panel blows out, a curved jet forms, the waterline falls.\n3. Reset (R) restores the identical intact scene. ↑↓ nudge the crack, Space pause.\nTELEMETRY\nwaterline y: 232.0 px\ndepth over breach: 0.0 cm\njet speed: 0.00 m/s\noutflow Q: 0.0k px²/s\nwater level: 100 % of initial\nescaped volume: 0k px²\npuddle depth: 0.0 px (0 cm)\npuddle span: —\nfish washed out: 0 / 5\nobjects out: 0\nshards: 0\ndroplets: 0\nScene intact. Drag the crack, then press Breach.","images":"attached"}