Models 44

Gemini 3.8 Flash

by GoogleReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
45.8 s
Cost
$0.029
Output tokens
7,773
Spent thinking
0 tokens
Speed
179.4 tok/s
Console errors
0
First token after
2.4 s
First visible text after
2.4 s
Prompt tokens
448
Code size
25.4 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"low"}
Max output
65,536 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 653 lines

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Interactive Aquarium</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none;
        }
        body, html {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-color: #020b14;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        }
        #webgl-canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
        }
        #ui-overlay {
            position: absolute;
            top: 24px;
            left: 24px;
            z-index: 10;
            color: #d1f4ff;
            text-shadow: 0 2px 10px rgba(0, 30, 60, 0.8);
            pointer-events: none;
            backdrop-filter: blur(8px);
            background: rgba(2, 20, 40, 0.35);
            padding: 16px 22px;
            border-radius: 14px;
            border: 1px solid rgba(80, 200, 255, 0.25);
            box-shadow: 0 8px 32px rgba(0, 10, 25, 0.4);
            max-width: 320px;
        }
        h1 {
            font-size: 20px;
            font-weight: 600;
            letter-spacing: 0.5px;
            margin-bottom: 6px;
            color: #7ce8ff;
        }
        p {
            font-size: 13px;
            line-height: 1.4;
            color: #a4d8ec;
        }
        .hud-status {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            margin-top: 10px;
            font-size: 12px;
            color: #4ef2bb;
            font-weight: 500;
        }
        .status-dot {
            width: 8px;
            height: 8px;
            background: #4ef2bb;
            border-radius: 50%;
            box-shadow: 0 0 8px #4ef2bb;
            animation: pulse 1.8s infinite;
        }
        @keyframes pulse {
            0%, 100% { opacity: 0.4; transform: scale(0.9); }
            50% { opacity: 1; transform: scale(1.2); }
        }
        #reticle {
            position: absolute;
            width: 36px;
            height: 36px;
            border: 2px dashed rgba(110, 220, 255, 0.6);
            border-radius: 50%;
            pointer-events: none;
            transform: translate(-50%, -50%);
            z-index: 5;
            transition: width 0.2s, height 0.2s, border-color 0.2s;
            display: none;
        }
    </style>
    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
        }
    }
    </script>
</head>
<body>
    <div id="ui-overlay">
        <h1>Bioluminescent Tank</h1>
        <p>Move mouse or touch to disturb the water. Fish react dynamically and scatter from your reach.</p>
        <div class="hud-status">
            <span class="status-dot"></span>
            <span id="fish-count">48 Schooling Fish active</span>
        </div>
    </div>
    <div id="reticle"></div>
    <canvas id="webgl-canvas"></canvas>

    <script type="module">
        import * as THREE from 'three';

        // --- CONSTANTS & CONFIG ---
        const TANK_WIDTH = 52;
        const TANK_HEIGHT = 30;
        const TANK_DEPTH = 32;
        const FISH_COUNT = 52;
        const BUBBLE_COUNT = 140;

        // --- SETUP SCENE, CAMERA, RENDERER ---
        const canvas = document.getElementById('webgl-canvas');
        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, powerPreference: 'high-performance' });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.15;
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;

        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x021124);
        scene.fog = new THREE.FogExp2(0x021124, 0.024);

        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
        camera.position.set(0, 2, 48);

        // --- LIGHTING ---
        const ambientLight = new THREE.AmbientLight(0x0a324b, 1.8);
        scene.add(ambientLight);

        // Sun caustics beam from above
        const topLight = new THREE.DirectionalLight(0x73e2ff, 3.2);
        topLight.position.set(5, 30, 8);
        topLight.castShadow = true;
        topLight.shadow.mapSize.width = 1024;
        topLight.shadow.mapSize.height = 1024;
        topLight.shadow.camera.near = 10;
        topLight.shadow.camera.far = 60;
        topLight.shadow.camera.left = -30;
        topLight.shadow.camera.right = 30;
        topLight.shadow.camera.top = 25;
        topLight.shadow.camera.bottom = -25;
        scene.add(topLight);

        const fillLight = new THREE.PointLight(0x1a8bc8, 2.5, 70);
        fillLight.position.set(-15, -8, 10);
        scene.add(fillLight);

        // Interactive cursor spotlight in 3D
        const mouseLight = new THREE.PointLight(0x52f6ff, 3.0, 25);
        mouseLight.position.set(0, 0, 0);
        scene.add(mouseLight);

        // --- TANK BOUNDS & ENVIRONMENT ---
        // Sandy bottom
        const floorGeo = new THREE.PlaneGeometry(TANK_WIDTH * 1.1, TANK_DEPTH * 1.1, 48, 48);
        const floorPos = floorGeo.attributes.position;
        for (let i = 0; i < floorPos.count; i++) {
            const u = floorPos.getX(i);
            const v = floorPos.getY(i);
            // gentle sand ripples
            const zRipples = Math.sin(u * 0.4) * 0.35 + Math.cos(v * 0.4) * 0.28 + Math.sin(u * 0.9 + v * 0.7) * 0.15;
            floorPos.setZ(i, zRipples);
        }
        floorGeo.computeVertexNormals();

        const floorMat = new THREE.MeshStandardMaterial({
            color: 0x1f3c4c,
            roughness: 0.85,
            metalness: 0.1,
            flatShading: true
        });
        const floor = new THREE.Mesh(floorGeo, floorMat);
        floor.rotation.x = -Math.PI / 2;
        floor.position.y = -TANK_HEIGHT / 2;
        floor.receiveShadow = true;
        scene.add(floor);

        // Tank Frame / Glass Edges
        const tankBox = new THREE.BoxGeometry(TANK_WIDTH, TANK_HEIGHT, TANK_DEPTH);
        const tankEdges = new THREE.EdgesGeometry(tankBox);
        const tankLineMat = new THREE.LineBasicMaterial({ color: 0x41a8d8, opacity: 0.35, transparent: true });
        const tankWire = new THREE.LineSegments(tankEdges, tankLineMat);
        scene.add(tankWire);

        // Rocks at the bottom
        const rockGeo = new THREE.DodecahedronGeometry(1, 1);
        const rockMat = new THREE.MeshStandardMaterial({ color: 0x152834, roughness: 0.9 });
        for (let i = 0; i < 18; i++) {
            const rock = new THREE.Mesh(rockGeo, rockMat);
            const rx = (Math.random() - 0.5) * (TANK_WIDTH - 8);
            const rz = (Math.random() - 0.5) * (TANK_DEPTH - 8);
            const scale = 1.4 + Math.random() * 2.8;
            rock.scale.set(scale * (0.8 + Math.random() * 0.4), scale * (0.6 + Math.random() * 0.8), scale * (0.8 + Math.random() * 0.4));
            rock.position.set(rx, -TANK_HEIGHT / 2 + scale * 0.4, rz);
            rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
            rock.receiveShadow = true;
            rock.castShadow = true;
            scene.add(rock);
        }

        // Aquatic Plants (Seaweed ribbon strands)
        const seaweeds = [];
        const seaweedMat = new THREE.MeshStandardMaterial({
            color: 0x0fa37f,
            roughness: 0.6,
            side: THREE.DoubleSide
        });

        for (let j = 0; j < 14; j++) {
            const height = 9 + Math.random() * 11;
            const segments = 10;
            const plantGeo = new THREE.PlaneGeometry(0.75, height, 1, segments);
            const plant = new THREE.Mesh(plantGeo, seaweedMat);
            const px = (Math.random() - 0.5) * (TANK_WIDTH - 10);
            const pz = (Math.random() - 0.5) * (TANK_DEPTH - 10);
            plant.position.set(px, -TANK_HEIGHT / 2 + height / 2, pz);
            plant.rotation.y = Math.random() * Math.PI;
            scene.add(plant);
            seaweeds.push({ mesh: plant, originalGeo: plantGeo.clone(), speed: 1.5 + Math.random() * 1.5, offset: Math.random() * 10 });
        }

        // --- BUBBLES ---
        const bubbleGeo = new THREE.SphereGeometry(0.18, 8, 8);
        const bubbleMat = new THREE.MeshPhysicalMaterial({
            color: 0xffffff,
            transmission: 0.85,
            opacity: 1,
            transparent: true,
            roughness: 0.1,
            ior: 1.15
        });
        const bubbles = [];
        for (let i = 0; i < BUBBLE_COUNT; i++) {
            const b = new THREE.Mesh(bubbleGeo, bubbleMat);
            b.position.set(
                (Math.random() - 0.5) * TANK_WIDTH,
                -TANK_HEIGHT / 2 + Math.random() * TANK_HEIGHT,
                (Math.random() - 0.5) * TANK_DEPTH
            );
            const scale = 0.4 + Math.random() * 0.9;
            b.scale.set(scale, scale, scale);
            b.userData = {
                speedY: 0.05 + Math.random() * 0.08,
                wobbleSpeed: 2 + Math.random() * 3,
                wobbleAmp: 0.015 + Math.random() * 0.02,
                seed: Math.random() * 100
            };
            scene.add(b);
            bubbles.push(b);
        }

        // --- PROCEDURAL FISH MESH CREATION ---
        function createFishGeometry() {
            // Fish body: elongated tapered shape
            const bodyGeo = new THREE.ConeGeometry(0.55, 2.2, 10, 4);
            bodyGeo.rotateZ(Math.PI / 2); // points forward along +X initially
            bodyGeo.scale(1.0, 1.25, 0.55);

            // Tail fin
            const tailGeo = new THREE.BufferGeometry();
            const tailVertices = new Float32Array([
                -1.1,  0.0,  0.0,
                -2.0,  0.8,  0.0,
                -1.8,  0.0,  0.0,

                -1.1,  0.0,  0.0,
                -1.8,  0.0,  0.0,
                -2.0, -0.8,  0.0
            ]);
            tailGeo.setAttribute('position', new THREE.BufferAttribute(tailVertices, 3));
            tailGeo.computeVertexNormals();

            // Dorsal fin
            const dorsalGeo = new THREE.BufferGeometry();
            const dorsalVerts = new Float32Array([
                0.1,  0.65, 0.0,
                -0.6, 1.1, 0.0,
                -0.7, 0.4, 0.0
            ]);
            dorsalGeo.setAttribute('position', new THREE.BufferAttribute(dorsalVerts, 3));
            dorsalGeo.computeVertexNormals();

            // Pectoral fin right & left
            const pecGeo = new THREE.BufferGeometry();
            const pecVerts = new Float32Array([
                0.2, -0.1, 0.35,
                -0.3, -0.3, 0.95,
                -0.4, -0.2, 0.3
            ]);
            pecGeo.setAttribute('position', new THREE.BufferAttribute(pecVerts, 3));
            pecGeo.computeVertexNormals();

            return { bodyGeo, tailGeo, dorsalGeo, pecGeo };
        }

        const fishGeos = createFishGeometry();

        // Palette presets for varied vibrant tropical fish
        const FISH_PALETTES = [
            { body: 0xff5e3a, accent: 0xffd152, emissive: 0x441100 }, // Clownfish Orange
            { body: 0x00c4cc, accent: 0x88ffaa, emissive: 0x002228 }, // Teal Neon
            { body: 0x9333ea, accent: 0xf43f5e, emissive: 0x24063c }, // Royal Violet/Pink
            { body: 0xfacc15, accent: 0x2563eb, emissive: 0x332600 }, // Tang Gold/Cobalt
            { body: 0x10b981, accent: 0x38bdf8, emissive: 0x022c22 }  // Emerald Laguna
        ];

        class Fish {
            constructor(id) {
                this.id = id;
                this.group = new THREE.Group();

                const pal = FISH_PALETTES[Math.floor(Math.random() * FISH_PALETTES.length)];
                this.baseColor = pal.body;

                const bodyMat = new THREE.MeshStandardMaterial({
                    color: pal.body,
                    roughness: 0.35,
                    metalness: 0.15,
                    emissive: pal.emissive,
                    emissiveIntensity: 0.6
                });

                const finMat = new THREE.MeshStandardMaterial({
                    color: pal.accent,
                    roughness: 0.4,
                    side: THREE.DoubleSide,
                    transparent: true,
                    opacity: 0.85
                });

                // Body Mesh
                this.body = new THREE.Mesh(fishGeos.bodyGeo, bodyMat);
                this.body.castShadow = true;
                this.group.add(this.body);

                // Fin hinges & meshes for realistic swimming animation
                this.tailHinge = new THREE.Group();
                this.tailHinge.position.set(-0.3, 0, 0);
                const tailMesh = new THREE.Mesh(fishGeos.tailGeo, finMat);
                tailMesh.position.set(0.3, 0, 0);
                this.tailHinge.add(tailMesh);
                this.group.add(this.tailHinge);

                const dorsalMesh = new THREE.Mesh(fishGeos.dorsalGeo, finMat);
                this.group.add(dorsalMesh);

                const pecR = new THREE.Mesh(fishGeos.pecGeo, finMat);
                const pecL = pecR.clone();
                pecL.scale.set(1, 1, -1);
                this.group.add(pecR);
                this.group.add(pecL);

                // Scale variation
                const s = 0.8 + Math.random() * 0.7;
                this.group.scale.set(s, s, s);

                // Physics state
                this.position = new THREE.Vector3(
                    (Math.random() - 0.5) * (TANK_WIDTH - 6),
                    (Math.random() - 0.5) * (TANK_HEIGHT - 6),
                    (Math.random() - 0.5) * (TANK_DEPTH - 6)
                );
                this.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 2,
                    (Math.random() - 0.5) * 1,
                    (Math.random() - 0.5) * 2
                ).normalize().multiplyScalar(0.08 + Math.random() * 0.08);

                this.acceleration = new THREE.Vector3();
                this.maxSpeed = 0.22 + Math.random() * 0.06;
                this.maxForce = 0.012;
                this.frightenedTimer = 0;
                this.tailWiggle = Math.random() * 10;
                this.wiggleFreq = 7;

                this.group.position.copy(this.position);
                scene.add(this.group);
            }

            flee(targetPos, radius = 13.0) {
                const dist = this.position.distanceTo(targetPos);
                if (dist < radius && dist > 0.001) {
                    const fleeForce = new THREE.Vector3().subVectors(this.position, targetPos);
                    const strength = Math.pow((1.0 - dist / radius), 2.2); // Exponential panic when closer
                    fleeForce.normalize().multiplyScalar(this.maxSpeed * 3.5 * strength);
                    
                    const steer = new THREE.Vector3().subVectors(fleeForce, this.velocity);
                    steer.clampLength(0, this.maxForce * 4.0);
                    this.acceleration.add(steer);
                    this.frightenedTimer = 1.8; // Burst state
                }
            }

            flock(allFish) {
                let neighborDist = 7.0;
                let sepDist = 3.0;

                let sep = new THREE.Vector3();
                let ali = new THREE.Vector3();
                let coh = new THREE.Vector3();

                let totalCohesion = 0;
                let totalAlignment = 0;
                let totalSeparation = 0;

                for (let other of allFish) {
                    if (other === this) continue;
                    const d = this.position.distanceTo(other.position);

                    // Separation
                    if (d < sepDist && d > 0) {
                        const diff = new THREE.Vector3().subVectors(this.position, other.position);
                        diff.normalize().divideScalar(d);
                        sep.add(diff);
                        totalSeparation++;
                    }

                    // Alignment & Cohesion
                    if (d < neighborDist) {
                        ali.add(other.velocity);
                        coh.add(other.position);
                        totalCohesion++;
                        totalAlignment++;
                    }
                }

                if (totalSeparation > 0) {
                    sep.divideScalar(totalSeparation);
                    sep.setLength(this.maxSpeed);
                    sep.sub(this.velocity);
                    sep.clampLength(0, this.maxForce * 1.5);
                    this.acceleration.add(sep.multiplyScalar(1.6));
                }

                if (totalAlignment > 0) {
                    ali.divideScalar(totalAlignment);
                    ali.setLength(this.maxSpeed);
                    ali.sub(this.velocity);
                    ali.clampLength(0, this.maxForce);
                    this.acceleration.add(ali.multiplyScalar(0.9));
                }

                if (totalCohesion > 0) {
                    coh.divideScalar(totalCohesion);
                    const desired = new THREE.Vector3().subVectors(coh, this.position);
                    desired.setLength(this.maxSpeed);
                    const steer = new THREE.Vector3().subVectors(desired, this.velocity);
                    steer.clampLength(0, this.maxForce);
                    this.acceleration.add(steer.multiplyScalar(0.7));
                }
            }

            containment() {
                const padX = TANK_WIDTH / 2 - 2.5;
                const padY = TANK_HEIGHT / 2 - 2.5;
                const padZ = TANK_DEPTH / 2 - 2.5;

                const wallForce = 0.025;
                if (this.position.x > padX) this.velocity.x -= wallForce;
                if (this.position.x < -padX) this.velocity.x += wallForce;
                if (this.position.y > padY) this.velocity.y -= wallForce;
                if (this.position.y < -padY) this.velocity.y += wallForce;
                if (this.position.z > padZ) this.velocity.z -= wallForce;
                if (this.position.z < -padZ) this.velocity.z += wallForce;
            }

            update(delta) {
                // Apply frightened sprint
                const currentMaxSpeed = this.frightenedTimer > 0 ? this.maxSpeed * 2.2 : this.maxSpeed;
                
                this.velocity.add(this.acceleration);
                this.velocity.clampLength(0.02, currentMaxSpeed);
                this.position.add(this.velocity);
                this.group.position.copy(this.position);

                // Smooth rotation towards velocity direction
                if (this.velocity.lengthSq() > 0.0001) {
                    const lookTarget = this.position.clone().add(this.velocity);
                    const targetQuat = new THREE.Quaternion();
                    const dummy = new THREE.Object3D();
                    dummy.position.copy(this.position);
                    dummy.lookAt(lookTarget);
                    // Fish model faces +X by default; orient properly
                    dummy.rotateY(Math.PI / 2);
                    this.group.quaternion.slerp(dummy.quaternion, 0.12);
                }

                // Fin wiggle animation proportional to speed
                const speed = this.velocity.length();
                const freq = (this.frightenedTimer > 0 ? 22 : 9) * (speed / this.maxSpeed);
                this.tailWiggle += freq * delta;
                const tailAngle = Math.sin(this.tailWiggle) * 0.45;
                this.tailHinge.rotation.y = tailAngle;

                if (this.frightenedTimer > 0) {
                    this.frightenedTimer -= delta;
                }

                // Reset force accumulator
                this.acceleration.set(0, 0, 0);
            }
        }

        const fishes = [];
        for (let i = 0; i < FISH_COUNT; i++) {
            fishes.push(new Fish(i));
        }

        // --- MOUSE & RAYCAST INTERACTION ---
        const raycaster = new THREE.Raycaster();
        const pointer = new THREE.Vector2(-999, -999);
        const virtualInteractionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
        const target3D = new THREE.Vector3(0, 0, 0);
        let hasUserMovedMouse = false;
        let idleTimer = 0;

        const reticle = document.getElementById('reticle');

        function onPointerMove(e) {
            hasUserMovedMouse = true;
            idleTimer = 0;
            const x = (e.clientX / window.innerWidth) * 2 - 1;
            const y = -(e.clientY / window.innerHeight) * 2 + 1;
            pointer.set(x, y);

            // Reticle visual position
            reticle.style.display = 'block';
            reticle.style.left = `${e.clientX}px`;
            reticle.style.top = `${e.clientY}px`;
        }

        window.addEventListener('pointermove', onPointerMove);
        window.addEventListener('pointerdown', (e) => {
            onPointerMove(e);
            // Pulse on click to startle all fish in wide radius
            fishes.forEach(fish => fish.flee(target3D, 24.0));
            reticle.style.width = '64px';
            reticle.style.height = '64px';
            reticle.style.borderColor = '#ff6b81';
            setTimeout(() => {
                reticle.style.width = '36px';
                reticle.style.height = '36px';
                reticle.style.borderColor = 'rgba(110, 220, 255, 0.6)';
            }, 250);
        });

        // --- RESIZE HANDLER ---
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // --- ANIMATION LOOP ---
        const clock = new THREE.Clock();

        function animate() {
            requestAnimationFrame(animate);

            const delta = Math.min(clock.getDelta(), 0.05);
            const time = clock.getElapsedTime();

            // Self-playing / idle demo patrol if user hasn't interacted
            if (!hasUserMovedMouse) {
                target3D.x = Math.sin(time * 0.9) * 16.0;
                target3D.y = Math.sin(time * 1.3) * 8.0;
                target3D.z = Math.cos(time * 0.7) * 8.0;
                mouseLight.position.copy(target3D);
                mouseLight.intensity = 2.2 + Math.sin(time * 3) * 0.8;
            } else {
                // Raycast against the depth plane of the aquarium center
                raycaster.setFromCamera(pointer, camera);
                raycaster.ray.intersectPlane(virtualInteractionPlane, target3D);
                // Clamp target within tank limits so cursor doesn't project to infinity
                target3D.x = THREE.MathUtils.clamp(target3D.x, -TANK_WIDTH / 2, TANK_WIDTH / 2);
                target3D.y = THREE.MathUtils.clamp(target3D.y, -TANK_HEIGHT / 2, TANK_HEIGHT / 2);
                target3D.z = THREE.MathUtils.clamp(target3D.z, -TANK_DEPTH / 2, TANK_DEPTH / 2);
                mouseLight.position.lerp(target3D, 0.15);
                mouseLight.intensity = 3.5;
            }

            // Animate Seaweeds (undulating aquatic sway)
            for (let i = 0; i < seaweeds.length; i++) {
                const sw = seaweeds[i];
                const pos = sw.mesh.geometry.attributes.position;
                const orig = sw.originalGeo.attributes.position;
                for (let k = 0; k < pos.count; k++) {
                    const y = orig.getY(k);
                    // Higher vertices sway wider
                    const swayFactor = Math.max(0, (y + 5) / 10);
                    const wave = Math.sin(time * sw.speed + y * 0.4 + sw.offset) * 0.45 * swayFactor;
                    pos.setX(k, orig.getX(k) + wave);
                }
                pos.needsUpdate = true;
            }

            // Animate Rising Air Bubbles
            for (let i = 0; i < bubbles.length; i++) {
                const b = bubbles[i];
                b.position.y += b.userData.speedY;
                b.position.x += Math.sin(time * b.userData.wobbleSpeed + b.userData.seed) * b.userData.wobbleAmp;

                // Reset to tank floor when reaching top
                if (b.position.y > TANK_HEIGHT / 2) {
                    b.position.y = -TANK_HEIGHT / 2 + 0.5;
                    b.position.x = (Math.random() - 0.5) * (TANK_WIDTH - 4);
                    b.position.z = (Math.random() - 0.5) * (TANK_DEPTH - 4);
                }
            }

            // Update Schooling Fish
            for (let i = 0; i < fishes.length; i++) {
                const fish = fishes[i];
                
                // Repel violently away from mouse / threat location
                fish.flee(mouseLight.position, 13.5);

                // Natural Boids flocking rules
                fish.flock(fishes);

                // Keep inside tank
                fish.containment();

                // Move & animate
                fish.update(delta);
            }

            // Gentle cinematic camera drift
            camera.position.x = Math.sin(time * 0.18) * 4.5;
            camera.position.y = 2.0 + Math.cos(time * 0.22) * 2.0;
            camera.lookAt(0, 0, 0);

            renderer.render(scene, camera);
        }

        // Start render loop immediately
        animate();
    </script>
</body>
</html>