Aquarium breach · ling-3.0-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
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
Create a large glass aquarium whose side panel develops a visible crack and then bursts. The simulation must include: - Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains - A curved water jet affected by gravity - A spreading puddle that collides with the room boundaries - Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current - Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions - Fish attempting to swim against the current before being swept through the breach - Glass fragments with angular velocity, collisions, and water resistance - A visible waterline that lowers continuously rather than disappearing all at once Let the user drag the crack vertically before triggering the failure. A lower crack should initially produce a stronger jet than a higher crack. Give me 1 html file
thinking
Let me create a complex aquarium simulation with all the requested features. This is a physics-based simulation that needs:
1. A glass aquarium with a crack that can be dragged vertically
2. Water escaping through the opening with flow based on depth
3. Curved water jets affected by gravity
4. Spreading puddle with boundary collision
5. Fish, rocks, plants, floating toy with density/buoyancy/drag/current
6. Objects transitioning underwater → airborne → floor
7. Fish swimming against current before being swept
8. Glass fragments with angular velocity, collisions, water resistance
9. Visible lowering waterline
10. Drag crack vertically before triggering failure
This is going to be a substantial single HTML file with Canvas-based rendering and custom physics. Let me plan the architecture:
- Canvas for rendering
- Physics engine (custom, simple)
- Water simulation (particle-based or simplified)
- Object physics with buoyancy
- Glass shatter effect
- UI for crack positioning
Let me think about the water simulation approach:
- Use a grid-based water simulation or particle-based
- For performance and visual quality, I'll use a hybrid approach
- Water particles for the jet and puddle
- A simplified water level for the aquarium
Actually, for a single HTML file, I need to be careful about complexity. Let me use a particle-based approach for water and simple physics for objects.
Let me design the simulation:
**Water System:**
- Store water as a height field in the aquarium (for the main body)
- Particle system for the jet and puddle
- Water level decreases as particles escape
**Objects:**
- Fish: buoyant, swims, affected by drag and current
- Rocks: dense, sinks, affected by drag
- Plants: semi-buoyant, sways
- Floating toy: very buoyant
**Glass:**
- Crack line that user can drag
- When triggered, fragments fly out with angular velocity
- Fragments affected by gravity, water resistance, collisions
**Physics:**
- Simple Euler integration
- Gravity, buoyancy (Archimedes), drag
- Collision detection with walls and floor
Let me code this up. I'll use a canvas-based approach with requestAnimationFrame.
For the water, I'll use a simplified approach:
- The aquarium has a water level (height)
- When crack is open, water particles spawn at the crack and follow physics
- The water level drops based on flow rate (Torricelli's law: v = sqrt(2gh))
For the crack:
- User can drag a vertical line up/down on the aquarium wall
- Click "trigger" to break the glass
- Lower crack = higher pressure = stronger jet
Let me think about the rendering:
- Background: room with walls
- Aquarium: glass walls with transparency
- Water: blue semi-transparent fill with animated surface
- Objects inside
- Crack line (before trigger)
- Glass fragments (after trigger)
- Water jet particles
- Puddle on floor
Let me write this step by step.
I'll structure the code as:
1. HTML structure with canvas and controls
2. CSS for layout
3. JS for simulation
Let me write the full implementation now.
Actually, let me think more carefully about the water simulation. For a convincing effect:
1. **Main aquarium water**: Represented as a water level (height). When crack opens, water drains based on Torricelli's law. Flow rate = A * sqrt(2gh) where A is crack area, g is gravity, h is water height above crack.
2. **Jet particles**: When crack is open, spawn particles at the crack position. They have initial velocity based on water depth (horizontal velocity from Torricelli's law, vertical = 0), then affected by gravity.
3. **Puddle**: Jet particles that hit the floor spread out. Track them as particles that settle on the floor.
4. **Waterline**: The main water level decreases continuously as water drains.
For objects in water:
- Buoyancy force = ρ_water * V_displaced * g (upward)
- Weight = m * g (downward)
- Drag force proportional to velocity
- Current force from water flow direction
For fish:
- They try to swim against current (add a swim force opposing flow)
- When swept, they go with the flow
- Transition from underwater to airborne when water level drops below them
For glass fragments:
- When crack triggers, create angular fragments from the crack line
- Each fragment has position, velocity, angular velocity
- Affected by gravity, water resistance (when underwater), collision with walls/floor
Let me code this now. I need to be very careful about performance since this is all in one file.
Let me think about the crack dragging mechanic:
- There's a crack indicator on the right side of the aquarium
- User can click and drag it up/down
- A "BREAK" button triggers the failure
- The crack position affects jet strength
For the visual design:
- Dark room background
- Aquarium with glass-like borders
- Blue water with transparency
- Colorful fish
- Rocks and plants
- Floating toy (like a small boat or duck)
Let me write the code now. This will be long but self-contained.
I'll use a class-based approach for organization.
Let me think about the water particle system more carefully:
When the crack is triggered:
1. Water starts flowing out from the crack position
2. Particles are emitted horizontally (or at an angle) from the crack
3. The emission rate depends on water depth above the crack
4. As water level drops, emission rate decreases
5. Particles are affected by gravity (curved trajectory)
6. Particles that hit the floor become part of the puddle
7. Particles that hit walls bounce/stop
For the puddle:
- Particles that reach the floor settle and spread
- Puddle radius grows over time
- Puddle has a flat, spreading appearance
Let me simplify: I'll track water as both a height field (for the aquarium interior) and particles (for the jet and puddle).
Actually, for visual quality, let me use a different approach:
- The aquarium interior water is rendered as a filled shape with the current water level
- The jet is rendered as a stream of particles
- The puddle is rendered as a growing ellipse on the floor
- This gives good visuals without full particle simulation for the bulk water
For objects, I need proper physics:
- Position, velocity
- Forces: gravity, buoyancy, drag, current
- Collision with walls, floor, and water surface
Let me now write the complete code.
I need to think about the coordinate system:
- Canvas is the full screen
- Aquarium is positioned in the center-left
- Room extends to the right and bottom
- Floor is at the bottom of the canvas
Aquarium dimensions:
- Let's say the aquarium is about 500px wide and 400px tall
- Positioned at (100, 100)
- Floor is at y = 550 (below the aquarium)
Wait, let me reconsider. The aquarium sits on the floor. So:
- Floor at y = canvas.height - 50
- Aquarium bottom at floor level
- Aquarium top at floor - 400
- Aquarium left at x = 100
- Aquarium right at x = 600
The crack is on the right side (the side panel that bursts).
Let me code this up now. I'll aim for a comprehensive, visually impressive simulation.
Let me think about the structure more carefully:
```javascript
// Constants
const AQUARIUM = { x: 100, y: 150, width: 500, height: 400 };
const FLOOR_Y = AQUARIUM.y + AQUARIUM.height; // 550
const GRAVITY = 980; // pixels/s^2
const WATER_DENSITY = 1000;
// State
let waterLevel = AQUARIUM.height; // starts full
let crackY = AQUARIUM.y + AQUARIUM.height * 0.5; // middle
let crackOpen = false;
let crackTriggered = false;
let crackDragging = false;
// Objects
let fish = [];
let rocks = [];
let plants = [];
let floatingToy = null;
// Glass fragments
let fragments = [];
// Water particles (jet)
let waterParticles = [];
// Puddle
let puddle = { x: 0, y: 0, radius: 0 };
```
For the fish swimming behavior:
- Each fish has a target velocity (swimming speed)
- When water is present and there's current, fish try to swim against it
- If current is too strong, fish get swept
- Fish transition from underwater to airborne when water level drops below them
Let me think about the rendering order:
1. Room background (walls, floor)
2. Aquarium back wall
3. Water in aquarium
4. Objects (rocks, plants, fish, toy) - sorted by y position
5. Crack line (before trigger) or glass fragments (after trigger)
6. Water jet particles
7. Puddle on floor
8. UI elements
For the water rendering in the aquarium:
- Draw a blue semi-transparent rectangle from the bottom up to waterLevel
- Add some wave animation on the surface
- Add some light refraction effect
For the jet:
- Emit particles from the crack
- Each particle has position, velocity, lifetime
- Draw as blue circles with decreasing opacity
For the puddle:
- Draw an ellipse on the floor that grows
- Blue, semi-transparent
Let me now write the full code. This is going to be quite long.
Actually, let me think about what makes this really impressive:
1. **Water surface animation**: Waves on the water surface in the aquarium
2. **Refraction effect**: Slight distortion of objects through water
3. **Bubble particles**: Rising bubbles in the water
4. **Splash effect**: When water hits the floor
5. **Glass reflection**: Shiny glass appearance
6. **Fish animation**: Fish that actually swim (fin movement, direction changes)
7. **Current visualization**: Slight visual indication of water flow
Let me prioritize what's most important and make sure the core simulation works well.
OK, let me write the code now. I'll be thorough but focused.
Let me think about the crack dragging UI:
- There's a vertical line on the right side of the aquarium representing the crack
- User clicks on it and drags up/down
- A "TRIGGER FAILURE" button appears
- When clicked, the glass shatters
For the crack position affecting jet strength:
- The crack is at position crackY relative to aquarium
- Water depth above crack = waterLevel - (crackY - aquarium.y)
- Jet velocity = sqrt(2 * g * depth) (Torricelli's law)
- Lower crack → more depth → stronger jet ✓
Let me also think about the flow rate:
- Flow rate = A_crack * sqrt(2 * g * h)
- As water level drops, h decreases, so flow rate decreases
- This naturally gives decreasing flow strength ✓
Now let me write the actual code. I'll aim for about 800-1200 lines of well-organized JavaScript.
Let me think about the physics timestep. I'll use a fixed timestep with accumulation for stability.
Actually, for simplicity and reliability, I'll use variable timestep with a cap on dt.
Let me write the code now:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aquarium Breach Simulation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; overflow: hidden; font-family: 'Segoe UI', sans-serif; }
canvas { display: block; }
#controls { position: fixed; top: 20px; right: 20px; background: rgba(0,0,0,0.7); color: #fff; padding: 15px; border-radius: 10px; z-index: 10; }
#controls label { display: block; margin: 8px 0; }
#controls button { margin-top: 10px; padding: 8px 16px; background: #e74c3c; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
#controls button:hover { background: #c0392b; }
#controls button:disabled { background: #555; cursor: not-allowed; }
#info { position: fixed; bottom: 20px; left: 20px; background: rgba(0,0,0,0.7); color: #fff; padding: 10px 15px; border-radius: 10px; font-size: 13px; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="controls">
<h3 style="margin-bottom:10px;">Aquarium Breach</h3>
<label>Drag the crack line up/down on the tank wall</label>
<label>Water Level: <span id="waterLevel">100%</span></label>
<label>Jet Strength: <span id="jetStrength">Medium</span></label>
<button id="triggerBtn" disabled>TRIGGER FAILURE</button>
<button id="resetBtn">RESET</button>
</div>
<div id="info">Click and drag the crack line on the right side of the tank to position it. Then click TRIGGER FAILURE.</div>
<script>
// ... all the simulation code
</script>
</body>
</html>
```
Now let me write the full simulation code. This is going to be extensive.
Let me plan the key classes/objects:
1. **Aquarium**: Position, dimensions, state
2. **Water**: Level, particles, flow
3. **Crack**: Position, state (dragging, triggered)
4. **Fish**: Position, velocity, swimming behavior
5. **Rock**: Position, velocity, density
6. **Plant**: Position, velocity, buoyancy
7. **FloatingToy**: Position, velocity, buoyancy
8. **Fragment**: Position, velocity, angular velocity, shape
9. **WaterParticle**: For jet and puddle
10. **Bubbles**: For visual effect
Let me write this all out now. I need to be very careful with the physics and make sure everything works together.
For the water physics:
- Water level decreases based on flow rate through crack
- Flow rate = crackWidth * sqrt(2 * GRAVITY * waterDepthAboveCrack)
- waterDepthAboveCrack = waterLevel - (crackY - aquarium.y)
- If crackY is below waterLevel, water flows out
- If crackY is above waterLevel, no flow
For objects in water:
- Buoyancy: F_b = ρ_water * V * g (upward)
- Weight: F_g = m * g (downward)
- Net vertical force = F_b - F_g = (ρ_water * V - m) * g
- If positive, object floats; if negative, sinks
- Drag: F_d = -0.5 * C_d * ρ * A * v * |v|
- Current: force from water flow direction
For transition from underwater to airborne:
- When object's y position + height < waterLevel, it's in air
- Remove buoyancy, only gravity and drag
- When object touches water surface again, add buoyancy
For fish swimming against current:
- Fish have a "swim force" that tries to maintain position or swim against flow
- If flow force > swim force, fish gets swept
- Fish visual: swimming animation (fin flutter, direction change)
OK let me write the full code now. I'll be comprehensive.
Let me think about the crack fragments more carefully:
When the glass breaks:
- Create fragments along the crack line
- Each fragment is a polygon (triangle or quad)
- Fragments get initial velocity (outward from the aquarium)
- Angular velocity random
- Fragments affected by gravity, water drag (when underwater), air drag
- Collision with floor and walls
- Fragments that go underwater experience water resistance
For fragment shapes, I'll create triangular and quadrilateral pieces with random vertices.
Let me also add some visual polish:
- Water surface waves (sinusoidal animation)
- Bubbles rising in the water
- Light caustics on the floor
- Splash particles when jet hits floor
- Glass shine/reflection
OK, I think I have a good plan. Let me write the code now.
Actually, let me reconsider the scope. This needs to be a single HTML file that works well. Let me focus on making the core simulation solid and visually impressive, rather than trying to do everything perfectly.
Key priorities:
1. ✅ Crack dragging and triggering
2. ✅ Water draining with decreasing flow
3. ✅ Curved water jet
4. ✅ Spreading puddle
5. ✅ Fish, rocks, plants, floating toy with proper physics
6. ✅ Object transitions (underwater → airborne → floor)
7. ✅ Fish swimming against current
8. ✅ Glass fragments with angular velocity and collisions
9. ✅ Visible lowering waterline
10. ✅ Visual polish (waves, bubbles, etc.)
Let me write this now. I'll use a procedural approach with objects for organization.
Let me think about the canvas size. I'll make it responsive but target a good size like 1200x700.
Aquarium position: centered-left, maybe at x=80, y=120, width=480, height=380
Floor at y = 120 + 380 = 500
Room extends from aquarium to the right and down to the floor
Actually, let me make the aquarium a bit bigger for visual impact:
- Aquarium: x=80, y=100, width=520, height=400
- Floor at y=500
- Room floor extends across the full width
Let me code this now. I'll write it all at once.
Here's my plan for the water simulation:
The main water body in the aquarium is represented by a water level (a number from 0 to aquarium.height). This level decreases over time based on the flow through the crack.
For visual rendering of the water in the aquarium, I draw a filled shape from the bottom up to the water level, with a wavy surface.
For the jet, I use a particle system. Each particle has:
- x, y position
- vx, vy velocity
- lifetime, maxLifetime
- size
Particles are emitted from the crack at a rate proportional to the flow.
For the puddle, I track a set of particles that have settled on the floor. They spread out over time.
Actually, for the puddle, I'll just use an expanding ellipse on the floor that grows as more water hits it. That's simpler and looks good.
Let me also add splash particles when the jet hits the floor.
Now let me write the actual code. I'll aim for a complete, working simulation.
Let me think about the drag interaction for the crack:
- The crack is shown as a vertical line on the right wall of the aquarium
- When the user clicks near this line, they can drag it up or down
- The crack line is highlighted when being dragged
- A "TRIGGER FAILURE" button becomes enabled when the crack is positioned
For the crack triggering:
- When the user clicks "TRIGGER FAILURE", the glass on the right side of the aquarium at the crack position shatters
- Glass fragments fly outward
- Water starts flowing through the opening
- The crack line disappears and is replaced by fragments
Let me now write the complete code. I'll be very thorough.
One more thing about the fish behavior:
- Fish have a "swim direction" and "swim speed"
- When there's current (water flowing), fish try to swim against it
- The swim force is proportional to their swim speed
- If the current force exceeds the swim force, the fish gets swept
- Fish also have some random wandering behavior
For the floating toy:
- Very buoyant (density < water)
- Floats on the surface
- Gets carried by the current when water flows
- When airborne, falls with gravity
For rocks:
- Dense (density > water)
- Sink to the bottom
- Sit on the floor
- When water drains, they remain on the floor
For plants:
- Semi-buoyant, rooted to the bottom
- Sway with the current
- When water drains, they're left on the floor
OK, I think I have enough planning. Let me write the code.
Let me also think about the color scheme:
- Room background: dark blue-gray (#1a1a2e or similar)
- Aquarium glass: light blue with transparency
- Water: deep blue with lighter areas
- Fish: orange, red, yellow, etc.
- Rocks: gray/brown
- Plants: green
- Floating toy: red/white
- Glass fragments: light blue/white with shine
- Puddle: blue on the floor
Let me write the complete HTML file now.
I realize this is going to be quite long. Let me be efficient but thorough.
Let me structure the JavaScript code:
```javascript
// === CONSTANTS ===
// === STATE ===
// === INITIALIZATION ===
// === PHYSICS UPDATE ===
// === RENDERING ===
// === INPUT HANDLING ===
// === GAME LOOP ===
```
Let me write it all now.
For the water particle system, I need to be careful about performance. I'll limit the number of particles and use object pooling.
Let me also think about the water surface in the aquarium more carefully. I want it to look good:
- The water fills from the bottom to the waterLevel
- The surface has a slight wave animation
- Below the surface, there's a slight gradient (darker at bottom)
- Light rays from above (caustics effect on the floor)
For the aquarium glass:
- Draw the glass walls with a slight blue tint and transparency
- Add a shine/highlight on the glass
- When cracked, the glass on the right side breaks away
OK let me just write the code now. I've planned enough.
Let me write a comprehensive simulation. I'll use these key techniques:
- Canvas 2D rendering
- Simple Euler integration for physics
- Particle system for water jet
- Height field for aquarium water
- Object pool for particles
- Proper layering for rendering
Here we go:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aquarium Breach</title>
<style>
/* styles */
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">...</div>
<script>
// Full simulation code
</script>
</body>
</html>
```
Let me write the full thing now. I'll be very detailed.
Actually, let me think about one more thing: the crack dragging mechanic. How should the user interact with it?
Option 1: There's a visible crack line on the right wall of the aquarium. The user clicks and drags this line up and down.
Option 2: There's a separate slider/control for the crack position.
I think Option 1 is more immersive and visually clear. The crack line is on the aquarium wall, and the user drags it.
When the user clicks on the right wall of the aquarium (within a certain y-range), they can drag the crack up and down. The crack line follows the mouse y position.
After positioning, the user clicks "TRIGGER FAILURE" to break the glass.
Let me implement this with proper hit detection.
Now, for the fish swimming against current behavior, I need to model the current in the aquarium. When the crack is open but before the glass breaks, there might be some initial water movement. After the break, there's a strong current flowing out through the crack.
The current in the aquarium:
- Direction: toward the crack (rightward)
- Strength: proportional to flow rate
- Decreases as water level drops
Fish in the aquarium:
- Each fish has a position, velocity, and target swimming direction
- When there's current, fish apply a swim force against the current
- If the current is too strong, the fish gets swept
- Fish also have some autonomous wandering behavior
Let me also add some bubbles for visual effect. Bubbles rise from the crack area and from the water surface.
OK, I'm going to write the complete code now. Let me be very thorough and make sure everything works.
Let me think about the coordinate system one more time:
Canvas: 1200 x 700 (or responsive)
Aquarium: x=100, y=100, width=500, height=400
Floor: y = 500 (bottom of aquarium)
Room extends from x=0 to x=1200, y=0 to y=700
The crack is on the right wall of the aquarium at x = 100 + 500 = 600
When the crack is triggered, glass fragments fly to the right (out of the aquarium).
Water jet goes to the right and downward (affected by gravity).
Puddle forms on the floor to the right of the aquarium.
Let me now write the complete code. This will be long but I'll make it work.
Let me also think about the glass fragments more carefully. When the glass breaks:
- The right wall of the aquarium shatters
- Fragments are created along the crack line and the entire right wall
- Each fragment has:
- A polygon shape (2-4 vertices)
- Position (centered on the wall)
- Velocity (outward, with some randomness)
- Angular velocity (random)
- Size varies
The fragments should:
- Fly outward from the aquarium
- Be affected by gravity
- Slow down due to air resistance
- Bounce off the floor and walls
- If they hit water (before it drains), experience water resistance
- Eventually come to rest
For collision detection of fragments:
- Check against floor (y = FLOOR_Y)
- Check against left wall (x = AQUARIUM.x)
- Check against right wall of room (x = canvas.width)
- Check against top wall (y = 0 or AQUARIUM.y)
Actually, fragments can also fly into the room area, so they need collision with the room boundaries too.
Let me simplify: fragments collide with the floor and the walls of the room.
For the room, I'll define:
- Left boundary: x = 0
- Right boundary: x = canvas.width
- Floor: y = FLOOR_Y
- Ceiling: y = 0
Fragments bounce off these boundaries with some energy loss.
OK, I think I have enough detail planned. Let me write the code now.
One more consideration: the water in the aquarium should have a nice visual. I'll draw it as:
1. A filled blue shape from the bottom to the water level
2. With a wavy surface line
3. Slight gradient (lighter at top, darker at bottom)
4. Some light rays/shimmer effects
For the aquarium itself:
1. Draw the glass walls (transparent with slight blue tint)
2. Draw the frame/trim
3. When cracked, the right wall is missing (replaced by fragments)
Let me also add some background details to the room:
- A floor with wood texture (simple pattern)
- Wall color
- Maybe some furniture or decorations
OK, now I'm really going to write the code. Let me do it in one go.
I'll write it section by section in my head and then output the complete file.
Let me think about the physics timestep. I'll use:
- dt = 1/60 seconds (16.67ms)
- Fixed timestep for physics
- Render at requestAnimationFrame rate
For the water draining:
- Flow rate Q = A_crack * sqrt(2 * g * h)
- Where A_crack is the crack area (width * height of crack opening)
- h is the water depth above the crack
- The water level drops by dh/dt = -Q / A_tank
- Where A_tank is the cross-sectional area of the aquarium
So: dh/dt = -(crackWidth * sqrt(2 * g * h)) / tankWidth
This gives a nice decreasing flow rate as the tank drains.
For the jet particles:
- Spawn at the crack position
- Initial velocity: vx = sqrt(2 * g * h), vy = 0 (horizontal)
- Actually, the jet should have some spread. Let me add some vertical randomness.
- vx = sqrt(2 * g * h) + random(-spread, spread)
- vy = random(-spread, spread) * 0.5
For gravity affecting the jet:
- Particles follow parabolic trajectories
- vy increases downward due to gravity
- Particles curve downward as they travel
For the puddle:
- When a jet particle hits the floor (y >= FLOOR_Y), it becomes a puddle particle
- Puddle particles spread out on the floor
- The puddle grows as a circle/ellipse centered below the crack
Let me also add some splash particles when the jet hits the floor.
OK, I'm going to write the code now. For real this time.
Let me think about the structure of the JavaScript:
```javascript
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// Resize canvas
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// Constants
const GRAVITY = 800; // px/s^2
const AQUARIUM = { x: 100, y: 80, w: 500, h: 400 };
const FLOOR_Y = AQUARIUM.y + AQUARIUM.h; // 480
const WATER_DENSITY = 1000;
const AIR_DRAG = 0.99;
const WATER_DRAG = 0.95;
// State
let waterLevel = AQUARIUM.h; // starts full (0 = empty, AQUARIUM.h = full)
let crackY = AQUARIUM.y + AQUARIUM.h * 0.5; // middle of aquarium
let crackDragging = false;
let crackTriggered = false;
let crackWidth = 0; // how wide the opening is after breaking
let gameTime = 0;
let dt = 0;
// Objects
let fishList = [];
let rockList = [];
let plantList = [];
let floatingToy = null;
let fragments = [];
let jetParticles = [];
let splashParticles = [];
let bubbles = [];
let puddleRadius = 0;
// Input
let mouseX = 0, mouseY = 0;
let mouseDown = false;
// ... etc
```
Let me now write the complete file. I'll aim for a really impressive simulation.
Actually, I realize I should think more carefully about the water rendering. The water in the aquarium needs to look really good.
For the aquarium water:
1. Draw a clip region for the aquarium interior
2. Fill with blue gradient from bottom to water level
3. Draw wavy surface line
4. Add some light effects
For the water outside the aquarium (jet + puddle):
1. Jet particles are blue circles
2. Puddle is a blue ellipse on the floor
3. Splash particles are small blue dots
Let me also add some reflections on the glass walls.
OK, I'm going to write the complete code now. Let me be very thorough and make sure everything works correctly.
Let me think about one more thing: the fish behavior in more detail.
Each fish has:
- x, y position
- vx, vy velocity
- size (width, height)
- color
- swimSpeed (how fast they can swim)
- swimDir (1 or -1, facing direction)
- fatigue (how tired they are from swimming against current)
Fish behavior:
1. If underwater (y < waterLevel):
a. If there's current (crack is open), apply current force
b. Fish try to swim against current (apply force opposite to current)
c. If current > swim force, fish gets carried
d. Fish also have random wandering
2. If at water surface (y ≈ waterLevel):
a. Fish may jump if startled
3. If airborne (y > waterLevel):
a. Only gravity and air drag
b. Fish fall and eventually hit the floor
When fish hit the floor:
- They slide along the floor
- They try to swim if there's still water nearby
For the floating toy:
- Very buoyant, sits on the water surface
- When water level drops below it, it becomes airborne
- Gets carried by the current when in water
- Floats on the surface otherwise
For rocks:
- Very dense, sink to the bottom
- Sit on the floor
- When water drains, they're on the floor
- They don't move much (high drag, low buoyancy)
For plants:
- Rooted to the bottom
- Sway with current
- When water drains, they're left on the floor
- They bend/lean with the current
OK, I think I have a comprehensive plan. Let me write the code now.
Let me also think about the visual quality of the glass aquarium:
- Draw the aquarium frame (dark gray/black border)
- Glass walls: very light blue, semi-transparent
- When intact: all four walls visible
- When cracked: right wall is gone (fragments flying out)
- Glass shine: a white highlight streak on the glass
For the crack line before triggering:
- Draw a jagged red line on the right wall
- Show it can be dragged
Let me write the complete code now. I'll be very careful with all the details.
Here's my final plan for the code structure:
1. HTML + CSS (minimal, just canvas and UI overlay)
2. Canvas setup and resize
3. Constants and state variables
4. Initialization function (create fish, rocks, plants, toy)
5. Input handling (mouse/touch for crack dragging)
6. Physics update:
a. Water level update
b. Jet particle update
c. Puddle update
d. Object physics (fish, rocks, plants, toy)
e. Fragment physics
f. Bubble update
7. Rendering:
a. Room background
b. Aquarium frame
c. Water in aquarium
d. Objects in aquarium
e. Crack line (if not triggered)
f. Glass fragments (if triggered)
g. Water jet particles
h. Puddle
i. Bubbles
j. UI overlay
8. Game loop (requestAnimationFrame)
Let me write it all now. This will be a long file but it needs to be complete and working.
One more thing: I should add some sound effects using the Web Audio API. Actually, that might be too complex and could cause issues. Let me skip sound and focus on visuals and physics.
Let me also think about the initial state:
- Aquarium is full of water
- Fish are swimming around
- Rocks are on the bottom
- Plants are rooted to the bottom
- Floating toy is on the surface
- Crack line is visible on the right wall at mid-height
- User can drag the crack line up or down
- "TRIGGER FAILURE" button is available once crack is positioned
- When triggered, glass shatters, water starts flowing
Let me write the code now.
For the fish, I'll create 3-4 fish with different colors and sizes. Each fish will have:
- A simple fish shape drawn with canvas paths
- Swimming animation (tail fin movement)
- Color variation
For rocks, I'll create 3-4 rocks with irregular shapes.
For plants, I'll create 2-3 plants with leaf shapes.
For the floating toy, I'll draw a small rubber duck or boat shape.
Let me write the rendering code for each object type.
Fish rendering:
```javascript
function drawFish(fish, ctx) {
ctx.save();
ctx.translate(fish.x, fish.y);
ctx.scale(fish.swimDir, 1);
// Body
ctx.fillStyle = fish.color;
ctx.beginPath();
ctx.ellipse(0, 0, fish.size, fish.size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Tail
ctx.beginPath();
ctx.moveTo(-fish.size, 0);
ctx.lineTo(-fish.size * 1.5, -fish.size * 0.4);
ctx.lineTo(-fish.size * 1.5, fish.size * 0.4);
ctx.closePath();
ctx.fill();
// Eye
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(fish.size * 0.4, -fish.size * 0.1, fish.size * 0.15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(fish.size * 0.45, -fish.size * 0.1, fish.size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
```
OK, I think I have enough planning. Let me write the complete HTML file now.
I need to be careful about:
1. Making sure the crack dragging works correctly
2. Making sure the physics is stable
3. Making sure the water drains properly
4. Making sure objects transition correctly
5. Making sure the glass shattering looks good
6. Making sure the jet and puddle look good
Let me write the code. I'll try to keep it well-organized and commented.
Here's the complete code I'll write:
Actually, let me reconsider the aquarium dimensions. I want the aquarium to be large and prominent on screen. Let me use:
Canvas: full window (responsive)
Aquarium: centered, taking up about 40% of the width and 50% of the height
AQUARIUM = { x: canvas.width * 0.15, y: canvas.height * 0.1, w: canvas.width * 0.4, h: canvas.height * 0.55 }
FLOOR_Y = AQUARIUM.y + AQUARIUM.h
This will make the aquarium a good size and leave room for the room environment and the puddle.
Let me write the code now. For real this time. No more planning.
I'll write it section by section and assemble the complete file.
Let me also think about how to handle the crack dragging more carefully:
1. The crack is shown as a vertical line on the right wall of the aquarium
2. The line has a grab handle (a small circle) that the user can click and drag
3. When dragging, the crack line follows the mouse y position
4. The crack line is constrained to be within the aquarium's vertical range
5. A small indicator shows the crack position (high = weak jet, low = strong jet)
6. The "TRIGGER FAILURE" button becomes active when the crack is positioned
When the user clicks "TRIGGER FAILURE":
1. The crack line disappears
2. Glass fragments are created along the right wall
3. Fragments fly outward with angular velocity
4. Water starts flowing through the opening
5. The aquarium's right wall is now "open" at the crack position
The crack opening width could be proportional to the crack height (the crack was a line, but after breaking, it becomes an opening). Let me make the opening width about 20-30px.
Actually, for the crack, let me make it a vertical line that the user drags. When triggered, the glass along that vertical line breaks, creating an opening. The opening height is about 30px (the crack line thickness).
Wait, that doesn't make sense for the crack being draggable. Let me reconsider:
The crack is a vertical line on the right wall. The user can drag this line up and down. When triggered:
- The glass along the right wall at the crack position breaks
- The opening is a vertical slit at the crack position
- Water flows out through this slit
- The slit height could be the crack length (let's say 40px)
So the crack has a position (y) and a fixed length (40px). The user drags the center of the crack up and down.
This makes sense. The lower the crack, the more water above it, the stronger the jet.
Let me implement this.
For the glass fragments:
- When the glass breaks, create fragments from the right wall area
- The right wall is at x = AQUARIUM.x + AQUARIUM.w
- Fragments are created in the region from (crackY - 20) to (crackY + 20) vertically, and across the wall thickness
- Each fragment is a small polygon
- They fly outward (to the right) with some velocity
Let me now write the complete code. I'll aim for about 600-800 lines of JavaScript.
Let me start writing the actual code now. I'll be very careful and thorough.
```javascript
// ============ SETUP ============
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ============ CONSTANTS ============
const G = 700; // gravity px/s^2
const AQUA = { x: 0, y: 0, w: 0, h: 0 }; // set in resize
const FLOOR = 0; // set in resize
const CRACK_LEN = 35; // crack height in pixels
const CRACK_OPEN = 8; // opening width after break
const MAX_JET_PARTICLES = 500;
const MAX_BUBBLES = 100;
// ============ STATE ============
let waterLevel = 1; // 0 to 1 (fraction of aquarium height)
let crackY = 0.5; // 0 to 1 (fraction of aquarium height from top)
let crackDragging = false;
let crackTriggered = false;
let crackBreakTime = 0;
let gameTime = 0;
let lastTime = 0;
// ============ OBJECTS ============
let fish = [];
let rocks = [];
let plants = [];
let toy = null;
let fragments = [];
let jetParticles = [];
let splashParticles = [];
let bubbles = [];
let puddleX = 0, puddleY = 0, puddleSize = 0;
// ============ INPUT ============
let mouse = { x: 0, y: 0, down: false };
// ============ INIT ============
function init() {
// Set aquarium dimensions based on canvas
AQUA.x = canvas.width * 0.12;
AQUA.y = canvas.height * 0.08;
AQUA.w = canvas.width * 0.38;
AQUA.h = canvas.height * 0.62;
FLOOR = AQUA.y + AQUA.h;
// Reset state
waterLevel = 1;
crackY = 0.5;
crackTriggered = false;
crackBreakTime = 0;
gameTime = 0;
jetParticles = [];
splashParticles = [];
bubbles = [];
fragments = [];
puddleSize = 0;
// Create fish
fish = [];
for (let i = 0; i < 5; i++) {
fish.push({
x: AQUA.x + 50 + Math.random() * (AQUA.w - 100),
y: AQUA.y + 30 + Math.random() * (AQUA.h - 60),
vx: (Math.random() - 0.5) * 40,
vy: (Math.random() - 0.5) * 20,
size: 8 + Math.random() * 6,
color: ['#e74c3c', '#3498db', '#f1c40f', '#2ecc71', '#9b59b6'][i],
dir: Math.random() > 0.5 ? 1 : -1,
swimForce: 60 + Math.random() * 40,
fatigue: 0,
tailPhase: Math.random() * Math.PI * 2
});
}
// Create rocks
rocks = [];
for (let i = 0; i < 4; i++) {
rocks.push({
x: AQUA.x + 30 + Math.random() * (AQUA.w - 60),
y: FLOOR - 5 - Math.random() * 20,
vx: 0, vy: 0,
size: 8 + Math.random() * 12,
color: ['#7f8c8d', '#95a5a6', '#6c7a7a', '#8e8e8e'][i],
density: 2.5 + Math.random() * 1.5, // > 1 = sinks
drag: 0.98
});
}
// Create plants
plants = [];
for (let i = 0; i < 3; i++) {
plants.push({
x: AQUA.x + 60 + Math.random() * (AQUA.w - 120),
y: FLOOR,
height: 30 + Math.random() * 40,
color: ['#27ae60', '#2ecc71', '#1abc9c'][i],
sway: 0,
swaySpeed: 1 + Math.random() * 2
});
}
// Create floating toy
toy = {
x: AQUA.x + AQUA.w * 0.3,
y: FLOOR - 5 - 15, // on the surface
vx: 0, vy: 0,
size: 12,
buoyancy: 1.5, // > 1 = floats
drag: 0.97
};
}
```
OK this is getting very long in planning. Let me just write the complete code directly now.
Let me write the complete HTML file. I'll be comprehensive and make sure everything works.
I need to be careful about:
1. The crack dragging interaction
2. The physics of water draining
3. The jet particle system
4. The puddle formation
5. Object physics with buoyancy
6. Glass fragment physics
7. Rendering everything nicely
Let me write it all now. I'll output the complete file.
For the crack dragging, I need to detect when the mouse is near the crack line on the right wall of the aquarium, and allow dragging.
The crack line is at x = AQUA.x + AQUA.w, y = AQUA.y + crackY * AQUA.h
When the user clicks near this line (within ~10px), they can drag it up and down.
When the user clicks "TRIGGER FAILURE", the glass breaks.
Let me also think about what happens after the tank is fully drained. The simulation should continue showing the aftermath: objects on the floor, puddle, glass fragments, etc.
OK, writing the complete code now.
Let me think about one more thing: the water rendering in the aquarium. I want it to look good.
For the water in the aquarium:
1. Draw a blue gradient rectangle from the bottom to the water level
2. Add a wavy surface line
3. Add some light rays from above
4. Add some subtle wave animation
For the water surface wave:
```javascript
ctx.beginPath();
ctx.moveTo(AQUA.x, waterSurfaceY);
for (let x = AQUA.x; x <= AQUA.x + AQUA.w; x += 2) {
let waveY = waterSurfaceY + Math.sin(x * 0.05 + gameTime * 2) * 2 + Math.sin(x * 0.03 + gameTime * 1.5) * 1.5;
ctx.lineTo(x, waveY);
}
ctx.lineTo(AQUA.x + AQUA.w, AQUA.y + AQUA.h);
ctx.lineTo(AQUA.x, AQUA.y + AQUA.h);
ctx.closePath();
ctx.fill();
```
This will give a nice wavy water surface.
For the water color, I'll use a gradient from lighter blue at the top to darker blue at the bottom.
OK, I'm going to write the complete code now. No more planning.
Let me write it all at once, carefully.
Actually, one more critical thing: I need to make sure the aquarium is drawn with glass-like walls. The glass should be:
- Slightly transparent
- Slight blue tint
- A shine/highlight on one side
- When cracked, the right wall is missing
For the glass walls, I'll draw them as semi-transparent rectangles with a white highlight line.
Let me also add a floor for the room. The floor should be a wooden brown color, and the puddle should be visible on it.
OK, writing the complete code now.
Let me also think about the UI more carefully. I want:
1. A control panel in the top-right corner
2. Instructions at the bottom
3. Real-time stats (water level, jet strength)
The control panel should have:
- Title: "Aquarium Breach"
- Instructions for crack dragging
- Current crack position indicator
- TRIGGER FAILURE button (enabled when crack is positioned)
- RESET button
Let me finalize the UI design and write the complete code.
I think I've planned enough. Let me write the complete HTML file now.
I need to be very careful about the physics and make sure everything is consistent. Let me think through the key physics one more time:
**Water Level Update:**
- Each frame, calculate flow rate based on Torricelli's law
- dh/dt = -C * sqrt(2 * g * h) where h is depth above crack, C is a constant
- Update waterLevel accordingly
- WaterLevel is in pixels (0 to AQUA.h)
**Jet Particle Emission:**
- When crack is triggered and waterLevel > crackPosition:
- Spawn particles at the crack opening
- Initial velocity: vx = sqrt(2 * g * h_depth), vy = small random
- h_depth = waterLevel - (crackY - AQUA.y) (depth of water above crack)
- As waterLevel drops, h_depth decreases, so velocity decreases
**Jet Particle Physics:**
- Each particle: x, y, vx, vy
- vy += G * dt (gravity)
- x += vx * dt, y += vy * dt
- Lifetime decreases
- Remove when lifetime expires or out of bounds
**Puddle:**
- When jet particles hit the floor, they contribute to the puddle
- Puddle grows as a circle/ellipse
- Puddle is drawn on the floor
**Object Physics:**
- Each object has x, y, vx, vy
- Forces: gravity (down), buoyancy (up if submerged), drag (opposite velocity), current (toward crack)
- When submerged: buoyancy = (1 - density/waterDensity) * g * volume
- If density < 1 (fish, toy): net upward force
- If density > 1 (rock): net downward force
- If density ≈ 1: neutral buoyancy
- Drag: F_drag = -drag_coefficient * v
- Current: F_current = current_strength * (toward crack direction)
**Object Transitions:**
- If object.y > waterLevel (in aquarium coords): in air
- Only gravity and air drag
- If object.y <= waterLevel: in water
- Gravity, buoyancy, water drag, current
- If object is on the floor (y >= FLOOR - object.size): on floor
- Gravity, friction
**Fish Swimming Against Current:**
- Fish apply a swim force in the direction opposite to the current
- If current force > swim force, fish gets swept
- Fish have some random wandering when no current
**Glass Fragments:**
- Created when glass breaks
- Each fragment: x, y, vx, vy, angle, angularVel, vertices
- Affected by gravity, air drag, water drag (if underwater)
- Bounce off walls and floor with energy loss
- Eventually come to rest
OK, I'm confident in the physics. Let me write the complete code now.
I'll write it as one continuous HTML file with everything inline.
Let me also add some nice visual effects:
- Caustic light patterns on the floor from the water
- Bubbles rising in the aquarium
- Splash particles when jet hits the floor
- Glass shine effect
- Fish tail fin animation
Here goes the complete code:
Wait, I should also think about how to handle the crack line interaction. When the user hovers over the crack line on the right wall, the cursor should change to a grab cursor. When they click and drag, the crack line follows.
For the crack line rendering before triggering:
- Draw a red/orange jagged line on the right wall
- Draw a grab handle (circle) that the user can drag
- The crack line should be clearly visible and interactive
Let me also add a visual indicator of the crack position - maybe a small label showing "JET STRENGTH: STRONG/MEDIUM/WEAK" based on the crack position.
OK, writing the complete code now. For real this time.
Let me write the complete file. I'll organize it well and make sure everything works.
I realize I should also handle the case where the aquarium is on a surface (floor). The aquarium bottom is at FLOOR_Y, and objects inside sit on the aquarium floor. When water drains, objects that were submerged become exposed.
For the aquarium interior:
- The floor of the aquarium is at y = AQUA.y + AQUA.h
- Objects sit on this floor when their y position reaches the bottom
- Water fills from the bottom up to waterLevel
Wait, I need to be careful about coordinate systems. Let me define:
- y increases downward (standard canvas coordinates)
- AQUA.y is the top of the aquarium
- AQUA.y + AQUA.h is the bottom of the aquarium (which is the floor)
- waterLevel is the height of the water surface from the top of the aquarium
- So water surface y = AQUA.y + (AQUA.h - waterLevel * AQUA.h) = AQUA.y + AQUA.h * (1 - waterLevel)
Actually, let me simplify. Let me define waterLevel as the y-coordinate of the water surface. When full, waterLevel = AQUA.y (top of aquarium). When empty, waterLevel = AQUA.y + AQUA.h (bottom).
So:
- waterLevel = AQUA.y when full
- waterLevel = AQUA.y + AQUA.h when empty
- Water fills from waterLevel down to AQUA.y + AQUA.h
This is more intuitive for rendering.
For physics:
- An object is submerged if its y position is below waterLevel (y > waterLevel)
- Water depth above a point = waterLevel - point.y (negative means above water)
- Actually, depth below surface = waterLevel - y (for y > waterLevel, this is positive)
Wait, that's confusing. Let me use a different convention:
Let me define:
- waterSurfaceY = y-coordinate of the water surface
- When aquarium is full, waterSurfaceY = AQUA.y (top of aquarium)
- When empty, waterSurfaceY = AQUA.y + AQUA.h (bottom)
- Objects below the surface have y > waterSurfaceY
For Torricelli's law:
- Depth of water above a point at y = waterSurfaceY - y
- Wait, that's wrong. Depth above the crack = waterSurfaceY - crackY... no.
Let me think again. The crack is at position crackY (a y-coordinate on the right wall). The water surface is at waterSurfaceY. The depth of water above the crack is:
h = waterSurfaceY - crackY (if crackY < waterSurfaceY, i.e., crack is below the surface)
Wait no. In canvas coordinates, y increases downward. So the water surface is at a smaller y value than the crack (if the crack is below the surface).
h = crackY - waterSurfaceY (depth of water above the crack)
If crackY > waterSurfaceY, the crack is submerged and water flows out.
If crackY < waterSurfaceY, the crack is above the water and nothing flows.
Flow velocity v = sqrt(2 * G * h) where h = crackY - waterSurfaceY
This makes sense. Lower crack (larger crackY) → more depth → stronger jet. ✓
OK, I'll use this convention. Let me finalize:
- waterSurfaceY: y-coordinate of the water surface (starts at AQUA.y when full)
- crackY: y-coordinate of the crack (starts at AQUA.y + AQUA.h * 0.5)
- Depth above crack: h = crackY - waterSurfaceY (positive when submerged)
- Flow velocity: v = sqrt(2 * G * max(0, h))
Water level change:
- Flow rate Q = crackWidth * v (volume per second)
- Water surface drops: d(waterSurfaceY)/dt = Q / AQUA.w (positive = surface goes down)
- So waterSurfaceY += (crackWidth * v) / AQUA.w * dt
Wait, I need to be more careful. The volume flow rate is Q = A_crack * v, where A_crack is the area of the opening.
The change in water volume in the tank: dV/dt = -Q
V = AQUA.w * (waterSurfaceY - AQUA.y) ... no wait.
The volume of water in the tank = AQUA.w * (AQUA.y + AQUA.h - waterSurfaceY)
So dV/dt = -AQUA.w * d(waterSurfaceY)/dt
And dV/dt = -Q = -A_crack * v = -crackWidth * sqrt(2 * G * h)
So: -AQUA.w * d(waterSurfaceY)/dt = -crackWidth * sqrt(2 * G * h)
d(waterSurfaceY)/dt = (crackWidth * sqrt(2 * G * h)) / AQUA.w
Since h = crackY - waterSurfaceY, and waterSurfaceY increases as the tank drains (water level goes down in canvas coords), this is correct.
OK, this all makes sense. Let me write the code now.
For the aquarium water rendering:
- Draw a filled rectangle from (AQUA.x, waterSurfaceY) to (AQUA.x + AQUA.w, AQUA.y + AQUA.h)
- Add a wavy surface at waterSurfaceY
- Use a blue gradient
For the jet:
- Spawn particles at x = AQUA.x + AQUA.w, y = crackY
- Initial velocity: vx = sqrt(2 * G * h) * 0.8 (some efficiency loss), vy = random(-10, 10)
- Particles are affected by gravity
For the puddle:
- When particles hit the floor, they add to the puddle
- Puddle is an ellipse centered at (AQUA.x + AQUA.w + puddleSize/2, FLOOR)
- Puddle grows over time
OK, I'm writing the code now. This is the final time I'm saying this. Let me just write it.
Let me also think about the room environment. I want:
- A dark room with walls
- A wooden floor
- The aquarium sits on the floor
- Maybe some decorative elements (a shelf, a window)
For simplicity, I'll draw:
- Dark room background
- Wooden floor (brown gradient)
- The aquarium on the floor
- Maybe a window or picture frame on the wall
This will make the scene more immersive.
OK, writing the code now. Final answer coming up.
Let me think about one more thing: the fish swimming animation. I want the fish to look like they're actually swimming. I'll draw them with:
- An elliptical body
- A tail fin that oscillates
- A dorsal fin
- An eye
- The fish faces left or right based on swimming direction
The tail fin oscillation can be done with a sine wave based on time and the fish's swimming speed.
For plants, I'll draw them as green stems with leaf shapes that sway with the current.
For the floating toy, I'll draw a simple rubber duck shape.
For rocks, I'll draw irregular oval shapes in gray/brown.
OK, I'm going to write the complete code now. Let me be thorough and make sure everything works.
Let me think about the total line count. This will probably be 800-1200 lines of code. That's fine for a single HTML file.
Let me write it now.
Actually, I want to make sure the crack dragging works well. Here's my plan:
1. Draw the crack as a vertical line on the right wall of the aquarium
2. The line has a thickness of about 4px and is red/orange
3. There's a small circular handle at the center of the line
4. When the user clicks within 15px of the line, they start dragging
5. The crackY follows the mouse y position, constrained to the aquarium's vertical range
6. The crack line is drawn as a jagged/irregular line (to look like a crack)
When the user clicks "TRIGGER FAILURE":
1. The crack line disappears
2. Glass fragments are created
3. Water starts flowing
4. The "TRIGGER" button changes to "BROKEN" or is disabled
Let me implement this.
For the crack line, I'll draw it as a series of small line segments with slight offsets to make it look like a crack:
```javascript
function drawCrack(ctx) {
const cx = AQUA.x + AQUA.w;
const cy = AQUA.y + crackY * AQUA.h;
const len = CRACK_LEN;
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.beginPath();
for (let i = -len/2; i <= len/2; i += 3) {
const offset = Math.sin(i * 0.5) * 3;
if (i === -len/2) ctx.moveTo(cx + offset, cy + i);
else ctx.lineTo(cx + offset, cy + i);
}
ctx.stroke();
// Grab handle
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(cx, cy, 8, 0, Math.PI * 2);
ctx.fill();
}
```
OK, I'm writing the complete code now. No more deliberation.
Let me write the complete HTML file. I'll include everything in one go.
I need to be careful about:
1. Variable initialization order
2. Canvas coordinate system consistency
3. Physics stability (clamp dt)
4. Memory management (remove dead particles)
5. Visual quality
Here's the complete code:
Let me write it now. I'll be thorough but efficient.
Let me think about one more visual element: the water surface in the aquarium when it's draining. I want the surface to have waves that get calmer as the water level drops. And maybe some ripples near the crack.
Also, I should add some light rays coming through the aquarium (caustics effect). This can be done with subtle gradient overlays.
And I should add some reflection on the glass walls.
OK, writing the complete code now. For real. No more planning.
Let me write it section by section:
1. HTML structure
2. CSS styles
3. Canvas setup
4. Constants and state
5. Initialization
6. Input handling
7. Physics update
8. Rendering
9. Game loop
Let me write each section carefully.
For the physics update, I need to be careful about the order of operations:
1. Update water level
2. Update jet particles
3. Update splash particles
4. Update bubbles
5. Update objects (fish, rocks, plants, toy)
6. Update fragments
7. Update puddle
8. Update current in the aquarium (for fish behavior)
The current in the aquarium is a horizontal force pointing to the right (toward the crack). Its strength depends on the flow rate.
currentStrength = crackWidth * sqrt(2 * G * max(0, crackY - waterSurfaceY)) / AQUA.w
This gives a current that pushes objects toward the crack (rightward).
Fish try to swim against this current (leftward force). If the current is too strong, they get swept.
OK, writing the complete code now.
I realize I should also handle the case where the crack is above the water level. In that case, no water flows. The user can position the crack anywhere, but only cracks below the water surface produce flow.
Also, I should show a visual indicator of whether the crack is submerged or not. Maybe the crack line turns green when submerged and red when above water.
Let me also add a small indicator showing the flow rate or jet strength.
OK, I'm writing the complete code now. Here goes.
One final thought on the glass fragments: I want them to look like real broken glass. Each fragment should be:
- A small polygon (3-5 vertices)
- Light blue/white color with some transparency
- Has a subtle shine highlight
- Flies outward with rotation
- Bounces off surfaces
- Eventually comes to rest
For the fragment creation:
- Create fragments along the right wall from (crackY - CRACK_LEN/2) to (crackY + CRACK_LEN/2)
- Also create some fragments from the rest of the right wall (since the whole panel bursts)
- Each fragment gets random velocity outward (to the right) and upward/downward
- Random angular velocity
- Fragments are affected by gravity and drag
For collision with the floor:
- If fragment bottom edge hits FLOOR, bounce with energy loss
- If fragment hits the left wall (AQUA.x), bounce
- If fragment hits the right wall (canvas.width), bounce
- If fragment hits the ceiling (0), bounce
For fragment rotation:
- Each frame, angle += angularVel * dt
- angularVel decreases over time due to friction
OK, writing the complete code now. I'll aim for a comprehensive, working simulation.
Let me write it all in one go. This will be a long file but it needs to be complete and working.
Let me also make sure the reset button works properly, resetting all state and reinitializing everything.
And the trigger button should only be clickable once (after that, the simulation is running).
OK here we go. Writing the complete HTML file now.## 1:index.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 Breach Simulation</title> <style> *{margin:0;padding:0;box-sizing:border-box} body{background:#0d1b2a;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;user-select:none} canvas{display:block;cursor:default} #ui{position:fixed;top:16px;right:16px;background:rgba(10,20,40,0.88);color:#c8d6e5;padding:14px 18px;border-radius:12px;border:1px solid rgba(100,180,255,0.15);font-size:13px;line-height:1.7;backdrop-filter:blur(8px);min-width:210px;z-index:10;box-shadow:0 4px 24px rgba(0,0,0,0.4)} #ui h3{margin-bottom:8px;color:#7ec8e3;font-size:15px;letter-spacing:0.5px} #ui .row{display:flex;justify-content:space-between;align-items:center;margin:3px 0} #ui .val{color:#5dade2;font-weight:600;font-variant-numeric:tabular-nums} #ui .bar{width:80px;height:6px;background:rgba(255,255,255,0.08);border-radius:3px;overflow:hidden;display:inline-block;vertical-align:middle;margin-left:6px} #ui .bar-fill{height:100%;border-radius:3px;transition:width 0.3s} #ui button{margin-top:10px;padding:7px 16px;background:linear-gradient(135deg,#c0392b,#e74c3c);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:0.3px;transition:all 0.2s;width:100%} #ui button:hover:not(:disabled){background:linear-gradient(135deg,#a93226,#c0392b);transform:translateY(-1px)} #ui button:disabled{background:#3a3a3a;color:#666;cursor:not-allowed;transform:none} #ui button.secondary{margin-top:6px;background:linear-gradient(135deg,#1a5276,#2980b9)} #ui button.secondary:hover:not(:disabled){background:linear-gradient(135deg,#154360,#2471a3)} #hint{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:rgba(10,20,40,0.82);color:#85929e;padding:8px 20px;border-radius:20px;font-size:12px;border:1px solid rgba(100,180,255,0.1);z-index:10;pointer-events:none;white-space:nowrap;transition:opacity 0.5s} #status{position:fixed;top:16px;left:50%;transform:translateX(-50%);background:rgba(10,20,40,0.85);color:#e74c3c;padding:6px 18px;border-radius:20px;font-size:13px;font-weight:600;border:1px solid rgba(231,76,60,0.3);z-index:10;opacity:0;transition:opacity 0.4s;pointer-events:none} #status.show{opacity:1} </style> </head> <body> <canvas id="c"></canvas> <div id="ui"> <h3>🐠 Aquarium Breach</h3> <div class="row"><span>Water Level</span><span class="val" id="wl">100%</span></div> <div class="row"><span>Jet Power</span><span class="val" id="jp">—</span></div> <div class="row"><span>Crack Depth</span><span class="val" id="cd">—</span></div> <div class="row"><span>Flow Rate</span><span class="val" id="fr">0 L/s</span></div> <div class="row"><span>Crack Position</span><span class="val" id="cp">Mid</span></div> <div class="row"><span>Status</span><span class="val" id="st" style="color:#2ecc71">Ready</span></div> <button id="triggerBtn" disabled>⚡ TRIGGER FAILURE</button> <button id="resetBtn" class="secondary">🔄 Reset</button> </div> <div id="hint">Drag the crack line on the tank wall to position it, then click TRIGGER FAILURE</div> <div id="status"></div> <script> (function(){ 'use strict'; /* ============================================================ CONFIGURATION — all tunable constants in one place ============================================================ */ const CFG = { gravity: 750, tank: { xRatio: 0.10, yRatio: 0.07, wRatio: 0.38, hRatio: 0.60 }, crackLen: 38, crackOpenWidth: 10, crackHeight: 6, maxJetParticles: 600, maxBubbles: 120, maxFragments: 120, maxSplash: 200, puddleGrowthRate: 0.8, puddleMaxRadius: 350, waterDensity: 1000, fishCount: 5, rockCount: 5, plantCount: 4, dt: 1 / 60, subSteps: 3, wallThickness: 8, aquariumFrameWidth: 14, aquariumFrameHeight: 14, dragThreshold: 18, breakAnimDuration: 0.6, breakAnimDurationLong: 1.2, fishWanderStrength: 25, fishSwimStrength: 80, fishMaxSpeed: 120, fishFatigueDecay: 0.3, fishBounceDamp: 0.3, fishFloorFriction: 0.92, fishWaterDrag: 0.92, fishAirDrag: 0.995, fragBounce: 0.4, fragAirDrag: 0.995, fragWaterDrag: 0.94, fragAngDamp: 0.97, fragFloorFriction: 0.85, fragMinSize: 6, fragMaxSize: 18, toyBuoyancy: 1.6, toyDrag: 0.96, rockDrag: 0.96, plantSwayStrength: 0.6, bubbleRiseSpeed: 30, bubbleSizeRange: [2, 5], splashSpeed: 80, splashLifetime: 0.5, jetSpread: 0.15, jetVMin: 0.3, jetVMax: 0.7, waterColorTop: [135, 206, 250], waterColorBot: [15, 60, 120], waterSurfaceAlpha: 0.35, glassAlpha: 0.12, glassHighlightAlpha: 0.25, causticAlpha: 0.08, fragColorBase: [180, 220, 255], fragColorVar: 40, roomBg: '#0d1b2a', floorColor: '#3d2b1f', floorHighlight: '#5a3e2b', wallColor: '#1b2838', crackColor: '#e74c3c', crackHandleColor: '#ff6b6b', fishColors: ['#e74c3c','#3498db','#f1c40f','#2ecc71','#e67e22','#9b59b6','#1abc9c'], rockColors: ['#7f8c8d','#95a5a6','#6c7a7a','#8e8e8e','#5d6d7e'], plantColors: ['#27ae60','#2ecc71','#1abc9c','#229954'], toyColor: '#e74c3c', toyHighlight: '#f5b7b1', toyEye: '#2c3e50', }; /* ============================================================ CANVAS SETUP ============================================================ */ const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); let W, H; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; recalcLayout(); } window.addEventListener('resize', resize); /* ============================================================ LAYOUT — computed from canvas size ============================================================ */ let aqua, floorY, crackX; function recalcLayout() { aqua = { x: W * CFG.tank.xRatio, y: H * CFG.tank.yRatio, w: W * CFG.tank.wRatio, h: H * CFG.tank.hRatio }; floorY = aqua.y + aqua.h; crackX = aqua.x + aqua.w; } /* ============================================================ STATE ============================================================ */ let waterSurfY; let crackY; let crackDragging = false; let crackTriggered = false; let breakProgress = 0; let breakStartTime = 0; let gameTime = 0; let lastTimestamp = 0; let hasTriggered = false; /* ============================================================ OBJECT POOLS — pre-allocated arrays for performance ============================================================ */ const jetPool = new Array(CFG.maxJetParticles); const splashPool = new Array(CFG.maxSplash); const bubblePool = new Array(CFG.maxBubbles); const fragPool = new Array(CFG.maxFragments); function initPool(pool, factory) { for (let i = 0; i < pool.length; i++) { pool[i] = factory(); pool[i].alive = false; } } initPool(jetPool, () => ({ x:0,y:0,vx:0,vy:0,life:0,maxLife:0,size:0,alive:false })); initPool(splashPool, () => ({ x:0,y:0,vx:0,vy:0,life:0,maxLife:0,size:0,alive:false })); initPool(bubblePool, () => ({ x:0,y:0,r:0,vy:0,life:0,maxLife:0,alive:false })); initPool(fragPool, () => ({ x:0,y:0,vx:0,vy:0,angle:0,angVel:0, verts:[],size:0,alive:false,onFloor:false, restingTime:0 })); /* ============================================================ ENTITY ARRAYS ============================================================ */ let fish = []; let rocks = []; let plants = []; let toy = null; let puddleRadius = 0; let puddleX = 0; /* ============================================================ INPUT ============================================================ */ const mouse = { x: 0, y: 0, down: false, overCrack: false }; let hintVisible = true; function getMousePos(e) { const r = canvas.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; } canvas.addEventListener('mousedown', e => { const p = getMousePos(e); mouse.down = true; mouse.x = p.x; mouse.y = p.y; if (!crackTriggered && !hasTriggered) { const cy = aqua.y + crackY * aqua.h; if (Math.abs(p.x - crackX) < CFG.dragThreshold && Math.abs(p.y - cy) < CFG.crackLen / 2 + 12) { crackDragging = true; if (hintVisible) { hintVisible = false; document.getElementById('hint').style.opacity = '0'; } } } }); canvas.addEventListener('mousemove', e => { const p = getMousePos(e); mouse.x = p.x; mouse.y = p.y; if (!crackTriggered && !hasTriggered) { const cy = aqua.y + crackY * aqua.h; mouse.overCrack = Math.abs(p.x - crackX) < CFG.dragThreshold && Math.abs(p.y - cy) < CFG.crackLen / 2 + 12; canvas.style.cursor = mouse.overCrack ? 'ns-resize' : 'default'; } else { canvas.style.cursor = 'default'; } if (crackDragging) { crackY = Math.max(0.05, Math.min(0.95, (p.y - aqua.y) / aqua.h)); } }); canvas.addEventListener('mouseup', () => { crackDragging = false; }); canvas.addEventListener('touchstart', e => { e.preventDefault(); const t = e.touches[0]; const r = canvas.getBoundingClientRect(); const p = { x: t.clientX - r.left, y: t.clientY - r.top }; mouse.down = true; mouse.x = p.x; mouse.y = p.y; if (!crackTriggered && !hasTriggered) { const cy = aqua.y + crackY * aqua.h; if (Math.abs(p.x - crackX) < CFG.dragThreshold * 1.5 && Math.abs(p.y - cy) < CFG.crackLen / 2 + 20) { crackDragging = true; if (hintVisible) { hintVisible = false; document.getElementById('hint').style.opacity = '0'; } } } }, { passive: false }); canvas.addEventListener('touchmove', e => { e.preventDefault(); const t = e.touches[0]; const r = canvas.getBoundingClientRect(); mouse.x = t.clientX - r.left; mouse.y = t.clientY - r.top; if (crackDragging) { crackY = Math.max(0.05, Math.min(0.95, (mouse.y - aqua.y) / aqua.h)); } }, { passive: false }); canvas.addEventListener('touchend', () => { crackDragging = false; }); document.getElementById('triggerBtn').addEventListener('click', triggerBreak); document.getElementById('resetBtn').addEventListener('click', resetSim); /* ============================================================ INITIALIZATION ============================================================ */ function init() { recalcLayout(); waterSurfY = aqua.y; crackY = 0.5; crackTriggered = false; breakProgress = 0; hasTriggered = false; breakStartTime = 0; gameTime = 0; puddleRadius = 0; puddleX = crackX + 20; // Reset pools for (let i = 0; i < jetPool.length; i++) jetPool[i].alive = false; for (let i = 0; i < splashPool.length; i++) splashPool[i].alive = false; for (let i = 0; i < bubblePool.length; i++) bubblePool[i].alive = false; for (let i = 0; i < fragPool.length; i++) fragPool[i].alive = false; createFish(); createRocks(); createPlants(); createToy(); document.getElementById('triggerBtn').disabled = false; document.getElementById('triggerBtn').textContent = '⚡ TRIGGER FAILURE'; document.getElementById('st').textContent = 'Ready'; document.getElementById('st').style.color = '#2ecc71'; document.getElementById('hint').style.opacity = '1'; hintVisible = true; canvas.style.cursor = 'default'; } function createFish() { fish = []; for (let i = 0; i < CFG.fishCount; i++) { fish.push({ x: aqua.x + 40 + Math.random() * (aqua.w - 80), y: aqua.y + 30 + Math.random() * (aqua.h - 60), vx: (Math.random() - 0.5) * 50, vy: (Math.random() - 0.5) * 20, size: 7 + Math.random() * 5, color: CFG.fishColors[i % CFG.fishColors.length], dir: Math.random() > 0.5 ? 1 : -1, swimForce: 55 + Math.random() * 35, fatigue: 0, tailPhase: Math.random() * Math.PI * 2, submerged: true }); } } function createRocks() { rocks = []; for (let i = 0; i < CFG.rockCount; i++) { rocks.push({ x: aqua.x + 25 + Math.random() * (aqua.w - 50), y: floorY - 3 - Math.random() * 15, vx: 0, vy: 0, size: 6 + Math.random() * 10, color: CFG.rockColors[i % CFG.rockColors.length], density: 2.2 + Math.random() * 1.8, drag: CFG.rockDrag, submerged: true, onFloor: false }); } } function createPlants() { plants = []; for (let i = 0; i < CFG.plantCount; i++) { plants.push({ x: aqua.x + 50 + Math.random() * (aqua.w - 100), y: floorY, height: 25 + Math.random() * 35, color: CFG.plantColors[i % CFG.plantColors.length], sway: 0, swaySpeed: 1.5 + Math.random() * 2, submerged: true }); } } function createToy() { toy = { x: aqua.x + aqua.w * 0.35, y: floorY - 4 - 14, vx: 0, vy: 0, size: 11, buoyancy: CFG.toyBuoyancy, drag: CFG.toyDrag, submerged: true, onFloor: false }; } /* ============================================================ PHYSICS — water flow & drain ============================================================ */ function getCrackDepth() { return Math.max(0, crackY * aqua.h - (waterSurfY - aqua.y)); } function getJetVelocity() { const h = getCrackDepth(); return h > 0 ? Math.sqrt(2 * CFG.gravity * h) : 0; } function getFlowRate() { const h = getCrackDepth(); if (h <= 0) return 0; return CFG.crackOpenWidth * Math.sqrt(2 * CFG.gravity * h); } function updateWater(dt) { if (!crackTriggered || waterSurfY >= aqua.y + aqua.h) return; const h = getCrackDepth(); if (h <= 0) return; const v = Math.sqrt(2 * CFG.gravity * h); const flowRate = CFG.crackOpenWidth * v; const dh = (flowRate / aqua.w) * dt; waterSurfY = Math.min(aqua.y + aqua.h, waterSurfY + dh); } /* ============================================================ PHYSICS — jet particles ============================================================ */ function spawnJetParticle() { if (!crackTriggered) return; const h = getCrackDepth(); if (h <= 0) return; // Find inactive slot let slot = -1; for (let i = 0; i < jetPool.length; i++) { if (!jetPool[i].alive) { slot = i; break; } } if (slot === -1) return; const v = Math.sqrt(2 * CFG.gravity * h); const p = jetPool[slot]; p.x = crackX; p.y = aqua.y + crackY * aqua.h + (Math.random() - 0.5) * CFG.crackLen; p.vx = v * (CFG.jetVMin + Math.random() * (CFG.jetVMax - CFG.jetVMin)); p.vy = (Math.random() - 0.5) * v * CFG.jetSpread; p.life = 2 + Math.random() * 2; p.maxLife = p.life; p.size = 2 + Math.random() * 3; p.alive = true; } function updateJetParticles(dt) { for (let i = 0; i < jetPool.length; i++) { const p = jetPool[i]; if (!p.alive) continue; p.vy += CFG.gravity * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt; if (p.life <= 0 || p.y > floorY + 10 || p.x > W + 50) { p.alive = false; // Spawn splash if hitting floor if (p.y >= floorY - 2 && p.vy > 20) spawnSplash(p.x, floorY); } } } /* ============================================================ PHYSICS — splash particles ============================================================ */ function spawnSplash(x, y) { const count = 3 + Math.floor(Math.random() * 4); let spawned = 0; for (let i = 0; i < splashPool.length && spawned < count; i++) { if (!splashPool[i].alive) { const s = splashPool[i]; s.x = x + (Math.random() - 0.5) * 10; s.y = y; s.vx = (Math.random() - 0.5) * CFG.splashSpeed; s.vy = -Math.random() * CFG.splashSpeed * 0.8; s.life = CFG.splashLifetime * (0.5 + Math.random() * 0.5); s.maxLife = s.life; s.size = 1.5 + Math.random() * 2; s.alive = true; spawned++; } } } function updateSplashParticles(dt) { for (let i = 0; i < splashPool.length; i++) { const s = splashPool[i]; if (!s.alive) continue; s.vy += CFG.gravity * dt; s.x += s.vx * dt; s.y += s.vy * dt; s.life -= dt; if (s.life <= 0 || s.y > floorY + 5) s.alive = false; } } /* ============================================================ PHYSICS — bubbles ============================================================ */ function spawnBubble() { if (!crackTriggered || waterSurfY >= aqua.y + aqua.h) return; let slot = -1; for (let i = 0; i < bubblePool.length; i++) { if (!bubblePool[i].alive) { slot = i; break; } } if (slot === -1) return; const b = bubblePool[slot]; b.x = crackX + (Math.random() - 0.5) * 8; b.y = aqua.y + crackY * aqua.h + Math.random() * CFG.crackLen; b.r = 1.5 + Math.random() * 3; b.vy = -(CFG.bubbleRiseSpeed + Math.random() * 20); b.life = 1.5 + Math.random() * 2; b.maxLife = b.life; b.alive = true; } function updateBubbles(dt) { for (let i = 0; i < bubblePool.length; i++) { const b = bubblePool[i]; if (!b.alive) continue; b.x += Math.sin(gameTime * 3 + b.y * 0.02) * 8 * dt; b.y += b.vy * dt; b.life -= dt; if (b.life <= 0 || b.y < waterSurfY - 5) b.alive = false; } } /* ============================================================ PHYSICS — glass fragments ============================================================ */ function createFragments() { const count = 35 + Math.floor(Math.random() * 20); let created = 0; for (let i = 0; i < fragPool.length && created < count; i++) { if (!fragPool[i].alive) { const f = fragPool[i]; const fy = aqua.y + crackY * aqua.h + (Math.random() - 0.5) * CFG.crackLen * 1.5; const fw = CFG.fragMinSize + Math.random() * (CFG.fragMaxSize - CFG.fragMinSize); const fh = CFG.fragMinSize + Math.random() * (CFG.fragMaxSize - CFG.fragMinSize); f.x = crackX - fw / 2 + (Math.random() - 0.5) * 6; f.y = fy - fh / 2; f.vx = 100 + Math.random() * 200; f.vy = (Math.random() - 0.5) * 150; f.angle = Math.random() * Math.PI * 2; f.angVel = (Math.random() - 0.5) * 12; f.size = Math.max(4, fw); f.alive = true; f.onFloor = false; f.restingTime = 0; // Build polygon vertices f.verts = [ { x: -fw / 2, y: -fh / 2 }, { x: fw / 2, y: -fh / 3 }, { x: fw / 3, y: fh / 2 }, { x: -fw / 3, y: fh / 2 }, { x: -fw / 2, y: fh / 3 } ]; // Add some extra vertices for angular look if (Math.random() > 0.5) { f.verts.splice(2, 0, { x: 0, y: -fh / 2 }); } created++; } } } function updateFragments(dt) { for (let i = 0; i < fragPool.length; i++) { const f = fragPool[i]; if (!f.alive) continue; if (!f.onFloor) { f.vy += CFG.gravity * dt; f.x += f.vx * dt; f.y += f.vy * dt; f.angle += f.angVel * dt; f.angVel *= CFG.fragAngDamp; // Water drag if underwater if (f.y + f.size < waterSurfY) { f.vx *= CFG.fragWaterDrag; f.vy *= CFG.fragWaterDrag; } else { f.vx *= CFG.fragAirDrag; f.vy *= CFG.fragAirDrag; } // Floor collision if (f.y + f.size / 2 > floorY) { f.y = floorY - f.size / 2; f.vy = -f.vy * CFG.fragBounce; f.vx *= CFG.fragFloorFriction; f.angVel *= 0.7; if (Math.abs(f.vy) < 15 && Math.abs(f.vx) < 8) { f.onFloor = true; f.restingTime = 0; } } // Left wall if (f.x - f.size / 2 < aqua.x) { f.x = aqua.x + f.size / 2; f.vx = Math.abs(f.vx) * CFG.fragBounce; f.angVel *= 0.5; } // Right wall if (f.x + f.size / 2 > W) { f.x = W - f.size / 2; f.vx = -Math.abs(f.vx) * CFG.fragBounce; f.angVel *= 0.5; } // Ceiling if (f.y - f.size / 2 < 0) { f.y = f.size / 2; f.vy = Math.abs(f.vy) * CFG.fragBounce; } } else { f.restingTime += dt; f.vx *= 0.95; f.vy *= 0.95; if (Math.abs(f.vx) < 0.5 && Math.abs(f.vy) < 0.5) { f.vx = 0; f.vy = 0; } } } } /* ============================================================ PHYSICS — objects (fish, rocks, plants, toy) ============================================================ */ function getCurrentStrength() { if (!crackTriggered) return 0; const h = getCrackDepth(); if (h <= 0) return 0; return CFG.crackOpenWidth * Math.sqrt(2 * CFG.gravity * h) / aqua.w; } function updateObject(obj, dt, isFish) { const depth = waterSurfY - obj.y; const submerged = depth > 0; obj.submerged = submerged; let fx = 0, fy = 0; if (submerged) { // Buoyancy const buoyancy = obj.buoyancy || 1; const netBuoy = (buoyancy - 1) * CFG.gravity; fy += netBuoy; // Water drag const drag = obj.drag || CFG.fishWaterDrag; fx -= obj.vx * (1 - drag) * 3; fy -= obj.vy * (1 - drag) * 3; // Current force const current = getCurrentStrength(); fx += current * 200; // Fish: swim against current if (isFish) { if (current > 5) { obj.fatigue = Math.min(1, obj.fatigue + dt * 0.5); const swimDir = obj.vx < 0 ? -1 : 1; fx -= swimDir * obj.swimForce * (1 - obj.fatigue * 0.5); } else { obj.fatigue = Math.max(0, obj.fatigue - dt * CFG.fishFatigueDecay); // Wander fx += (Math.random() - 0.5) * CFG.fishWanderStrength; } // Tail animation obj.tailPhase += dt * 8; } } else { // In air — gravity + air drag fy += CFG.gravity; const drag = obj.drag || CFG.fishAirDrag; fx -= obj.vx * (1 - drag) * 0.5; fy -= obj.vy * (1 - drag) * 0.5; } // Integrate obj.vx += fx * dt; obj.vy += fy * dt; obj.x += obj.vx * dt; obj.y += obj.vy * dt; // Floor collision if (obj.y + obj.size > floorY) { obj.y = floorY - obj.size; if (obj.vy > 0) obj.vy = -obj.vy * CFG.fishBounceDamp; if (Math.abs(obj.vy) < 5) obj.vy = 0; obj.vx *= 0.9; obj.onFloor = true; } else { obj.onFloor = false; } // Aquarium walls if (obj.x - obj.size < aqua.x) { obj.x = aqua.x + obj.size; obj.vx = Math.abs(obj.vx) * 0.5; } if (obj.x + obj.size > crackX) { obj.x = crackX - obj.size; obj.vx = -Math.abs(obj.vx) * 0.5; } if (obj.y - obj.size < aqua.y) { obj.y = aqua.y + obj.size; obj.vy = Math.abs(obj.vy) * 0.5; } // Water surface boundary — prevent going above water when submerged if (submerged && obj.y - obj.size < waterSurfY) { obj.y = waterSurfY + obj.size; if (obj.vy < 0) obj.vy *= -0.3; } } function updateAllObjects(dt) { for (let i = 0; i < fish.length; i++) updateObject(fish[i], dt, true); for (let i = 0; i < rocks.length; i++) updateObject(rocks[i], dt, false); if (toy) updateObject(toy, dt, false); // Plants sway for (let i = 0; i < plants.length; i++) { const p = plants[i]; p.sway += dt * p.swaySpeed; const current = getCurrentStrength(); // Plants are rooted — they lean with current } } /* ============================================================ PHYSICS — puddle ============================================================ */ function updatePuddle(dt) { if (!crackTriggered) return; const h = getCrackDepth(); if (h <= 0) return; const v = Math.sqrt(2 * CFG.gravity * h); const flow = CFG.crackOpenWidth * v; puddleRadius = Math.min(CFG.puddleMaxRadius, puddleRadius + flow * CFG.puddleGrowthRate * dt * 0.05); } /* ============================================================ RENDERING ============================================================ */ function drawRoom() { // Room background const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, '#0d1b2a'); grad.addColorStop(0.7, '#1b2838'); grad.addColorStop(1, '#1a2a1a'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); // Wall texture — subtle vertical lines ctx.strokeStyle = 'rgba(255,255,255,0.015)'; ctx.lineWidth = 1; for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); } // Floor const floorGrad = ctx.createLinearGradient(0, floorY, 0, H); floorGrad.addColorStop(0, CFG.floorColor); floorGrad.addColorStop(1, '#2c1a0e'); ctx.fillStyle = floorGrad; ctx.fillRect(0, floorY, W, H - floorY); // Floor wood grain ctx.strokeStyle = 'rgba(0,0,0,0.15)'; ctx.lineWidth = 1; for (let y = floorY + 5; y < H; y += 12) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y + Math.sin(y * 0.1) * 2); ctx.stroke(); } // Caustic light on floor from aquarium if (waterSurfY < aqua.y + aqua.h) { const waterDepth = aqua.y + aqua.h - waterSurfY; if (waterDepth > 10) { const causticAlpha = Math.min(CFG.causticAlpha, waterDepth / 300); ctx.fillStyle = `rgba(100,180,255,${causticAlpha})`; for (let i = 0; i < 6; i++) { const cx = crackX + 30 + i * 50 + Math.sin(gameTime * 0.5 + i) * 15; const cy = floorY + 10 + Math.sin(gameTime * 0.7 + i * 2) * 8; const r = 20 + Math.sin(gameTime + i) * 8; ctx.beginPath(); ctx.ellipse(cx, cy, r, r * 0.5, 0, 0, Math.PI * 2); ctx.fill(); } } } } function drawAquarium() { // Aquarium background (dark interior) ctx.fillStyle = '#0a1628'; ctx.fillRect(aqua.x, aqua.y, aqua.w, aqua.h); // Aquarium frame const fw = CFG.aquariumFrameWidth; const fh = CFG.aquariumFrameHeight; ctx.fillStyle = '#2c3e50'; // Top ctx.fillRect(aqua.x - fw, aqua.y - fh, aqua.w + fw * 2, fh); // Bottom ctx.fillRect(aqua.x - fw, floorY, aqua.w + fw * 2, fh); // Left ctx.fillRect(aqua.x - fw, aqua.y, fw, aqua.h); // Right (only if not broken) if (!crackTriggered || breakProgress < 0.3) { ctx.fillRect(crackX, aqua.y, fw, aqua.h); } // Glass walls ctx.strokeStyle = `rgba(150,210,255,${CFG.glassAlpha})`; ctx.lineWidth = 2; ctx.strokeRect(aqua.x, aqua.y, aqua.w, aqua.h); // Glass highlight (left edge shine) const shineGrad = ctx.createLinearGradient(aqua.x, aqua.y, aqua.x + 20, aqua.y); shineGrad.addColorStop(0, `rgba(255,255,255,${CFG.glassHighlightAlpha})`); shineGrad.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = shineGrad; ctx.fillRect(aqua.x, aqua.y, 15, aqua.h); // Bottom reflection const reflectGrad = ctx.createLinearGradient(aqua.x, floorY - 10, aqua.x, floorY); reflectGrad.addColorStop(0, 'rgba(255,255,255,0)'); reflectGrad.addColorStop(1, `rgba(150,210,255,0.04)`); ctx.fillStyle = reflectGrad; ctx.fillRect(aqua.x, floorY - 10, aqua.w, 10); } function drawWater() { if (waterSurfY >= aqua.y + aqua.h) return; const waterTop = Math.max(aqua.y, waterSurfY); const waterHeight = aqua.y + aqua.h - waterTop; if (waterHeight <= 0) return; // Water fill with gradient const wGrad = ctx.createLinearGradient(0, waterTop, 0, aqua.y + aqua.h); wGrad.addColorStop(0, `rgba(${CFG.waterColorTop.join(',')},0.82)`); wGrad.addColorStop(0.5, `rgba(${CFG.waterColorBot.join(',')},0.88)`); wGrad.addColorStop(1, `rgba(${CFG.waterColorBot.join(',')},0.95)`); ctx.fillStyle = wGrad; // Clipped to aquarium ctx.save(); ctx.beginPath(); ctx.rect(aqua.x, waterTop, aqua.w, waterHeight); ctx.clip(); // Draw water body ctx.fillRect(aqua.x, waterTop, aqua.w, waterHeight); // Water surface waves ctx.beginPath(); ctx.moveTo(aqua.x, waterTop); for (let x = aqua.x; x <= aqua.x + aqua.w; x += 2) { const wave = Math.sin(x * 0.03 + gameTime * 2.5) * 2.5 + Math.sin(x * 0.07 + gameTime * 1.8) * 1.2 + Math.sin(x * 0.015 + gameTime * 1.2) * 1.8; ctx.lineTo(x, waterTop + wave); } ctx.lineTo(aqua.x + aqua.w, aqua.y + aqua.h); ctx.lineTo(aqua.x, aqua.y + aqua.h); ctx.closePath(); ctx.fillStyle = `rgba(${CFG.waterColorTop.join(',')},${CFG.waterSurfaceAlpha})`; ctx.fill(); // Light rays from above ctx.globalAlpha = 0.04; for (let i = 0; i < 5; i++) { const rx = aqua.x + aqua.w * (0.15 + i * 0.18) + Math.sin(gameTime * 0.8 + i * 1.5) * 15; const rw = 8 + Math.sin(gameTime + i) * 4; ctx.fillStyle = '#aaddff'; ctx.beginPath(); ctx.moveTo(rx - rw, waterTop); ctx.lineTo(rx + rw, waterTop); ctx.lineTo(rx + rw * 2.5, aqua.y + aqua.h); ctx.lineTo(rx - rw * 2.5, aqua.y + aqua.h); ctx.closePath(); ctx.fill(); } ctx.globalAlpha = 1; ctx.restore(); } function drawCrack() { if (crackTriggered) return; const cy = aqua.y + crackY * aqua.h; const isSubmerged = cy < waterSurfY; // Crack glow ctx.shadowColor = isSubmerged ? '#e74c3c' : '#e67e22'; ctx.shadowBlur = 12; // Draw jagged crack line ctx.strokeStyle = isSubmerged ? '#e74c3c' : '#e67e22'; ctx.lineWidth = 3; ctx.beginPath(); for (let i = -CFG.crackLen / 2; i <= CFG.crackLen / 2; i += 2) { const offset = Math.sin(i * 0.4) * 4 + Math.sin(i * 0.9) * 2; const px = crackX + offset; const py = cy + i; if (i === -CFG.crackLen / 2) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); ctx.shadowBlur = 0; // Secondary crack lines ctx.strokeStyle = isSubmerged ? 'rgba(231,76,60,0.4)' : 'rgba(230,126,34,0.4)'; ctx.lineWidth = 1.5; for (let j = 0; j < 3; j++) { ctx.beginPath(); const offY = (j - 1) * 8; for (let i = -CFG.crackLen / 2; i <= CFG.crackLen / 2; i += 3) { const offset = Math.sin(i * 0.5 + j) * 3 + Math.cos(i * 0.3 + j * 2) * 2; const px = crackX + 3 + offset; const py = cy + i + offY; if (i === -CFG.crackLen / 2) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); } // Grab handle const handleY = cy; ctx.fillStyle = crackDragging ? '#ff9f43' : '#e74c3c'; ctx.beginPath(); ctx.arc(crackX, handleY, 7, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); // Direction arrows ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('↑↓', crackX + 18, handleY + 3); } function drawJetParticles() { for (let i = 0; i < jetPool.length; i++) { const p = jetPool[i]; if (!p.alive) continue; const alpha = Math.max(0, p.life / p.maxLife); const r = p.size * (0.5 + alpha * 0.5); ctx.fillStyle = `rgba(135,206,250,${alpha * 0.7})`; ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill(); // Glow ctx.fillStyle = `rgba(175,220,255,${alpha * 0.2})`; ctx.beginPath(); ctx.arc(p.x, p.y, r * 2.5, 0, Math.PI * 2); ctx.fill(); } } function drawSplashParticles() { for (let i = 0; i < splashPool.length; i++) { const s = splashPool[i]; if (!s.alive) continue; const alpha = Math.max(0, s.life / s.maxLife); ctx.fillStyle = `rgba(180,220,255,${alpha * 0.6})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.size * alpha, 0, Math.PI * 2); ctx.fill(); } } function drawBubbles() { for (let i = 0; i < bubblePool.length; i++) { const b = bubblePool[i]; if (!b.alive) continue; const alpha = Math.max(0, b.life / b.maxLife) * 0.5; ctx.strokeStyle = `rgba(180,220,255,${alpha})`; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.stroke(); // Highlight ctx.fillStyle = `rgba(255,255,255,${alpha * 0.5})`; ctx.beginPath(); ctx.arc(b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.25, 0, Math.PI * 2); ctx.fill(); } } function drawPuddle() { if (puddleRadius < 2) return; const px = puddleX; const py = floorY; const grad = ctx.createRadialGradient(px, py, 0, px, py, puddleRadius); grad.addColorStop(0, 'rgba(30,100,180,0.5)'); grad.addColorStop(0.6, 'rgba(30,100,180,0.25)'); grad.addColorStop(1, 'rgba(30,100,180,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.ellipse(px, py, puddleRadius, puddleRadius * 0.3, 0, 0, Math.PI * 2); ctx.fill(); // Puddle edge highlight ctx.strokeStyle = 'rgba(100,180,255,0.15)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.ellipse(px, py, puddleRadius, puddleRadius * 0.3, 0, 0, Math.PI * 2); ctx.stroke(); } function drawFish(f) { ctx.save(); ctx.translate(f.x, f.y); ctx.scale(f.dir, 1); const s = f.size; const tailWag = Math.sin(f.tailPhase) * 0.3; // Body ctx.fillStyle = f.color; ctx.beginPath(); ctx.ellipse(0, 0, s, s * 0.45, 0, 0, Math.PI * 2); ctx.fill(); // Tail ctx.beginPath(); ctx.moveTo(-s * 0.8, 0); ctx.lineTo(-s * 1.4, -s * 0.4 + tailWag * s); ctx.lineTo(-s * 1.4, s * 0.4 + tailWag * s); ctx.closePath(); ctx.fill(); // Dorsal fin ctx.beginPath(); ctx.moveTo(-s * 0.2, -s * 0.35); ctx.quadraticCurveTo(0, -s * 0.7, s * 0.3, -s * 0.35); ctx.fill(); // Eye ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s * 0.35, -s * 0.08, s * 0.16, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(s * 0.4, -s * 0.08, s * 0.08, 0, Math.PI * 2); ctx.fill(); // Highlight on eye ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.beginPath(); ctx.arc(s * 0.42, -s * 0.12, s * 0.03, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawRocks(r) { ctx.fillStyle = r.color; ctx.beginPath(); const s = r.size; ctx.moveTo(r.x - s, r.y - s * 0.3); ctx.quadraticCurveTo(r.x - s * 0.5, r.y - s, r.x, r.y - s * 0.7); ctx.quadraticCurveTo(r.x + s * 0.8, r.y - s * 0.9, r.x + s, r.y - s * 0.2); ctx.quadraticCurveTo(r.x + s * 1.1, r.y + s * 0.3, r.x + s * 0.5, r.y + s * 0.5); ctx.quadraticCurveTo(r.x, r.y + s * 0.8, r.x - s * 0.3, r.y + s * 0.4); ctx.quadraticCurveTo(r.x - s * 0.8, r.y + s * 0.1, r.x - s, r.y - s * 0.3); ctx.fill(); // Highlight ctx.fillStyle = 'rgba(255,255,255,0.08)'; ctx.beginPath(); ctx.ellipse(r.x - s * 0.2, r.y - s * 0.3, s * 0.3, s * 0.15, -0.3, 0, Math.PI * 2); ctx.fill(); } function drawPlants(p) { const sway = Math.sin(p.sway) * CFG.plantSwayStrength * getCurrentStrength(); ctx.strokeStyle = '#2d5a27'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.quadraticCurveTo(p.x + sway * 0.5, p.y - p.height * 0.5, p.x + sway, p.y - p.height); ctx.stroke(); // Leaves ctx.fillStyle = p.color; for (let i = 0; i < 3; i++) { const t = 0.3 + i * 0.25; const lx = p.x + sway * t; const ly = p.y - p.height * t; const leafSize = 5 + (1 - t) * 4; ctx.beginPath(); ctx.ellipse(lx + leafSize * 0.5, ly, leafSize, leafSize * 0.4, 0.3 + sway * 0.02, 0, Math.PI * 2); ctx.fill(); } } function drawToy() { if (!toy) return; ctx.save(); ctx.translate(toy.x, toy.y); // Body ctx.fillStyle = CFG.toyColor; ctx.beginPath(); ctx.ellipse(0, 0, toy.size, toy.size * 0.7, 0, 0, Math.PI * 2); ctx.fill(); // Head ctx.beginPath(); ctx.arc(toy.size * 0.7, -toy.size * 0.15, toy.size * 0.45, 0, Math.PI * 2); ctx.fill(); // Beak ctx.fillStyle = '#f39c12'; ctx.beginPath(); ctx.moveTo(toy.size * 1.05, -toy.size * 0.1); ctx.lineTo(toy.size * 1.4, 0); ctx.lineTo(toy.size * 1.05, toy.size * 0.1); ctx.closePath(); ctx.fill(); // Eye ctx.fillStyle = CFG.toyEye; ctx.beginPath(); ctx.arc(toy.size * 0.8, -toy.size * 0.25, 1.5, 0, Math.PI * 2); ctx.fill(); // Highlight ctx.fillStyle = CFG.toyHighlight; ctx.beginPath(); ctx.ellipse(-toy.size * 0.2, -toy.size * 0.15, toy.size * 0.2, toy.size * 0.1, -0.2, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawFragments() { for (let i = 0; i < fragPool.length; i++) { const f = fragPool[i]; if (!f.alive) continue; ctx.save(); ctx.translate(f.x, f.y); ctx.rotate(f.angle); // Glass fragment color with slight variation const r = CFG.fragColorBase[0] + (Math.random() - 0.5) * CFG.fragColorVar; const g = CFG.fragColorBase[1] + (Math.random() - 0.5) * CFG.fragColorVar; const b = CFG.fragColorBase[2] + (Math.random() - 0.5) * CFG.fragColorVar; const alpha = f.onFloor ? 0.6 : 0.8; ctx.fillStyle = `rgba(${r|0},${g|0},${b|0},${alpha})`; ctx.strokeStyle = `rgba(255,255,255,${alpha * 0.5})`; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(f.verts[0].x, f.verts[0].y); for (let j = 1; j < f.verts.length; j++) { ctx.lineTo(f.verts[j].x, f.verts[j].y); } ctx.closePath(); ctx.fill(); ctx.stroke(); // Shine highlight ctx.fillStyle = `rgba(255,255,255,${alpha * 0.3})`; ctx.beginPath(); ctx.ellipse(-f.size * 0.2, -f.size * 0.2, f.size * 0.2, f.size * 0.1, -0.5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } function drawWaterline() { if (crackTriggered && waterSurfY > aqua.y) { // Dashed waterline indicator ctx.setLineDash([4, 4]); ctx.strokeStyle = 'rgba(100,180,255,0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(aqua.x, waterSurfY); ctx.lineTo(crackX, waterSurfY); ctx.stroke(); ctx.setLineDash([]); } } function drawBreakAnimation() { if (!crackTriggered || breakProgress <= 0) return; const progress = Math.min(1, breakProgress); const cx = crackX; const cy = aqua.y + crackY * aqua.h; // Shockwave ring const ringRadius = progress * 80; const ringAlpha = (1 - progress) * 0.5; ctx.strokeStyle = `rgba(200,220,255,${ringAlpha})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(cx, cy, ringRadius, 0, Math.PI * 2); ctx.stroke(); // Second ring if (progress > 0.2) { const r2 = (progress - 0.2) / 0.8 * 120; const a2 = (1 - progress) * 0.3; ctx.strokeStyle = `rgba(200,220,255,${a2})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r2, 0, Math.PI * 2); ctx.stroke(); } // Flying debris particles if (progress < 0.5) { const debrisCount = Math.floor(progress * 20); for (let i = 0; i < debrisCount; i++) { const angle = (i / debrisCount) * Math.PI * 2 + gameTime * 5; const dist = progress * 100; const dx = Math.cos(angle) * dist; const dy = Math.sin(angle) * dist; ctx.fillStyle = `rgba(180,220,255,${(1 - progress) * 0.5})`; ctx.fillRect(cx + dx - 1, cy + dy - 1, 2, 2); } } } function drawCurrentIndicator() { if (!crackTriggered || getCurrentStrength() < 2) return; const current = getCurrentStrength(); const alpha = Math.min(0.15, current * 0.005); ctx.fillStyle = `rgba(100,180,255,${alpha})`; for (let y = aqua.y + 20; y < waterSurfY; y += 25) { const offset = Math.sin(y * 0.05 + gameTime * 3) * 8; ctx.fillRect(aqua.x + 10 + offset, y, aqua.w - 20, 2); } } /* ============================================================ UI UPDATE ============================================================ */ function updateUI() { const pct = Math.max(0, Math.min(100, ((waterSurfY - aqua.y) / aqua.h) * 100)); document.getElementById('wl').textContent = pct.toFixed(1) + '%'; const h = getCrackDepth(); const vel = getJetVelocity(); const flow = getFlowRate(); const jp = document.getElementById('jp'); if (vel < 10) jp.textContent = '—'; else if (vel < 80) jp.textContent = 'Weak'; else if (vel < 180) jp.textContent = 'Medium'; else if (vel < 300) jp.textContent = 'Strong'; else jp.textContent = 'Very Strong'; document.getElementById('cd').textContent = h > 0 ? h.toFixed(0) + ' px' : 'Above water'; document.getElementById('fr').textContent = flow.toFixed(1) + ' px/s'; const pos = crackY; const cp = document.getElementById('cp'); if (pos < 0.2) cp.textContent = 'High (Weak)'; else if (pos < 0.4) cp.textContent = 'Upper-Mid'; else if (pos < 0.6) cp.textContent = 'Mid'; else if (pos < 0.8) cp.textContent = 'Lower-Mid'; else cp.textContent = 'Low (Strong)'; const st = document.getElementById('st'); if (crackTriggered) { st.textContent = 'BREACHING'; st.style.color = '#e74c3c'; } else if (hasTriggered) { st.textContent = 'Draining...'; st.style.color = '#f39c12'; } else { st.textContent = 'Ready'; st.style.color = '#2ecc71'; } // Update crack position display in UI const bar = document.querySelector('#ui .bar-fill'); // We update it via inline style const wlBar = document.getElementById('wl'); // Water level bar (simplified — just text) } /* ============================================================ MAIN LOOP ============================================================ */ function triggerBreak() { if (hasTriggered) return; hasTriggered = true; crackTriggered = true; breakStartTime = performance.now() / 1000; breakProgress = 0; document.getElementById('triggerBtn').disabled = true; document.getElementById('triggerBtn').textContent = '💥 BROKEN'; document.getElementById('st').textContent = 'BREACHING'; document.getElementById('st').style.color = '#e74c3c'; createFragments(); } function resetSim() { init(); } function gameLoop(timestamp) { if (!lastTimestamp) lastTimestamp = timestamp; const rawDt = (timestamp - lastTimestamp) / 1000; lastTimestamp = timestamp; const dt = Math.min(0.05, rawDt); gameTime += dt; // Break animation progress if (crackTriggered) { breakProgress = Math.min(1, (gameTime - breakStartTime) / CFG.breakAnimDuration); } // Physics substeps for stability const subDt = dt / CFG.subSteps; for (let step = 0; step < CFG.subSteps; step++) { updateWater(subDt); updateJetParticles(subDt); updateSplashParticles(subDt); updateBubbles(subDt); updateFragments(subDt); updateAllObjects(subDt); updatePuddle(subDt); } // Spawn jet particles if (crackTriggered && getCrackDepth() > 0) { const rate = Math.min(8, getFlowRate() * 0.02); for (let i = 0; i < rate; i++) { if (Math.random() < rate * 0.3) spawnJetParticle(); } } // Spawn bubbles if (crackTriggered && Math.random() < 0.3 && getCrackDepth() > 0) { spawnBubble(); } // Puddle growth from settled water if (crackTriggered && puddleRadius < CFG.puddleMaxRadius) { const h = getCrackDepth(); if (h > 0) { puddleRadius = Math.min(CFG.puddleMaxRadius, puddleRadius + getFlowRate() * 0.003); } } // Render ctx.clearRect(0, 0, W, H); drawRoom(); drawAquarium(); drawWater(); drawWaterline(); drawCurrentIndicator(); // Draw objects in aquarium (sorted by y for depth) const allObjs = []; for (let i = 0; i < rocks.length; i++) allObjs.push(rocks[i]); for (let i = 0; i < plants.length; i++) allObjs.push(plants[i]); for (let i = 0; i < fish.length; i++) allObjs.push(fish[i]); if (toy) allObjs.push(toy); allObjs.sort((a, b) => a.y - b.y); for (let i = 0; i < allObjs.length; i++) { const obj = allObjs[i]; if (obj === toy) drawToy(); else if (obj.size > 10) drawRocks(obj); else if (obj.sway !== undefined) drawPlants(obj); else drawFish(obj); } drawCrack(); drawBreakAnimation(); drawFragments(); drawJetParticles(); drawSplashParticles(); drawBubbles(); drawPuddle(); updateUI(); // Check if simulation is "done" (tank empty, everything settled) if (crackTriggered && waterSurfY >= aqua.y + aqua.h - 2 && puddleRadius > 5) { const allSettled = fish.every(f => f.onFloor || Math.abs(f.vx) < 2) && rocks.every(r => r.onFloor || Math.abs(r.vx) < 2) && (!toy || toy.onFloor || Math.abs(toy.vx) < 2); if (allSettled && breakProgress > 0.5) { document.getElementById('st').textContent = 'Complete'; document.getElementById('st').style.color = '#2ecc71'; } } requestAnimationFrame(gameLoop); } /* ============================================================ START ============================================================ */ resize(); init(); requestAnimationFrame(gameLoop); })(); </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.