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
1 min 21 s
Cost
$0.043
Output tokens
11,309
Spent thinking
796 tokens
Speed
148.6 tok/s
Console errors
0
First token after
4.6 s
First visible text after
14.2 s
Prompt tokens
448
Code size
35.2 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"medium"}
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 · 907 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 {
            overflow: hidden;
            background: #020b14;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            color: #d8f3dc;
        }
        #webgl-canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            display: block;
            z-index: 1;
        }
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 10;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            padding: 24px;
        }
        .header {
            background: rgba(3, 20, 36, 0.45);
            backdrop-filter: blur(10px);
            border: 1px solid rgba(82, 182, 154, 0.25);
            border-radius: 12px;
            padding: 12px 20px;
            max-width: 380px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            pointer-events: auto;
        }
        h1 {
            font-size: 1.25rem;
            font-weight: 600;
            letter-spacing: 0.5px;
            color: #74c69d;
            margin-bottom: 4px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .subtitle {
            font-size: 0.82rem;
            color: #b7e4c7;
            opacity: 0.9;
            line-height: 1.35;
        }
        .badge {
            display: inline-block;
            background: rgba(45, 106, 79, 0.5);
            border: 1px solid #52b788;
            padding: 2px 7px;
            border-radius: 6px;
            font-size: 0.72rem;
            color: #d8f3dc;
            font-weight: bold;
        }
        .stats {
            align-self: flex-end;
            background: rgba(3, 20, 36, 0.4);
            backdrop-filter: blur(8px);
            border: 1px solid rgba(82, 182, 154, 0.2);
            border-radius: 10px;
            padding: 10px 16px;
            font-size: 0.78rem;
            color: #95d5b2;
            display: flex;
            gap: 16px;
        }
        .stat-item {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .stat-val {
            font-weight: 700;
            color: #e8f5e9;
            font-size: 1.05rem;
        }
    </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>
    <canvas id="webgl-canvas"></canvas>
    
    <div id="ui-layer">
        <div class="header">
            <h1><span>🐠</span> Bio-Luminescent Tank</h1>
            <p class="subtitle">
                Move cursor near fish to <strong>scare them away</strong>.<br>
                <span class="badge">Click</span> anywhere to drop fish food flakes!
            </p>
        </div>
        <div class="stats">
            <div class="stat-item">
                <span class="stat-val" id="fish-count">48</span>
                <span>Fish</span>
            </div>
            <div class="stat-item">
                <span class="stat-val" id="scared-count">0</span>
                <span>Fleeing</span>
            </div>
            <div class="stat-item">
                <span class="stat-val" id="food-count">0</span>
                <span>Food Pellets</span>
            </div>
        </div>
    </div>

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

        // --- Tank Dimensions ---
        const TANK = {
            width: 44,
            height: 24,
            depth: 26,
            minX: -22, maxX: 22,
            minY: -11, maxY: 11,
            minZ: -12.5, maxZ: 12.5
        };

        // --- Setup Scene, Camera, Renderer ---
        const canvas = document.getElementById('webgl-canvas');
        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, 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(0x02131d);
        scene.fog = new THREE.FogExp2(0x021926, 0.022);

        const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 120);
        camera.position.set(0, 1.5, 34);

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

        // Overhead sunlight filtered by water
        const sunLight = new THREE.SpotLight(0x71e5f5, 5.0);
        sunLight.position.set(0, 28, 4);
        sunLight.angle = Math.PI / 2.6;
        sunLight.penumbra = 0.8;
        sunLight.castShadow = true;
        sunLight.shadow.mapSize.width = 1024;
        sunLight.shadow.mapSize.height = 1024;
        sunLight.shadow.camera.near = 10;
        sunLight.shadow.camera.far = 40;
        scene.add(sunLight);

        // Underwater bottom bounce light
        const bounceLight = new THREE.DirectionalLight(0x0a4247, 1.2);
        bounceLight.position.set(0, -15, 5);
        scene.add(bounceLight);

        // Glowing cursor threat sphere & light
        const threatLight = new THREE.PointLight(0x00f2fe, 3.5, 14);
        scene.add(threatLight);

        const threatGeo = new THREE.SphereGeometry(0.35, 16, 16);
        const threatMat = new THREE.MeshBasicMaterial({
            color: 0x48cae4,
            transparent: true,
            opacity: 0.6,
            wireframe: true
        });
        const threatMesh = new THREE.Mesh(threatGeo, threatMat);
        scene.add(threatMesh);

        // Ripples / Sensor Ring around pointer
        const ringGeo = new THREE.RingGeometry(1.2, 1.35, 32);
        const ringMat = new THREE.MeshBasicMaterial({
            color: 0x48cae4,
            transparent: true,
            opacity: 0.35,
            side: THREE.DoubleSide
        });
        const threatRing = new THREE.Mesh(ringGeo, ringMat);
        scene.add(threatRing);

        // --- Aquarium Glass Tank & Frame ---
        function createAquariumStructure() {
            // Tank wireframe / glass edges
            const boxGeo = new THREE.BoxGeometry(TANK.width, TANK.height, TANK.depth);
            const edges = new THREE.EdgesGeometry(boxGeo);
            const edgeMat = new THREE.LineBasicMaterial({ color: 0x1f6f8b, transparent: true, opacity: 0.45 });
            const edgeMesh = new THREE.LineSegments(edges, edgeMat);
            scene.add(edgeMesh);

            // Water surface plane with shimmer
            const waterPlaneGeo = new THREE.PlaneGeometry(TANK.width, TANK.depth, 32, 32);
            waterPlaneGeo.rotateX(-Math.PI / 2);
            const waterPlaneMat = new THREE.MeshStandardMaterial({
                color: 0x3a86c8,
                roughness: 0.1,
                metalness: 0.8,
                transparent: true,
                opacity: 0.4,
                side: THREE.DoubleSide
            });
            const waterPlane = new THREE.Mesh(waterPlaneGeo, waterPlaneMat);
            waterPlane.position.y = TANK.maxY;
            scene.add(waterPlane);

            // Sand floor with dunes
            const floorGeo = new THREE.PlaneGeometry(TANK.width + 4, TANK.depth + 4, 48, 48);
            floorGeo.rotateX(-Math.PI / 2);
            const pos = floorGeo.attributes.position;
            for (let i = 0; i < pos.count; i++) {
                const x = pos.getX(i);
                const z = pos.getZ(i);
                const dune = Math.sin(x * 0.25) * Math.cos(z * 0.3) * 0.8 + Math.sin(x * 0.1) * 0.5;
                pos.setY(i, dune);
            }
            floorGeo.computeVertexNormals();

            // Procedural sand canvas texture
            const canvasTex = document.createElement('canvas');
            canvasTex.width = 512;
            canvasTex.height = 512;
            const ctx = canvasTex.getContext('2d');
            ctx.fillStyle = '#173338';
            ctx.fillRect(0, 0, 512, 512);
            for (let i = 0; i < 20000; i++) {
                ctx.fillStyle = Math.random() > 0.5 ? '#1f444b' : '#112529';
                ctx.fillRect(Math.random() * 512, Math.random() * 512, 1.5, 1.5);
            }
            const sandTex = new THREE.CanvasTexture(canvasTex);
            sandTex.wrapS = THREE.RepeatWrapping;
            sandTex.wrapT = THREE.RepeatWrapping;
            sandTex.repeat.set(4, 3);

            const floorMat = new THREE.MeshStandardMaterial({
                map: sandTex,
                color: 0x1b434a,
                roughness: 0.9,
                metalness: 0.1
            });
            const floorMesh = new THREE.Mesh(floorGeo, floorMat);
            floorMesh.position.y = TANK.minY;
            floorMesh.receiveShadow = true;
            scene.add(floorMesh);

            // Background subtle light wall for depth
            const backGeo = new THREE.PlaneGeometry(TANK.width * 1.4, TANK.height * 1.3);
            const backMat = new THREE.MeshBasicMaterial({
                color: 0x01121d
            });
            const backMesh = new THREE.Mesh(backGeo, backMat);
            backMesh.position.set(0, 0, TANK.minZ - 4);
            scene.add(backMesh);
        }
        createAquariumStructure();

        // --- Coral / Sea Rock Clusters ---
        function createRocksAndCorals() {
            const rockMat = new THREE.MeshStandardMaterial({
                color: 0x112d32,
                roughness: 0.85,
                metalness: 0.1
            });

            // Clusters in corners
            const clusters = [
                { x: -16, z: -7, count: 6, scale: 2.2 },
                { x: 16, z: -6, count: 5, scale: 2.0 },
                { x: -10, z: 6, count: 4, scale: 1.5 },
                { x: 12, z: 5, count: 4, scale: 1.6 }
            ];

            clusters.forEach(c => {
                for (let i = 0; i < c.count; i++) {
                    const geo = new THREE.DodecahedronGeometry(c.scale * (0.6 + Math.random() * 0.7), 1);
                    const rock = new THREE.Mesh(geo, rockMat);
                    rock.position.set(
                        c.x + (Math.random() - 0.5) * 6,
                        TANK.minY + Math.random() * 1.5,
                        c.z + (Math.random() - 0.5) * 5
                    );
                    rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
                    rock.scale.set(1 + Math.random() * 0.5, 0.8 + Math.random() * 0.8, 1 + Math.random() * 0.5);
                    rock.receiveShadow = true;
                    rock.castShadow = true;
                    scene.add(rock);
                }
            });
        }
        createRocksAndCorals();

        // --- Swaying Seaweed / Plants ---
        const kelpSegments = [];
        function createKelpField() {
            const kelpPositions = [
                [-18, -8], [-16, -9], [-15, -6], [-19, -5],
                [17, -7], [18, -9], [15, -8], [19, -6],
                [-11, 4], [-12, 6], [13, 4], [14, 6]
            ];

            const kelpMat = new THREE.MeshStandardMaterial({
                color: 0x1b4332,
                roughness: 0.4,
                metalness: 0.1,
                side: THREE.DoubleSide
            });

            kelpPositions.forEach(([bx, bz]) => {
                const height = 10 + Math.random() * 8;
                const joints = 7;
                const segHeight = height / joints;
                let parent = scene;
                const chain = [];

                let baseY = TANK.minY;
                for (let j = 0; j < joints; j++) {
                    const width = 0.55 * (1 - (j / joints) * 0.6);
                    const leafGeo = new THREE.PlaneGeometry(width, segHeight);
                    const leaf = new THREE.Mesh(leafGeo, kelpMat);
                    leaf.position.y = (j === 0) ? baseY + segHeight / 2 : segHeight;
                    leaf.position.x = (j === 0) ? bx + (Math.random() - 0.5) : 0;
                    leaf.position.z = (j === 0) ? bz + (Math.random() - 0.5) : 0;
                    leaf.rotation.y = Math.random() * Math.PI;

                    parent.add(leaf);
                    chain.push(leaf);
                    parent = leaf;
                }
                kelpSegments.push(chain);
            });
        }
        createKelpField();

        // --- Rising Bubbles System ---
        const BUBBLE_COUNT = 90;
        const bubbleGeo = new THREE.SphereGeometry(0.12, 8, 8);
        const bubbleMat = new THREE.MeshPhysicalMaterial({
            color: 0xade8f4,
            transmission: 0.8,
            opacity: 0.85,
            transparent: true,
            roughness: 0.1,
            ior: 1.15
        });
        const bubbleInstanced = new THREE.InstancedMesh(bubbleGeo, bubbleMat, BUBBLE_COUNT);
        const bubbleData = [];
        const dummy = new THREE.Object3D();

        for (let i = 0; i < BUBBLE_COUNT; i++) {
            const data = {
                x: (Math.random() - 0.5) * TANK.width * 0.85,
                y: TANK.minY + Math.random() * TANK.height,
                z: (Math.random() - 0.5) * TANK.depth * 0.85,
                speedY: 2.2 + Math.random() * 2.5,
                wobbleSpeed: 2 + Math.random() * 3,
                wobbleAmp: 0.15 + Math.random() * 0.25,
                phase: Math.random() * Math.PI * 2,
                scale: 0.4 + Math.random() * 0.85
            };
            bubbleData.push(data);
        }
        scene.add(bubbleInstanced);

        // --- Caustic Light Shafts ---
        const causticsGroup = new THREE.Group();
        const shaftMat = new THREE.MeshBasicMaterial({
            color: 0x48cae4,
            transparent: true,
            opacity: 0.08,
            blending: THREE.AdditiveBlending,
            side: THREE.DoubleSide,
            depthWrite: false
        });
        for (let i = 0; i < 7; i++) {
            const shaftGeo = new THREE.CylinderGeometry(0.8 + Math.random() * 1.2, 3.5 + Math.random() * 2.5, TANK.height * 1.1, 12, 1, true);
            const shaft = new THREE.Mesh(shaftGeo, shaftMat);
            shaft.position.set((i - 3) * 6 + (Math.random() - 0.5) * 3, 1, (Math.random() - 0.5) * 8);
            shaft.rotation.z = (Math.random() - 0.5) * 0.25;
            shaft.rotation.x = (Math.random() - 0.5) * 0.25;
            causticsGroup.add(shaft);
        }
        scene.add(causticsGroup);

        // --- Procedural 3D Fish Geometry Generator ---
        const fishPalette = [
            { name: 'Clownfish', body: 0xff7b00, stripe: 0xffffff, accent: 0x111111, length: 1.5, height: 0.8 },
            { name: 'Blue Tang', body: 0x1976d2, stripe: 0x0d47a1, accent: 0xffeb3b, length: 1.7, height: 0.95 },
            { name: 'Yellow Tang', body: 0xffd100, stripe: 0xffaa00, accent: 0xffffff, length: 1.4, height: 1.0 },
            { name: 'Neon Tetra', body: 0x00f5d4, stripe: 0xf72585, accent: 0x00b4d8, length: 1.2, height: 0.45 },
            { name: 'Angel Fish', body: 0x3a0ca3, stripe: 0x4cc9f0, accent: 0x7209b7, length: 1.8, height: 1.25 }
        ];

        function buildFishModel(palette) {
            const root = new THREE.Group();

            // Fish body: elongated tapered shape
            const bodyGeo = new THREE.ConeGeometry(palette.height * 0.6, palette.length, 12);
            bodyGeo.rotateZ(-Math.PI / 2); // points along +X
            bodyGeo.scale(1, 0.4, 1);

            const bodyMat = new THREE.MeshStandardMaterial({
                color: palette.body,
                roughness: 0.35,
                metalness: 0.2
            });
            const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
            bodyMesh.castShadow = true;
            root.add(bodyMesh);

            // Striped banding on body
            const stripeGeo = new THREE.TorusGeometry(palette.height * 0.42, 0.05, 8, 16);
            stripeGeo.scale(0.5, 1, 0.8);
            const stripeMat = new THREE.MeshStandardMaterial({
                color: palette.stripe,
                roughness: 0.3
            });
            const stripeMesh = new THREE.Mesh(stripeGeo, stripeMat);
            stripeMesh.position.x = -palette.length * 0.1;
            root.add(stripeMesh);

            // Eyes
            const eyeGeo = new THREE.SphereGeometry(0.1, 8, 8);
            const eyeMat = new THREE.MeshBasicMaterial({ color: 0x050505 });
            const eyeLeft = new THREE.Mesh(eyeGeo, eyeMat);
            eyeLeft.position.set(palette.length * 0.28, palette.height * 0.12, 0.22);
            const eyeRight = eyeLeft.clone();
            eyeRight.position.z = -0.22;
            root.add(eyeLeft);
            root.add(eyeRight);

            // Pectoral Fins (slight angle)
            const finGeo = new THREE.BufferGeometry();
            const finVerts = new Float32Array([
                0, 0, 0,
                -0.35, 0.2, 0.45,
                -0.45, -0.15, 0.35
            ]);
            finGeo.setAttribute('position', new THREE.BufferAttribute(finVerts, 3));
            finGeo.computeVertexNormals();

            const finMat = new THREE.MeshStandardMaterial({
                color: palette.accent,
                roughness: 0.3,
                transparent: true,
                opacity: 0.85,
                side: THREE.DoubleSide
            });
            const finLeft = new THREE.Mesh(finGeo, finMat);
            finLeft.position.set(0.1, -0.05, 0.15);
            const finRight = finLeft.clone();
            finRight.scale.set(1, 1, -1);
            finRight.position.z = -0.15;
            root.add(finLeft);
            root.add(finRight);

            // Hinged Tail (for natural wagging)
            const tailPivot = new THREE.Group();
            tailPivot.position.x = -palette.length * 0.45;

            const tailFinGeo = new THREE.BufferGeometry();
            const tailVerts = new Float32Array([
                0, 0, 0,
                -0.7, palette.height * 0.65, 0,
                -0.5, 0, 0,
                -0.7, -palette.height * 0.65, 0
            ]);
            const tailIndices = [
                0, 1, 2,
                0, 2, 3
            ];
            tailFinGeo.setIndex(tailIndices);
            tailFinGeo.setAttribute('position', new THREE.BufferAttribute(tailVerts, 3));
            tailFinGeo.computeVertexNormals();

            const tailMesh = new THREE.Mesh(tailFinGeo, finMat);
            tailPivot.add(tailMesh);
            root.add(tailPivot);

            return { root, tailPivot, finLeft, finRight };
        }

        // --- Fish Boid / Swarm Class ---
        class Fish {
            constructor(id) {
                this.id = id;
                this.palette = fishPalette[id % fishPalette.length];
                const model = buildFishModel(this.palette);
                this.mesh = model.root;
                this.tailPivot = model.tailPivot;
                this.finLeft = model.finLeft;
                this.finRight = model.finRight;

                // Initial transform
                this.position = new THREE.Vector3(
                    (Math.random() - 0.5) * TANK.width * 0.7,
                    (Math.random() - 0.5) * TANK.height * 0.7,
                    (Math.random() - 0.5) * TANK.depth * 0.7
                );
                this.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 3,
                    (Math.random() - 0.5) * 1.5,
                    (Math.random() - 0.5) * 3
                ).normalize().multiplyScalar(2 + Math.random() * 2);

                this.acceleration = new THREE.Vector3();
                this.mesh.position.copy(this.position);
                scene.add(this.mesh);

                // Movement personality
                this.normalSpeed = 2.4 + Math.random() * 1.6;
                this.maxSpeed = this.normalSpeed;
                this.maxForce = 4.2;
                this.isScared = false;
                this.scaredTimer = 0;
                this.fleeDirection = new THREE.Vector3();

                // Tail oscillation
                this.wagFrequency = 6;
                this.wagPhase = Math.random() * Math.PI * 2;
                this.scale = 0.85 + Math.random() * 0.35;
                this.mesh.scale.setScalar(this.scale);
            }

            update(delta, time, allFish, mousePos, mouseActive, foodPellets) {
                this.acceleration.set(0, 0, 0);

                // Check distance to mouse cursor threat
                if (mouseActive) {
                    const distToMouse = this.position.distanceTo(mousePos);
                    const scareRadius = 8.5;

                    if (distToMouse < scareRadius) {
                        this.isScared = true;
                        this.scaredTimer = 1.4; // seconds of frantic flee

                        // Burst force directly away from threat
                        const fleeForce = new THREE.Vector3().subVectors(this.position, mousePos);
                        fleeForce.normalize();
                        const urgency = (1 - distToMouse / scareRadius);
                        fleeForce.multiplyScalar(26 * (urgency + 0.3));
                        this.acceleration.add(fleeForce);

                        // Spawn small startled bubble at fish mouth occasionally
                        if (Math.random() < 0.25) {
                            triggerBurstBubble(this.position);
                        }
                    }
                }

                // Handle Scared state countdown
                if (this.scaredTimer > 0) {
                    this.scaredTimer -= delta;
                    this.maxSpeed = this.normalSpeed * 3.2; // super swim boost
                    this.wagFrequency = 18; // frantic tail wagging
                    if (this.scaredTimer <= 0) {
                        this.isScared = false;
                    }
                } else {
                    this.maxSpeed = this.normalSpeed;
                    this.wagFrequency = 6;

                    // If not scared, search for food flakes!
                    let closestFood = null;
                    let minDist = 14;
                    for (let f of foodPellets) {
                        const d = this.position.distanceTo(f.position);
                        if (d < minDist) {
                            minDist = d;
                            closestFood = f;
                        }
                    }

                    if (closestFood) {
                        const seekFood = new THREE.Vector3().subVectors(closestFood.position, this.position);
                        seekFood.normalize().multiplyScalar(4.5);
                        this.acceleration.add(seekFood);

                        // Eat food if close
                        if (minDist < 1.0) {
                            closestFood.eaten = true;
                        }
                    } else {
                        // Flocking behavior (Separation, Alignment, Cohesion)
                        this.flock(allFish);
                    }
                }

                // Tank boundary avoidance
                this.boundaryRepulsion();

                // Physics integration
                this.velocity.addScaledVector(this.acceleration, delta);
                const currentSpeedLimit = this.isScared ? this.normalSpeed * 3.0 : this.normalSpeed;
                this.velocity.clampLength(0.8, currentSpeedLimit);
                this.position.addScaledVector(this.velocity, delta);

                this.mesh.position.copy(this.position);

                // Smooth rotation towards movement direction
                if (this.velocity.lengthSq() > 0.01) {
                    const targetForward = this.velocity.clone().normalize();
                    const targetRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), targetForward);
                    this.mesh.quaternion.slerp(targetRotation, delta * 7);
                }

                // Animate tail fin and pectoral fins
                const currentWagFreq = this.isScared ? 20 : (4 + this.velocity.length() * 2);
                const wagAngle = Math.sin(time * currentWagFreq + this.wagPhase) * (this.isScared ? 0.65 : 0.4);
                this.tailPivot.rotation.y = wagAngle;
                this.finLeft.rotation.y = Math.cos(time * currentWagFreq) * 0.3;
                this.finRight.rotation.y = -Math.cos(time * currentWagFreq) * 0.3;
            }

            boundaryRepulsion() {
                const margin = 3.5;
                const turnForce = 8.0;

                if (this.position.x < TANK.minX + margin) this.acceleration.x += turnForce;
                if (this.position.x > TANK.maxX - margin) this.acceleration.x -= turnForce;
                if (this.position.y < TANK.minY + margin * 0.8) this.acceleration.y += turnForce;
                if (this.position.y > TANK.maxY - margin * 0.8) this.acceleration.y -= turnForce;
                if (this.position.z < TANK.minZ + margin) this.acceleration.z += turnForce;
                if (this.position.z > TANK.maxZ - margin) this.acceleration.z -= turnForce;

                // Hard clamp just in case
                this.position.x = THREE.MathUtils.clamp(this.position.x, TANK.minX + 0.5, TANK.maxX - 0.5);
                this.position.y = THREE.MathUtils.clamp(this.position.y, TANK.minY + 0.5, TANK.maxY - 0.5);
                this.position.z = THREE.MathUtils.clamp(this.position.z, TANK.minZ + 0.5, TANK.maxZ - 0.5);
            }

            flock(allFish) {
                const sepDist = 2.2;
                const neighborDist = 6.0;

                const sep = new THREE.Vector3();
                const ali = new THREE.Vector3();
                const coh = new THREE.Vector3();
                let sepCount = 0;
                let neighborCount = 0;

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

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

                    if (d < neighborDist) {
                        ali.add(other.velocity);
                        coh.add(other.position);
                        neighborCount++;
                    }
                }

                if (sepCount > 0) {
                    sep.divideScalar(sepCount);
                    sep.normalize().multiplyScalar(4.0);
                    this.acceleration.add(sep);
                }

                if (neighborCount > 0) {
                    ali.divideScalar(neighborCount);
                    ali.normalize().multiplyScalar(1.5);
                    this.acceleration.add(ali);

                    coh.divideScalar(neighborCount);
                    const cohDir = new THREE.Vector3().subVectors(coh, this.position);
                    cohDir.normalize().multiplyScalar(1.2);
                    this.acceleration.add(cohDir);
                }
            }
        }

        // --- Create School of Fish ---
        const FISH_COUNT = 48;
        const fishSchool = [];
        for (let i = 0; i < FISH_COUNT; i++) {
            fishSchool.push(new Fish(i));
        }

        // --- Food Pellets System ---
        const foodPellets = [];
        const foodGeo = new THREE.DodecahedronGeometry(0.2, 0);
        const foodMat = new THREE.MeshStandardMaterial({
            color: 0xffaa00,
            emissive: 0x994400,
            roughness: 0.3
        });

        function spawnFood(worldX, worldZ) {
            const mesh = new THREE.Mesh(foodGeo, foodMat);
            mesh.position.set(worldX, TANK.maxY - 0.5, worldZ);
            scene.add(mesh);

            foodPellets.push({
                mesh,
                position: mesh.position,
                velocity: new THREE.Vector3((Math.random() - 0.5) * 0.4, -1.8 - Math.random() * 0.8, (Math.random() - 0.5) * 0.4),
                eaten: false
            });
        }

        // Dynamic Burst Bubbles when fish dart away
        const burstBubbles = [];
        function triggerBurstBubble(pos) {
            if (burstBubbles.length > 25) return;
            const geo = new THREE.SphereGeometry(0.12 + Math.random() * 0.1, 6, 6);
            const m = new THREE.Mesh(geo, bubbleMat);
            m.position.copy(pos);
            scene.add(m);
            burstBubbles.push({
                mesh: m,
                vy: 3.5 + Math.random() * 2,
                life: 1.2
            });
        }

        // --- Mouse / Raycasting Target Plane ---
        const mouse2D = new THREE.Vector2(-999, -999);
        const raycaster = new THREE.Raycaster();
        const interactionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); // Z=0 mid-tank plane
        const mouse3D = new THREE.Vector3(0, 0, 0);
        let mouseActive = false;

        function updateMouse3D(clientX, clientY) {
            mouse2D.x = (clientX / window.innerWidth) * 2 - 1;
            mouse2D.y = -(clientY / window.innerHeight) * 2 + 1;

            raycaster.setFromCamera(mouse2D, camera);
            const hitPoint = new THREE.Vector3();
            if (raycaster.ray.intersectPlane(interactionPlane, hitPoint)) {
                // Clamp within tank boundaries
                hitPoint.x = THREE.MathUtils.clamp(hitPoint.x, TANK.minX + 2, TANK.maxX - 2);
                hitPoint.y = THREE.MathUtils.clamp(hitPoint.y, TANK.minY + 2, TANK.maxY - 2);
                mouse3D.copy(hitPoint);
                mouseActive = true;
            }
        }

        window.addEventListener('pointermove', (e) => {
            updateMouse3D(e.clientX, e.clientY);
        });

        window.addEventListener('pointerleave', () => {
            mouseActive = false;
        });

        // Click to drop food
        window.addEventListener('pointerdown', (e) => {
            updateMouse3D(e.clientX, e.clientY);
            spawnFood(mouse3D.x + (Math.random() - 0.5) * 1.5, mouse3D.z + (Math.random() - 0.5) * 1.5);
            // Drop a couple extra for fun
            if (Math.random() > 0.4) {
                setTimeout(() => spawnFood(mouse3D.x + (Math.random() - 0.5) * 2, mouse3D.z + (Math.random() - 0.5) * 2), 120);
            }
        });

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

        // --- Animation Loop ---
        const clock = new THREE.Clock();
        const scaredCountEl = document.getElementById('scared-count');
        const foodCountEl = document.getElementById('food-count');

        function animate() {
            requestAnimationFrame(animate);

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

            // Self-patrolling idle threat when mouse is not active so demo is lively on initial load
            let effectiveThreatPos = mouse3D;
            let effectiveThreatActive = mouseActive;

            if (!mouseActive) {
                const autoX = Math.sin(time * 0.9) * 12;
                const autoY = Math.cos(time * 0.7) * 5 + 1;
                effectiveThreatPos = new THREE.Vector3(autoX, autoY, 0);
                effectiveThreatActive = true; // gently drives movement in demo mode!
            }

            // Update glowing pointer & visual sensor rings
            threatLight.position.copy(effectiveThreatPos);
            threatMesh.position.copy(effectiveThreatPos);
            threatRing.position.copy(effectiveThreatPos);
            threatRing.rotation.z = time * 2;
            const ringScale = 1.0 + Math.sin(time * 6) * 0.15;
            threatRing.scale.set(ringScale, ringScale, ringScale);

            // Update Kelp plant sway
            kelpSegments.forEach((chain, idx) => {
                const swayOffset = idx * 0.8;
                chain.forEach((seg, sIdx) => {
                    const wave = Math.sin(time * 2.2 + swayOffset + sIdx * 0.4) * 0.08;
                    seg.rotation.z = wave;
                    seg.rotation.x = Math.cos(time * 1.6 + swayOffset + sIdx * 0.3) * 0.05;
                });
            });

            // Update Rising Bubbles
            for (let i = 0; i < BUBBLE_COUNT; i++) {
                const b = bubbleData[i];
                b.y += b.speedY * delta;
                b.x += Math.sin(time * b.wobbleSpeed + b.phase) * (b.wobbleAmp * delta * 2.5);

                // Reset at bottom when reaching surface
                if (b.y > TANK.maxY) {
                    b.y = TANK.minY + 0.5;
                    b.x = (Math.random() - 0.5) * TANK.width * 0.85;
                    b.z = (Math.random() - 0.5) * TANK.depth * 0.85;
                }

                dummy.position.set(b.x, b.y, b.z);
                dummy.scale.setScalar(b.scale);
                dummy.updateMatrix();
                bubbleInstanced.setMatrixAt(i, dummy.matrix);
            }
            bubbleInstanced.instanceMatrix.needsUpdate = true;

            // Update Burst Bubbles
            for (let i = burstBubbles.length - 1; i >= 0; i--) {
                const bb = burstBubbles[i];
                bb.life -= delta;
                bb.mesh.position.y += bb.vy * delta;
                bb.mesh.scale.multiplyScalar(0.98);
                if (bb.life <= 0 || bb.mesh.position.y > TANK.maxY) {
                    scene.remove(bb.mesh);
                    bb.mesh.geometry.dispose();
                    burstBubbles.splice(i, 1);
                }
            }

            // Update Food Pellets
            for (let i = foodPellets.length - 1; i >= 0; i--) {
                const f = foodPellets[i];
                f.position.addScaledVector(f.velocity, delta);
                f.mesh.rotation.x += delta * 2;
                f.mesh.rotation.y += delta * 1.5;

                // Stop at bottom or remove if eaten
                if (f.position.y <= TANK.minY + 0.2 || f.eaten) {
                    scene.remove(f.mesh);
                    f.mesh.geometry.dispose();
                    foodPellets.splice(i, 1);
                }
            }

            // Update Fish School
            let fleeingCount = 0;
            fishSchool.forEach(fish => {
                fish.update(delta, time, fishSchool, effectiveThreatPos, effectiveThreatActive, foodPellets);
                if (fish.isScared) fleeingCount++;
            });

            // Gentle light shaft drift
            causticsGroup.rotation.y = Math.sin(time * 0.25) * 0.12;

            // UI counts
            scaredCountEl.textContent = fleeingCount;
            foodCountEl.textContent = foodPellets.length;

            renderer.render(scene, camera);
        }

        animate();
    </script>
</body>
</html>
Reasoning trace (796 tokens)
**Crafting 3D Aquarium**

I'm currently focused on building a dynamic 3D aquarium environment using Three.js. The core of my work involves implementing realistic water physics and subtle reflections for the tank edges. I'm also beginning to integrate the fish AI, ensuring they react realistically to user interaction.

**Designing Underwater Elements**

I'm now concentrating on populating the aquarium. My focus has shifted to creating distinct fish species, animating their movements, and implementing schooling behaviors with obstacle avoidance. I'm also beginning to integrate interactive elements like food dropping and mouse-based startling mechanics.

**Detailing Aquatic Scenery**

I'm now detailing the aquarium's physical elements. My efforts are concentrated on developing a textured sandy floor with subtle undulations, and placing coral formations at the corners. I'm also implementing swaying seaweed and a bubbling mechanism for visual depth.