Models 44

Gemini 4 Argon

by Googlereasoning defaultClean
Open in new window

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

How it went

Total time
3 min 24 s
Cost
$0.132
Output tokens
13,093
Spent thinking
0 tokens
Speed
65.2 tok/s
Console errors
0
First token after
2.8 s
First visible text after
2.8 s
Prompt tokens
486
Code size
35.0 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-4-argon
Reasoning sent
model default
Max output
943,718 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 · 1034 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Interactive 3D Aquarium</title>
  <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>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
    }
    body {
      width: 100vw;
      height: 100vh;
      overflow: hidden;
      background: radial-gradient(circle at 50% 35%, #162638 0%, #070d14 75%, #020408 100%);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      color: #e2f1ff;
    }
    canvas {
      display: block;
      width: 100%;
      height: 100%;
    }
    #hud {
      position: absolute;
      top: 20px;
      left: 20px;
      z-index: 10;
      background: rgba(8, 22, 36, 0.72);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border: 1px solid rgba(110, 210, 255, 0.25);
      border-radius: 16px;
      padding: 18px 22px;
      max-width: 340px;
      box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.15);
      pointer-events: auto;
    }
    .title-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 6px;
      gap: 10px;
    }
    h1 {
      font-size: 1.15rem;
      font-weight: 700;
      letter-spacing: 0.4px;
      background: linear-gradient(90deg, #7ef0ff, #42b0ff);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    .badge {
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      padding: 3px 8px;
      border-radius: 99px;
      background: rgba(0, 229, 255, 0.16);
      border: 1px solid rgba(0, 229, 255, 0.4);
      color: #84f2ff;
      letter-spacing: 0.6px;
    }
    p.desc {
      font-size: 0.82rem;
      line-height: 1.45;
      color: #a8cbe4;
      margin-bottom: 14px;
    }
    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 12px;
    }
    button {
      flex: 1 1 auto;
      background: rgba(20, 60, 95, 0.65);
      color: #d8f3ff;
      border: 1px solid rgba(100, 200, 255, 0.3);
      border-radius: 9px;
      padding: 8px 11px;
      font-size: 0.78rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.18s ease;
    }
    button:hover {
      background: rgba(35, 105, 160, 0.8);
      border-color: rgba(130, 230, 255, 0.7);
      transform: translateY(-1px);
    }
    button.active {
      background: linear-gradient(135deg, #00b4d8, #0077b6);
      border-color: #90e0ef;
      color: #ffffff;
      box-shadow: 0 0 12px rgba(0, 180, 216, 0.45);
    }
    .stats {
      display: flex;
      justify-content: space-between;
      font-size: 0.76rem;
      color: #82b2d0;
      border-top: 1px solid rgba(110, 210, 255, 0.15);
      padding-top: 10px;
    }
    .stats span strong {
      color: #dbf6ff;
    }
    #hint-pill {
      position: absolute;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(8, 24, 40, 0.75);
      backdrop-filter: blur(8px);
      border: 1px solid rgba(0, 229, 255, 0.35);
      padding: 9px 20px;
      border-radius: 999px;
      font-size: 0.85rem;
      color: #ccf4ff;
      pointer-events: none;
      box-shadow: 0 6px 20px rgba(0,0,0,0.45);
      white-space: nowrap;
    }
    @media (max-width: 550px) {
      #hud {
        top: 12px;
        left: 12px;
        right: 12px;
        max-width: none;
        padding: 14px 16px;
      }
      #hint-pill {
        font-size: 0.75rem;
        bottom: 14px;
        padding: 7px 14px;
      }
    }
  </style>
</head>
<body>
  <div id="hud">
    <div class="title-row">
      <h1>Reef Sanctuary 3D</h1>
      <span class="badge" id="mode-badge">Auto Probe</span>
    </div>
    <p class="desc">Move your cursor into the tank to startle the fish school. Click inside the glass to drop food pellets, or right-drag to orbit.</p>
    <div class="controls">
      <button id="btn-drop-food">Drop Food</button>
      <button id="btn-scare-pulse">Shockwave</button>
      <button id="btn-toggle-probe" class="active">Auto-Scare: ON</button>
    </div>
    <div class="stats">
      <span>Fish: <strong id="fish-count">60</strong></span>
      <span>Startled: <strong id="scared-count">0</strong></span>
      <span>Pellets: <strong id="food-count">0</strong></span>
    </div>
  </div>

  <div id="hint-pill">Move mouse across the tank to scatter the fish!</div>

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

    // --- Tank Dimensions ---
    const TANK = {
      width: 30,
      height: 17,
      depth: 18,
      wallThickness: 0.35,
      sandHeight: 1.5
    };
    const BOUNDS = {
      x: TANK.width / 2 - 1.4,
      yMin: -TANK.height / 2 + TANK.sandHeight + 0.9,
      yMax: TANK.height / 2 - 1.3,
      z: TANK.depth / 2 - 1.4
    };

    // --- Scene, Camera, Renderer ---
    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x092438, 0.014);

    const camera = new THREE.PerspectiveCamera(44, window.innerWidth / window.innerHeight, 0.1, 150);
    camera.position.set(0, 2.5, 34);

    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;
    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.06;
    controls.maxDistance = 55;
    controls.minDistance = 14;
    controls.maxPolarAngle = Math.PI * 0.56;
    controls.minPolarAngle = Math.PI * 0.15;
    // Allow left-click to interact with fish/food and right-click or two-finger to orbit
    controls.mouseButtons = {
      LEFT: null,
      MIDDLE: THREE.MOUSE.DOLLY,
      RIGHT: THREE.MOUSE.ROTATE
    };

    // --- Procedural Caustic & Sand Textures ---
    function createCausticTexture() {
      const canvas = document.createElement('canvas');
      canvas.width = 256;
      canvas.height = 256;
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = '#073b54';
      ctx.fillRect(0, 0, 256, 256);

      for (let i = 0; i < 45; i++) {
        const x = Math.random() * 256;
        const y = Math.random() * 256;
        const r = 18 + Math.random() * 42;
        const grad = ctx.createRadialGradient(x, y, 2, x, y, r);
        grad.addColorStop(0, 'rgba(130, 245, 255, 0.35)');
        grad.addColorStop(0.6, 'rgba(40, 195, 235, 0.12)');
        grad.addColorStop(1, 'rgba(0, 80, 120, 0)');
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.fill();
      }
      const tex = new THREE.CanvasTexture(canvas);
      tex.wrapS = THREE.RepeatWrapping;
      tex.wrapT = THREE.RepeatWrapping;
      tex.repeat.set(3, 2);
      return tex;
    }

    const causticTex = createCausticTexture();

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

    const hemiLight = new THREE.HemisphereLight(0x88f3ff, 0x102838, 1.4);
    scene.add(hemiLight);

    const sunLight = new THREE.DirectionalLight(0xbbf7ff, 2.8);
    sunLight.position.set(6, 22, 8);
    sunLight.castShadow = true;
    sunLight.shadow.mapSize.set(2048, 2048);
    sunLight.shadow.camera.left = -18;
    sunLight.shadow.camera.right = 18;
    sunLight.shadow.camera.top = 12;
    sunLight.shadow.camera.bottom = -12;
    sunLight.shadow.camera.near = 5;
    sunLight.shadow.camera.far = 38;
    sunLight.shadow.bias = -0.0008;
    scene.add(sunLight);

    const reefGlow1 = new THREE.PointLight(0xff4da6, 6, 15);
    reefGlow1.position.set(-7, -4, -1);
    scene.add(reefGlow1);

    const reefGlow2 = new THREE.PointLight(0x00f5d4, 7, 16);
    reefGlow2.position.set(7, -3, 2);
    scene.add(reefGlow2);

    // --- Aquarium Tank Construction ---
    const tankGroup = new THREE.Group();
    scene.add(tankGroup);

    // Inner water volume tint
    const waterGeo = new THREE.BoxGeometry(TANK.width - 0.15, TANK.height - 0.6, TANK.depth - 0.15);
    const waterMat = new THREE.MeshPhysicalMaterial({
      color: 0x0e6b8c,
      transparent: true,
      opacity: 0.16,
      roughness: 0.1,
      metalness: 0.0,
      side: THREE.BackSide,
      depthWrite: false
    });
    const waterMesh = new THREE.Mesh(waterGeo, waterMat);
    waterMesh.position.y = -0.2;
    tankGroup.add(waterMesh);

    // Animated top water surface
    const surfaceGeo = new THREE.PlaneGeometry(TANK.width - 0.2, TANK.depth - 0.2, 48, 32);
    surfaceGeo.rotateX(-Math.PI / 2);
    const surfaceMat = new THREE.MeshPhysicalMaterial({
      color: 0x6ce8ff,
      emissive: 0x0d5c75,
      emissiveMap: causticTex,
      emissiveIntensity: 0.85,
      transparent: true,
      opacity: 0.55,
      roughness: 0.15,
      metalness: 0.1,
      side: THREE.DoubleSide
    });
    const surfaceMesh = new THREE.Mesh(surfaceGeo, surfaceMat);
    surfaceMesh.position.y = TANK.height / 2 - 0.5;
    tankGroup.add(surfaceMesh);

    // Outer glass walls
    const glassGeo = new THREE.BoxGeometry(TANK.width, TANK.height, TANK.depth);
    const glassMat = new THREE.MeshPhysicalMaterial({
      color: 0xc8f4ff,
      transparent: true,
      opacity: 0.13,
      roughness: 0.05,
      reflectivity: 0.9,
      clearcoat: 1.0,
      depthWrite: false
    });
    const glassMesh = new THREE.Mesh(glassGeo, glassMat);
    tankGroup.add(glassMesh);

    // Crisp metallic frame & corners
    const edgesGeo = new THREE.EdgesGeometry(glassGeo);
    const edgesMat = new THREE.LineBasicMaterial({ color: 0x62d2ff, transparent: true, opacity: 0.45 });
    tankGroup.add(new THREE.LineSegments(edgesGeo, edgesMat));

    const baseFrameGeo = new THREE.BoxGeometry(TANK.width + 0.6, 0.9, TANK.depth + 0.6);
    const frameMat = new THREE.MeshStandardMaterial({ color: 0x101822, roughness: 0.4, metalness: 0.7 });
    const baseFrame = new THREE.Mesh(baseFrameGeo, frameMat);
    baseFrame.position.y = -TANK.height / 2 - 0.35;
    tankGroup.add(baseFrame);

    const topRimGeo = new THREE.BoxGeometry(TANK.width + 0.4, 0.45, TANK.depth + 0.4);
    const topRim = new THREE.Mesh(topRimGeo, frameMat);
    topRim.position.y = TANK.height / 2 + 0.15;
    tankGroup.add(topRim);

    // --- Sculpted Sandy Seabed ---
    const sandGeo = new THREE.BoxGeometry(TANK.width - 0.25, TANK.sandHeight, TANK.depth - 0.25, 60, 1, 36);
    const posAttr = sandGeo.attributes.position;
    for (let i = 0; i < posAttr.count; i++) {
      const vy = posAttr.getY(i);
      if (vy > 0) {
        const vx = posAttr.getX(i);
        const vz = posAttr.getZ(i);
        const wave = Math.sin(vx * 0.35) * Math.cos(vz * 0.35) * 0.38 +
                     Math.sin(vx * 0.9 + vz * 0.7) * 0.12;
        posAttr.setY(i, vy + wave);
      }
    }
    sandGeo.computeVertexNormals();

    const sandMat = new THREE.MeshStandardMaterial({
      color: 0xd9c28c,
      roughness: 0.88,
      metalness: 0.05,
      emissive: 0x164c64,
      emissiveMap: causticTex,
      emissiveIntensity: 0.45
    });
    const sandMesh = new THREE.Mesh(sandGeo, sandMat);
    sandMesh.position.y = -TANK.height / 2 + TANK.sandHeight / 2;
    sandMesh.receiveShadow = true;
    tankGroup.add(sandMesh);

    // --- Reef Rocks, Corals & Swaying Seaweed ---
    const decorGroup = new THREE.Group();
    const sandTopY = -TANK.height / 2 + TANK.sandHeight;
    tankGroup.add(decorGroup);

    // Rocks
    const rockMat = new THREE.MeshStandardMaterial({ color: 0x3b4d5e, roughness: 0.85, flatShading: true });
    const rockClusters = [
      { x: -8.5, z: -2.5, s: 2.2 },
      { x: -6.2, z: -3.2, s: 1.5 },
      { x: -9.8, z: -0.5, s: 1.6 },
      { x: 7.2, z: 1.5, s: 2.4 },
      { x: 9.2, z: 0.2, s: 1.7 },
      { x: 5.5, z: 2.8, s: 1.3 },
      { x: 0.2, z: -4.2, s: 2.7 },
      { x: -2.0, z: -4.8, s: 1.8 },
      { x: 2.5, z: -3.6, s: 1.6 }
    ];
    rockClusters.forEach(rc => {
      const geo = new THREE.DodecahedronGeometry(rc.s, 1);
      const p = geo.attributes.position;
      for (let i = 0; i < p.count; i++) {
        p.setXYZ(
          i,
          p.getX(i) * (0.85 + Math.sin(i * 2.1) * 0.2),
          p.getY(i) * (0.7 + Math.cos(i * 1.7) * 0.2),
          p.getZ(i) * (0.85 + Math.sin(i * 3.3) * 0.2)
        );
      }
      geo.computeVertexNormals();
      const mesh = new THREE.Mesh(geo, rockMat);
      mesh.position.set(rc.x, sandTopY + rc.s * 0.35, rc.z);
      mesh.rotation.set(Math.random(), Math.random() * Math.PI, 0);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      decorGroup.add(mesh);
    });

    // Vibrant Corals
    const coralColors = [0xff5d8f, 0xff9e00, 0xb5179e, 0x00f5d4, 0xff4d6d];
    rockClusters.forEach((rc, idx) => {
      const color = coralColors[idx % coralColors.length];
      const cMat = new THREE.MeshStandardMaterial({
        color,
        roughness: 0.4,
        emissive: color,
        emissiveIntensity: 0.22
      });
      const branches = 5 + (idx % 4);
      for (let b = 0; b < branches; b++) {
        const h = 1.1 + Math.random() * 1.4;
        const cGeo = new THREE.CapsuleGeometry(0.14, h, 5, 8);
        const cMesh = new THREE.Mesh(cGeo, cMat);
        cMesh.position.set(
          rc.x + (Math.random() - 0.5) * 1.6,
          sandTopY + rc.s * 0.55 + h * 0.4,
          rc.z + (Math.random() - 0.5) * 1.6
        );
        cMesh.rotation.z = (Math.random() - 0.5) * 0.65;
        cMesh.rotation.x = (Math.random() - 0.5) * 0.65;
        cMesh.castShadow = true;
        decorGroup.add(cMesh);
      }
    });

    // Swaying Kelp / Seaweed
    const kelpUniforms = { uTime: { value: 0 } };
    const kelpMat = new THREE.MeshStandardMaterial({
      color: 0x1dd38f,
      roughness: 0.5,
      side: THREE.DoubleSide
    });
    kelpMat.onBeforeCompile = (shader) => {
      shader.uniforms.uTime = kelpUniforms.uTime;
      shader.vertexShader = `
        uniform float uTime;
        attribute float aPhase;
      ` + shader.vertexShader;
      shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        `
        #include <begin_vertex>
        float heightFactor = clamp((position.y + 2.5) / 5.0, 0.0, 1.0);
        transformed.x += sin(uTime * 2.2 + position.y * 1.1 + aPhase) * 0.45 * heightFactor;
        transformed.z += cos(uTime * 1.7 + position.y * 0.9 + aPhase) * 0.25 * heightFactor;
        `
      );
    };

    for (let i = 0; i < 38; i++) {
      const h = 3.0 + Math.random() * 4.2;
      const kGeo = new THREE.PlaneGeometry(0.38, h, 2, 14);
      const phases = new Float32Array(kGeo.attributes.position.count).fill(Math.random() * Math.PI * 2);
      kGeo.setAttribute('aPhase', new THREE.BufferAttribute(phases, 1));

      const kMesh = new THREE.Mesh(kGeo, kelpMat);
      const side = i % 2 === 0 ? -1 : 1;
      const kx = side * (5 + Math.random() * 8.5);
      const kz = -6.5 + Math.random() * 12;
      kMesh.position.set(kx, sandTopY + h / 2 - 0.2, kz);
      kMesh.rotation.y = Math.random() * Math.PI;
      kMesh.castShadow = true;
      decorGroup.add(kMesh);
    }

    // --- Rising Air Bubbles ---
    const BUBBLE_COUNT = 120;
    const bubbleGeo = new THREE.SphereGeometry(0.09, 8, 8);
    const bubbleMat = new THREE.MeshPhysicalMaterial({
      color: 0xffffff,
      transmission: 0.9,
      opacity: 0.65,
      transparent: true,
      roughness: 0.05,
      ior: 1.33
    });
    const bubbleInst = new THREE.InstancedMesh(bubbleGeo, bubbleMat, BUBBLE_COUNT);
    tankGroup.add(bubbleInst);

    const bubbleData = [];
    const emitters = [
      new THREE.Vector3(-8.2, sandTopY + 0.4, -2.2),
      new THREE.Vector3(7.4, sandTopY + 0.4, 1.6),
      new THREE.Vector3(0.4, sandTopY + 0.4, -4.0)
    ];
    for (let i = 0; i < BUBBLE_COUNT; i++) {
      const em = emitters[i % emitters.length];
      bubbleData.push({
        emitter: em,
        pos: new THREE.Vector3(
          em.x + (Math.random() - 0.5) * 0.8,
          sandTopY + Math.random() * (TANK.height - 2),
          em.z + (Math.random() - 0.5) * 0.8
        ),
        speed: 2.0 + Math.random() * 2.2,
        scale: 0.4 + Math.random() * 1.1,
        wobbleOffset: Math.random() * Math.PI * 2
      });
    }

    // --- Procedural Fish Geometry & Shader ---
    function createFishGeometry() {
      const geo = new THREE.SphereGeometry(0.5, 24, 14);
      const pos = geo.attributes.position;

      for (let i = 0; i < pos.count; i++) {
        let x = pos.getX(i);
        let y = pos.getY(i);
        let z = pos.getZ(i);

        // Stretch along Z (head at +Z, tail at -Z)
        z *= 2.4;

        if (z < -0.25) {
          // Pinch body into tail peduncle and fan out tail fin
          const tailT = (-z - 0.25) / 0.95;
          y *= 0.28 + Math.pow(tailT, 1.5) * 1.65;
          x *= Math.max(0.06, 0.55 * (1.0 - tailT * 0.88));
          // Forked tail notch
          if (z < -0.95 && Math.abs(y) < 0.25) {
            z += (0.25 - Math.abs(y)) * 0.55;
          }
        } else {
          // Streamlined oval body
          const bodyFactor = 1.0 - Math.pow(z / 1.25, 2) * 0.35;
          x *= 0.48 * bodyFactor;
          y *= 0.88 * bodyFactor;

          // Dorsal fin ridge on top
          if (y > 0.32 && z > -0.25 && z < 0.35) {
            y += (0.35 - Math.abs(z)) * 0.7;
            x *= 0.25;
          }
        }

        pos.setXYZ(i, x, y, z);
      }
      geo.computeVertexNormals();
      return geo;
    }

    const fishBaseGeo = createFishGeometry();
    const eyeGeo = new THREE.SphereGeometry(0.055, 8, 8);
    const eyeMat = new THREE.MeshBasicMaterial({ color: 0x05070a });

    const fishSpecies = [
      { primary: 0xff6b00, secondary: 0xffffff, scale: 0.95, speed: 3.6 }, // Clownfish Orange
      { primary: 0x00d4ff, secondary: 0xffbe0b, scale: 1.05, speed: 3.9 }, // Blue Tang
      { primary: 0xff2a85, secondary: 0xffd166, scale: 0.85, speed: 4.2 }, // Anthias Pink/Gold
      { primary: 0x19e68c, secondary: 0x00bbf9, scale: 0.90, speed: 3.8 }, // Damselfish Neon Green
      { primary: 0xffbe0b, secondary: 0xfb5607, scale: 1.12, speed: 3.4 }  // Golden Butterflyfish
    ];

    const FISH_COUNT = 60;
    const fishes = [];
    const fishGroup = new THREE.Group();
    scene.add(fishGroup);

    for (let i = 0; i < FISH_COUNT; i++) {
      const spec = fishSpecies[i % fishSpecies.length];
      const swimUniforms = {
        uTime: { value: Math.random() * 100 },
        uSpeed: { value: 1.0 },
        uScared: { value: 0.0 },
        uColorA: { value: new THREE.Color(spec.primary) },
        uColorB: { value: new THREE.Color(spec.secondary) }
      };

      const mat = new THREE.MeshStandardMaterial({
        roughness: 0.22,
        metalness: 0.18
      });

      mat.onBeforeCompile = (shader) => {
        shader.uniforms.uTime = swimUniforms.uTime;
        shader.uniforms.uSpeed = swimUniforms.uSpeed;
        shader.uniforms.uScared = swimUniforms.uScared;
        shader.uniforms.uColorA = swimUniforms.uColorA;
        shader.uniforms.uColorB = swimUniforms.uColorB;

        shader.vertexShader = `
          uniform float uTime;
          uniform float uSpeed;
          varying vec3 vLocalPos;
        ` + shader.vertexShader;

        shader.vertexShader = shader.vertexShader.replace(
          '#include <begin_vertex>',
          `
          #include <begin_vertex>
          vLocalPos = position;
          float tailFactor = clamp((0.45 - position.z) / 1.65, 0.0, 1.0);
          float wiggle = sin(uTime * 11.0 - position.z * 4.2) * 0.24 * tailFactor * tailFactor * uSpeed;
          transformed.x += wiggle;
          `
        );

        shader.fragmentShader = `
          uniform vec3 uColorA;
          uniform vec3 uColorB;
          uniform float uScared;
          varying vec3 vLocalPos;
        ` + shader.fragmentShader;

        shader.fragmentShader = shader.fragmentShader.replace(
          '#include <color_fragment>',
          `
          #include <color_fragment>
          float stripe = smoothstep(-0.15, 0.25, sin(vLocalPos.z * 8.5 + vLocalPos.y * 2.5));
          vec3 baseCol = mix(uColorA, uColorB, stripe * 0.75);
          // Subtle startled flash
          diffuseColor.rgb = mix(baseCol, vec3(0.85, 0.98, 1.0), uScared * 0.35);
          `
        );
      };

      const root = new THREE.Group();
      const bodyMesh = new THREE.Mesh(fishBaseGeo, mat);
      bodyMesh.castShadow = true;
      root.add(bodyMesh);

      // Eyes
      const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
      leftEye.position.set(0.16, 0.1, 0.68);
      const rightEye = new THREE.Mesh(eyeGeo, eyeMat);
      rightEye.position.set(-0.16, 0.1, 0.68);
      root.add(leftEye, rightEye);

      const scaleVariation = spec.scale * (0.8 + Math.random() * 0.4);
      root.scale.setScalar(scaleVariation);

      const pos = new THREE.Vector3(
        (Math.random() - 0.5) * BOUNDS.x * 1.7,
        BOUNDS.yMin + Math.random() * (BOUNDS.yMax - BOUNDS.yMin),
        (Math.random() - 0.5) * BOUNDS.z * 1.6
      );
      root.position.copy(pos);

      const angle = Math.random() * Math.PI * 2;
      const vel = new THREE.Vector3(Math.sin(angle), (Math.random() - 0.5) * 0.3, Math.cos(angle))
        .normalize()
        .multiplyScalar(spec.speed);

      fishGroup.add(root);

      fishes.push({
        mesh: root,
        pos,
        vel,
        acc: new THREE.Vector3(),
        baseSpeed: spec.speed,
        maxForce: 14.0,
        scaredTimer: 0,
        uniforms: swimUniforms,
        wanderSeed: Math.random() * 100
      });
    }

    // --- Interactive Threat / Cursor Repeller Orb ---
    const repellerGroup = new THREE.Group();
    scene.add(repellerGroup);

    const coreOrbGeo = new THREE.SphereGeometry(0.38, 24, 24);
    const coreOrbMat = new THREE.MeshBasicMaterial({
      color: 0x70f8ff,
      transparent: true,
      opacity: 0.9
    });
    const coreOrb = new THREE.Mesh(coreOrbGeo, coreOrbMat);
    repellerGroup.add(coreOrb);

    const auraGeo = new THREE.SphereGeometry(4.8, 32, 24);
    const auraMat = new THREE.MeshBasicMaterial({
      color: 0x00e5ff,
      transparent: true,
      opacity: 0.11,
      wireframe: true
    });
    const auraMesh = new THREE.Mesh(auraGeo, auraMat);
    repellerGroup.add(auraMesh);

    const cursorLight = new THREE.PointLight(0x56f0ff, 10, 11);
    repellerGroup.add(cursorLight);

    // State for cursor & automatic demo probe
    const repellerPos = new THREE.Vector3(0, 0, 0);
    const targetRepellerPos = new THREE.Vector3(0, 0, 0);
    const raycaster = new THREE.Raycaster();
    const pointerNDC = new THREE.Vector2();
    const interactionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);

    let autoProbeActive = true;
    let lastPointerMoveTime = -999;
    let shockwaveBoost = 0;

    // --- Food Pellets System ---
    const foodPellets = [];
    const pelletGeo = new THREE.SphereGeometry(0.14, 10, 10);
    const pelletMat = new THREE.MeshStandardMaterial({
      color: 0xffaa2b,
      emissive: 0xff6600,
      emissiveIntensity: 0.4,
      roughness: 0.4
    });

    function spawnFoodCluster(centerX = (Math.random() - 0.5) * 14, centerZ = (Math.random() - 0.5) * 8) {
      for (let i = 0; i < 6; i++) {
        const mesh = new THREE.Mesh(pelletGeo, pelletMat);
        mesh.position.set(
          THREE.MathUtils.clamp(centerX + (Math.random() - 0.5) * 3.5, -BOUNDS.x, BOUNDS.x),
          TANK.height / 2 - 0.7,
          THREE.MathUtils.clamp(centerZ + (Math.random() - 0.5) * 3.5, -BOUNDS.z, BOUNDS.z)
        );
        scene.add(mesh);
        foodPellets.push({
          mesh,
          velY: -(1.1 + Math.random() * 0.6),
          driftPhase: Math.random() * Math.PI * 2
        });
      }
      updateHUDStats();
    }

    // --- UI & Pointer Event Listeners ---
    const badgeEl = document.getElementById('mode-badge');
    const toggleBtn = document.getElementById('btn-toggle-probe');
    const scaredCountEl = document.getElementById('scared-count');
    const foodCountEl = document.getElementById('food-count');
    const hintPillEl = document.getElementById('hint-pill');

    function setProbeMode(isAuto) {
      autoProbeActive = isAuto;
      if (isAuto) {
        badgeEl.textContent = 'Auto Probe';
        toggleBtn.textContent = 'Auto-Scare: ON';
        toggleBtn.classList.add('active');
      } else {
        badgeEl.textContent = 'Mouse Control';
        toggleBtn.textContent = 'Auto-Scare: OFF';
        toggleBtn.classList.remove('active');
      }
    }

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

      // Align plane normal with camera view direction for natural 3D cursor placement
      const camDir = new THREE.Vector3();
      camera.getWorldDirection(camDir);
      interactionPlane.setFromNormalAndCoplanarPoint(camDir, new THREE.Vector3(0, 0, 0));

      raycaster.setFromCamera(pointerNDC, camera);
      const hitPoint = new THREE.Vector3();
      if (raycaster.ray.intersectPlane(interactionPlane, hitPoint)) {
        targetRepellerPos.set(
          THREE.MathUtils.clamp(hitPoint.x, -BOUNDS.x, BOUNDS.x),
          THREE.MathUtils.clamp(hitPoint.y, BOUNDS.yMin, BOUNDS.yMax),
          THREE.MathUtils.clamp(hitPoint.z, -BOUNDS.z, BOUNDS.z)
        );
      }
    }

    window.addEventListener('pointermove', (e) => {
      if (e.target.closest('#hud')) return;
      lastPointerMoveTime = performance.now() * 0.001;
      if (autoProbeActive) setProbeMode(false);
      hintPillEl.textContent = 'Fish flee from your cursor! Click to drop food pellets.';
      updatePointerTarget(e.clientX, e.clientY);
    });

    renderer.domElement.addEventListener('pointerdown', (e) => {
      if (e.button !== 0) return;
      lastPointerMoveTime = performance.now() * 0.001;
      if (autoProbeActive) setProbeMode(false);
      updatePointerTarget(e.clientX, e.clientY);
      // Spawn a few pellets above where the user clicked
      spawnFoodCluster(targetRepellerPos.x, targetRepellerPos.z);
    });

    document.getElementById('btn-drop-food').addEventListener('click', () => {
      spawnFoodCluster();
    });

    document.getElementById('btn-scare-pulse').addEventListener('click', () => {
      shockwaveBoost = 1.0;
    });

    toggleBtn.addEventListener('click', () => {
      setProbeMode(!autoProbeActive);
    });

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

    function updateHUDStats(scaredCount = 0) {
      scaredCountEl.textContent = scaredCount;
      foodCountEl.textContent = foodPellets.length;
    }

    // --- Boids Flocking & Repulsion Simulation ---
    const SCARE_RADIUS = 6.2;
    const NEIGHBOR_RADIUS = 3.8;
    const SEPARATION_RADIUS = 1.45;

    const tempVec = new THREE.Vector3();
    const sepForce = new THREE.Vector3();
    const aliForce = new THREE.Vector3();
    const cohForce = new THREE.Vector3();
    const dummyMatrix = new THREE.Matrix4();
    const dummyPos = new THREE.Vector3();
    const dummyScale = new THREE.Vector3();
    const dummyQuat = new THREE.Quaternion();
    const lookMatrix = new THREE.Matrix4();
    const targetQuat = new THREE.Quaternion();
    const upVec = new THREE.Vector3(0, 1, 0);

    let lastTime = performance.now() * 0.001;

    function animate() {
      requestAnimationFrame(animate);

      const now = performance.now() * 0.001;
      const dt = Math.min(now - lastTime, 0.05);
      lastTime = now;

      // Resume auto-probe if mouse hasn't moved for 9 seconds
      if (!autoProbeActive && (now - lastPointerMoveTime > 9.0)) {
        setProbeMode(true);
      }

      // Update Repeller Position (Auto figure-8 patrol vs User Cursor)
      if (autoProbeActive) {
        targetRepellerPos.set(
          Math.sin(now * 0.95) * 9.5,
          Math.sin(now * 1.7) * 3.6,
          Math.cos(now * 0.75) * 4.5
        );
      }
      repellerPos.lerp(targetRepellerPos, Math.min(1.0, dt * 12.0));
      repellerGroup.position.copy(repellerPos);

      // Shockwave decay
      if (shockwaveBoost > 0) {
        shockwaveBoost = Math.max(0, shockwaveBoost - dt * 2.2);
      }

      const activeScareRadius = SCARE_RADIUS + shockwaveBoost * 7.5;
      const pulseScale = (activeScareRadius / 4.8) * (1.0 + Math.sin(now * 6.0) * 0.05);
      auraMesh.scale.setScalar(pulseScale);
      auraMesh.rotation.y = now * 0.6;
      auraMesh.rotation.z = now * 0.4;

      // Animate caustics & kelp
      causticTex.offset.x = now * 0.03;
      causticTex.offset.y = now * 0.02;
      kelpUniforms.uTime.value = now;

      // Update Falling Food Pellets
      for (let i = foodPellets.length - 1; i >= 0; i--) {
        const p = foodPellets[i];
        p.mesh.position.y += p.velY * dt;
        p.mesh.position.x += Math.sin(now * 3.0 + p.driftPhase) * 0.4 * dt;
        if (p.mesh.position.y <= sandTopY + 0.15) {
          scene.remove(p.mesh);
          foodPellets.splice(i, 1);
        }
      }

      // Update Rising Bubbles
      for (let i = 0; i < BUBBLE_COUNT; i++) {
        const b = bubbleData[i];
        b.pos.y += b.speed * dt;
        b.pos.x += Math.sin(now * 4.0 + b.wobbleOffset) * 0.35 * dt;
        if (b.pos.y > TANK.height / 2 - 0.6) {
          b.pos.y = sandTopY + 0.3;
          b.pos.x = b.emitter.x + (Math.random() - 0.5) * 0.7;
          b.pos.z = b.emitter.z + (Math.random() - 0.5) * 0.7;
        }
        dummyPos.copy(b.pos);
        dummyScale.setScalar(b.scale);
        dummyMatrix.compose(dummyPos, dummyQuat, dummyScale);
        bubbleInst.setMatrixAt(i, dummyMatrix);
      }
      bubbleInst.instanceMatrix.needsUpdate = true;

      // Update Fish School (Boids + Mouse Evasion + Food Seeking)
      let scaredFishCount = 0;

      for (let i = 0; i < FISH_COUNT; i++) {
        const f = fishes[i];
        f.acc.set(0, 0, 0);

        sepForce.set(0, 0, 0);
        aliForce.set(0, 0, 0);
        cohForce.set(0, 0, 0);
        let neighborCount = 0;

        for (let j = 0; j < FISH_COUNT; j++) {
          if (i === j) continue;
          const other = fishes[j];
          const dSq = f.pos.distanceToSquared(other.pos);

          if (dSq < NEIGHBOR_RADIUS * NEIGHBOR_RADIUS && dSq > 0.0001) {
            const d = Math.sqrt(dSq);
            cohForce.add(other.pos);
            aliForce.add(other.vel);
            neighborCount++;

            if (d < SEPARATION_RADIUS) {
              tempVec.subVectors(f.pos, other.pos).divideScalar(d * d);
              sepForce.add(tempVec);
            }
          }
        }

        if (neighborCount > 0) {
          cohForce.divideScalar(neighborCount).sub(f.pos).normalize().multiplyScalar(2.4);
          aliForce.divideScalar(neighborCount).normalize().multiplyScalar(3.2);
          sepForce.multiplyScalar(5.8);
          f.acc.add(cohForce).add(aliForce).add(sepForce);
        }

        // Subtle organic wander
        f.acc.x += Math.sin(now * 1.4 + f.wanderSeed) * 1.1;
        f.acc.y += Math.cos(now * 1.9 + f.wanderSeed) * 0.7;

        // Seek nearest food pellet when not startled
        if (foodPellets.length > 0 && f.scaredTimer < 0.2) {
          let bestDistSq = 110;
          let targetPelletIdx = -1;
          for (let pIdx = 0; pIdx < foodPellets.length; pIdx++) {
            const dSq = f.pos.distanceToSquared(foodPellets[pIdx].mesh.position);
            if (dSq < bestDistSq) {
              bestDistSq = dSq;
              targetPelletIdx = pIdx;
            }
          }
          if (targetPelletIdx !== -1) {
            const pelletPos = foodPellets[targetPelletIdx].mesh.position;
            if (bestDistSq < 0.55) {
              // Eat pellet
              scene.remove(foodPellets[targetPelletIdx].mesh);
              foodPellets.splice(targetPelletIdx, 1);
            } else {
              tempVec.subVectors(pelletPos, f.pos).normalize().multiplyScalar(11.0);
              f.acc.add(tempVec);
            }
          }
        }

        // --- Mouse / Probe Evasion Force ---
        const distToThreat = f.pos.distanceTo(repellerPos);
        if (distToThreat < activeScareRadius) {
          const urgency = 1.0 - distToThreat / activeScareRadius;
          tempVec.subVectors(f.pos, repellerPos).normalize();
          // Add slight vertical scatter so fish dart dynamically in 3D
          tempVec.y += (f.pos.y >= repellerPos.y ? 0.25 : -0.25);
          tempVec.normalize().multiplyScalar((48.0 + shockwaveBoost * 35.0) * urgency * urgency);
          f.acc.add(tempVec);
          f.scaredTimer = Math.max(f.scaredTimer, Math.min(1.0, urgency * 1.5));
        } else {
          f.scaredTimer = Math.max(0.0, f.scaredTimer - dt * 2.2);
        }

        if (f.scaredTimer > 0.15) scaredFishCount++;

        // Soft Tank Boundary Containment
        const turnAccel = 22.0;
        if (f.pos.x > BOUNDS.x) f.acc.x -= turnAccel * ((f.pos.x - BOUNDS.x) + 0.5);
        if (f.pos.x < -BOUNDS.x) f.acc.x += turnAccel * ((-BOUNDS.x - f.pos.x) + 0.5);
        if (f.pos.y > BOUNDS.yMax) f.acc.y -= turnAccel * ((f.pos.y - BOUNDS.yMax) + 0.5);
        if (f.pos.y < BOUNDS.yMin) f.acc.y += turnAccel * ((BOUNDS.yMin - f.pos.y) + 0.5);
        if (f.pos.z > BOUNDS.z) f.acc.z -= turnAccel * ((f.pos.z - BOUNDS.z) + 0.5);
        if (f.pos.z < -BOUNDS.z) f.acc.z += turnAccel * ((-BOUNDS.z - f.pos.z) + 0.5);

        // Integrate Velocity & Clamp Speed
        f.vel.addScaledVector(f.acc, dt);
        const maxSpd = f.baseSpeed * (1.0 + f.scaredTimer * 2.1);
        const minSpd = f.baseSpeed * 0.55;
        const curSpd = f.vel.length();
        if (curSpd > maxSpd) {
          f.vel.multiplyScalar(maxSpd / curSpd);
        } else if (curSpd < minSpd) {
          f.vel.multiplyScalar(minSpd / (curSpd || 1));
        }

        // Keep vertical pitch reasonable unless escaping strongly
        f.vel.y = THREE.MathUtils.clamp(f.vel.y, -maxSpd * 0.65, maxSpd * 0.65);

        f.pos.addScaledVector(f.vel, dt);

        // Hard safety clamp inside glass walls
        f.pos.x = THREE.MathUtils.clamp(f.pos.x, -BOUNDS.x - 0.7, BOUNDS.x + 0.7);
        f.pos.y = THREE.MathUtils.clamp(f.pos.y, BOUNDS.yMin - 0.4, BOUNDS.yMax + 0.5);
        f.pos.z = THREE.MathUtils.clamp(f.pos.z, -BOUNDS.z - 0.7, BOUNDS.z + 0.7);

        f.mesh.position.copy(f.pos);

        // Smoothly orient fish along velocity vector (+Z is forward)
        tempVec.addVectors(f.pos, f.vel);
        lookMatrix.lookAt(tempVec, f.pos, upVec);
        targetQuat.setFromRotationMatrix(lookMatrix);
        f.mesh.quaternion.slerp(targetQuat, Math.min(1.0, dt * 12.0));

        // Update tail wiggle shader uniforms
        const speedRatio = curSpd / f.baseSpeed;
        f.uniforms.uTime.value += dt * (1.0 + speedRatio * 0.85);
        f.uniforms.uSpeed.value = THREE.MathUtils.clamp(speedRatio, 0.8, 2.8);
        f.uniforms.uScared.value = f.scaredTimer;
      }

      updateHUDStats(scaredFishCount);
      controls.update();
      renderer.render(scene, camera);
    }

    animate();
  </script>
</body>
</html>