Aquarium breach · qwen3.6-plus
Create a large glass aquarium whose side panel develops a visible crack and then bursts. The simulation must include: - Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains - A curved water jet affected by gravity - A spreading puddle that collides with the room boundaries - Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current - Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions - Fish attempting to swim against the current before being swept through the breach - Glass fragments with angular velocity, collisions, and water resistance - A visible waterline that lowers continuously rather than disappearing all at once Let the user drag the crack vertically before triggering the failure. A lower crack should initially produce a stronger jet than a higher crack. Give me 1 html file
transcript · 9 messages
Create a large glass aquarium whose side panel develops a visible crack and then bursts. The simulation must include: - Water escaping through the opening with flow strength based on water depth and decreasing as the tank drains - A curved water jet affected by gravity - A spreading puddle that collides with the room boundaries - Fish, rocks, plants, and a floating toy reacting differently according to density, buoyancy, drag, and current - Objects transitioning correctly from underwater motion to airborne motion and then to floor collisions - Fish attempting to swim against the current before being swept through the breach - Glass fragments with angular velocity, collisions, and water resistance - A visible waterline that lowers continuously rather than disappearing all at once Let the user drag the crack vertically before triggering the failure. A lower crack should initially produce a stronger jet than a higher crack. Give me 1 html file
thinking
The user wants a complex physics simulation of a glass aquarium bursting. Let me build this as a single self-contained HTML file with inline CSS and JS. This needs: 1. A glass aquarium tank drawn on canvas 2. Water inside the tank with a visible waterline 3. Fish, rocks, plants, floating toy objects 4. A crack that can be dragged vertically 5. Physics simulation for: - Water jet escaping through crack (flow based on depth, gravity curve) - Spreading puddle with room boundary collision - Objects with density, buoyancy, drag, current - Objects transitioning from underwater to airborne to floor - Fish swimming against current - Glass fragments with angular velocity, collisions, water resistance - Lowering waterline This is going to be a substantial canvas-based simulation. Let me design the physics carefully. Key physics: - Water pressure at crack: P = ρgh (depth from water surface to crack) - Flow velocity from crack: v = √(2gh) (Torricelli's law) - Jet trajectory: parabolic under gravity - Puddle: spreading ellipse that grows and hits walls - Buoyancy: F_b = ρ_water * V_displaced * g - Drag: F_d = 0.5 * Cd * ρ * A * v² - Objects underwater: buoyancy + drag + gravity + current force - Objects airborne: gravity + air drag - Objects on floor: friction + gravity - Glass fragments: gravity + angular velocity + water resistance Let me build this step by step. I'll use a canvas for rendering and implement the physics in the animation loop. Let me think about the layout: - Tank is centered in the scene - Room floor, walls visible - Water fills the tank to some level - Crack is a line on the side panel - Objects inside: fish (swim), rocks (heavy, sit on bottom), plants (rooted), toy (floats) States: 1. BEFORE_BURST: User drags crack position 2. CRACKING: Crack appears and grows 3. BURSTING: Water jets out, objects react 4. DRAINING: Water level drops, objects fall 5. EMPTY: Tank empty, objects on floor Let me code this up. This will be a large file.
/tmp/ws/index.html (41499 bytes)Successfully wrote 41499 bytes to /tmp/ws/index.html
thinking
Let me review the code to make sure it works properly. I want to verify a few things:
1. The crack dragging works
2. The burst animation plays correctly
3. The water draining logic is correct
4. All objects behave properly
Let me check if there are any issues by reviewing the code more carefully. I see a potential issue - the water surface Y calculation might not match properly with the water level. Also, I should make sure the puddle and jet rendering look good. Let me also check that the fish fleeing behavior and the physics feel right.
Actually, looking more carefully, I see a few issues I should fix:
1. The water surface Y formula: `waterSurfaceY = tank.y + tank.h * 0.05 + (1 - waterLevel) * tank.h * 0.95` - when waterLevel is 1.0, surface is at `tank.y + tank.h * 0.05` which is near the top. When waterLevel is 0, surface is at `tank.y + tank.h`. This seems right - the water surface goes DOWN as water level decreases.
Wait, that's wrong. When water drains, the water surface should go DOWN (increase Y value). When waterLevel = 1.0 (full), the surface should be at the top (tank.y + small offset). When waterLevel = 0 (empty), the surface should be at the bottom.
So: waterSurfaceY = tank.y + tank.h * 0.05 + (1 - waterLevel) * tank.h * 0.95
- waterLevel = 1.0 → surface at tank.y + tank.h * 0.05 (near top) ✓
- waterLevel = 0 → surface at tank.y + tank.h * 0.05 + tank.h * 0.95 = tank.y + tank.h (bottom) ✓
That's correct.
But the jet pressure calculation: `waterDepthAboveCrack = Math.max(0, waterSurfaceY - crackY)` - this is the distance from the surface DOWN to the crack. When the surface is above the crack, waterDepthAboveCrack > 0 and we get flow. This is correct.
However, when the tank is full (waterLevel = 1.0), waterSurfaceY is at the TOP, so waterDepthAboveCrack for a crack in the middle would be large. When the crack is near the bottom, waterDepthAboveCrack is even larger. This matches the requirement that "a lower crack should initially produce a stronger jet."
Let me verify the overall flow of the simulation. I think it looks good, but let me also check if there are rendering issues. One concern is the jet particles spawning too many or too few. Let me trace through:
- pressure = waterDepthAboveCrack / tank.h (0 to ~0.5 range typically)
- jetSpeed = sqrt(2 * GRAVITY * max(10, waterDepthAboveCrack)) * 0.8
- numJets = floor(flowRate * 5) + 1 = floor(pressure * 3 * 5) + 1
When pressure is 0.5: numJets = floor(7.5) + 1 = 8 per frame. That seems reasonable.
Let me also make sure the fish fleeing logic works. The currentVx is pressure * 300, so when pressure is 0.5, currentVx = 150. The fish checks if currentStrength > 10 and then swims against it. The fleeing is also tied to stressTimer which shows a visual indicator.
I think the simulation should work well. Let me check if there are any syntax errors or bugs...
Actually, I notice one issue: in the Fish update method, the `underwater` check. For the fish, `underwater = f.y > waterSurfaceY && !f.airborne && !f.onFloor`. This means the fish is underwater only when below the water surface. But when the water level drops below the fish, the fish would NOT be considered underwater, and it would go into the else branch which sets `this.airborne = true`. But wait, the `else` branch is:
```
} else {
// In air but not yet airborne flag — transition
this.airborne = true;
}
```
This only sets airborne = true but doesn't update physics. Then on the next frame, it would enter the airborne branch and fall due to gravity. This should work but the transition might be a bit abrupt. Let me check if the fish has proper velocity when transitioning...
Actually, when the fish is last underwater, it has some vx and vy. Then when underwater becomes false, airborne is set to true. On the next frame, it enters the airborne branch which applies gravity and uses the existing vx/vy. This should be fine.
I think the code looks good overall. Let me verify it runs by checking for obvious syntax errors... I don't see any. The code structure is clean.
One more thing - I should verify the glass fragments spawning. When the burst triggers, `spawnGlassFragments()` creates 25-40 fragments at positions along the crack. They have random sizes, velocities, and rotations. This should create a nice shattering effect.
I'm satisfied with the code. Let me present it to the user.Now let me review it and fix a few issues I spotted:
{"path":"/tmp/ws/index.html","offset":1,"limit":30}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Aquarium Burst Simulation</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; overflow: hidden; font-family: 'Segoe UI', sans-serif; color: #eee; } #ui { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 14px; align-items: center; z-index: 10; } #ui button { [1300 more lines in file. Use offset=31 to continue.]
thinking
The file is written but I should test it works properly. Let me also fix a few potential issues I noticed: 1. The puddle needs to properly stop at room boundaries 2. Some objects might not properly transition between states 3. The crack dragging might have issues Let me look at a few more sections and make improvements.
Let me review the key physics and fix issues with puddle boundary collision, object transitions, and the crack interaction:
{"path":"/tmp/ws/index.html","offset":150,"limit":120}if (this.y >= tank.floorY) { this.y = tank.floorY; this.vx *= 0.5; this.vy = 0; this.decay = 2.0; } } draw() { if (this.life <= 0) return; const a = Math.min(1, this.life); ctx.globalAlpha = a * 0.7; ctx.fillStyle = `hsl(200, 80%, ${55 + this.life * 20}%)`; ctx.beginPath(); ctx.ellipse(this.x, this.y, this.size * (0.8 + this.life * 0.2), this.size * 0.5, Math.atan2(this.vy, this.vx), 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } } // ─── Puddle ───────────────────────────────────────────────── let puddle = { cx: 0, cy: 0, rx: 0, ry: 0, volume: 0 }; // ─── Fish ─────────────────────────────────────────────────── let fishes = []; class Fish { constructor(x, y, size, hue) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.size = size; this.hue = hue; this.density = FISH_DENSITY; this.phase = Math.random() * Math.PI * 2; this.swimAngle = 0; this.alive = true; this.airborne = false; this.onFloor = false; this.rotation = 0; this.rotSpeed = 0; this.stressTimer = 0; this.fleeing = false; this.fleeAngle = 0; } update(dt, currentVx, currentVy, underwater) { if (!this.alive) return; this.phase += dt * 3; this.stressTimer = Math.max(0, this.stressTimer - dt); if (this.onFloor) { this.vx *= 0.92; this.vy = 0; this.x += this.vx * dt; if (this.x < tank.leftWallX + this.size) { this.x = tank.leftWallX + this.size; this.vx *= -0.3; } if (this.x > tank.rightWallX - this.size) { this.x = tank.rightWallX - this.size; this.vx *= -0.3; } this.airborne = false; return; } if (this.airborne) { this.vy += GRAVITY * dt; this.vx *= 0.995; this.rotSpeed += this.vx * 0.001 * dt; this.rotation += this.rotSpeed; this.x += this.vx * dt; this.y += this.vy * dt; if (this.y >= tank.floorY - this.size * 0.3) { this.y = tank.floorY - this.size * 0.3; this.onFloor = true; this.airborne = false; this.vx *= 0.4; this.rotSpeed *= 0.2; } if (this.x > tank.rightWallX + 50) this.alive = false; return; } // Swimming behavior if (underwater) { // Buoyancy const buoyancy = (WATER_DENSITY / this.density - 1) * GRAVITY; this.vy += buoyancy * dt; // Water drag const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); if (speed > 0.01) { const dragMag = 0.5 * WATER_DRAG_COEFF * WATER_DENSITY * Math.PI * this.size * this.size * speed * speed; const dragAx = dragMag * (this.vx / speed) / (this.density * this.size * this.size); const dragAy = dragMag * (this.vy / speed) / (this.density * this.size * this.size); this.vx -= dragAx * dt * 0.01; this.vy -= dragAy * dt * 0.01; } // Current force const currentStrength = Math.sqrt(currentVx * currentVx + currentVy * currentVy); if (currentStrength > 10) { this.stressTimer = 2.0; this.fleeing = true; // Swim against current const fleeAngle = Math.atan2(-currentVy, -currentVx); this.fleeAngle = fleeAngle; const swimForce = 200 + Math.random() * 100; this.vx += Math.cos(fleeAngle) * swimForce * dt; this.vy += Math.sin(fleeAngle) * swimForce * dt; } else { this.fleeing = false; // Gentle swimming this.vx += Math.sin(this.phase) * 40 * dt; this.vy += Math.cos(this.phase * 0.7) * 20 * dt; } // Keep in tank if (this.x < tank.leftWallX + this.size + 5) { this.x = tank.leftWallX + this.size + 5; this.vx = Math.abs(this.vx) * 0.5; } if (this.x > tank.rightWallX - this.size - 5) { this.x = tank.rightWallX - this.size - 5; this.vx = -Math.abs(this.vx) * 0.5; } if (this.y < waterSurfaceY + this.size) { this.y = waterSurfaceY + this.size; this.vy = Math.abs(this.vy) * 0.5; } if (this.y > tank.bottomY - this.size) { this.y = tank.bottomY - this.size; this.vy = -Math.abs(this.vy) * 0.5; } // Check if sucked out through crack const distToCrack = Math.abs(this.x - tank.rightWallX) + Math.abs(this.y - crackY); if (distToCrack < this.size * 3 && currentVx > 50 && this.x > tank.rightWallX - this.size * 4) { // Swept out! this.vx = Math.max(this.vx, currentVx * 1.5); [1061 more lines in file. Use offset=270 to continue.]
thinking
Let me read the rest of the file to understand the complete simulation, then I'll make targeted fixes to improve the physics and visual quality.
{"offset":270,"path":"/tmp/ws/index.html"}this.vy += currentVy * 0.3; if (this.x >= tank.rightWallX) { this.airborne = true; } } this.vx *= 0.97; this.vy *= 0.97; this.x += this.vx * dt; this.y += this.vy * dt; this.rotation = Math.atan2(this.vy, this.vx); } else { // In air but not yet airborne flag — transition this.airborne = true; } } draw() { if (!this.alive) return; ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation); const s = this.size; // Body ctx.fillStyle = `hsl(${this.hue}, 70%, 55%)`; ctx.beginPath(); ctx.ellipse(0, 0, s, s * 0.5, 0, 0, Math.PI * 2); ctx.fill(); // Tail ctx.beginPath(); ctx.moveTo(-s * 0.7, 0); ctx.lineTo(-s * 1.3, -s * 0.5); ctx.lineTo(-s * 1.3, s * 0.5); ctx.closePath(); ctx.fill(); // Eye ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s * 0.4, -s * 0.1, s * 0.18, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(s * 0.45, -s * 0.1, s * 0.09, 0, Math.PI * 2); ctx.fill(); // Stress indicator if (this.stressTimer > 0) { ctx.globalAlpha = 0.5 + Math.sin(this.stressTimer * 10) * 0.3; ctx.strokeStyle = '#ff0'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, 0, s * 1.3, 0, Math.PI * 2); ctx.stroke(); ctx.globalAlpha = 1; } ctx.restore(); } } // ─── Rocks ────────────────────────────────────────────────── let rocks = []; class Rock { constructor(x, y, size) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.size = size; this.density = ROCK_DENSITY; this.rot = Math.random() * Math.PI * 2; this.rotSpeed = 0; this.onFloor = false; this.airborne = false; this.points = this.generateShape(); } generateShape() { const pts = []; const n = 6 + Math.floor(Math.random() * 3); for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2; const r = this.size * (0.7 + Math.random() * 0.3); pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r }); } return pts; } update(dt, currentVx, currentVy, underwater) { if (this.onFloor) { this.vx *= 0.9; this.x += this.vx * dt; this.rot += this.rotSpeed * dt; this.rotSpeed *= 0.95; if (this.x < tank.leftWallX + this.size) { this.x = tank.leftWallX + this.size; this.vx *= -0.2; } if (this.x > tank.rightWallX + 200) this.rotSpeed *= 0.9; return; } if (this.airborne) { this.vy += GRAVITY * dt; this.rot += this.rotSpeed * dt; this.rotSpeed *= 0.998; this.x += this.vx * dt; this.y += this.vy * dt; if (this.y >= tank.floorY - this.size * 0.5) { this.y = tank.floorY - this.size * 0.5; this.onFloor = true; this.airborne = false; this.vx *= 0.3; this.rotSpeed *= 0.1; } return; } if (underwater) { const buoyancy = (WATER_DENSITY / this.density - 1) * GRAVITY; this.vy += buoyancy * dt; const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); if (speed > 0.01) { const dragMag = 0.5 * WATER_DRAG_COEFF * WATER_DENSITY * this.size * this.size * speed * speed * 0.1; this.vx -= (this.vx / speed) * dragMag * dt / (this.density * this.size); this.vy -= (this.vy / speed) * dragMag * dt / (this.density * this.size); } // Current this.vx += currentVx * 0.02 * dt * 60; this.vy += currentVy * 0.01 * dt * 60; // Gravity settles if (this.y >= tank.bottomY - this.size * 0.6) { this.y = tank.bottomY - this.size * 0.6; this.vy = 0; this.onFloor = true; } if (this.x < tank.leftWallX + this.size) { this.x = tank.leftWallX + this.size; this.vx *= -0.3; } if (this.x > tank.rightWallX - this.size) { this.x = tank.rightWallX - this.size; this.vx *= -0.3; } this.vx *= 0.98; this.vy *= 0.98; this.x += this.vx * dt; this.y += this.vy * dt; this.rot += this.rotSpeed * dt; this.rotSpeed *= 0.97; // Check if pushed out if (this.x >= tank.rightWallX - this.size * 0.3 && currentVx > 100) { this.airborne = true; this.vx = Math.max(this.vx, currentVx * 0.6); } } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rot); ctx.fillStyle = '#888'; ctx.beginPath(); ctx.moveTo(this.points[0].x, this.points[0].y); for (let i = 1; i < this.points.length; i++) ctx.lineTo(this.points[i].x, this.points[i].y); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#666'; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } } // ─── Plants ───────────────────────────────────────────────── let plants = []; class Plant { constructor(x) { this.baseX = x; this.baseY = tank.bottomY; this.segments = 8; this.segLen = 30 + Math.random() * 20; this.hue = 100 + Math.random() * 40; this.swayPhase = Math.random() * Math.PI * 2; this.broken = false; this.floating = false; this.vx = 0; this.vy = 0; this.rot = 0; this.rotSpeed = 0; this.onFloor = false; this.airborne = false; this.density = PLANT_DENSITY; } update(dt, currentVx, currentVy, underwater) { this.swayPhase += dt * 2; if (this.broken) { if (this.onFloor) { this.vx *= 0.9; this.x += this.vx * dt; return; } if (this.airborne) { this.vy += GRAVITY * dt; this.rot += this.rotSpeed * dt; this.x += this.vx * dt; this.y += this.vy * dt; if (this.y >= tank.floorY - 10) { this.y = tank.floorY - 10; this.onFloor = true; this.airborne = false; this.vx *= 0.3; } return; } if (underwater) { const buoyancy = (WATER_DENSITY / this.density - 1) * GRAVITY; this.vy += buoyancy * dt * 0.5; this.vx += currentVx * 0.05 * dt * 60; this.vy += currentVy * 0.03 * dt * 60; this.vx *= 0.96; this.vy *= 0.96; this.x += this.vx * dt; this.y += this.vy * dt; this.rot += this.rotSpeed * dt; if (this.y < waterSurfaceY) { this.y = waterSurfaceY; this.vy = Math.abs(this.vy) * 0.3; this.floating = true; } if (this.x >= tank.rightWallX && currentVx > 60) { this.airborne = true; this.vx = currentVx * 0.4; } return; } return; } // Check if near crack const distToCrack = Math.sqrt((this.baseX - tank.rightWallX) ** 2 + (this.baseY - crackY) ** 2); if (simState === 'BURSTING' && distToCrack < 80 && crackLength > 30) { this.broken = true; this.x = this.baseX; this.y = this.baseY; this.vx = 0; this.vy = 0; } } draw() { const segs = this.segments; const totalH = this.segLen * segs; ctx.strokeStyle = `hsl(${this.hue}, 60%, 40%)`; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.beginPath(); let px = this.broken ? this.x : this.baseX; let py = this.broken ? this.y : this.baseY; ctx.moveTo(px, py); for (let i = 1; i <= segs; i++) { const t = i / segs; const sway = Math.sin(this.swayPhase + i * 0.5) * (this.broken ? 2 : 8) * t; px += sway * 0.3; py -= this.segLen; ctx.lineTo(px, py); } ctx.stroke(); // Leaves ctx.fillStyle = `hsl(${this.hue}, 55%, 35%)`; for (let i = 2; i <= segs; i++) { const t = i / segs; const sway = Math.sin(this.swayPhase + i * 0.5) * (this.broken ? 2 : 8) * t; const lx = (this.broken ? this.x : this.baseX) + sway * 0.3 * i; const ly = (this.broken ? this.y : this.baseY) - this.segLen * i; ctx.beginPath(); ctx.ellipse(lx + 5, ly, 6, 3, Math.sin(this.swayPhase + i) * 0.5, 0, Math.PI * 2); ctx.fill(); } } } // ─── Floating Toy ─────────────────────────────────────────── let toy = null; class Toy { constructor(x, y) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.density = TOY_DENSITY; this.size = 18; this.rot = 0; this.rotSpeed = 0; this.onFloor = false; this.airborne = false; this.bobPhase = Math.random() * Math.PI * 2; this.hue = 30; } update(dt, currentVx, currentVy, underwater) { this.bobPhase += dt * 4; if (this.onFloor) { this.vx *= 0.9; this.x += this.vx * dt; this.rot += this.rotSpeed * dt; this.rotSpeed *= 0.95; return; } if (this.airborne) { this.vy += GRAVITY * dt; this.rot += this.rotSpeed * dt; this.rotSpeed *= 0.995; this.x += this.vx * dt; this.y += this.vy * dt; if (this.y >= tank.floorY - this.size * 0.5) { this.y = tank.floorY - this.size * 0.5; this.onFloor = true; this.airborne = false; this.vx *= 0.3; this.rotSpeed *= 0.2; } return; } if (underwater) { // Strong buoyancy const buoyancy = (WATER_DENSITY / this.density - 1) * GRAVITY * 2; this.vy += buoyancy * dt; const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); if (speed > 0.01) { const dragMag = 0.5 * WATER_DRAG_COEFF * WATER_DENSITY * this.size * this.size * speed * speed * 0.05; this.vx -= (this.vx / speed) * dragMag * dt / (this.density * this.size); this.vy -= (this.vy / speed) * dragMag * dt / (this.density * this.size); } this.vx += currentVx * 0.08 * dt * 60; this.vy += currentVy * 0.04 * dt * 60; // Keep near surface if (this.y < waterSurfaceY - this.size * 0.3) { this.y = waterSurfaceY - this.size * 0.3; this.vy = Math.max(0, this.vy) * 0.5; } if (this.y > tank.bottomY - this.size) this.y = tank.bottomY - this.size; if (this.x < tank.leftWallX + this.size) { this.x = tank.leftWallX + this.size; this.vx *= -0.3; } if (this.x > tank.rightWallX - this.size) { this.x = tank.rightWallX - this.size; this.vx *= -0.3; } this.vx *= 0.97; this.vy *= 0.97; this.x += this.vx * dt; this.y += this.vy * dt; this.rot += this.rotSpeed * dt; // Bobbing this.y += Math.sin(this.bobPhase) * 0.3; // Check if pushed out if (this.x >= tank.rightWallX - this.size * 0.3 && currentVx > 50) { this.airborne = true; this.vx = Math.max(this.vx, currentVx * 0.8); this.rotSpeed = (Math.random() - 0.5) * 5; } } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rot); const s = this.size; // Rubber duck simplified ctx.fillStyle = '#FFD700'; ctx.beginPath(); ctx.ellipse(0, 0, s, s * 0.7, 0, 0, Math.PI * 2); ctx.fill(); // Head ctx.beginPath(); ctx.arc(s * 0.5, -s * 0.4, s * 0.45, 0, Math.PI * 2); ctx.fill(); // Beak ctx.fillStyle = '#FF8C00'; ctx.beginPath(); ctx.moveTo(s * 0.9, -s * 0.4); ctx.lineTo(s * 1.3, -s * 0.35); ctx.lineTo(s * 0.9, -s * 0.3); ctx.closePath(); ctx.fill(); // Eye ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(s * 0.6, -s * 0.5, s * 0.08, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } // ─── Glass Fragments ──────────────────────────────────────── let glassFragments = []; class GlassFragment { constructor(x, y, size, vx, vy) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.size = size; this.rot = Math.random() * Math.PI * 2; this.rotSpeed = (Math.random() - 0.5) * 10; this.density = GLASS_DENSITY; this.points = this.generateShape(); this.onFloor = false; this.airborne = true; this.inWater = false; this.alpha = 1; } generateShape() { const pts = []; const n = 3 + Math.floor(Math.random() * 2); for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2 + Math.random() * 0.3; const r = this.size * (0.5 + Math.random() * 0.5); pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r }); } return pts; } update(dt) { if (this.onFloor) { this.vx *= 0.88; this.rotSpeed *= 0.95; this.x += this.vx * dt; this.rot += this.rotSpeed * dt; return; } // Gravity this.vy += GRAVITY * dt; // Water resistance if in water if (this.inWater) { const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); if (speed > 0.01) { const dragMag = 0.5 * 1.0 * WATER_DENSITY * this.size * this.size * speed * speed * 0.02; this.vx -= (this.vx / speed) * dragMag * dt / (this.density * this.size); this.vy -= (this.vy / speed) * dragMag * dt / (this.density * this.size); } this.rotSpeed *= 0.95; } else { this.rotSpeed *= 0.998; } this.rot += this.rotSpeed * dt; this.x += this.vx * dt; this.y += this.vy * dt; if (this.y >= tank.floorY - 2) { this.y = tank.floorY - 2; this.onFloor = true; this.airborne = false; this.vx *= 0.35; this.rotSpeed *= 0.15; } this.inWater = this.y > waterSurfaceY && this.y < tank.bottomY; } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rot); ctx.globalAlpha = this.alpha * 0.6; ctx.fillStyle = '#aaddff'; ctx.strokeStyle = '#7799bb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(this.points[0].x, this.points[0].y); for (let i = 1; i < this.points.length; i++) ctx.lineTo(this.points[i].x, this.points[i].y); ctx.closePath(); ctx.fill(); ctx.stroke(); // Shine ctx.globalAlpha = this.alpha * 0.3; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.moveTo(this.points[0].x * 0.3, this.points[0].y * 0.3); ctx.lineTo(this.points[1].x * 0.3, this.points[1].y * 0.3); ctx.lineTo(0, 0); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1; ctx.restore(); } } // ─── Bubble particles ─────────────────────────────────────── let bubbles = []; class Bubble { constructor(x, y) { this.x = x; this.y = y; this.vx = (Math.random() - 0.5) * 20; this.vy = -30 - Math.random() * 40; this.size = 2 + Math.random() * 4; this.life = 1.0; } update(dt) { this.x += this.vx * dt; this.y += this.vy * dt; this.life -= dt * 0.8; this.vx += (Math.random() - 0.5) * 30 * dt; } draw() { if (this.life <= 0) return; ctx.globalAlpha = this.life * 0.4; ctx.fillStyle = '#aaddff'; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } } // ─── Splash particles ─────────────────────────────────────── let splashParticles = []; class Splash { constructor(x, y, angle) { this.x = x; this.y = y; const speed = 100 + Math.random() * 200; const a = angle + (Math.random() - 0.5) * 1.2; this.vx = Math.cos(a) * speed; this.vy = Math.sin(a) * speed; this.size = 1 + Math.random() * 3; this.life = 1.0; this.decay = 1.0 + Math.random() * 1.5; } update(dt) { this.vy += GRAVITY * dt; this.x += this.vx * dt; this.y += this.vy * dt; this.life -= this.decay * dt; if (this.y >= tank.floorY) { this.y = tank.floorY; this.vy = 0; this.decay = 3; } } draw() { if (this.life <= 0) return; ctx.globalAlpha = this.life * 0.6; ctx.fillStyle = '#88ccff'; ctx.beginPath(); ctx.arc(this.x, this.y, this.size * this.life, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } } // ─── Initialize objects ───────────────────────────────────── function init() { calcTank(); simState = 'DRAG'; waterLevel = 1.0; burstTime = 0; crackActive = false; draggingCrack = false; burstTriggered = false; crackGrowing = false; crackGrowTime = 0; crackLength = 0; jetParticles = []; bubbles = []; splashParticles = []; glassFragments = []; puddle = { cx: tank.rightWallX + 10, cy: tank.floorY, rx: 0, ry: 0, volume: 0 }; crackY = tank.y + tank.h * 0.5; waterSurfaceY = tank.y + tank.h * 0.05; fishes = []; const fishData = [ { x: tank.x + tank.w * 0.3, y: tank.y + tank.h * 0.3, s: 14, h: 15 }, { x: tank.x + tank.w * 0.6, y: tank.y + tank.h * 0.5, s: 12, h: 200 }, { x: tank.x + tank.w * 0.4, y: tank.y + tank.h * 0.7, s: 16, h: 340 }, { x: tank.x + tank.w * 0.75, y: tank.y + tank.h * 0.2, s: 10, h: 50 }, { x: tank.x + tank.w * 0.2, y: tank.y + tank.h * 0.6, s: 13, h: 120 }, ]; fishData.forEach(f => fishes.push(new Fish(f.x, f.y, f.s, f.h))); rocks = []; for (let i = 0; i < 6; i++) { rocks.push(new Rock( tank.x + 30 + Math.random() * (tank.w - 60), tank.bottomY - 8 - Math.random() * 5, 6 + Math.random() * 10 )); } plants = []; plants.push(new Plant(tank.x + 40)); plants.push(new Plant(tank.x + tank.w * 0.25)); plants.push(new Plant(tank.x + tank.w * 0.7)); plants.push(new Plant(tank.x + tank.w - 40)); toy = new Toy(tank.x + tank.w * 0.5, tank.y + tank.h * 0.1); burstBtn.disabled = false; statusEl.textContent = 'Drag the crack vertically on the tank, then trigger the burst.'; waterLevelEl.textContent = 'Water: 100%'; } // ─── Trigger burst ────────────────────────────────────────── function triggerBurst() { if (simState !== 'DRAG') return; simState = 'CRACKING'; crackGrowing = true; crackGrowTime = 0; crackActive = true; burstBtn.disabled = true; statusEl.textContent = '💥 Glass cracking...'; } burstBtn.addEventListener('click', triggerBurst); resetBtn.addEventListener('click', init); // ─── Mouse / touch for crack dragging ────────────────────── canvas.addEventListener('mousedown', (e) => { if (simState !== 'DRAG') return; const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; if (Math.abs(mx - tank.rightWallX) < 20 && my > tank.y && my < tank.bottomY) { draggingCrack = true; dragOffset = crackY - my; } }); canvas.addEventListener('mousemove', (e) => { if (!draggingCrack) return; const rect = canvas.getBoundingClientRect(); const my = e.clientY - rect.top; crackY = Math.max(tank.y + 10, Math.min(tank.bottomY - 10, my + dragOffset)); }); canvas.addEventListener('mouseup', () => { draggingCrack = false; }); canvas.addEventListener('mouseleave', () => { draggingCrack = false; }); // Touch support canvas.addEventListener('touchstart', (e) => { if (simState !== 'DRAG') return; e.preventDefault(); const rect = canvas.getBoundingClientRect(); const t = e.touches[0]; const mx = t.clientX - rect.left; const my = t.clientY - rect.top; if (Math.abs(mx - tank.rightWallX) < 30 && my > tank.y && my < tank.bottomY) { draggingCrack = true; dragOffset = crackY - my; } }, { passive: false }); canvas.addEventListener('touchmove', (e) => { if (!draggingCrack) return; e.preventDefault(); const rect = canvas.getBoundingClientRect(); const t = e.touches[0]; const my = t.clientY - rect.top; crackY = Math.max(tank.y + 10, Math.min(tank.bottomY - 10, my + dragOffset)); }, { passive: false }); canvas.addEventListener('touchend', () => { draggingCrack = false; }); // ─── Spawn glass fragments ────────────────────────────────── function spawnGlassFragments() { const numFragments = 25 + Math.floor(Math.random() * 15); for (let i = 0; i < numFragments; i++) { const fy = crackY + (Math.random() - 0.5) * Math.min(crackLength, tank.h * 0.6); const size = 3 + Math.random() * 12; const vx = 50 + Math.random() * 200; const vy = (Math.random() - 0.5) * 150; glassFragments.push(new GlassFragment(tank.rightWallX, fy, size, vx, vy)); } } // ─── Spawn splash ─────────────────────────────────────────── function spawnSplash(x, y, angle, count) { for (let i = 0; i < count; i++) { splashParticles.push(new Splash(x, y, angle)); } } // ─── Main update ──────────────────────────────────────────── let lastTime = 0; let drainedVolume = 0; let jetActive = false; let firstSplash = true; function update(timestamp) { const dt = Math.min((timestamp - lastTime) / 1000, 0.05); lastTime = timestamp; waterSurfaceY = tank.y + tank.h * 0.05 + (1 - waterLevel) * tank.h * 0.95; // ── State transitions ── if (simState === 'CRACKING') { crackGrowTime += dt; crackLength = Math.min(tank.h * 0.55, crackGrowTime * 150); if (crackGrowTime > 1.2) { simState = 'BURSTING'; burstTime = 0; jetActive = true; spawnGlassFragments(); spawnSplash(tank.rightWallX, crackY, 0, 30); statusEl.textContent = '💧 Water escaping!'; } } if (simState === 'BURSTING' || simState === 'DRAINING') { burstTime += dt; // Flow rate based on water depth above crack const waterDepthAboveCrack = Math.max(0, waterSurfaceY - crackY); const pressure = waterDepthAboveCrack / tank.h; const jetSpeed = Math.sqrt(2 * GRAVITY * Math.max(10, waterDepthAboveCrack)) * 0.8; const flowRate = pressure * 3; if (waterLevel > 0.01) { // Spawn jet particles const numJets = Math.floor(flowRate * 5) + 1; for (let i = 0; i < numJets; i++) { const spread = (Math.random() - 0.5) * 20; const angle = (Math.random() - 0.5) * 0.15; jetParticles.push(new JetParticle( tank.rightWallX + 3, crackY + (Math.random() - 0.5) * crackLength * 0.3 + spread, jetSpeed * 0.3 + Math.random() * jetSpeed * 0.15, Math.sin(angle) * jetSpeed * 0.1, 2 + Math.random() * 4 )); } // Bubbles inside tank near crack if (Math.random() < 0.3) { bubbles.push(new Bubble( tank.rightWallX - 10 - Math.random() * 20, crackY + (Math.random() - 0.5) * 30 )); } // Drain water const drainRate = flowRate * dt * 0.15; waterLevel = Math.max(0, waterLevel - drainRate); drainedVolume += drainRate; // Puddle growth puddle.volume += drainRate * 500; puddle.rx = Math.min(W - tank.rightWallX - 20, Math.sqrt(puddle.volume) * 0.8); puddle.ry = Math.min(30, puddle.volume * 0.02); puddle.cx = tank.rightWallX + puddle.rx * 0.5; // Check if puddle hits wall if (puddle.cx + puddle.rx >= W - 20) { puddle.rx = W - 20 - puddle.cx; } // First splash if (firstSplash && burstTime > 0.3) { firstSplash = false; } // Splash where jet hits floor const jetHitX = tank.rightWallX + jetSpeed * 0.15; const jetHitY = crackY + 0.5 * GRAVITY * (0.15) * (0.15); if (jetHitY >= tank.floorY - 20 && Math.random() < 0.4) { spawnSplash(jetHitX + Math.random() * puddle.rx, tank.floorY, -Math.PI * 0.4, 2); } // Current near crack const currentVx = pressure * 300; const currentVy = -pressure * 50; // Update fishes fishes.forEach(f => { const underwater = f.y > waterSurfaceY && !f.airborne && !f.onFloor; f.update(dt, currentVx, currentVy, underwater); }); // Update rocks rocks.forEach(r => { const underwater = r.y > waterSurfaceY && !r.airborne && !r.onFloor; r.update(dt, currentVx, currentVy, underwater); }); // Update plants plants.forEach(p => { const underwater = (p.broken ? p.y : p.baseY) > waterSurfaceY; p.update(dt, currentVx, currentVy, underwater); }); // Update toy const toyUnderwater = toy.y > waterSurfaceY && !toy.airborne && !toy.onFloor; toy.update(dt, currentVx, currentVy, toyUnderwater); } else { simState = 'EMPTY'; jetActive = false; statusEl.textContent = '🐟 Tank drained. Fish and objects settled.'; } if (waterLevel < 0.3 && simState !== 'EMPTY') simState = 'DRAINING'; } if (simState === 'DRAINING' || simState === 'EMPTY') { // Objects still update (gravity, floor) fishes.forEach(f => f.update(dt, 0, 0, false)); rocks.forEach(r => r.update(dt, 0, 0, false)); plants.forEach(p => p.update(dt, 0, 0, false)); if (toy) toy.update(dt, 0, 0, false); } // Update jet particles jetParticles = jetParticles.filter(p => p.life > 0); jetParticles.forEach(p => p.update(dt)); // Update bubbles bubbles = bubbles.filter(b => b.life > 0); bubbles.forEach(b => b.update(dt)); // Update splash splashParticles = splashParticles.filter(s => s.life > 0); splashParticles.forEach(s => s.update(dt)); // Update glass glassFragments.forEach(g => g.update(dt)); waterLevelEl.textContent = `Water: ${Math.round(waterLevel * 100)}%`; draw(); requestAnimationFrame(update); } // ─── Draw ─────────────────────────────────────────────────── function draw() { ctx.clearRect(0, 0, W, H); // Room background const bgGrad = ctx.createLinearGradient(0, 0, 0, H); bgGrad.addColorStop(0, '#2c3e50'); bgGrad.addColorStop(0.7, '#34495e'); bgGrad.addColorStop(1, '#1a1a2e'); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H); // Floor const floorGrad = ctx.createLinearGradient(0, tank.floorY, 0, H); floorGrad.addColorStop(0, '#5d4e37'); floorGrad.addColorStop(1, '#3d3225'); ctx.fillStyle = floorGrad; ctx.fillRect(0, tank.floorY, W, H - tank.floorY); // Floor line ctx.strokeStyle = '#7a6b55'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, tank.floorY); ctx.lineTo(W, tank.floorY); ctx.stroke(); // ── Tank background (inside) ── ctx.fillStyle = 'rgba(100, 180, 220, 0.08)'; ctx.fillRect(tank.x, tank.y, tank.w, tank.h); // ── Water inside tank ── if (waterLevel > 0.005) { const waterTop = waterSurfaceY; const waterBottom = tank.bottomY; const waterGrad = ctx.createLinearGradient(0, waterTop, 0, waterBottom); waterGrad.addColorStop(0, 'rgba(60, 160, 220, 0.35)'); waterGrad.addColorStop(0.5, 'rgba(40, 130, 200, 0.45)'); waterGrad.addColorStop(1, 'rgba(30, 100, 170, 0.55)'); ctx.fillStyle = waterGrad; ctx.fillRect(tank.x + 2, waterTop, tank.w - 4, waterBottom - waterTop); // Water surface ctx.strokeStyle = 'rgba(140, 210, 255, 0.6)'; ctx.lineWidth = 2; ctx.beginPath(); for (let x = tank.x + 2; x < tank.x + tank.w - 2; x += 2) { const wave = Math.sin(x * 0.05 + Date.now() * 0.003) * 2; if (x === tank.x + 2) ctx.moveTo(x, waterTop + wave); else ctx.lineTo(x, waterTop + wave); } ctx.stroke(); // Light caustics ctx.globalAlpha = 0.06; for (let i = 0; i < 5; i++) { const cx = tank.x + 40 + Math.sin(Date.now() * 0.001 + i * 2) * 60 + i * 80; const cy = waterTop + 30 + Math.cos(Date.now() * 0.0015 + i) * 20 + i * 25; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(cx, cy, 30 + Math.sin(Date.now() * 0.002 + i) * 10, 8, 0.3, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } // ── Sand / gravel bottom ── const sandGrad = ctx.createLinearGradient(0, tank.bottomY - 12, 0, tank.bottomY); sandGrad.addColorStop(0, '#c2a66b'); sandGrad.addColorStop(1, '#a08850'); ctx.fillStyle = sandGrad; ctx.beginPath(); ctx.moveTo(tank.x + 2, tank.bottomY - 8); for (let x = tank.x + 2; x < tank.x + tank.w - 2; x += 8) { ctx.lineTo(x, tank.bottomY - 8 + Math.sin(x * 0.1) * 2); } ctx.lineTo(tank.x + tank.w - 2, tank.bottomY); ctx.lineTo(tank.x + 2, tank.bottomY); ctx.closePath(); ctx.fill(); // ── Draw plants (behind objects) ── plants.forEach(p => p.draw()); // ── Draw rocks ── rocks.forEach(r => r.draw()); // ── Draw fishes ── fishes.forEach(f => f.draw()); // ── Draw toy ── if (toy) toy.draw(); // ── Draw bubbles ── bubbles.forEach(b => b.draw()); // ── Tank walls ── ctx.strokeStyle = 'rgba(180, 220, 255, 0.5)'; ctx.lineWidth = tank.wallThickness; ctx.strokeRect(tank.x, tank.y, tank.w, tank.h); // Wall highlights ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(tank.x + 4, tank.y + 4); ctx.lineTo(tank.x + 4, tank.bottomY - 4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(tank.x + tank.w - 4, tank.y + 4); ctx.lineTo(tank.x + tank.w - 4, tank.bottomY - 4); ctx.stroke(); // ── Crack ── if (crackActive || simState === 'DRAG') { const crackLen = Math.max(5, crackLength || 20); const crackTop = crackY - crackLen * 0.3; const crackBot = crackY + crackLen * 0.3; ctx.strokeStyle = simState === 'CRACKING' ? '#ff4444' : (simState === 'DRAG' ? 'rgba(255,255,255,0.5)' : '#ff6644'); ctx.lineWidth = simState === 'CRACKING' ? 2 : (simState === 'DRAG' ? 1.5 : 2.5); ctx.setLineDash(simState === 'DRAG' ? [4, 4] : []); ctx.beginPath(); let cx = tank.rightWallX; let cy = crackTop; ctx.moveTo(cx, cy); const segs = Math.floor(crackLen / 5); for (let i = 1; i <= segs; i++) { cx += (Math.random() - 0.5) * 6; cy = crackTop + (i / segs) * crackLen; ctx.lineTo(cx, cy); } ctx.stroke(); ctx.setLineDash([]); // Crack glow when bursting if (simState === 'BURSTING' || simState === 'DRAINING') { ctx.globalAlpha = 0.3 + Math.sin(Date.now() * 0.01) * 0.15; ctx.strokeStyle = '#ff8866'; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(tank.rightWallX, crackTop); for (let i = 1; i <= segs; i++) { ctx.lineTo(tank.rightWallX + (Math.random() - 0.5) * 6, crackTop + (i / segs) * crackLen); } ctx.stroke(); ctx.globalAlpha = 1; } } // ── Crack drag indicator ── if (simState === 'DRAG') { ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.beginPath(); ctx.arc(tank.rightWallX, crackY, 8, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(tank.rightWallX, crackY, 8, 0, Math.PI * 2); ctx.stroke(); // Double arrow ctx.fillStyle = '#fff'; ctx.font = '12px sans-serif'; ctx.fillText('↕', tank.rightWallX - 6, crackY - 14); ctx.fillText('↕', tank.rightWallX - 6, crackY + 22); } // ── Jet stream ── if (jetActive && waterLevel > 0.01) { // Main jet arc const waterDepth = Math.max(10, waterSurfaceY - crackY); const jetSpeed = Math.sqrt(2 * GRAVITY * waterDepth) * 0.8; ctx.strokeStyle = 'rgba(100, 180, 240, 0.4)'; ctx.lineWidth = 8 + waterDepth * 0.05; ctx.beginPath(); let jx = tank.rightWallX + 3; let jy = crackY; let jvx = jetSpeed * 0.25; let jvy = 0; ctx.moveTo(jx, jy); for (let t = 0; t < 0.5; t += 0.01) { jvy += GRAVITY * 0.01; jx += jvx * 0.01; jy += jvy * 0.01; ctx.lineTo(jx, jy); if (jy >= tank.floorY) break; } ctx.stroke(); // Inner bright jet ctx.strokeStyle = 'rgba(180, 220, 255, 0.5)'; ctx.lineWidth = 3; ctx.beginPath(); jx = tank.rightWallX + 3; jy = crackY; jvx = jetSpeed * 0.25; jvy = 0; ctx.moveTo(jx, jy); for (let t = 0; t < 0.5; t += 0.01) { jvy += GRAVITY * 0.01; jx += jvx * 0.01; jy += jvy * 0.01; ctx.lineTo(jx, jy); if (jy >= tank.floorY) break; } ctx.stroke(); } // ── Jet particles ── jetParticles.forEach(p => p.draw()); // ── Splash particles ── splashParticles.forEach(s => s.draw()); // ── Glass fragments ── glassFragments.forEach(g => g.draw()); // ── Puddle ── if (puddle.volume > 1) { const px = puddle.cx; const py = puddle.cy; const prx = puddle.rx; const pry = puddle.ry; ctx.globalAlpha = 0.5; const puddleGrad = ctx.createRadialGradient(px, py, 0, px, py, prx); puddleGrad.addColorStop(0, 'rgba(60, 140, 200, 0.6)'); puddleGrad.addColorStop(0.7, 'rgba(50, 120, 180, 0.3)'); puddleGrad.addColorStop(1, 'rgba(40, 100, 160, 0)'); ctx.fillStyle = puddleGrad; ctx.beginPath(); ctx.ellipse(px, py, prx, Math.max(pry, 4), 0, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; // Puddle shine ctx.globalAlpha = 0.15; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(px - prx * 0.3, py - pry * 0.3, prx * 0.2, Math.max(pry * 0.3, 2), -0.3, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } // ── Water outside tank (flowing down wall) ── if (jetActive && waterLevel > 0.01) { ctx.globalAlpha = 0.15; ctx.fillStyle = '#66aadd'; const flowWidth = 3 + waterLevel * 8; ctx.fillRect(tank.rightWallX - flowWidth * 0.5, crackY, flowWidth, tank.bottomY - crackY); ctx.globalAlpha = 1; } // ── Tank label ── ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('AQUARIUM', tank.x + tank.w / 2, tank.y - 10); ctx.textAlign = 'left'; // ── Depth indicator on tank ── if (simState === 'DRAG') { ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.font = '11px sans-serif'; const depth = ((crackY - tank.y) / tank.h * 100).toFixed(0); ctx.fillText(`Crack depth: ${depth}%`, tank.x + 10, tank.y + tank.h + 18); } } // ─── Start ────────────────────────────────────────────────── init(); requestAnimationFrame(update); </script> </body> </html>
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Aquarium with fish, plants, crack slider renders but burst never triggers; tank stays static across all inputs.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.