oneshotlm
Compare all models on this prompt →

Aquarium breach · step-3.7-flash

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

⚠ no runnable output

This one-shot run produced no runnable artifact.

agent produced no index.html

The transcript below shows what the model did — read it to see why the one shot fell short.

transcript · 2 messages
user

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

assistant
thinking
The user wants a single self-contained HTML file with a simulated glass aquarium that develops a crack and bursts, with detailed physics for water, fish, rocks, plants, a toy, glass fragments, and more. This is a complex interactive 2D simulation. I need to build this entirely in one HTML file with inline CSS and JS, no external resources.

Key requirements:
- Large glass aquarium with side panel
- Visible crack that user can drag vertically
- Burst triggered somehow (maybe a button or after dragging to a certain point, or auto after some time)
- Water escaping through opening, flow strength based on water depth, decreasing as tank drains
- Curved water jet affected by gravity
- Spreading puddle that collides with room boundaries
- Fish, rocks, plants, floating toy reacting according to density, buoyancy, drag, current
- Objects transition correctly from underwater motion to airborne motion and then to floor collisions
- Fish attempting to swim against current before being swept through breach
- Glass fragments with angular velocity, collisions, water resistance
- Visible waterline that lowers continuously rather than disappearing all at once

This is a complex physics simulation. I'll use Canvas API for rendering and implement a simple particle-based physics system with rigid bodies for larger objects.

Design:
- Use a 2D canvas
- Aquarium as a rectangular tank with glass walls (left, right, bottom, top maybe open)
- Water is simulated as particles (SPH-like or just many particles with simple forces)
- The side panel (left or right wall) has a crack that can be dragged vertically. When user triggers failure, the crack widens and water jets out.
- Objects: fish (ellipses with fins), rocks (irregular polygons or circles), plants (sine wave curves), floating toy (circle with pattern)
- Physics: custom simple physics engine handling particles, rigid bodies, collisions, buoyancy, drag, gravity

Given complexity, I'll need to carefully design:
1. Particle system for water: each particle has position, velocity. Water particles are subject to gravity, pressure (simplified), drag, collisions with tank walls, objects, and floor. They exit through the breach and form puddle on floor.
2. Rigid bodies: fish, rocks, toy. Each has position, velocity, angle, angular velocity, shape. They interact with water particles (buoyancy, drag) and collisions with walls, floor, each other.
3. Glass fragments: generated when tank breaks. They are small rigid bodies with angular velocity, affected by gravity, water resistance, collisions.

To keep performance reasonable, limit particle count (maybe 500-1000). Use spatial hashing or just brute force O(n^2) with small n.

Simplifications:
- Water particles: treat as small circles. Apply gravity. Apply simplified pressure force to maintain density: if too close to neighbors, push apart. Also apply drag from objects and walls.
- Buoyancy: for each rigid body, count water particles inside/under it, apply upward force proportional to displaced water.
- Current: inside tank, water flows toward breach. Apply force on objects based on local water velocity.

Aquarium geometry:
- Tank: rectangle, say width 500, height 400, positioned at center. Water initially fills tank to near top.
- Left wall is glass panel with crack. Initially crack is thin (maybe 2px). User can drag crack handle vertically. Then click "Break" or after some time/click, crack expands and water jets out.

Alternative: crack is a line segment on the left wall that user can drag vertically to set position, and then a separate "Burst" button. The prompt says "Let the user drag the crack vertically before triggering the failure." So yes, crack position is draggable, then a button or action triggers the burst.

Implementation plan:
- HTML: canvas element, some controls (button "Burst!" maybe).
- CSS: styling, dark room background.
- JS:
  - Setup canvas, context.
  - Define tank dimensions (in canvas coords).
  - Water particles array. Initialize them inside tank, maybe in a grid.
  - Define objects: fish, rocks, plants, toy. Each as an object with shape, position, velocity, angle, etc.
  - Crack position (y coordinate on left wall).
  - Mouse interaction: drag crack handle.
  - Burst function: expand crack to full height? Or a hole? The prompt says "burst", so likely the whole side panel breaks. But we need waterline lowering continuously. So perhaps when burst occurs, a large hole opens in the side panel at the crack location, water escapes from that hole. The waterline lowers as water leaves. The side panel may shatter into fragments.

I think a more dramatic effect: The crack is a line on the left wall. When the user triggers failure, the crack propagates, glass shatters, and a hole opens at the crack position, possibly expanding. Water jets out from that hole. As water drains, waterline lowers.

Objects: initially inside tank. As water drains, some may be left on the bottom. Fish swim until swept out or water runs out.

Let's define structures:

```javascript
// Constants
const TANK_X = 150; // left wall x
const TANK_Y = 100; // top wall y (tank top)
const TANK_W = 500;
const TANK_H = 400;
const WATER_INITIAL_LEVEL = TANK_H - 20; // water height from tank bottom
```

Actually, water particles initially fill tank up to a certain level.

Particle properties:
```javascript
class Particle {
  constructor(x, y) {
    this.x = x; this.y = y;
    this.vx = 0; this.vy = 0;
    this.r = 3; // radius
  }
}
```

Physics loop (per frame):
1. Apply gravity to particles and rigid bodies.
2. Particle-particle repulsion (to simulate incompressibility). Check neighbors within some radius. If dist < 2r, apply force. Use simple position-based dynamics or velocity correction.
3. Particle-wall collisions: tank walls (left, right, bottom, top? top is open? aquarium usually open top, but water surface is at top). So top boundary is water surface, not a wall. Particles should not go above water surface? Actually, they can splash. We'll have a water surface that can deform. Simpler: treat top as open, particles can escape if they have enough upward velocity, but we want to keep them mostly inside. Actually, the aquarium is open at top, so water surface is free. We need to keep particles roughly within tank until they exit through breach. We can just let them bounce off top boundary with some damping, or not bounce and just cap. But water spilling over top? The burst is on the side panel, not top. So top is free surface. Particles should not leave through top. We'll add a soft constraint: if particle y < TANK_Y + 10, apply upward force? Actually, surface tension is complex. Simpler: treat water as having a free surface but particles are kept inside by gravity and pressure. If a particle tries to go above a certain level, we push it down with a force proportional to depth beyond surface. But since water is incompressible, we can rely on particle repulsion to maintain density. With many particles, they will form a surface naturally if we enforce density. But we need a way to prevent them from escaping top. Maybe add a "lid" at TANK_Y that is a soft wall with some bounce. Or simply define the water volume and spawn particles only inside; if a particle goes outside, remove it or push back. Since water escapes through breach, particles can leave through that hole.

Simpler approach: Use a height field for water surface? That would give smooth waterline but not individual particles. But the requirement says water escaping with particles and puddle. Particles are better for interaction.

Performance: 800 particles, O(n^2) repulsion = 640k checks per frame, might be okay in JS with optimizations (limit neighbor radius). We can use a grid-based spatial hash to speed up.

Let's outline the physics steps:
- Update velocities: gravity (0.2 for water, 0.5 for objects).
- Apply forces:
  - Water particles: gravity, pressure from neighbors (density constraint), drag with objects, drag with walls.
  - Rigid bodies: gravity, buoyancy (sum of water particle forces), drag from water (based on relative velocity), collisions with walls/floor/other objects.
- Integrate positions.
- Resolve collisions:
  - Particle vs tank walls (left, right, bottom, top optional). Elastic or inelastic with restitution.
  - Particle vs breach hole: if particle crosses breach plane (left wall) and is within crack segment, allow passage (remove or move outside).
  - Rigid body vs tank walls and breach.
  - Rigid body vs floor (outside tank).
  - Rigid body vs puddle? We'll treat puddle as just water particles on floor.
  - Rigid body vs rigid body: simple circle-circle or polygon-polygon collisions.

Water draining:
- Water particles that exit through breach are removed or marked as "escaped". They fall with gravity outside tank and form puddle on floor.
- Puddle particles: once they land on floor (y >= floor level), they slide/spread? Simplification: once they hit floor, they stop or slide with friction. We can just let them accumulate and spread laterally. To make spreading puddle, we can allow them to move horizontally on floor with some friction until they hit walls. But collisions with many particles on floor may be heavy. Alternative: after landing, they become part of a "puddle" that is a collection of static particles that can be pushed by new incoming water but not move much. Simpler: just let them settle with high damping.

Waterline: We can track the highest water particle y-position within tank. That gives waterline. As particles escape, waterline lowers.

Fish behavior: Fish attempt to swim against current. We can model fish as having a "swim force" that opposes the local water velocity near them. When water rushes toward breach (if breach is on left wall, water flows left), fish on the right side will be pushed left. Fish can swim right (against current) but if current is too strong, they get swept. We can implement fish as having a target velocity (e.g., staying in place) and a thrust force to achieve it, limited by max speed.

Plants: anchored to bottom. They sway with water current. Represent as series of points connected by constraints (inverse kinematics). Or simpler: as a curve drawn with sine wave that is displaced by current.

Toy: floating object with low density, high buoyancy. It will bob on surface and be carried by current.

Rocks: high density, sink to bottom. They may roll.

Glass fragments: generated on burst. They are small polygons (triangles or quads). They have initial velocity outward, angular velocity. They interact with water and floor.

Given the complexity, I need to structure code clearly.

I'll use a class-based approach.

Classes:
- Particle (water, puddle)
- Body (rigid body: fish, rock, toy, glass fragment)
- Plant (special: anchored chain of segments)
- Aquarium (manages tank, crack, water level)

Simulation loop:
1. Input handling (drag crack, button)
2. Physics update
3. Rendering

Physics details:
- Use Verlet integration or simple Euler. Euler with small timestep.
- For water particles, use simplified Smoothed Particle Hydrodynamics (SPH):
  - Compute density for each particle by summing kernel contributions from neighbors within radius h.
  - Compute pressure force: -gradient of pressure.
  - Pressure from equation of state: p = k * (density - restDensity).
  - Viscosity: apply force to smooth velocities.
But full SPH is heavy. Alternative: "Particle-in-Cell" or just repulsion and gravity.

Simpler water model: 
- Each particle has position, velocity.
- Forces: gravity (down), repulsion from neighbors (if dist < 2r, apply force proportional to overlap), drag with objects and walls, pressure force approximating fluid.
- To maintain water volume, repulsion alone may cause clumping but with enough particles it can look okay. We can also add a global force pushing particles toward the center of the tank? No.

Maybe use a grid-based fluid simulation (Eulerian) for water, but then particles for objects? That would be more complex.

Given the constraints and that it's a single file demo, I'll go with particles and simple neighbor repulsion. I'll optimize with spatial grid.

Let's estimate: if water particles have radius 3, and tank volume is ~500x400=200k, each particle occupies ~28 area (pi*r^2 ~ 28), so ~7000 particles to fill. That's too many. So increase particle radius to 5 (area ~78) -> ~2500 particles. Still a lot. Maybe use 4 radius -> area 50 -> 4000. O(n^2) with 4000 is 16M, too slow. Need spatial hash and maybe fewer particles: radius 6 -> area 113 -> ~1800. Still heavy. Let's aim for 1000 particles of radius 5. We'll use spatial grid cell size = 2*radius = 10. Then neighbor search is fast.

But 1000 particles may not look very dense. We can increase visual radius for rendering but keep physics radius small. Or just accept moderate density.

Alternatively, use a hybrid approach: render water as a polygon mesh that deforms, but particles for interaction? That's more complex.

I'll go with particles: maybe 800 particles, radius 4. Use spatial hash.

Implement spatial grid:
- cells of size 20.
- For each particle, compute cell key (floor(x/size), floor(y/size)).
- For each cell, check particles in same and neighboring cells.
- Limit neighbor interactions.

Now, objects:
- Fish: ellipse shape. We'll approximate collision as circle for simplicity, radius ~15.
- Rock: irregular polygon, but collision as circle radius ~12.
- Toy: circle radius ~10.
- Glass fragments: small triangles, collision as circles radius ~5.

Plants: anchored at bottom, composed of segments. Each segment is a small circle for collision? Or just visual. For physics, they can exert force on water and be pushed by water. We can treat each plant as a chain of points. The base is fixed. Segments are connected by distance constraints. Water particles interact with segments via repulsion.

To keep performance, limit plant segments (maybe 10 per plant).

Now, the crack:
- Left wall at x = TANK_X. There is a crack line from (TANK_X, crackY - 20) to (TANK_X, crackY + 20) initially (thin).
- User can drag a handle at (TANK_X, crackY) vertically within tank bounds.
- When "Burst" is triggered:
  - The crack expands into a hole: a rectangle or ellipse centered at (TANK_X, crackY) with width ~30 and height ~60 (or larger).
  - Glass fragments are generated: small triangles around the hole, with initial velocities outward and angular velocities.
  - The hole becomes an opening: water particles and objects can exit.
  - The left wall piece (the glass panel) may become detached and fall? But the prompt says "side panel develops a visible crack and then bursts." So the side panel breaks. We can represent it as a large rigid body that detaches and falls. But that's complex. Instead, we can just have the hole and generate fragments. The tank structure remains but with a hole. Waterline lowers as water escapes.

We also need the crack to be visible before burst. So draw a line on the left wall. Dragging changes its position.

Trigger: button "Break!".

Now, water flow through breach:
- Water particles that cross the breach plane (x < TANK_X) and y within hole range are removed from tank and become "escaped" particles. They fall under gravity and form puddle.
- Flow strength depends on water depth: pressure at hole = rho * g * depth. So the force on particles exiting should be proportional to depth. We can achieve this by having a sink force that pulls particles toward hole when they are near it, with strength proportional to water depth. Or simply, when particle is within a certain distance of hole, apply a strong horizontal force pushing it out. The magnitude can be based on current water depth: depth = waterlineY - TANK_Y (water height). Force = k * depth * (hole opening area factor).

But we need the water jet to be curved due to gravity. So particles exiting get initial velocity mostly horizontal (outward) plus some vertical spread, then gravity acts.

Simpler: when particle exits hole, we give it a velocity: vx = -escapeSpeed (to left), vy = some random small value + current y velocity. escapeSpeed based on water depth. Then they follow normal gravity.

Puddle: escaped particles accumulate on floor. The floor is at some y coordinate (e.g., canvas height - 50). When they hit floor, they stop or slide. We can give them high damping on floor. Also, puddle should spread: particles on floor can move horizontally if pushed by new particles, but with friction. We can treat floor particles as having zero vertical velocity and a small horizontal velocity that decays.

Collisions with room boundaries: floor, left wall of room, right wall. Puddle spreads and collides with these.

Objects behavior:
- Fish: inside water, they have a "swim" force. We'll define fish as having a desired position (maybe stay near center). They produce thrust to move toward desired position, limited by max speed. They also experience drag from water (proportional to relative velocity). When water current is strong (near breach), the drag overcomes swim force, so they get swept. Once they exit through breach, they become airborne (no longer in water). Then they are subject to gravity, and eventually fall to floor. On floor, they collide and maybe slide. This requires tracking whether object is in water or not. We can determine by checking if its center is within tank water volume and not escaped. But fish could be partially out? We'll just use the object's position: if it's inside tank and water depth > its y, it's underwater. Once it crosses breach, it's out.

Better: check if object is within tank bounds (x between TANK_X and TANK_X+TANK_W, y between TANK_Y and TANK_Y+TANK_H) and water level at that x is above object y. If it exits, it's airborne.

Rocks: sink. They experience buoyancy upward force if in water. If buoyancy < weight, they sink. They settle on bottom. They may be pushed by water current.

Toy: floats. Buoyancy > weight. It stays at surface.

Plants: anchored at bottom. They are stationary but segments can be displaced by water. We can implement as a series of points from anchor (x0, y0) upward. Each point connected to next by distance constraint. Water particles exert repulsion on points, causing sway. When water drains, less force, they straighten.

Glass fragments: generated at burst. They are small rigid bodies. They have initial velocities. They collide with water, tank walls, floor, each other. They experience water drag.

Now, code structure:

HTML:
```html
<!DOCTYPE html>
<html>
<head>
  <style>
    body { margin:0; background:#111; display:flex; justify-content:center; align-items:center; height:100vh; }
    canvas { background:#222; border-radius:4px; }
    #controls { position:absolute; top:10px; left:10px; color:white; font-family:sans-serif; }
    button { padding:8px 16px; }
  </style>
</head>
<body>
  <div id="controls">
    <button id="burstBtn">Break!</button>
    <span id="info"></span>
  </div>
  <canvas id="canvas" width="900" height="700"></canvas>
  <script>
    // All JS here
  </script>
</body>
</html>
```

JavaScript:

Define constants, classes, simulation loop.

Given the massive complexity, I need to write a substantial amount of code. I'll focus on making it work reasonably well and meet requirements.

Let's plan the simulation in detail.

Tank:
- Left wall: x = TANK_X = 200
- Right wall: x = TANK_X + TANK_W = 700
- Bottom: y = TANK_BOTTOM = 500
- Top: y = TANK_TOP = 100
- Water initial level: y = TANK_BOTTOM - 50 (water fills to near top). So water surface at y = 450.

Room boundaries:
- Floor: y = 650
- Left wall: x = 0 (or maybe room left wall at x=0, tank left wall at 200)
- Right wall: x = 900 (canvas width)

Crack:
- Initial crackY = TANK_TOP + 150 (middle of left wall)
- Crack handle: drawn as a small circle at (TANK_X - 10, crackY) that can be dragged vertically.
- Visible crack: line on wall from (TANK_X, crackY-20) to (TANK_X, crackY+20). Maybe also some jagged lines.

Burst:
- When button clicked:
  - Create hole: holeTop = crackY - 30, holeBottom = crackY + 30.
  - Set breach = {x: TANK_X, y1: holeTop, y2: holeBottom, open: true}
  - Generate glass fragments: ~20 small triangles with positions around the hole, velocities outward (mostly left and up/down), angular velocities.
  - Maybe also create a large glass panel body that falls? Could be a rectangle that detaches and falls. That would be cool. Let's add a large glass panel (the side panel) as a rigid body that is initially attached, then detaches and falls. But that might be complex to draw and collide. Instead, fragments are enough.

- After burst, water particles near breach get sucked out.

Water particles:
- Initialize: fill tank area with particles in a grid with some jitter. Spawn only within water volume: x from TANK_X+10 to TANK_X+TANK_W-10, y from TANK_BOTTOM-10 to waterSurfaceY+10.
- Each particle: {x, y, vx, vy, r}
- Escaped particles: once they leave tank through breach, they become "puddle" particles. They have extra damping on floor.

Physics update (each frame, dt ~ 0.016):
- Apply gravity to all moving objects: vy += g * dt. g = 0.2 for particles, 0.5 for bodies? Let's use 0.15 for water, 0.4 for objects.
- Water particle forces:
  - For each particle, find neighbors within h = 15 (using spatial grid).
  - Compute density: sum(1 - dist/h)^2 or simpler: if dist < 2r, apply repulsion: force = k_repulse * (2r - dist) along normal.
  - Also apply viscosity: average velocity with neighbors.
  - Apply drag from objects: for each body, if particle is within influence radius, apply force proportional to (bodyVelocity - particleVelocity) * dragCoeff.
  - Apply wall repulsion: if near tank walls, push inward.
  - If breach is open and particle is near breach, apply suction force: if particle x near TANK_X and y in [holeTop, holeBottom], apply force leftward, strength = k_suction * waterDepth. Also, if particle actually crosses the wall (x < TANK_X), mark as escaped.
- Rigid body update:
  - Apply gravity.
  - Compute buoyancy: for each body, find water particles within its bounding box. For each such particle, compute submerged volume fraction (approximate by checking if particle is below water surface and inside body shape). Apply upward force = submergedVolume * density_water * g.
  - Compute drag from water: sum of forces from nearby water particles (same as above but on body).
  - Fish swim force: if fish is in water, apply force toward a target (e.g., random point or stay in center). The force magnitude limited. Also, fish can "swim against current": if water velocity at fish location is strong leftward, fish applies rightward force.
  - Integrate: vx += ax*dt, vy += ay*dt; x += vx*dt; y += vy*dt; angle += angVel*dt; angVel *= damping.
  - Collisions:
    - With tank walls: bounce with restitution.
    - With floor (y >= FLOOR_Y): bounce, set y = FLOOR_Y - radius, apply friction.
    - With breach: if body crosses breach, allow passage (mark as escaped).
    - With other bodies: simple circle-circle collision.

- Glass fragments: similar to bodies but smaller. They also have collision with water particles? Yes, water drag. They also collide with each other and walls.

Plants:
- Array of points: base at (x, y_anchor). Each point connected to next by distance constraint.
- In each step, for each point (except base), apply gravity, apply force from previous point to maintain length, apply repulsion from water particles (push away), apply drag. Then satisfy distance constraint iteratively.
- Draw as smooth curve through points.

Rendering:
- Draw room background.
- Draw tank glass walls (semi-transparent white/blue).
- Draw water particles: blue circles, maybe with slight transparency.
- Draw objects: fish (ellipse with tail), rocks (gray circles), toy (colored circle with pattern), glass fragments (triangles, white/light blue).
- Draw crack: line on wall.
- Draw crack handle: small rectangle.
- Draw water surface: not needed if particles show it.
- Draw waterline: maybe a line at average top of water particles? Or just the particle surface shows it.

To make waterline visible and lowering continuously, we can draw a line at the highest particle y within tank (or average). But particles at surface will be scattered. We can draw a smooth curve by connecting top particles. Or just rely on particle distribution.

Given time, I'll implement a simplified but visually plausible simulation.

Let's start coding.

First, setup canvas and basic variables.

```javascript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;

// Tank
const TANK = { x: 200, y: 100, w: 500, h: 400 };
const TANK_LEFT = TANK.x;
const TANK_RIGHT = TANK.x + TANK.w;
const TANK_TOP = TANK.y;
const TANK_BOT = TANK.y + TANK.h;
const WATER_SURFACE_INIT = TANK_BOT - 40; // water height 40px from bottom

// Room
const FLOOR_Y = 640;
const ROOM_LEFT = 0;
const ROOM_RIGHT = W;

// Crack
let crackY = TANK_TOP + 150;
let draggingCrack = false;
let breach = null; // {y1, y2} when open

// Buttons
document.getElementById('burstBtn').addEventListener('click', () => {
  if (!breach) {
    breach = { y1: crackY - 30, y2: crackY + 30 };
    spawnFragments();
  }
});
```

Initialize water particles:

```javascript
const particles = [];
const P_RADIUS = 4;
const P_REST_DENSITY = 1; // not used directly
const GRAVITY = 0.15;
const PARTICLE_GRAVITY = 0.2;

function initParticles() {
  particles.length = 0;
  const cols = Math.floor(TANK.w / (P_RADIUS*2));
  const rows = Math.floor((WATER_SURFACE_INIT - TANK_TOP) / (P_RADIUS*2));
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      let x = TANK_LEFT + P_RADIUS + i * P_RADIUS*2 + (Math.random()-0.5);
      let y = TANK_BOT - P_RADIUS - j * P_RADIUS*2 + (Math.random()-0.5);
      // ensure within tank
      if (x > TANK_LEFT+5 && x < TANK_RIGHT-5 && y > TANK_TOP+5 && y < WATER_SURFACE_INIT) {
        particles.push({x, y, vx:0, vy:0, r:P_RADIUS, escaped:false});
      }
    }
  }
}
```

Spatial grid:

```javascript
const GRID_SIZE = 20;
let grid = {};

function hashKey(cx, cy) { return cx + ',' + cy; }

function buildGrid() {
  grid = {};
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    const cx = Math.floor(p.x / GRID_SIZE);
    const cy = Math.floor(p.y / GRID_SIZE);
    const key = hashKey(cx, cy);
    if (!grid[key]) grid[key] = [];
    grid[key].push(i);
  }
}

function getNeighbors(p) {
  const cx = Math.floor(p.x / GRID_SIZE);
  const cy = Math.floor(p.y / GRID_SIZE);
  const result = [];
  for (let dx = -1; dx <= 1; dx++) {
    for (let dy = -1; dy <= 1; dy++) {
      const key = hashKey(cx+dx, cy+dy);
      if (grid[key]) {
        for (let idx of grid[key]) {
          result.push(idx);
        }
      }
    }
  }
  return result;
}
```

Physics update for particles:

```javascript
function updateParticles(dt) {
  buildGrid();
  const waterDepth = TANK_BOT - (breach ? getWaterLevel() : WATER_SURFACE_INIT);
  
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    if (p.escaped) {
      // escaped particle: gravity, floor collision
      p.vy += GRAVITY;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      // floor
      if (p.y > FLOOR_Y - p.r) {
        p.y = FLOOR_Y - p.r;
        p.vy = 0;
        p.vx *= 0.9; // friction
      }
      // room walls
      if (p.x < ROOM_LEFT + p.r) { p.x = ROOM_LEFT + p.r; p.vx *= -0.5; }
      if (p.x > ROOM_RIGHT - p.r) { p.x = ROOM_RIGHT - p.r; p.vx *= -0.5; }
      continue;
    }
    
    // Gravity
    p.vy += PARTICLE_GRAVITY;
    
    // Neighbor repulsion
    const neighbors = getNeighbors(p);
    for (let j of neighbors) {
      if (j <= i) continue;
      const q = particles[j];
      if (q.escaped) continue;
      const dx = q.x - p.x;
      const dy = q.y - p.y;
      const distSq = dx*dx + dy*dy;
      const minDist = p.r + q.r;
      if (distSq < minDist*minDist && distSq > 0.001) {
        const dist = Math.sqrt(distSq);
        const overlap = minDist - dist;
        const nx = dx / dist;
        const ny = dy / dist;
        const force = 0.5 * overlap; // stiffness
        p.vx -= nx * force;
        p.vy -= ny * force;
        q.vx += nx * force;
        q.vy += ny * force;
      }
    }
    
    // Wall collisions
    if (p.x < TANK_LEFT + p.r) { p.x = TANK_LEFT + p.r; p.vx *= -0.3; }
    if (p.x > TANK_RIGHT - p.r) { p.x = TANK_RIGHT - p.r; p.vx *= -0.3; }
    if (p.y < TANK_TOP + p.r) { p.y = TANK_TOP + p.r; p.vy *= -0.3; }
    if (p.y > TANK_BOT - p.r) { p.y = TANK_BOT - p.r; p.vy *= -0.3; }
    
    // Breach suction
    if (breach) {
      if (p.x < TANK_LEFT + 10 && p.x > TANK_LEFT - 5 && p.y > breach.y1 && p.y < breach.y2) {
        // Suck out with force based on water depth
        const depth = TANK_BOT - getWaterLevel();
        const suction = 0.5 + depth * 0.01;
        p.vx -= suction;
        // Also push vertically to spread jet
        const dy = p.y - (breach.y1 + breach.y2)/2;
        p.vy += dy * 0.01;
      }
      // If particle has crossed left wall (x < TANK_LEFT)
      if (p.x < TANK_LEFT - p.r) {
        p.escaped = true;
        p.vx = -2; // initial jet velocity
        p.vy = (p.y - (breach.y1+breach.y2)/2) * 0.05; // slight vertical component
      }
    }
    
    // Integrate
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    
    // Drag (simple velocity damping)
    p.vx *= 0.999;
    p.vy *= 0.999;
  }
}
```

Water level calculation:
```javascript
function getWaterLevel() {
  let minY = TANK_BOT;
  for (let p of particles) {
    if (!p.escaped && p.y < minY) minY = p.y;
  }
  return minY;
}
```

This gives the top of water particles. It will lower as particles escape.

Rigid bodies:

Define Body class:

```javascript
class Body {
  constructor(x, y, radius, mass, type) {
    this.x = x; this.y = y;
    this.vx = 0; this.vy = 0;
    this.radius = radius; // collision radius
    this.mass = mass;
    this.invMass = 1/mass;
    this.angle = 0;
    this.angVel = 0;
    this.type = type; // 'fish', 'rock', 'toy', 'fragment'
    this.escaped = false;
    this.color = 'white';
    // For fish
    this.targetX = x;
    this.targetY = y;
    this.maxSpeed = 2;
    this.swimForce = 0.1;
  }
  
  update(dt) {
    this.vy += GRAVITY * 1.5; // gravity stronger for bodies
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.angle += this.angVel * dt;
    this.angVel *= 0.98; // damping
    
    // Floor collision
    if (this.y > FLOOR_Y - this.radius) {
      this.y = FLOOR_Y - this.radius;
      this.vy *= -0.3;
      this.vx *= 0.9;
      this.angVel *= 0.9;
    }
    // Room walls
    if (this.x < ROOM_LEFT + this.radius) { this.x = ROOM_LEFT + this.radius; this.vx *= -0.5; }
    if (this.x > ROOM_RIGHT - this.radius) { this.x = ROOM_RIGHT - this.radius; this.vx *= -0.5; }
    // Tank walls (if not escaped)
    if (!this.escaped) {
      if (this.x < TANK_LEFT + this.radius) { this.x = TANK_LEFT + this.radius; this.vx *= -0.3; }
      if (this.x > TANK_RIGHT - this.radius) { this.x = TANK_RIGHT - this.radius; this.vx *= -0.3; }
      if (this.y < TANK_TOP + this.radius) { this.y = TANK_TOP + this.radius; this.vy *= -0.3; }
      if (this.y > TANK_BOT - this.radius) { this.y = TANK_BOT - this.radius; this.vy *= -0.3; }
      
      // Breach exit
      if (breach && this.x < TANK_LEFT + this.radius && this.y > breach.y1 && this.y < breach.y2) {
        this.escaped = true;
        this.vx = -3; // jet out
        this.vy = (this.y - (breach.y1+breach.y2)/2) * 0.05;
      }
    }
  }
  
  applyForce(fx, fy) {
    this.vx += fx * this.invMass;
    this.vy += fy * this.invMass;
  }
}
```

But we need to compute buoyancy and drag from water. We can do that in the main loop by iterating over water particles near the body.

```javascript
function applyFluidForces(body) {
  // Buoyancy: count submerged particles
  let submergedCount = 0;
  let dragForceX = 0, dragForceY = 0;
  // check particles within body.radius + some margin
  const cx = Math.floor(body.x / GRID_SIZE);
  const cy = Math.floor(body.y / GRID_SIZE);
  const range = Math.ceil((body.radius + 10) / GRID_SIZE);
  for (let dx = -range; dx <= range; dx++) {
    for (let dy = -range; dy <= range; dy++) {
      const key = hashKey(cx+dx, cy+dy);
      if (!grid[key]) continue;
      for (let idx of grid[key]) {
        const p = particles[idx];
        if (p.escaped) continue;
        const ddx = p.x - body.x;
        const ddy = p.y - body.y;
        const distSq = ddx*ddx + ddy*ddy;
        const maxDist = body.radius + 5;
        if (distSq < maxDist*maxDist) {
          submergedCount++;
          // Buoyancy: if particle is below body center (submerged), apply upward force proportional to displaced volume
          // Simple: for each particle, apply upward force if body is above particle? Actually buoyancy acts on body.
          // We'll just apply a constant upward force if body is in water.
          // Better: compute fraction of body submerged based on water level.
          // Simpler: if body.y + body.radius > waterLevel, then body is partially submerged.
          // We'll handle buoyancy after water level check.
          // Drag: force opposite to relative velocity
          const relVx = p.vx - body.vx;
          const relVy = p.vy - body.vy;
          dragForceX += relVx * 0.01;
          dragForceY += relVy * 0.01;
        }
      }
    }
  }
  
  // Buoyancy based on submerged volume: if body is in water, apply upward force = weight of displaced water
  const waterLevel = getWaterLevel();
  if (body.y + body.radius > waterLevel && body.y - body.radius < TANK_BOT) {
    // approximate submerged fraction
    let submergedFraction = 0;
    if (body.y - body.radius >= waterLevel) {
      submergedFraction = 0;
    } else if (body.y + body.radius <= TANK_BOT) {
      submergedFraction = 1;
    } else {
      const totalH = body.radius * 2;
      const submergedH = TANK_BOT - Math.max(waterLevel, body.y - body.radius);
      submergedFraction = submergedH / totalH;
    }
    const buoyancyForce = 0.5 * submergedFraction; // scale
    body.vy -= buoyancyForce;
  }
  
  // Apply drag
  body.vx += dragForceX * body.invMass;
  body.vy += dragForceY * body.invMass;
}
```

Fish-specific behavior:

```javascript
function updateFish(fish, dt) {
  if (fish.escaped) return;
  // Check if in water
  const waterLevel = getWaterLevel();
  if (fish.y > waterLevel && fish.y < TANK_BOT) {
    // Swim: try to stay near initial position or random
    const dx = fish.targetX - fish.x;
    const dy = fish.targetY - fish.y;
    const dist = Math.sqrt(dx*dx+dy*dy);
    if (dist > 5) {
      const fx = (dx/dist) * fish.swimForce;
      const fy = (dy/dist) * fish.swimForce;
      fish.applyForce(fx, fy);
    }
    // Also apply a small random force to simulate movement
    fish.vx += (Math.random()-0.5) * 0.05;
    fish.vy += (Math.random()-0.5) * 0.05;
  }
}
```

But fish should also be affected by water current (drag already does that). The swim force allows them to resist.

Rocks: no special behavior, just buoyancy and drag.

Toy: maybe float, so buoyancy is high. We can set mass low.

Plants:
```javascript
class Plant {
  constructor(x, y, height) {
    this.baseX = x;
    this.baseY = y;
    this.segments = [];
    const numSeg = 8;
    for (let i = 0; i <= numSeg; i++) {
      this.segments.push({x: x, y: y - i * (height/numSeg)});
    }
  }
  
  update(dt) {
    // Base fixed
    // For each segment (except base), apply gravity, water repulsion, and constraint to previous
    for (let i = 1; i < this.segments.length; i++) {
      const seg = this.segments[i];
      const prev = this.segments[i-1];
      // gravity
      seg.vy = (seg.vy || 0) + 0.05;
      // water repulsion: find nearby particles
      const cx = Math.floor(seg.x / GRID_SIZE);
      const cy = Math.floor(seg.y / GRID_SIZE);
      let fx = 0, fy = 0;
      for (let dx = -1; dx <= 1; dx++) {
        for (let dy = -1; dy <= 1; dy++) {
          const key = hashKey(cx+dx, cy+dy);
          if (!grid[key]) continue;
          for (let idx of grid[key]) {
            const p = particles[idx];
            if (p.escaped) continue;
            const ddx = seg.x - p.x;
            const ddy = seg.y - p.y;
            const distSq = ddx*ddx + ddy*ddy;
            const minDist = 3; // segment radius
            if (distSq < minDist*minDist && distSq > 0.001) {
              const dist = Math.sqrt(distSq);
              const overlap = minDist - dist;
              fx += (ddx/dist) * overlap * 0.1;
              fy += (ddy/dist) * overlap * 0.1;
            }
          }
        }
      }
      seg.vx = (seg.vx || 0) + fx;
      seg.vy = (seg.vy || 0) + fy;
      // Integrate
      seg.x += seg.vx * dt;
      seg.y += seg.vy * dt;
      // Damping
      seg.vx *= 0.9;
      seg.vy *= 0.9;
      // Constraint distance to prev
      const cdx = seg.x - prev.x;
      const cdy = seg.y - prev.y;
      const cdist = Math.sqrt(cdx*cdx + cdy*cdy);
      const segLen = 10; // segment length
      if (cdist > segLen) {
        const diff = (cdist - segLen) / cdist;
        const offsetX = cdx * diff * 0.5;
        const offsetY = cdy * diff * 0.5;
        seg.x -= offsetX;
        seg.y -= offsetY;
        // prev also moves? But base fixed, so only move seg
      }
    }
  }
  
  draw(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.segments[0].x, this.segments[0].y);
    for (let i = 1; i < this.segments.length; i++) {
      ctx.lineTo(this.segments[i].x, this.segments[i].y);
    }
    ctx.strokeStyle = 'green';
    ctx.lineWidth = 3;
    ctx.stroke();
  }
}
```

Glass fragments:

```javascript
let fragments = [];
function spawnFragments() {
  const count = 30;
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 2 + Math.random() * 4;
    const size = 5 + Math.random() * 8;
    fragments.push({
      x: TANK_LEFT,
      y: breach.y1 + Math.random() * (breach.y2 - breach.y1),
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed - 2,
      size: size,
      angVel: (Math.random()-0.5) * 0.2,
      rotation: Math.random() * Math.PI * 2,
      color: 'rgba(200,230,255,0.8)'
    });
  }
}

function updateFragments(dt) {
  for (let f of fragments) {
    f.vy += GRAVITY;
    f.x += f.vx * dt;
    f.y += f.vy * dt;
    f.rotation += f.angVel;
    // water drag if in water
    if (f.y > getWaterLevel() && f.y < TANK_BOT && f.x > TANK_LEFT && f.x < TANK_RIGHT) {
      f.vx *= 0.95;
      f.vy *= 0.95;
    }
    // floor
    if (f.y > FLOOR_Y - f.size/2) {
      f.y = FLOOR_Y - f.size/2;
      f.vy *= -0.3;
      f.vx *= 0.8;
    }
    // room walls
    if (f.x < ROOM_LEFT + f.size/2) { f.x = ROOM_LEFT + f.size/2; f.vx *= -0.5; }
    if (f.x > ROOM_RIGHT - f.size/2) { f.x = ROOM_RIGHT - f.size/2; f.vx *= -0.5; }
  }
}

function drawFragments(ctx) {
  for (let f of fragments) {
    ctx.save();
    ctx.translate(f.x, f.y);
    ctx.rotate(f.rotation);
    ctx.fillStyle = f.color;
    ctx.beginPath();
    ctx.moveTo(0, -f.size/2);
    ctx.lineTo(f.size/2, f.size/2);
    ctx.lineTo(-f.size/2, f.size/2);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }
}
```

Create objects:

```javascript
const bodies = [];
function initBodies() {
  bodies.length = 0;
  // Fish
  bodies.push(new Body(TANK_LEFT + 200, TANK_BOT - 100, 12, 1, 'fish'));
  bodies.push(new Body(TANK_LEFT + 300, TANK_BOT - 150, 12, 1, 'fish'));
  bodies.push(new Body(TANK_LEFT + 400, TANK_BOT - 200, 12, 1, 'fish'));
  // Rocks
  bodies.push(new Body(TANK_LEFT + 250, TANK_BOT - 30, 10, 5, 'rock'));
  bodies.push(new Body(TANK_LEFT + 450, TANK_BOT - 40, 12, 6, 'rock'));
  bodies.push(new Body(TANK_LEFT + 350, TANK_BOT - 20, 9, 4, 'rock'));
  // Toy (floating)
  bodies.push(new Body(TANK_LEFT + 300, TANK_BOT - 200, 10, 0.2, 'toy'));
  // Glass fragments not here, they are separate
}
```

But Body class currently uses applyForce. We'll need to integrate that in the update loop.

In main loop:
```javascript
function update(dt) {
  updateParticles(dt);
  
  // Apply fluid forces to bodies
  for (let b of bodies) {
    applyFluidForces(b);
    if (b.type === 'fish') updateFish(b, dt);
    b.update(dt);
  }
  
  // Body-body collisions
  for (let i = 0; i < bodies.length; i++) {
    for (let j = i+1; j < bodies.length; j++) {
      resolveBodyCollision(bodies[i], bodies[j]);
    }
  }
  
  updatePlants(dt);
  updateFragments(dt);
}
```

Body collision resolution (circle-circle):

```javascript
function resolveBodyCollision(a, b) {
  if (a.escaped !== b.escaped) {
    // if one escaped and other in tank? they might still collide if escaped is near breach. We'll allow.
  }
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  const distSq = dx*dx + dy*dy;
  const minDist = a.radius + b.radius;
  if (distSq < minDist*minDist && distSq > 0.001) {
    const dist = Math.sqrt(distSq);
    const overlap = minDist - dist;
    const nx = dx / dist;
    const ny = dy / dist;
    // separate
    const totalMass = a.mass + b.mass;
    const aRatio = b.mass / totalMass;
    const bRatio = a.mass / totalMass;
    a.x -= nx * overlap * aRatio;
    a.y -= ny * overlap * aRatio;
    b.x += nx * overlap * bRatio;
    b.y += ny * overlap * bRatio;
    // impulse
    const relVx = b.vx - a.vx;
    const relVy = b.vy - a.vy;
    const relVn = relVx * nx + relVy * ny;
    if (relVn > 0) return; // moving apart
    const restitution = 0.3;
    const j = -(1 + restitution) * relVn / totalMass;
    const impulseX = j * nx;
    const impulseY = j * ny;
    a.vx -= impulseX * aRatio;
    a.vy -= impulseY * aRatio;
    b.vx += impulseX * bRatio;
    b.vy += impulseY * bRatio;
    // angular impulse (simplified)
    a.angVel += (Math.random()-0.5) * 0.1;
    b.angVel += (Math.random()-0.5) * 0.1;
  }
}
```

Fish rendering:

```javascript
function drawFish(ctx, fish) {
  ctx.save();
  ctx.translate(fish.x, fish.y);
  ctx.rotate(fish.angle);
  // body
  ctx.fillStyle = 'orange';
  ctx.beginPath();
  ctx.ellipse(0, 0, fish.radius*1.5, fish.radius, 0, 0, Math.PI*2);
  ctx.fill();
  // tail
  ctx.beginPath();
  ctx.moveTo(-fish.radius, 0);
  ctx.lineTo(-fish.radius*2.5, -fish.radius*0.8);
  ctx.lineTo(-fish.radius*2.5, fish.radius*0.8);
  ctx.closePath();
  ctx.fill();
  // eye
  ctx.fillStyle = 'white';
  ctx.beginPath();
  ctx.arc(fish.radius*0.6, -fish.radius*0.3, 3, 0, Math.PI*2);
  ctx.fill();
  ctx.restore();
}
```

Rock rendering: gray circle.

Toy rendering: colored circle with a star or something.

Plant rendering: green curve.

Now, handling crack dragging:

```javascript
canvas.addEventListener('mousedown', e => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;
  // check if near crack handle
  if (Math.abs(mx - (TANK_LEFT - 10)) < 10 && Math.abs(my - crackY) < 15) {
    draggingCrack = true;
  }
});

canvas.addEventListener('mousemove', e => {
  if (draggingCrack) {
    const rect = canvas.getBoundingClientRect();
    const my = e.clientY - rect.top;
    crackY = Math.max(TANK_TOP + 20, Math.min(TANK_BOT - 20, my));
  }
});

canvas.addEventListener('mouseup', () => draggingCrack = false);
```

Now, the main loop:

```javascript
let lastTime = performance.now();
function loop(now) {
  const dt = Math.min((now - lastTime) / 16, 3); // normalize to ~1 at 60fps
  lastTime = now;
  
  update(dt);
  
  // Render
  ctx.clearRect(0,0,W,H);
  
  // Room
  ctx.fillStyle = '#222';
  ctx.fillRect(0,0,W,H);
  
  // Floor
  ctx.fillStyle = '#444';
  ctx.fillRect(0, FLOOR_Y, W, H - FLOOR_Y);
  
  // Tank glass
  ctx.strokeStyle = 'rgba(255,255,255,0.6)';
  ctx.lineWidth = 4;
  ctx.strokeRect(TANK_LEFT, TANK_TOP, TANK.w, TANK.h);
  // Left wall special (crack)
  // Draw crack line
  ctx.beginPath();
  ctx.moveTo(TANK_LEFT, crackY - 20);
  ctx.lineTo(TANK_LEFT, crackY + 20);
  ctx.strokeStyle = 'rgba(255,255,255,0.9)';
  ctx.lineWidth = 2;
  ctx.stroke();
  
  // Draw crack handle
  ctx.fillStyle = 'red';
  ctx.fillRect(TANK_LEFT - 12, crackY - 8, 8, 16);
  
  // Draw water particles
  ctx.fillStyle = 'rgba(30,144,255,0.6)';
  for (let p of particles) {
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
    ctx.fill();
  }
  
  // Draw water surface line (optional)
  const waterLevel = getWaterLevel();
  if (waterLevel < TANK_BOT) {
    ctx.beginPath();
    ctx.moveTo(TANK_LEFT, waterLevel);
    ctx.lineTo(TANK_RIGHT, waterLevel);
    ctx.strokeStyle = 'rgba(255,255,255,0.5)';
    ctx.lineWidth = 1;
    ctx.stroke();
  }
  
  // Draw plants
  for (let plant of plants) plant.draw(ctx);
  
  // Draw bodies
  for (let b of bodies) {
    if (b.type === 'fish') drawFish(ctx, b);
    else if (b.type === 'rock') {
      ctx.fillStyle = '#888';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2);
      ctx.fill();
    }
    else if (b.type === 'toy') {
      ctx.fillStyle = 'red';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2);
      ctx.fill();
      // star pattern
      ctx.fillStyle = 'yellow';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.radius*0.5, 0, Math.PI*2);
      ctx.fill();
    }
  }
  
  // Draw fragments
  drawFragments(ctx);
  
  // Draw puddle? Already drawn as particles
  
  requestAnimationFrame(loop);
}
```

This is a rough skeleton. We need to flesh out many details.

Issues to address:
- Water particles escaping through breach need to be removed from tank array or marked escaped. In updateParticles, we mark escaped. But we still iterate over all particles. That's okay. Escaped particles still update but simpler.
- Water level: getWaterLevel scans all particles each frame. Could be expensive but okay for 800 particles.
- Fish swim force: we need to make them actually swim against current. The drag from water will push them left when water flows out. The swim force should be directed right (toward target). But if water is very strong, they get swept. We can set swim force magnitude proportional to current velocity difference.
- Buoyancy: we need to ensure rocks sink and toy floats. We can set mass accordingly and let buoyancy force be based on submerged volume. In applyFluidForces, we compute submerged fraction and apply upward force. But the force magnitude should be enough to overcome gravity for light objects. We'll tune.
- Plants: need to be initialized with positions inside tank. Add to plants array.
- Puddle spreading: escaped particles on floor should spread. We already give them friction. New particles hitting floor will stack. We might need to handle particle-particle collisions for escaped particles too, so they don't overlap infinitely. We can use same repulsion for escaped particles on floor. But they are many; performance? We can include them in spatial grid and apply repulsion. That will also cause them to spread.

Modify particle update: for escaped particles, also apply repulsion from neighbors (both escaped and non-escaped? non-escaped are in water). We'll just do neighbor repulsion for all particles regardless of escaped status. That will cause puddle to spread and compress.

So in updateParticles, move the repulsion loop before the escaped check? Actually, we need to handle escaped particles differently for wall collisions. We can still compute repulsion for all.

Revised updateParticles:

```javascript
function updateParticles(dt) {
  buildGrid();
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    // Gravity
    p.vy += PARTICLE_GRAVITY;
    
    // Neighbor repulsion (for all particles)
    const neighbors = getNeighbors(p);
    for (let j of neighbors) {
      if (j <= i) continue;
      const q = particles[j];
      const dx = q.x - p.x;
      const dy = q.y - p.y;
      const distSq = dx*dx + dy*dy;
      const minDist = p.r + q.r;
      if (distSq < minDist*minDist && distSq > 0.001) {
        const dist = Math.sqrt(distSq);
        const overlap = minDist - dist;
        const nx = dx / dist;
        const ny = dy / dist;
        const force = 0.3 * overlap;
        p.vx -= nx * force;
        p.vy -= ny * force;
        q.vx += nx * force;
        q.vy += ny * force;
      }
    }
    
    // Wall / boundary interactions
    if (!p.escaped) {
      // tank walls
      if (p.x < TANK_LEFT + p.r) { p.x = TANK_LEFT + p.r; p.vx *= -0.3; }
      if (p.x > TANK_RIGHT - p.r) { p.x = TANK_RIGHT - p.r; p.vx *= -0.3; }
      if (p.y < TANK_TOP + p.r) { p.y = TANK_TOP + p.r; p.vy *= -0.3; }
      if (p.y > TANK_BOT - p.r) { p.y = TANK_BOT - p.r; p.vy *= -0.3; }
      
      // Breach
      if (breach) {
        // Suction near hole
        if (p.x < TANK_LEFT + 15 && p.x > TANK_LEFT - 5 && p.y > breach.y1 && p.y < breach.y2) {
          const depth = TANK_BOT - getWaterLevel();
          const suction = 0.3 + depth * 0.02;
          p.vx -= suction;
          // spread jet
          const cy = (breach.y1 + breach.y2)/2;
          p.vy += (p.y - cy) * 0.02;
        }
        // Exit
        if (p.x < TANK_LEFT) {
          p.escaped = true;
        }
      }
    } else {
      // escaped: room boundaries
      if (p.y > FLOOR_Y - p.r) {
        p.y = FLOOR_Y - p.r;
        p.vy = 0;
        p.vx *= 0.95;
      }
      if (p.x < ROOM_LEFT + p.r) { p.x = ROOM_LEFT + p.r; p.vx *= -0.5; }
      if (p.x > ROOM_RIGHT - p.r) { p.x = ROOM_RIGHT - p.r; p.vx *= -0.5; }
    }
    
    // Integrate
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    
    // Damping
    p.vx *= 0.999;
    p.vy *= 0.999;
  }
}
```

Now, fish swim force: need to add to body update or separate. Let's add a method to Body.

In Body class:
```javascript
swim(dt) {
  if (this.type !== 'fish' || this.escaped) return;
  const waterLevel = getWaterLevel();
  if (this.y < waterLevel) { // underwater
    // Desired: stay near original position or move slowly
    const dx = this.targetX - this.x;
    const dy = this.targetY - this.y;
    const dist = Math.sqrt(dx*dx+dy*dy);
    if (dist > 1) {
      const fx = (dx/dist) * this.swimForce;
      const fy = (dy/dist) * this.swimForce;
      this.vx += fx * dt;
      this.vy += fy * dt;
    }
    // If water current is strong (leftward), add rightward swim force
    // Estimate current from nearby particles' average vx
    let avgVx = 0; let count = 0;
    // ... but that's expensive. We can approximate based on proximity to breach.
    const distToBreach = Math.abs(this.x - TANK_LEFT);
    if (distToBreach < 100 && breach) {
      // current increases as water drains
      const depth = TANK_BOT - waterLevel;
      const currentStrength = depth * 0.02;
      this.vx += currentStrength; // swim against current (rightward)
    }
  }
}
```

But fish should also have limited speed. We'll clamp velocity.

```javascript
const speed = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
if (speed > this.maxSpeed) {
  this.vx = (this.vx/speed) * this.maxSpeed;
  this.vy = (this.vy/speed) * this.maxSpeed;
}
```

Add to Body.update before integration.

Now, buoyancy in applyFluidForces: we need to apply upward force proportional to submerged volume. We'll compute fraction submerged based on water level.

```javascript
// In applyFluidForces:
const waterLevel = getWaterLevel();
if (body.y + body.radius > waterLevel && body.y - body.radius < TANK_BOT) {
  let submergedFraction = 0;
  if (body.y - body.radius >= waterLevel) {
    submergedFraction = 0;
  } else if (body.y + body.radius <= TANK_BOT) {
    submergedFraction = 1;
  } else {
    const totalH = body.radius * 2;
    const submergedH = TANK_BOT - Math.max(waterLevel, body.y - body.radius);
    submergedFraction = submergedH / totalH;
  }
  const buoyancy = 0.8 * submergedFraction; // tune
  body.vy -= buoyancy;
}
```

But note: we apply drag from water particles already. That should be enough.

Now, plants: need to initialize inside tank.

```javascript
const plants = [];
function initPlants() {
  plants.length = 0;
  plants.push(new Plant(TANK_LEFT + 100, TANK_BOT, 120));
  plants.push(new Plant(TANK_LEFT + 300, TANK_BOT, 150));
  plants.push(new Plant(TANK_LEFT + 500, TANK_BOT, 100));
}
```

Glass fragments: spawned on burst. Also, we need to draw them. Already have spawnFragments and drawFragments.

Now, the crack dragging: handle events.

Also, need to make sure that after burst, the crack handle might not be needed, but we can still allow dragging? Probably disable after burst.

Now, performance considerations:
- Particle count ~800. Spatial grid should make neighbor search fast.
- Body-fluid interaction: for each body, we query grid cells around it. With ~10 bodies, that's okay.
- Plants: each segment queries grid; 3 plants * 8 segments = 24 queries, fine.

Now, let's code everything together.

I need to be careful with variable scoping and order.

I'll write the full HTML file.

Potential improvements:
- Water rendering: use gradient or transparent blue.
- Glass walls: draw with slight reflection.
- Background: room texture? Not needed.

Let's produce the final code.

Before writing, let's double-check requirements:
- "Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains" -> suction force based on depth.
- "Curved water jet affected by gravity" -> particles exiting get initial velocity and then gravity acts, forming arc.
- "Spreading puddle that collides with the room boundaries" -> escaped particles on floor with repulsion and room wall collisions.
- "Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current" -> we have different masses, buoyancy, drag.
- "Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions" -> when they exit breach, they become airborne (gravity only), then hit floor.
- "Fish attempting to swim against the current before being swept through the breach" -> fish have swim force against current.
- "Glass fragments with angular velocity, collisions, and water resistance" -> fragments have rotation, collide with floor, water drag.
- "Visible waterline that lowers continuously rather than disappearing all at once" -> we draw water surface line at particle top, and particles decrease.

All good.

Now, I'll write the complete HTML file.

But first, ensure that the simulation parameters produce a reasonable visual. I might need to tune forces.

Potential issue: Water particles may not fill the tank nicely with just repulsion. They might compress or leave gaps. We can increase repulsion force and add a small random jitter each frame to simulate turbulence.

Also, the water surface might be uneven. That's okay.

I'll add a small random force to water particles to keep them moving slightly.

Now, the code.

I'll write it as one large file. I need to be careful with syntax.

Let's start writing the HTML.

```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Aquarium Burst</title> <style> body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: sans-serif; } canvas { background: #1a1a1a; box-shadow: 0 0 20px rgba(0,0,0,0.8); } #controls { position: absolute; top: 20px; left: 20px; color: white; z-index: 10; } button { background: #c00; color: white; border: none; padding: 10px 20px; font-size: 16px; cursor: pointer; border-radius: 4px; } button:disabled { background: #555; cursor: not-allowed; } #info { margin-top: 10px; font-size: 14px; color: #aaa; } </style> </head> <body> <div id="controls"> <button id="burstBtn">BREAK!</button> <div id="info">Drag the red handle to position the crack.</div> </div> <canvas id="canvas" width="900" height="700"></canvas> <script> (() => { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Tank geometry const TANK = { x: 180, y: 80, w: 520, h: 420 }; const TANK_LEFT = TANK.x; const TANK_RIGHT = TANK.x + TANK.w; const TANK_TOP = TANK.y; const TANK_BOT = TANK.y + TANK.h; const WATER_SURFACE_INIT = TANK_BOT - 50; // Room const FLOOR_Y = 620; const ROOM_LEFT = 0; const ROOM_RIGHT = W; // Crack let crackY = TANK_TOP + 180; let draggingCrack = false; let breach = null; // {y1, y2} // Physics constants const GRAVITY = 0.18; const PARTICLE_GRAVITY = 0.22; const P_RADIUS = 4; const GRID_SIZE = 20; // Water particles const particles = []; let grid = {}; function hashKey(cx, cy) { return cx + ',' + cy; } function buildGrid() { grid = {}; for (let i = 0; i < particles.length; i++) { const p = particles[i]; const cx = Math.floor(p.x / GRID_SIZE); const cy = Math.floor(p.y / GRID_SIZE); const key = hashKey(cx, cy); if (!grid[key]) grid[key] = []; grid[key].push(i); } } function getNeighbors(p) { const cx = Math.floor(p.x / GRID_SIZE); const cy = Math.floor(p.y / GRID_SIZE); const result = []; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { const key = hashKey(cx+dx, cy+dy); if (grid[key]) { for (let idx of grid[key]) result.push(idx); } } } return result; } function initParticles() { particles.length = 0; const cols = Math.floor(TANK.w / (P_RADIUS*2.2)); const rows = Math.floor((WATER_SURFACE_INIT - TANK_TOP) / (P_RADIUS*2.2)); for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const x = TANK_LEFT + P_RADIUS*2 + i * (P_RADIUS*2.2) + (Math.random()-0.5)*2; const y = TANK_BOT - P_RADIUS*2 - j * (P_RADIUS*2.2) + (Math.random()-0.5)*2; if (x > TANK_LEFT+8 && x < TANK_RIGHT-8 && y > TANK_TOP+8 && y < WATER_SURFACE_INIT) { particles.push({x, y, vx:0, vy:0, r:P_RADIUS, escaped:false}); } } } } function getWaterLevel() { let minY = TANK_BOT; for (let p of particles) { if (!p.escaped && p.y < minY) minY = p.y; } return minY; } function updateParticles(dt) { buildGrid(); const waterLevel = getWaterLevel(); for (let i = 0; i < particles.length; i++) { const p = particles[i]; p.vy += PARTICLE_GRAVITY; // Neighbor repulsion (all particles) const neighbors = getNeighbors(p); for (let j of neighbors) { if (j <= i) continue; const q = particles[j]; const dx = q.x - p.x; const dy = q.y - p.y; const distSq = dx*dx + dy*dy; const minDist = p.r + q.r; if (distSq < minDist*minDist && distSq > 0.001) { const dist = Math.sqrt(distSq); const overlap = minDist - dist; const nx = dx / dist; const ny = dy / dist; const force = 0.4 * overlap; p.vx -= nx * force; p.vy -= ny * force; q.vx += nx * force; q.vy += ny * force; } } if (!p.escaped) { // Tank walls if (p.x < TANK_LEFT + p.r) { p.x = TANK_LEFT + p.r; p.vx *= -0.3; } if (p.x > TANK_RIGHT - p.r) { p.x = TANK_RIGHT - p.r; p.vx *= -0.3; } if (p.y < TANK_TOP + p.r) { p.y = TANK_TOP + p.r; p.vy *= -0.3; } if (p.y > TANK_BOT - p.r) { p.y = TANK_BOT - p.r; p.vy *= -0.3; } // Breach interaction if (breach) { if (p.x < TANK_LEFT + 12 && p.x > TANK_LEFT - 4 && p.y > breach.y1 && p.y < breach.y2) { const depth = TANK_BOT - waterLevel; const suction = 0.4 + depth * 0.025; p.vx -= suction; const cy = (breach.y1 + breach.y2) / 2; p.vy += (p.y - cy) * 0.03; } if (p.x < TANK_LEFT) { p.escaped = true; p.vx = -3; p.vy = (p.y - cy) * 0.1; } } } else { // Escaped: room boundaries if (p.y > FLOOR_Y - p.r) { p.y = FLOOR_Y - p.r; p.vy = 0; p.vx *= 0.92; } if (p.x < ROOM_LEFT + p.r) { p.x = ROOM_LEFT + p.r; p.vx *= -0.4; } if (p.x > ROOM_RIGHT - p.r) { p.x = ROOM_RIGHT - p.r; p.vx *= -0.4; } } p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= 0.998; p.vy *= 0.998; } } // Rigid bodies class Body { constructor(x, y, radius, mass, type) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.radius = radius; this.mass = mass; this.invMass = 1 / mass; this.type = type; this.angle = 0; this.angVel = 0; this.escaped = false; this.targetX = x; this.targetY = y; this.maxSpeed = type === 'fish' ? 2.5 : 1.5; this.swimForce = type === 'fish' ? 0.15 : 0; this.color = 'white'; } update(dt) { this.vy += GRAVITY * 1.2; // Swim for fish if (this.type === 'fish' && !this.escaped) { const waterLevel = getWaterLevel(); if (this.y < waterLevel) { const dx = this.targetX - this.x; const dy = this.targetY - this.y; const dist = Math.sqrt(dx*dx + dy*dy); if (dist > 2) { this.vx += (dx/dist) * this.swimForce * dt; this.vy += (dy/dist) * this.swimForce * dt; } // Against current near breach if (breach && Math.abs(this.x - TANK_LEFT) < 120) { const depth = TANK_BOT - waterLevel; const current = depth * 0.04; this.vx += current * dt; // swim right } } const speed = Math.sqrt(this.vx*this.vx + this.vy*this.vy); if (speed > this.maxSpeed) { this.vx = (this.vx/speed) * this.maxSpeed; this.vy = (this.vy/speed) * this.maxSpeed; } } this.x += this.vx * dt; this.y += this.vy * dt; this.angle += this.angVel * dt; this.angVel *= 0.97; // Floor if (this.y > FLOOR_Y - this.radius) { this.y = FLOOR_Y - this.radius; this.vy *= -0.3; this.vx *= 0.9; this.angVel *= 0.8; } // Room walls if (this.x < ROOM_LEFT + this.radius) { this.x = ROOM_LEFT + this.radius; this.vx *= -0.4; } if (this.x > ROOM_RIGHT - this.radius) { this.x = ROOM_RIGHT - this.radius; this.vx *= -0.4; } if (!this.escaped) { // Tank walls if (this.x < TANK_LEFT + this.radius) { this.x = TANK_LEFT + this.radius; this.vx *= -0.3; } if (this.x > TANK_RIGHT - this.radius) { this.x = TANK_RIGHT - this.radius; this.vx *= -0.3; } if (this.y < TANK_TOP + this.radius) { this.y = TANK_TOP + this.radius; this.vy *= -0.3; } if (this.y > TANK_BOT - this.radius) { this.y = TANK_BOT - this.radius; this.vy *= -0.3; } // Breach exit if (breach && this.x < TANK_LEFT + this.radius && this.y > breach.y1 && this.y < breach.y2) { this.escaped = true; this.vx = -3.5; this.vy = (this.y - (breach.y1+breach.y2)/2) * 0.08; } } } applyForce(fx, fy) { this.vx += fx * this.invMass; this.vy += fy * this.invMass; } } const bodies = []; function initBodies() { bodies.length = 0; bodies.push(new Body(TANK_LEFT + 220, TANK_BOT - 120, 14, 1.2, 'fish')); bodies.push(new Body(TANK_LEFT + 320, TANK_BOT - 180, 14, 1.2, 'fish')); bodies.push(new Body(TANK_LEFT + 420, TANK_BOT - 140, 14, 1.2, 'fish')); bodies.push(new Body(TANK_LEFT + 280, TANK_BOT - 40, 11, 5, 'rock')); bodies.push(new Body(TANK_LEFT + 480, TANK_BOT - 50, 13, 6, 'rock')); bodies.push(new Body(TANK_LEFT + 380, TANK_BOT - 30, 10, 4, 'rock')); bodies.push(new Body(TANK_LEFT + 350, TANK_BOT - 220, 11, 0.3, 'toy')); } function applyFluidForces(body) { const cx = Math.floor(body.x / GRID_SIZE); const cy = Math.floor(body.y / GRID_SIZE); const range = Math.ceil((body.radius + 10) / GRID_SIZE); let submergedCount = 0; let dragFx = 0, dragFy = 0; for (let dx = -range; dx <= range; dx++) { for (let dy = -range; dy <= range; dy++) { const key = hashKey(cx+dx, cy+dy); if (!grid[key]) continue; for (let idx of grid[key]) { const p = particles[idx]; if (p.escaped) continue; const ddx = p.x - body.x; const ddy = p.y - body.y; const distSq = ddx*ddx + ddy*ddy; const maxDist = body.radius + 6; if (distSq < maxDist*maxDist) { submergedCount++; const relVx = p.vx - body.vx; const relVy = p.vy - body.vy; dragFx += relVx * 0.02; dragFy += relVy * 0.02; } } } } // Buoyancy const waterLevel = getWaterLevel(); if (body.y + body.radius > waterLevel && body.y - body.radius < TANK_BOT) { let submergedFraction = 0; if (body.y - body.radius >= waterLevel) { submergedFraction = 0; } else if (body.y + body.radius <= TANK_BOT) { submergedFraction = 1; } else { const totalH = body.radius * 2; const submergedH = TANK_BOT - Math.max(waterLevel, body.y - body.radius); submergedFraction = submergedH / totalH; } const buoyancy = 0.7 * submergedFraction; body.vy -= buoyancy; } body.vx += dragFx * body.invMass; body.vy += dragFy * body.invMass; } function resolveBodyCollision(a, b) { const dx = b.x - a.x; const dy = b.y - a.y; const distSq = dx*dx + dy*dy; const minDist = a.radius + b.radius; if (distSq < minDist*minDist && distSq > 0.001) { const dist = Math.sqrt(distSq); const overlap = minDist - dist; const nx = dx / dist; const ny = dy / dist; const totalMass = a.mass + b.mass; const aRatio = b.mass / totalMass; const bRatio = a.mass / totalMass; a.x -= nx * overlap * aRatio; a.y -= ny * overlap * aRatio; b.x += nx * overlap * bRatio; b.y += ny * overlap * bRatio; const relVx = b.vx - a.vx; const relVy = b.vy - a.vy; const relVn = relVx * nx + relVy * ny; if (relVn > 0) return; const restitution = 0.3; const j = -(1 + restitution) * relVn / totalMass; const impulseX = j * nx; const impulseY = j * ny; a.vx -= impulseX * aRatio; a.vy -= impulseY * aRatio; b.vx += impulseX * bRatio; b.vy += impulseY * bRatio; a.angVel += (Math.random()-0.5) * 0.05; b.angVel += (Math.random()-0.5) * 0.05; } } // Plants class Plant { constructor(x, y, height) { this.baseX = x; this.baseY = y; this.segments = []; const numSeg = 8; for (let i = 0; i <= numSeg; i++) { this.segments.push({ x: x, y: y - i * (height/numSeg), vx: 0, vy: 0 }); } } update(dt) { for (let i = 1; i < this.segments.length; i++) { const seg = this.segments[i]; const prev = this.segments[i-1]; seg.vy += 0.04; // Water repulsion const cx = Math.floor(seg.x / GRID_SIZE); const cy = Math.floor(seg.y / GRID_SIZE); let fx = 0, fy = 0; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { const key = hashKey(cx+dx, cy+dy); if (!grid[key]) continue; for (let idx of grid[key]) { const p = particles[idx]; if (p.escaped) continue; const ddx = seg.x - p.x; const ddy = seg.y - p.y; const distSq = ddx*ddx + ddy*ddy; const minDist = 3; if (distSq < minDist*minDist && distSq > 0.001) { const dist = Math.sqrt(distSq); const overlap = minDist - dist; fx += (ddx/dist) * overlap * 0.15; fy += (ddy/dist) * overlap * 0.15; } } } } seg.vx += fx; seg.vy += fy; seg.x += seg.vx * dt; seg.y += seg.vy * dt; seg.vx *= 0.88; seg.vy *= 0.88; // Constraint const cdx = seg.x - prev.x; const cdy = seg.y - prev.y; const cdist = Math.sqrt(cdx*cdx + cdy*cdy); const segLen = 10; if (cdist > segLen) { const diff = (cdist - segLen) / cdist; seg.x -= cdx * diff; seg.y -= cdy * diff; } } } draw(ctx) { ctx.beginPath(); ctx.moveTo(this.segments[0].x, this.segments[0].y); for (let i = 1; i < this.segments.length; i++) { ctx.lineTo(this.segments[i].x, this.segments[i].y); } ctx.strokeStyle = '#2a5'; ctx.lineWidth = 3; ctx.stroke(); // Leaves ctx.fillStyle = '#2a5'; for (let i = 2; i < this.segments.length; i+=2) { const s = this.segments[i]; ctx.beginPath(); ctx.ellipse(s.x + 5, s.y - 5, 6, 3, Math.PI/4, 0, Math.PI*2); ctx.fill(); } } } const plants = []; function initPlants() { plants.length = 0; plants.push(new Plant(TANK_LEFT + 120, TANK_BOT, 130)); plants.push(new Plant(TANK_LEFT + 300, TANK_BOT, 160)); plants.push(new Plant(TANK_LEFT + 500, TANK_BOT, 110)); } // Glass fragments let fragments = []; function spawnFragments() { fragments = []; const count = 35; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 2 + Math.random() * 5; const size = 4 + Math.random() * 9; fragments.push({ x: TANK_LEFT, y: breach.y1 + Math.random() * (breach.y2 - breach.y1), vx: Math.cos(angle) * speed - 1, vy: Math.sin(angle) * speed - 3, size, angVel: (Math.random()-0.5) * 0.3, rotation: Math.random() * Math.PI * 2, color: `rgba(180,220,255,${0.6 + Math.random()*0.4})` }); } } function updateFragments(dt) { for (let f of fragments) { f.vy += GRAVITY; f.x += f.vx * dt; f.y += f.vy * dt; f.rotation += f.angVel; // Water drag if inside tank if (f.y > getWaterLevel() && f.y < TANK_BOT && f.x > TANK_LEFT && f.x < TANK_RIGHT) { f.vx *= 0.94; f.vy *= 0.94; } if (f.y > FLOOR_Y - f.size/2) { f.y = FLOOR_Y - f.size/2; f.vy *= -0.25; f.vx *= 0.85; } if (f.x < ROOM_LEFT + f.size/2) { f.x = ROOM_LEFT + f.size/2; f.vx *= -0.4; } if (f.x > ROOM_RIGHT - f.size/2) { f.x = ROOM_RIGHT - f.size/2; f.vx *= -0.4; } // Tank walls after break? fragments can hit left wall? left wall is broken, so ignore left wall. } } function drawFragments(ctx) { for (let f of fragments) { ctx.save(); ctx.translate(f.x, f.y); ctx.rotate(f.rotation); ctx.fillStyle = f.color; ctx.beginPath(); ctx.moveTo(0, -f.size/2); ctx.lineTo(f.size/2, f.size/2); ctx.lineTo(-f.size/2, f.size/2); ctx.closePath(); ctx.fill(); ctx.restore(); } } // Drawing helpers function drawFish(ctx, fish) { ctx.save(); ctx.translate(fish.x, fish.y); ctx.rotate(fish.angle); ctx.fillStyle = '#f80'; ctx.beginPath(); ctx.ellipse(0, 0, fish.radius*1.6, fish.radius*0.9, 0, 0, Math.PI*2); ctx.fill(); // tail ctx.beginPath(); ctx.moveTo(-fish.radius*1.2, 0); ctx.lineTo(-fish.radius*2.8, -fish.radius*0.9); ctx.lineTo(-fish.radius*2.8, fish.radius*0.9); ctx.closePath(); ctx.fill(); // eye ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(fish.radius*0.7, -fish.radius*0.3, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(fish.radius*0.8, -fish.radius*0.3, 1, 0, Math.PI*2); ctx.fill(); ctx.restore(); } // Mouse interaction for crack canvas.addEventListener('mousedown', e => { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; if (Math.abs(mx - (TANK_LEFT - 12)) < 12 && Math.abs(my - crackY) < 18 && !breach) { draggingCrack = true; } }); canvas.addEventListener('mousemove', e => { if (draggingCrack) { const rect = canvas.getBoundingClientRect(); const my = e.clientY - rect.top; crackY = Math.max(TANK_TOP + 30, Math.min(TANK_BOT - 30, my)); } }); canvas.addEventListener('mouseup', () => draggingCrack = false); // Burst button document.getElementById('burstBtn').addEventListener('click', () => { if (!breach) { breach = { y1: crackY - 35, y2: crackY + 35 }; spawnFragments(); document.getElementById('burstBtn').disabled = true; document.getElementById('info').textContent = 'The tank is breaking!'; } }); // Main loop function update(dt) { updateParticles(dt); for (let b of bodies) { applyFluidForces(b); b.update(dt); } // Body-body collisions for (let i = 0; i < bodies.length; i++) { for (let j = i+1; j < bodies.length; j++) { resolveBodyCollision(bodies[i], bodies[j]); } } for (let p of plants) p.update(dt); updateFragments(dt); } function draw() { ctx.clearRect(0,0,W,H); // Room background ctx.fillStyle = '#1e1e1e'; ctx.fillRect(0,0,W,H); // Floor ctx.fillStyle = '#333'; ctx.fillRect(0, FLOOR_Y, W, H - FLOOR_Y); // Floor boards ctx.strokeStyle = '#2a2a2a'; ctx.lineWidth = 1; for (let x = 0; x < W; x += 60) { ctx.beginPath(); ctx.moveTo(x, FLOOR_Y); ctx.lineTo(x, H); ctx.stroke(); } // Tank glass ctx.strokeStyle = 'rgba(200,220,255,0.5)'; ctx.lineWidth = 5; ctx.strokeRect(TANK_LEFT, TANK_TOP, TANK.w, TANK.h); // Glass shine ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(TANK_LEFT + 10, TANK_TOP + 10); ctx.lineTo(TANK_LEFT + 10, TANK_BOT - 10); ctx.stroke(); // Draw crack on left wall ctx.beginPath(); ctx.moveTo(TANK_LEFT, crackY - 25); ctx.lineTo(TANK_LEFT, crackY + 25); ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 2; ctx.setLineDash([4,2]); ctx.stroke(); ctx.setLineDash([]); // Crack handle ctx.fillStyle = breach ? '#555' : '#c00'; ctx.fillRect(TANK_LEFT - 14, crackY - 10, 10, 20); ctx.strokeStyle = 'white'; ctx.lineWidth = 1; ctx.strokeRect(TANK_LEFT - 14, crackY - 10, 10, 20); // Water particles ctx.fillStyle = 'rgba(20,100,220,0.55)'; for (let p of particles) { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill(); } // Water surface line const waterLevel = getWaterLevel(); if (waterLevel < TANK_BOT) { ctx.beginPath(); ctx.moveTo(TANK_LEFT + 2, waterLevel); // small wave effect for (let x = TANK_LEFT + 2; x < TANK_RIGHT; x += 20) { ctx.lineTo(x + 10, waterLevel + Math.sin(x * 0.05 + Date.now()*0.003) * 2); } ctx.lineTo(TANK_RIGHT - 2, waterLevel); ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.lineWidth = 2; ctx.stroke(); } // Plants for (let p of plants) p.draw(ctx); // Bodies for (let b of bodies) { if (b.type === 'fish') drawFish(ctx, b); else if (b.type === 'rock') { ctx.fillStyle = '#777'; ctx.beginPath(); ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(b.x - b.radius*0.3, b.y - b.radius*0.3, b.radius*0.4, 0, Math.PI*2); ctx.fill(); } else if (b.type === 'toy') { ctx.fillStyle = '#e33'; ctx.beginPath(); ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(b.x, b.y, b.radius*0.45, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#e33'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('★', b.x, b.y); } } // Fragments drawFragments(ctx); // Info overlay const depth = TANK_BOT - waterLevel; ctx.fillStyle = 'white'; ctx.font = '14px sans-serif'; ctx.fillText(`Water depth: ${Math.max(0, depth).toFixed(0)}`, 20, H - 20); } let lastTime = performance.now(); function loop(now) { let dt = (now - lastTime) / 16; if (dt < 0.1) dt = 0.1; if (dt > 3) dt = 3; lastTime = now; update(dt); draw(); requestAnimationFrame(loop); } initParticles(); initBodies(); initPlants(); requestAnimationFrame(loop); })(); </script> </body> </html> ```

TAKE IT TO CODE

This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.

RUN

modelstep-3.7-flash
vendorStepFun
harnesspi-agent
generated2026-08-02

METRICS

runtime142.9s
tokens in1.7k
tokens out22k
cached0
cost / run$0.0261