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.053
Output tokens
14,037
Spent thinking
1,453 tokens
Speed
179.5 tok/s
Console errors
0
First token after
2.3 s
First visible text after
16.0 s
Prompt tokens
448
Code size
35.5 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
model default
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 · 1032 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>3D Aquarium with Three.js</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    body, html {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #02121e;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    }
    #webgl-container {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
    }
    #ui-overlay {
      position: absolute;
      top: 20px;
      left: 20px;
      color: rgba(255, 255, 255, 0.88);
      text-shadow: 0 2px 10px rgba(0, 20, 40, 0.8);
      pointer-events: none;
      z-index: 10;
    }
    h1 {
      font-size: 1.35rem;
      font-weight: 600;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      margin-bottom: 4px;
      color: #7ce8ff;
    }
    p {
      font-size: 0.85rem;
      color: #d1f4ff;
      opacity: 0.85;
      margin-bottom: 2px;
    }
    .badge {
      display: inline-block;
      margin-top: 6px;
      padding: 3px 8px;
      border-radius: 20px;
      background: rgba(8, 70, 110, 0.45);
      border: 1px solid rgba(124, 232, 255, 0.35);
      font-size: 0.72rem;
      color: #9feeff;
    }
    #crosshair {
      position: absolute;
      width: 32px;
      height: 32px;
      border: 2px dashed rgba(124, 232, 255, 0.5);
      border-radius: 50%;
      pointer-events: none;
      transform: translate(-50%, -50%);
      transition: width 0.15s, height 0.15s, border-color 0.15s;
      z-index: 5;
      display: none;
    }
    #crosshair.scaring {
      width: 48px;
      height: 48px;
      border-color: rgba(255, 100, 120, 0.85);
      box-shadow: 0 0 15px rgba(255, 80, 100, 0.4);
    }
  </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="webgl-container"></div>
  <div id="crosshair"></div>
  <div id="ui-overlay">
    <h1>Living Coral Aquarium</h1>
    <p>Move mouse near fish to scatter them away</p>
    <p>Click water to drop fish food &bull; Drag to rotate view</p>
    <div class="badge" id="fish-count">48 Tropical Fish</div>
  </div>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // --- Scene Setup ---
    const container = document.getElementById('webgl-container');
    const crosshairEl = document.getElementById('crosshair');

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

    const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100);
    camera.position.set(0, 1.5, 23);

    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.15;
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.maxDistance = 34;
    controls.minDistance = 7;
    controls.maxPolarAngle = Math.PI * 0.52;
    controls.minPolarAngle = Math.PI * 0.15;
    controls.target.set(0, 0, 0);

    // --- Tank Bounds ---
    const TANK = {
      width: 32,
      height: 16,
      depth: 18,
      minY: -7.5,
      maxY: 7.5,
      minX: -15.5,
      maxX: 15.5,
      minZ: -8.5,
      maxZ: 8.5
    };

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

    const sunLight = new THREE.DirectionalLight(0xbbf0ff, 2.8);
    sunLight.position.set(2, 22, 4);
    sunLight.castShadow = true;
    sunLight.shadow.mapSize.width = 1024;
    sunLight.shadow.mapSize.height = 1024;
    sunLight.shadow.camera.near = 5;
    sunLight.shadow.camera.far = 35;
    sunLight.shadow.camera.left = -18;
    sunLight.shadow.camera.right = 18;
    sunLight.shadow.camera.top = 12;
    sunLight.shadow.camera.bottom = -12;
    sunLight.shadow.bias = -0.001;
    scene.add(sunLight);

    const fillLight = new THREE.DirectionalLight(0x18779c, 1.2);
    fillLight.position.set(-8, -4, -6);
    scene.add(fillLight);

    // Warm accent light for corals
    const coralLight = new THREE.PointLight(0xff7088, 2.5, 20);
    coralLight.position.set(-6, -4, 2);
    scene.add(coralLight);

    const cyanPointLight = new THREE.PointLight(0x40e0d0, 2.0, 22);
    cyanPointLight.position.set(7, -3, -2);
    scene.add(cyanPointLight);

    // --- Procedural Caustics Shader for Seabed ---
    const causticsVertexShader = `
      varying vec2 vUv;
      varying vec3 vWorldPos;
      void main() {
        vUv = uv;
        vec4 wp = modelMatrix * vec4(position, 1.0);
        vWorldPos = wp.xyz;
        gl_Position = projectionMatrix * viewMatrix * wp;
      }
    `;

    const causticsFragmentShader = `
      uniform float uTime;
      uniform vec3 uColorBase;
      varying vec2 vUv;
      varying vec3 vWorldPos;

      // Caustic sine pattern
      float caustic(vec2 p) {
        vec2 p1 = p * 1.6;
        float c1 = sin(p1.x * 1.8 + uTime * 2.2 + cos(p1.y * 1.5 - uTime * 1.2));
        float c2 = cos(p1.y * 2.1 - uTime * 1.8 + sin(p1.x * 2.0 + uTime * 1.5));
        float c3 = sin((p1.x + p1.y) * 2.4 + uTime * 2.0);
        return pow(clamp((c1 + c2 + c3) * 0.33 + 0.5, 0.0, 1.0), 3.5);
      }

      void main() {
        vec2 p = vWorldPos.xz * 0.22;
        float c = caustic(p);
        c += caustic(p * 1.8 + vec2(1.7, 3.2)) * 0.6;
        vec3 sand = uColorBase;
        // Subtle depth noise on sand
        float speckle = sin(vWorldPos.x * 25.0) * sin(vWorldPos.z * 25.0) * 0.04;
        sand += vec3(speckle);
        vec3 lightColor = vec3(0.5, 0.95, 1.0) * 1.35;
        vec3 finalColor = sand + lightColor * c * 0.85;
        gl_FragColor = vec4(finalColor, 1.0);
      }
    `;

    const sandMaterial = new THREE.ShaderMaterial({
      vertexShader: causticsVertexShader,
      fragmentShader: causticsFragmentShader,
      uniforms: {
        uTime: { value: 0 },
        uColorBase: { value: new THREE.Color(0xd2b57e).multiplyScalar(0.48) }
      }
    });

    // Sand floor with wavy sand dunes
    const floorGeo = new THREE.PlaneGeometry(38, 24, 64, 48);
    floorGeo.rotateX(-Math.PI / 2);
    const posAttr = floorGeo.attributes.position;
    for (let i = 0; i < posAttr.count; i++) {
      const x = posAttr.getX(i);
      const z = posAttr.getZ(i);
      const dune = Math.sin(x * 0.35) * Math.cos(z * 0.4) * 0.45 +
                   Math.sin(x * 0.8 + z * 0.5) * 0.2;
      posAttr.setY(i, dune);
    }
    floorGeo.computeVertexNormals();

    const floorMesh = new THREE.Mesh(floorGeo, sandMaterial);
    floorMesh.position.y = TANK.minY;
    floorMesh.receiveShadow = true;
    scene.add(floorMesh);

    // Water Surface (ceiling)
    const surfaceGeo = new THREE.PlaneGeometry(36, 22, 32, 32);
    surfaceGeo.rotateX(Math.PI / 2);
    const surfaceMat = new THREE.MeshStandardMaterial({
      color: 0x22c2dd,
      roughness: 0.1,
      metalness: 0.1,
      transparent: true,
      opacity: 0.35,
      side: THREE.DoubleSide
    });
    const surfaceMesh = new THREE.Mesh(surfaceGeo, surfaceMat);
    surfaceMesh.position.y = TANK.maxY + 0.1;
    scene.add(surfaceMesh);

    // Tank Frame / Glass Boundary Outline
    const boxGeo = new THREE.BoxGeometry(TANK.width, TANK.height, TANK.depth);
    const edges = new THREE.EdgesGeometry(boxGeo);
    const frameLine = new THREE.LineSegments(
      edges,
      new THREE.LineBasicMaterial({ color: 0x429bbb, transparent: true, opacity: 0.35 })
    );
    scene.add(frameLine);

    // Back glass gradient plane
    const backGlassMat = new THREE.MeshBasicMaterial({
      color: 0x011b2e,
      side: THREE.BackSide
    });
    const backGlass = new THREE.Mesh(new THREE.PlaneGeometry(TANK.width + 4, TANK.height + 4), backGlassMat);
    backGlass.position.set(0, 0, -TANK.depth / 2 - 0.5);
    scene.add(backGlass);

    // --- Underwater Decorations: Corals & Seaweed ---
    const seaweedGroups = [];
    const seaweedMaterial = new THREE.MeshStandardMaterial({
      color: 0x1f8f48,
      roughness: 0.4,
      side: THREE.DoubleSide
    });
    const seaweedMatAlt = new THREE.MeshStandardMaterial({
      color: 0x126c36,
      roughness: 0.5,
      side: THREE.DoubleSide
    });

    function createSeaweedCluster(cx, cz, count, heightScale) {
      const group = new THREE.Group();
      group.position.set(cx, TANK.minY, cz);
      for (let i = 0; i < count; i++) {
        const segCount = 7;
        const bladeHeight = (3.5 + Math.random() * 2.5) * heightScale;
        const segHeight = bladeHeight / segCount;
        const width = 0.25 + Math.random() * 0.15;

        const bladeGroup = new THREE.Group();
        bladeGroup.position.set((Math.random() - 0.5) * 1.5, 0, (Math.random() - 0.5) * 1.5);
        bladeGroup.rotation.y = Math.random() * Math.PI * 2;

        let parent = bladeGroup;
        const segments = [];
        for (let s = 0; s < segCount; s++) {
          const segGeo = new THREE.PlaneGeometry(width * (1 - (s / segCount) * 0.6), segHeight);
          segGeo.translate(0, segHeight / 2, 0);
          const mat = Math.random() > 0.4 ? seaweedMaterial : seaweedMatAlt;
          const segMesh = new THREE.Mesh(segGeo, mat);
          parent.add(segMesh);

          const nextJoint = new THREE.Group();
          nextJoint.position.set(0, segHeight, 0);
          parent.add(nextJoint);
          parent = nextJoint;
          segments.push(segMesh);
        }
        bladeGroup.userData = { segments, seed: Math.random() * 10 };
        group.add(bladeGroup);
        seaweedGroups.push(bladeGroup);
      }
      scene.add(group);
    }

    // Plant seaweed gardens along sides & back
    createSeaweedCluster(-12, -4, 9, 1.3);
    createSeaweedCluster(-10, 4, 7, 1.1);
    createSeaweedCluster(11, -3, 10, 1.4);
    createSeaweedCluster(13, 3, 8, 1.2);
    createSeaweedCluster(-4, -6, 8, 1.1);
    createSeaweedCluster(4, -6, 9, 1.2);

    // Rocks & Coral Formations
    const rockMat = new THREE.MeshStandardMaterial({ color: 0x4a4d52, roughness: 0.9 });
    const coralColors = [0xff4f6e, 0xffaa33, 0xa144eb, 0x22d2c2, 0xef238a];

    function createRockFormation(x, z, scale) {
      const rockGroup = new THREE.Group();
      rockGroup.position.set(x, TANK.minY, z);
      const mainRock = new THREE.Mesh(
        new THREE.DodecahedronGeometry(2 * scale, 1),
        rockMat
      );
      mainRock.scale.set(1.4, 0.7, 1.1);
      mainRock.castShadow = true;
      mainRock.receiveShadow = true;
      rockGroup.add(mainRock);

      // Add coral branches / cups on top of rock
      const coralCount = 4 + Math.floor(Math.random() * 4);
      for (let i = 0; i < coralCount; i++) {
        const color = coralColors[Math.floor(Math.random() * coralColors.length)];
        const coralMat = new THREE.MeshStandardMaterial({
          color: color,
          roughness: 0.35,
          metalness: 0.05
        });

        if (Math.random() > 0.45) {
          // Brain / Mushroom Coral
          const cGeo = new THREE.SphereGeometry(0.7 * scale, 12, 10);
          cGeo.scale(1.2, 0.55, 1.2);
          const coral = new THREE.Mesh(cGeo, coralMat);
          coral.position.set((Math.random() - 0.5) * 1.8 * scale, 0.9 * scale + Math.random() * 0.4, (Math.random() - 0.5) * 1.8 * scale);
          rockGroup.add(coral);
        } else {
          // Tube / Branch coral
          const branchGroup = new THREE.Group();
          branchGroup.position.set((Math.random() - 0.5) * 1.5 * scale, 0.7 * scale, (Math.random() - 0.5) * 1.5 * scale);
          const tubes = 3 + Math.floor(Math.random() * 3);
          for (let t = 0; t < tubes; t++) {
            const h = (0.8 + Math.random() * 1.0) * scale;
            const tube = new THREE.Mesh(new THREE.CylinderGeometry(0.12 * scale, 0.18 * scale, h, 8), coralMat);
            tube.position.set((Math.random() - 0.5) * 0.4 * scale, h / 2, (Math.random() - 0.5) * 0.4 * scale);
            tube.rotation.x = (Math.random() - 0.5) * 0.3;
            tube.rotation.z = (Math.random() - 0.5) * 0.3;
            branchGroup.add(tube);
          }
          rockGroup.add(branchGroup);
        }
      }
      scene.add(rockGroup);
    }

    createRockFormation(-6, -2, 1.4);
    createRockFormation(7, -1, 1.6);
    createRockFormation(1, -4.5, 1.2);
    createRockFormation(-11, 2, 1.1);

    // --- Bubbles Particle System ---
    const bubbleCount = 180;
    const bubbleGeo = new THREE.SphereGeometry(0.08, 8, 8);
    const bubbleMat = new THREE.MeshPhysicalMaterial({
      color: 0xffffff,
      transmission: 0.9,
      opacity: 0.8,
      transparent: true,
      roughness: 0.05,
      ior: 1.1
    });

    const bubbleInstanced = new THREE.InstancedMesh(bubbleGeo, bubbleMat, bubbleCount);
    const bubbleData = [];
    const dummy = new THREE.Object3D();

    for (let i = 0; i < bubbleCount; i++) {
      const sourceX = (Math.random() - 0.5) * (TANK.width * 0.85);
      const sourceZ = (Math.random() - 0.5) * (TANK.depth * 0.85);
      const y = TANK.minY + Math.random() * TANK.height;
      const scale = 0.5 + Math.random() * 1.6;
      const speed = 1.2 + Math.random() * 1.8;
      const wobbleSpeed = 1.5 + Math.random() * 2.5;
      const wobbleAmp = 0.08 + Math.random() * 0.12;

      bubbleData.push({ x: sourceX, y, z: sourceZ, scale, speed, wobbleSpeed, wobbleAmp, seed: Math.random() * 100 });
      dummy.position.set(sourceX, y, sourceZ);
      dummy.scale.setScalar(scale);
      dummy.updateMatrix();
      bubbleInstanced.setMatrixAt(i, dummy.matrix);
    }
    scene.add(bubbleInstanced);

    // --- Procedural Fish Creation ---
    // Varieties: Clownfish, Blue Tang, Yellow Tang, Coral Angel
    const FISH_TYPES = [
      {
        name: 'Clownfish',
        bodyColor: 0xff6611,
        stripeColor: 0xffffff,
        bellyColor: 0x221100,
        aspect: { length: 1.2, height: 0.65, thickness: 0.3 },
        speed: 2.2,
        schooling: true,
        tailLength: 0.45
      },
      {
        name: 'BlueTang',
        bodyColor: 0x1160ee,
        stripeColor: 0x112244,
        bellyColor: 0xffdd22, // bright yellow tail
        aspect: { length: 1.4, height: 0.8, thickness: 0.28 },
        speed: 2.6,
        schooling: true,
        tailLength: 0.5
      },
      {
        name: 'YellowTang',
        bodyColor: 0xffdd00,
        stripeColor: 0xfff066,
        bellyColor: 0xffaa00,
        aspect: { length: 1.2, height: 0.95, thickness: 0.22 },
        speed: 2.0,
        schooling: false,
        tailLength: 0.4
      },
      {
        name: 'AngelFish',
        bodyColor: 0x111111,
        stripeColor: 0xeeeeee,
        bellyColor: 0x33ddff,
        aspect: { length: 1.3, height: 1.2, thickness: 0.2 },
        speed: 1.8,
        schooling: false,
        tailLength: 0.55
      }
    ];

    function createFishMesh(type) {
      const root = new THREE.Group();
      const asp = type.aspect;

      // Fish Body Geometry (procedurally scaled & tapered)
      const bodyGeo = new THREE.SphereGeometry(1, 16, 12);
      bodyGeo.scale(asp.length * 0.5, asp.height * 0.5, asp.thickness * 0.5);

      // Vertex color or canvas texture for stripes
      const canvas = document.createElement('canvas');
      canvas.width = 128;
      canvas.height = 64;
      const ctx = canvas.getContext('2d');
      const hex1 = '#' + new THREE.Color(type.bodyColor).getHexString();
      const hex2 = '#' + new THREE.Color(type.stripeColor).getHexString();
      const hex3 = '#' + new THREE.Color(type.bellyColor).getHexString();

      ctx.fillStyle = hex1;
      ctx.fillRect(0, 0, 128, 64);

      if (type.name === 'Clownfish') {
        ctx.fillStyle = hex2;
        ctx.fillRect(36, 0, 14, 64);
        ctx.fillRect(72, 0, 12, 64);
        ctx.fillStyle = '#000000';
        ctx.fillRect(34, 0, 2, 64);
        ctx.fillRect(50, 0, 2, 64);
        ctx.fillRect(70, 0, 2, 64);
        ctx.fillRect(84, 0, 2, 64);
      } else if (type.name === 'BlueTang') {
        ctx.fillStyle = hex2;
        ctx.beginPath();
        ctx.arc(40, 20, 30, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = hex3;
        ctx.fillRect(100, 10, 28, 44);
      } else if (type.name === 'AngelFish') {
        ctx.fillStyle = hex2;
        ctx.fillRect(25, 0, 10, 64);
        ctx.fillRect(55, 0, 12, 64);
        ctx.fillRect(85, 0, 10, 64);
        ctx.fillStyle = hex3;
        ctx.fillRect(0, 28, 128, 8);
      } else {
        // Yellow tang subtle gradient
        const grad = ctx.createLinearGradient(0, 0, 128, 0);
        grad.addColorStop(0, hex1);
        grad.addColorStop(0.8, hex2);
        grad.addColorStop(1, hex3);
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, 128, 64);
      }

      const bodyTex = new THREE.CanvasTexture(canvas);
      bodyTex.wrapS = THREE.ClampToEdgeWrapping;
      bodyTex.wrapT = THREE.ClampToEdgeWrapping;

      const bodyMat = new THREE.MeshStandardMaterial({
        map: bodyTex,
        roughness: 0.3,
        metalness: 0.1
      });

      const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
      bodyMesh.castShadow = true;
      root.add(bodyMesh);

      // Eyes
      const eyeGeo = new THREE.SphereGeometry(0.08, 8, 8);
      const eyeMat = new THREE.MeshBasicMaterial({ color: 0x050505 });
      const eyeWhiteMat = new THREE.MeshBasicMaterial({ color: 0xffffff });

      const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
      const eyeWhiteL = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), eyeWhiteMat);
      eyeWhiteL.position.set(asp.length * 0.3, asp.height * 0.1, asp.thickness * 0.25);
      eyeL.position.set(asp.length * 0.32, asp.height * 0.1, asp.thickness * 0.28);
      root.add(eyeWhiteL);
      root.add(eyeL);

      const eyeWhiteR = eyeWhiteL.clone();
      eyeWhiteR.position.z = -eyeWhiteL.position.z;
      const eyeR = eyeL.clone();
      eyeR.position.z = -eyeL.position.z;
      root.add(eyeWhiteR);
      root.add(eyeR);

      // Fins Material
      const finMat = new THREE.MeshStandardMaterial({
        color: type.bodyColor,
        roughness: 0.4,
        transparent: true,
        opacity: 0.88,
        side: THREE.DoubleSide
      });

      // Tail Joint & Fin (animated)
      const tailJoint = new THREE.Group();
      tailJoint.position.set(-asp.length * 0.42, 0, 0);

      const tailGeo = new THREE.ConeGeometry(asp.height * 0.45, type.tailLength * 1.2, 3);
      tailGeo.rotateZ(Math.PI / 2);
      tailGeo.scale(1, 0.15, 1);
      const tailMesh = new THREE.Mesh(tailGeo, finMat);
      tailMesh.position.set(-type.tailLength * 0.5, 0, 0);
      tailJoint.add(tailMesh);
      root.add(tailJoint);

      // Dorsal Fin
      const dorsalGeo = new THREE.BufferGeometry();
      const dH = asp.height * (type.name === 'AngelFish' ? 0.9 : 0.45);
      const dVerts = new Float32Array([
        -asp.length * 0.25, asp.height * 0.35, 0,
        asp.length * 0.15, asp.height * 0.32, 0,
        -asp.length * 0.05, asp.height * 0.35 + dH, 0
      ]);
      dorsalGeo.setAttribute('position', new THREE.BufferAttribute(dVerts, 3));
      dorsalGeo.computeVertexNormals();
      const dorsalMesh = new THREE.Mesh(dorsalGeo, finMat);
      root.add(dorsalMesh);

      // Ventral / Pectoral fin accents
      const pectoralGeo = new THREE.ConeGeometry(0.18, 0.45, 3);
      pectoralGeo.rotateX(Math.PI / 2);
      const pecL = new THREE.Mesh(pectoralGeo, finMat);
      pecL.position.set(asp.length * 0.1, -asp.height * 0.2, asp.thickness * 0.22);
      pecL.rotation.y = 0.4;
      pecL.rotation.z = -0.3;
      root.add(pecL);

      const pecR = pecL.clone();
      pecR.position.z = -pecL.position.z;
      pecR.rotation.y = -0.4;
      root.add(pecR);

      return { mesh: root, tailJoint, pecL, pecR, type };
    }

    // --- Fish Agent Class (Flocking & Mouse Scattering) ---
    class FishAgent {
      constructor(type, x, y, z) {
        this.type = type;
        const fishObj = createFishMesh(type);
        this.mesh = fishObj.mesh;
        this.tailJoint = fishObj.tailJoint;
        this.pecL = fishObj.pecL;
        this.pecR = fishObj.pecR;

        this.position = new THREE.Vector3(x, y, z);
        this.mesh.position.copy(this.position);

        // Random heading
        const angle = Math.random() * Math.PI * 2;
        this.velocity = new THREE.Vector3(Math.cos(angle), (Math.random() - 0.5) * 0.4, Math.sin(angle)).normalize().multiplyScalar(type.speed * (0.8 + Math.random() * 0.4));
        this.acceleration = new THREE.Vector3();

        this.baseSpeed = this.velocity.length();
        this.maxSpeed = this.baseSpeed * 2.8; // Burst speed when panicked
        this.maxForce = 0.12;

        this.fear = 0; // 0 (calm) to 1 (scared)
        this.scareTimer = 0;
        this.tailPhase = Math.random() * Math.PI * 2;
        this.scale = 0.8 + Math.random() * 0.45;
        this.mesh.scale.setScalar(this.scale);

        scene.add(this.mesh);
      }

      applyForce(force) {
        this.acceleration.add(force);
      }

      scareAway(repelOrigin, strength = 1.0) {
        const diff = new THREE.Vector3().subVectors(this.position, repelOrigin);
        let dist = diff.length();
        if (dist < 0.001) {
          diff.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
          dist = 1.0;
        }

        // Strong repulsion impulse
        diff.normalize();
        const factor = Math.min(1.0, strength / Math.max(dist, 0.4));
        diff.multiplyScalar(factor * 2.2);
        this.applyForce(diff);

        // Instant panic state
        this.fear = 1.0;
        this.scareTimer = 1.4; // duration of heightened speed
      }

      update(delta, allFish, foodPellets, mouseRay) {
        // --- 1. Mouse Ray Hover Detection (3D Raycast Distance) ---
        if (mouseRay) {
          // Closest point on mouse ray to fish position
          const rayOrigin = mouseRay.origin;
          const rayDir = mouseRay.direction;
          const toFish = new THREE.Vector3().subVectors(this.position, rayOrigin);
          const projection = toFish.dot(rayDir);

          if (projection > 0) {
            const closestPoint = new THREE.Vector3().copy(rayOrigin).addScaledVector(rayDir, projection);
            const distToRay = this.position.distanceTo(closestPoint);
            const scareRadius = 4.8;

            if (distToRay < scareRadius) {
              const repelIntensity = (1.0 - distToRay / scareRadius) * 2.0;
              this.scareAway(closestPoint, repelIntensity);
            }
          }
        }

        // --- 2. Flocking Behaviors (Separation & Cohesion) ---
        let sepSum = new THREE.Vector3();
        let sepCount = 0;
        let alignSum = new THREE.Vector3();
        let alignCount = 0;
        let cohSum = new THREE.Vector3();
        let cohCount = 0;

        const neighborDist = 4.0;
        const desiredSeparation = 1.4;

        for (let i = 0; i < allFish.length; i++) {
          const other = allFish[i];
          if (other === this) continue;

          const d = this.position.distanceTo(other.position);
          if (d < desiredSeparation) {
            const diff = new THREE.Vector3().subVectors(this.position, other.position).normalize().divideScalar(Math.max(d, 0.2));
            sepSum.add(diff);
            sepCount++;
          }

          if (this.type.schooling && other.type.schooling && d < neighborDist) {
            alignSum.add(other.velocity);
            alignCount++;
            cohSum.add(other.position);
            cohCount++;
          }
        }

        if (sepCount > 0) {
          sepSum.divideScalar(sepCount).normalize().multiplyScalar(this.maxSpeed).sub(this.velocity).clampLength(0, this.maxForce * 1.5);
          this.applyForce(sepSum);
        }

        if (alignCount > 0 && this.fear < 0.3) {
          alignSum.divideScalar(alignCount).normalize().multiplyScalar(this.baseSpeed).sub(this.velocity).clampLength(0, this.maxForce * 0.8);
          this.applyForce(alignSum);
        }

        if (cohCount > 0 && this.fear < 0.3) {
          cohSum.divideScalar(cohCount);
          const seekVec = new THREE.Vector3().subVectors(cohSum, this.position).normalize().multiplyScalar(this.baseSpeed).sub(this.velocity).clampLength(0, this.maxForce * 0.7);
          this.applyForce(seekVec);
        }

        // --- 3. Attraction to Food Pellets (if not scared) ---
        if (this.fear < 0.2 && foodPellets.length > 0) {
          let nearestFood = null;
          let minFoodDist = 9.0;
          for (let f = 0; f < foodPellets.length; f++) {
            const fp = foodPellets[f];
            const dist = this.position.distanceTo(fp.position);
            if (dist < minFoodDist) {
              minFoodDist = dist;
              nearestFood = fp;
            }
          }
          if (nearestFood) {
            const foodForce = new THREE.Vector3().subVectors(nearestFood.position, this.position).normalize().multiplyScalar(this.baseSpeed * 1.2).sub(this.velocity).clampLength(0, this.maxForce * 1.2);
            this.applyForce(foodForce);
          }
        }

        // --- 4. Soft Boundary Walls (Keep fish swimming inside tank) ---
        const pad = 2.0;
        const turnForce = 0.16;
        if (this.position.x < TANK.minX + pad) this.velocity.x += turnForce;
        if (this.position.x > TANK.maxX - pad) this.velocity.x -= turnForce;
        if (this.position.y < TANK.minY + pad) this.velocity.y += turnForce * 1.2;
        if (this.position.y > TANK.maxY - pad) this.velocity.y -= turnForce * 1.2;
        if (this.position.z < TANK.minZ + pad) this.velocity.z += turnForce;
        if (this.position.z > TANK.maxZ - pad) this.velocity.z -= turnForce;

        // Hard clamp fallback
        this.position.x = THREE.MathUtils.clamp(this.position.x, TANK.minX, TANK.maxX);
        this.position.y = THREE.MathUtils.clamp(this.position.y, TANK.minY + 0.3, TANK.maxY - 0.2);
        this.position.z = THREE.MathUtils.clamp(this.position.z, TANK.minZ, TANK.maxZ);

        // --- 5. Physics Integration ---
        this.velocity.add(this.acceleration);

        // Speed management: high speed if scared, glide if calm
        if (this.scareTimer > 0) {
          this.scareTimer -= delta;
          this.fear = Math.max(0, this.scareTimer / 1.4);
        } else {
          this.fear = THREE.MathUtils.damp(this.fear, 0, 2.5, delta);
        }

        const targetMax = THREE.MathUtils.lerp(this.baseSpeed, this.maxSpeed, this.fear);
        if (this.velocity.length() > targetMax) {
          this.velocity.setLength(targetMax);
        } else if (this.velocity.length() < this.baseSpeed * 0.7) {
          this.velocity.setLength(this.baseSpeed * 0.7);
        }

        this.position.addScaledVector(this.velocity, delta);
        this.mesh.position.copy(this.position);
        this.acceleration.set(0, 0, 0);

        // --- 6. Smooth Mesh Orientation towards Velocity ---
        if (this.velocity.lengthSq() > 0.001) {
          const forward = this.velocity.clone().normalize();
          const targetRotation = new THREE.Quaternion();
          // Fish model faces +X initially; calculate rotation to align with velocity
          const m = new THREE.Matrix4();
          const up = new THREE.Vector3(0, 1, 0);
          const right = new THREE.Vector3().crossVectors(up, forward).normalize();
          const correctedUp = new THREE.Vector3().crossVectors(forward, right).normalize();
          m.makeBasis(forward, correctedUp, right);
          targetRotation.setFromRotationMatrix(m);

          // Add banking/tilt when turning
          const turnAngle = forward.dot(new THREE.Vector3(0, 1, 0));
          this.mesh.quaternion.slerp(targetRotation, Math.min(1.0, delta * 9.0));
        }

        // --- 7. Tail Wiggle & Fin Flutter Animation ---
        const speedRatio = this.velocity.length() / this.baseSpeed;
        const wagFreq = (4.0 + speedRatio * 8.0) * (1.0 + this.fear * 1.5);
        this.tailPhase += wagFreq * delta;

        const wagAngle = Math.sin(this.tailPhase) * (0.35 + this.fear * 0.4);
        this.tailJoint.rotation.y = wagAngle;

        // Pectoral fins flutter
        const finFlutter = Math.cos(this.tailPhase * 1.5) * 0.25;
        this.pecL.rotation.z = -0.3 + finFlutter;
        this.pecR.rotation.z = -0.3 - finFlutter;
      }
    }

    // --- Spawn School of Tropical Fish ---
    const fishList = [];
    const TOTAL_FISH = 48;

    for (let i = 0; i < TOTAL_FISH; i++) {
      const type = FISH_TYPES[i % FISH_TYPES.length];
      const x = (Math.random() - 0.5) * (TANK.width * 0.75);
      const y = TANK.minY + 2.0 + Math.random() * (TANK.height - 4.0);
      const z = (Math.random() - 0.5) * (TANK.depth * 0.75);

      const fish = new FishAgent(type, x, y, z);
      fishList.push(fish);
    }

    // --- Fish Food Dropping System ---
    const foodPellets = [];
    const foodGeo = new THREE.DodecahedronGeometry(0.12);
    const foodMat = new THREE.MeshStandardMaterial({ color: 0xcc6622, roughness: 0.8 });

    function dropFood(worldPos) {
      for (let i = 0; i < 4; i++) {
        const pellet = new THREE.Mesh(foodGeo, foodMat);
        pellet.position.copy(worldPos);
        pellet.position.x += (Math.random() - 0.5) * 0.8;
        pellet.position.z += (Math.random() - 0.5) * 0.8;
        pellet.position.y = Math.min(pellet.position.y, TANK.maxY - 0.5);

        pellet.userData = {
          vy: - (0.6 + Math.random() * 0.8),
          vx: (Math.random() - 0.5) * 0.3,
          vz: (Math.random() - 0.5) * 0.3,
          rotX: Math.random() * 2,
          rotZ: Math.random() * 2,
          life: 14.0
        };
        scene.add(pellet);
        foodPellets.push(pellet);
      }
    }

    // --- Mouse & Touch Interaction Raycasting ---
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2(-999, -999);
    let activeMouseRay = null;
    let isMouseInView = false;
    let isScaringAny = false;

    window.addEventListener('mousemove', (e) => {
      mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
      isMouseInView = true;

      crosshairEl.style.display = 'block';
      crosshairEl.style.left = `${e.clientX}px`;
      crosshairEl.style.top = `${e.clientY}px`;
    });

    window.addEventListener('mouseleave', () => {
      isMouseInView = false;
      activeMouseRay = null;
      crosshairEl.style.display = 'none';
    });

    // Click to drop food
    window.addEventListener('pointerdown', (e) => {
      // If clicking with left button without dragging
      if (e.button !== 0) return;

      const clickCoords = new THREE.Vector2(
        (e.clientX / window.innerWidth) * 2 - 1,
        -(e.clientY / window.innerHeight) * 2 + 1
      );

      const clickRay = new THREE.Raycaster();
      clickRay.setFromCamera(clickCoords, camera);

      // Plane in center of tank z=0 to drop food
      const dropPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
      const hitPoint = new THREE.Vector3();
      if (clickRay.ray.intersectPlane(dropPlane, hitPoint)) {
        hitPoint.x = THREE.MathUtils.clamp(hitPoint.x, TANK.minX + 1, TANK.maxX - 1);
        hitPoint.y = Math.min(hitPoint.y, TANK.maxY - 1);
        hitPoint.z = THREE.MathUtils.clamp(hitPoint.z, TANK.minZ + 1, TANK.maxZ - 1);
        dropFood(hitPoint);
      }
    });

    // Touch support for mobile devices
    window.addEventListener('touchmove', (e) => {
      if (e.touches.length > 0) {
        const t = e.touches[0];
        mouse.x = (t.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(t.clientY / window.innerHeight) * 2 + 1;
        isMouseInView = true;

        crosshairEl.style.display = 'block';
        crosshairEl.style.left = `${t.clientX}px`;
        crosshairEl.style.top = `${t.clientY}px`;
      }
    }, { passive: true });

    // --- Animation Loop ---
    const clock = new THREE.Clock();

    function animate() {
      requestAnimationFrame(animate);

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

      controls.update();

      // Update Caustics Shader Time
      sandMaterial.uniforms.uTime.value = elapsedTime;

      // Update Mouse 3D Ray for fish repelling
      if (isMouseInView) {
        raycaster.setFromCamera(mouse, camera);
        activeMouseRay = raycaster.ray;
      } else {
        activeMouseRay = null;
      }

      // Check if any fish is currently scattering (for UI cursor effect)
      isScaringAny = false;

      // Update Fish
      for (let i = 0; i < fishList.length; i++) {
        const fish = fishList[i];
        fish.update(delta, fishList, foodPellets, activeMouseRay);
        if (fish.fear > 0.5) {
          isScaringAny = true;
        }
      }

      if (isScaringAny) {
        crosshairEl.classList.add('scaring');
      } else {
        crosshairEl.classList.remove('scaring');
      }

      // Update Food Pellets
      for (let f = foodPellets.length - 1; f >= 0; f--) {
        const pellet = foodPellets[f];
        pellet.position.y += pellet.userData.vy * delta;
        pellet.position.x += pellet.userData.vx * delta;
        pellet.position.z += pellet.userData.vz * delta;
        pellet.rotation.x += pellet.userData.rotX * delta;
        pellet.rotation.z += pellet.userData.rotZ * delta;
        pellet.userData.life -= delta;

        // Fish eating food
        let eaten = false;
        for (let i = 0; i < fishList.length; i++) {
          if (fishList[i].position.distanceTo(pellet.position) < 0.65) {
            eaten = true;
            break;
          }
        }

        // Hits floor or eaten or expired
        if (pellet.position.y <= TANK.minY + 0.1 || eaten || pellet.userData.life <= 0) {
          scene.remove(pellet);
          foodPellets.splice(f, 1);
        }
      }

      // Update Seaweed Swaying
      for (let i = 0; i < seaweedGroups.length; i++) {
        const blade = seaweedGroups[i];
        const segs = blade.userData.segments;
        const seed = blade.userData.seed;
        const sway = Math.sin(elapsedTime * 1.8 + seed) * 0.08 + Math.cos(elapsedTime * 2.8 + seed) * 0.04;

        for (let s = 0; s < segs.length; s++) {
          const seg = segs[s];
          seg.rotation.z = sway * (s + 1) * 0.45;
        }
      }

      // Update Rising Bubbles
      for (let i = 0; i < bubbleCount; i++) {
        const b = bubbleData[i];
        b.y += b.speed * delta;
        const wobbleX = Math.sin(elapsedTime * b.wobbleSpeed + b.seed) * b.wobbleAmp;
        const wobbleZ = Math.cos(elapsedTime * b.wobbleSpeed + b.seed) * b.wobbleAmp;

        if (b.y > TANK.maxY) {
          b.y = TANK.minY;
          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 + wobbleX, b.y, b.z + wobbleZ);
        dummy.scale.setScalar(b.scale);
        dummy.updateMatrix();
        bubbleInstanced.setMatrixAt(i, dummy.matrix);
      }
      bubbleInstanced.instanceMatrix.needsUpdate = true;

      // Gentle caustics light movement
      sunLight.position.x = 2 + Math.sin(elapsedTime * 0.5) * 1.5;
      sunLight.position.z = 4 + Math.cos(elapsedTime * 0.4) * 1.5;

      renderer.render(scene, camera);
    }

    animate();

    // --- Window Resize ---
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>
Reasoning trace (1,453 tokens)
**Creating Dynamic Aquarium**

I'm currently implementing a 3D aquarium using Three.js. My focus is on making the fish react dynamically to user interaction, specifically by swimming away when the mouse cursor approaches. The provided import map is integrated to manage the Three.js library and its addons effectively.

**Developing Interactive Fish**

I'm refining the fish AI to exhibit more natural wandering behavior and integrating their tail wagging animation. My current focus is on ensuring they scatter convincingly when the cursor gets too close, initiating their panicked retreat. I'm also tweaking the interaction logic to feel more responsive and emergent.

**Enhancing Visuals and Interaction**

I'm now focusing on adding atmospheric elements like rising bubbles and realistic underwater caustics. My interaction logic is being refined with raycasting from mouse movement to create a dynamic repulsor sphere, offering subtle visual feedback on the cursor itself. Ensuring high performance and zero console errors remains a priority.

**Enhancing Fish Behavior**

I'm focusing on refining the fish AI with a more robust boids system. This includes implementing separation, alignment, cohesion, and boundary avoidance. The core of my current effort is enhancing the mouse interaction, making fish actively repel from a 3D point with adjustable force and triggering a frantic tail wag animation when scared. I'm also planning to integrate a click-to-feed mechanic with drifting food flakes.

**Refining Fish Movement**

I'm perfecting the fish's reaction to the mouse. Instead of just hitting a plane, I'm calculating the closest distance from each fish to the mouse's ray in 3D space. If a fish is within a certain range, it will be pushed away along a vector that's perpendicular to the ray and slightly along its direction, creating a more magical and responsive avoidance.