Models 44

Claude Opus 5.5

by Anthropicreasoning defaultClean
Open in new window

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

How it went

Total time
1 min 53 s
Cost
$0.271
Output tokens
13,417
Spent thinking
2,645 tokens
Speed
122.7 tok/s
Console errors
0
First token after
3.4 s
First visible text after
33.6 s
Prompt tokens
649
Code size
18.8 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-opus-5.5
Reasoning sent
model default
Max output
128,000 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 · 379 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>3D Aquarium – Shy Fish</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#021625;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
  canvas{display:block}
  #hud{position:fixed;left:50%;top:14px;transform:translateX(-50%);color:#cfefff;background:rgba(0,30,50,.45);
    border:1px solid rgba(140,220,255,.25);padding:8px 16px;border-radius:20px;font-size:14px;letter-spacing:.3px;
    backdrop-filter:blur(4px);pointer-events:none;text-align:center;white-space:nowrap;max-width:92vw;overflow:hidden;text-overflow:ellipsis}
  #mode{position:fixed;right:14px;bottom:12px;color:#8fd3f0;font-size:12px;opacity:.8;pointer-events:none}
  @media (max-width:500px){#hud{font-size:12px;padding:6px 12px}}
</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="hud">🐠 Move your mouse near the fish to scare them · drag to orbit</div>
<div id="mode">Demo: virtual cursor</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x03243a);
scene.fog = new THREE.FogExp2(0x05304a, 0.028);

const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0, 1.5, 19);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false; controls.enableDamping = true;
controls.minDistance = 8; controls.maxDistance = 32;
controls.minPolarAngle = 0.5; controls.maxPolarAngle = 1.75;
controls.target.set(0,0,0);

function onResize(){
  const a = innerWidth/innerHeight;
  camera.aspect = a;
  camera.fov = a < 1 ? 50 + (1-a)*45 : 50;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
}
addEventListener('resize', onResize); onResize();

// Lights
scene.add(new THREE.HemisphereLight(0x9fe8ff, 0x1a3a2a, 1.2));
const sun = new THREE.DirectionalLight(0xdff6ff, 2.2); sun.position.set(3,12,4); scene.add(sun);
const blueFill = new THREE.PointLight(0x2a8cff, 30, 30); blueFill.position.set(-8,-2,6); scene.add(blueFill);

const B = {x:10, y:5, z:5}; // tank half-size
const FLOOR = -B.y;

// Tank glass
const glass = new THREE.Mesh(new THREE.BoxGeometry(B.x*2+0.4, B.y*2+0.4, B.z*2+0.4),
  new THREE.MeshPhysicalMaterial({color:0x88ccee, transparent:true, opacity:0.08, roughness:0.05, side:THREE.BackSide, depthWrite:false}));
scene.add(glass);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(glass.geometry),
  new THREE.LineBasicMaterial({color:0x9fe4ff, transparent:true, opacity:0.35}));
scene.add(edges);

// Water surface
const surfMat = new THREE.ShaderMaterial({
  transparent:true, depthWrite:false, side:THREE.DoubleSide,
  uniforms:{t:{value:0}},
  vertexShader:`uniform float t; varying vec2 vUv; void main(){vUv=uv; vec3 p=position; p.z+=sin(p.x*1.2+t*1.5)*0.08+cos(p.y*1.5+t)*0.08; gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);}`,
  fragmentShader:`uniform float t; varying vec2 vUv; void main(){float w=sin(vUv.x*40.+t*2.)*sin(vUv.y*25.-t*1.5); gl_FragColor=vec4(vec3(0.5,0.85,1.)*(0.6+0.4*w),0.18);}`
});
const surf = new THREE.Mesh(new THREE.PlaneGeometry(B.x*2, B.z*2, 40, 20), surfMat);
surf.rotation.x = -Math.PI/2; surf.position.y = B.y+0.1; scene.add(surf);

// Sand floor with caustics
const floorGeo = new THREE.PlaneGeometry(B.x*2+0.3, B.z*2+0.3, 120, 60);
floorGeo.rotateX(-Math.PI/2);
{
  const p = floorGeo.attributes.position;
  for(let i=0;i<p.count;i++){
    const x=p.getX(i), z=p.getZ(i);
    p.setY(i, Math.sin(x*0.6)*0.18 + Math.cos(z*0.9+x*0.3)*0.15 + Math.sin(x*2.1+z*1.7)*0.04);
  }
  floorGeo.computeVertexNormals();
}
const floorMat = new THREE.ShaderMaterial({
  uniforms:{t:{value:0}},
  vertexShader:`varying vec3 vW; varying vec3 vN; void main(){vec4 w=modelMatrix*vec4(position,1.); vW=w.xyz; vN=normal; gl_Position=projectionMatrix*viewMatrix*w;}`,
  fragmentShader:`uniform float t; varying vec3 vW; varying vec3 vN;
    float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
    float caustic(vec2 uv,float time){
      vec2 p=mod(uv*6.28318,6.28318)-250.; vec2 i=p; float c=1.; float inten=.005;
      for(int n=0;n<5;n++){float tt=time*(1.-(3.5/float(n+1)));
        i=p+vec2(cos(tt-i.x)+sin(tt+i.y),sin(tt-i.y)+cos(tt+i.x));
        c+=1./length(vec2(p.x/(sin(i.x+tt)/inten),p.y/(cos(i.y+tt)/inten)));}
      c/=5.; c=1.17-pow(c,1.4); return pow(abs(c),8.);}
    void main(){
      float g=hash(floor(vW.xz*40.))*0.12;
      vec3 sand=vec3(0.78,0.68,0.48)*(0.85+g);
      float l=0.45+0.55*max(dot(normalize(vN),normalize(vec3(0.3,1.,0.4))),0.);
      float c=caustic(vW.xz*0.12,t*0.5);
      vec3 col=sand*l*vec3(0.55,0.75,0.85)+vec3(0.6,0.9,1.)*min(c,1.)*0.55;
      float d=length(vW-cameraPosition);
      col=mix(col,vec3(0.02,0.19,0.29),1.-exp(-d*d*0.0008));
      gl_FragColor=vec4(col,1.);
    }`
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.position.y = FLOOR; scene.add(floor);

// Rocks
function rock(x,z,s,color){
  const g = new THREE.DodecahedronGeometry(1, 2);
  const p = g.attributes.position, v=new THREE.Vector3();
  const seed=Math.random()*10;
  for(let i=0;i<p.count;i++){
    v.fromBufferAttribute(p,i);
    const n = 1+0.18*Math.sin(v.x*3+seed)*Math.cos(v.y*4-seed)+0.1*Math.sin(v.z*5+seed*2);
    v.multiplyScalar(n); p.setXYZ(i,v.x,v.y*0.7,v.z);
  }
  g.computeVertexNormals();
  const m = new THREE.Mesh(g, new THREE.MeshStandardMaterial({color, roughness:0.95, flatShading:true}));
  m.scale.setScalar(s); m.position.set(x, FLOOR+s*0.3, z); m.rotation.y=Math.random()*6;
  scene.add(m);
}
rock(-6,-2.5,1.8,0x6b6f78); rock(-4.3,-3,1.1,0x7d746a); rock(5.5,-2,2.2,0x5c6470);
rock(7.5,1.5,1.0,0x806e5e); rock(2,-3.5,0.9,0x6d7a80); rock(-8,2,0.8,0x77706a);

// Seaweed
const weedMat = new THREE.ShaderMaterial({
  side:THREE.DoubleSide, uniforms:{t:{value:0}},
  vertexShader:`uniform float t; attribute float phase; varying float vH; varying float vP;
    void main(){ vec3 p=position; float h=p.y; vH=uv.y; vP=phase;
      vec4 w=modelMatrix*vec4(p,1.);
      float k=h*h*0.06;
      w.x+=sin(t*1.2+phase+h*0.6)*k; w.z+=cos(t*0.9+phase*1.3+h*0.5)*k*0.7;
      gl_Position=projectionMatrix*viewMatrix*w;}`,
  fragmentShader:`varying float vH; varying float vP;
    void main(){ vec3 a=vec3(0.05,0.25,0.1), b=vec3(0.3,0.75,0.3)+0.1*sin(vP)*vec3(0.3,0.1,-0.1);
      gl_FragColor=vec4(mix(a,b,vH)*1.1,1.);}`
});
function weed(x,z){
  const blades = 3+Math.floor(Math.random()*4);
  for(let b=0;b<blades;b++){
    const h = 3+Math.random()*5;
    const g = new THREE.PlaneGeometry(0.35, h, 1, 14);
    g.translate(0,h/2,0);
    const p=g.attributes.position;
    for(let i=0;i<p.count;i++){ const y=p.getY(i); p.setX(i, p.getX(i)*(1-y/h*0.7)); }
    const ph = new Float32Array(p.count).fill(Math.random()*6.28);
    g.setAttribute('phase', new THREE.BufferAttribute(ph,1));
    const m = new THREE.Mesh(g, weedMat);
    m.position.set(x+(Math.random()-.5)*0.8, FLOOR, z+(Math.random()-.5)*0.8);
    m.rotation.y = Math.random()*Math.PI;
    scene.add(m);
  }
}
[[-7.5,-3.5],[-2.5,-3.8],[0.5,-4],[3.5,-3.6],[8.5,-3.8],[-8.8,0],[8.8,3],[-5,3.5],[6.5,3.8],[1,3.6]].forEach(a=>weed(a[0],a[1]));

// Light shafts
const shaftMat = new THREE.ShaderMaterial({
  transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, side:THREE.DoubleSide,
  uniforms:{t:{value:0}},
  vertexShader:`varying vec2 vUv; void main(){vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
  fragmentShader:`uniform float t; varying vec2 vUv; void main(){ float a=vUv.y*(1.-abs(vUv.x-.5)*2.); a*=0.06*(0.7+0.3*sin(t*0.8+vUv.x*6.)); gl_FragColor=vec4(0.6,0.9,1.,a);}`
});
const shafts=[];
for(let i=0;i<6;i++){
  const s = new THREE.Mesh(new THREE.PlaneGeometry(2.2+Math.random()*1.5, 11), shaftMat);
  s.position.set(-8+i*3.2, 0, -1+Math.random()*2); s.rotation.z = 0.25; s.userData.ph=Math.random()*6;
  scene.add(s); shafts.push(s);
}

// Bubbles
const BUB = 220;
const bubGeo = new THREE.BufferGeometry();
const bubPos = new Float32Array(BUB*3), bubSpd = new Float32Array(BUB);
const emitters = [[-6,-2.5],[5.5,-2],[0,-4]];
function resetBubble(i, anyY){
  const e = emitters[i%emitters.length];
  bubPos[i*3] = e[0]+(Math.random()-.5)*0.5;
  bubPos[i*3+1] = anyY ? FLOOR+Math.random()*B.y*2 : FLOOR+0.5;
  bubPos[i*3+2] = e[1]+(Math.random()-.5)*0.5;
  bubSpd[i] = 1+Math.random()*1.5;
}
for(let i=0;i<BUB;i++) resetBubble(i,true);
bubGeo.setAttribute('position', new THREE.BufferAttribute(bubPos,3));
const bubTex = (()=>{const c=document.createElement('canvas');c.width=c.height=64;const x=c.getContext('2d');
  const g=x.createRadialGradient(32,32,8,32,32,30);g.addColorStop(0,'rgba(255,255,255,0.05)');g.addColorStop(0.75,'rgba(200,240,255,0.5)');g.addColorStop(0.9,'rgba(255,255,255,0.9)');g.addColorStop(1,'rgba(255,255,255,0)');
  x.fillStyle=g;x.fillRect(0,0,64,64);return new THREE.CanvasTexture(c);})();
const bubbles = new THREE.Points(bubGeo, new THREE.PointsMaterial({size:0.18, map:bubTex, transparent:true, depthWrite:false}));
scene.add(bubbles);

// Floating particles
const PART=400, partPos=new Float32Array(PART*3);
for(let i=0;i<PART;i++){partPos[i*3]=(Math.random()-.5)*B.x*2;partPos[i*3+1]=(Math.random()-.5)*B.y*2;partPos[i*3+2]=(Math.random()-.5)*B.z*2;}
const partGeo=new THREE.BufferGeometry(); partGeo.setAttribute('position',new THREE.BufferAttribute(partPos,3));
const particles=new THREE.Points(partGeo,new THREE.PointsMaterial({size:0.04,color:0xbfeaff,transparent:true,opacity:0.5,depthWrite:false}));
scene.add(particles);

// Fish
function stripeTex(base, bands, bandColor, tailColor){
  const c=document.createElement('canvas'); c.width=64; c.height=128; const x=c.getContext('2d');
  x.fillStyle=base; x.fillRect(0,0,64,128);
  x.fillStyle=bandColor;
  bands.forEach(b=>x.fillRect(0,b[0]*128,64,b[1]*128));
  if(tailColor){x.fillStyle=tailColor;x.fillRect(0,110,64,18);}
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
const species = [
  {name:'clown', count:14, size:0.55, color:'#ff7a1a', fin:0xff8a30, tex:stripeTex('#ff7a1a',[[0.25,0.08],[0.5,0.1],[0.78,0.06]],'#ffffff'), speed:2.2},
  {name:'tang', count:10, size:0.75, color:'#2c6bff', fin:0xffd02a, tex:stripeTex('#2a60f0',[[0.3,0.35]],'#0a1a55','#ffd02a'), speed:2.0},
  {name:'neon', count:30, size:0.32, color:'#22d3ff', fin:0xff3355, tex:stripeTex('#30e0ff',[[0.0,0.45]],'#ff2a4a'), speed:3.0},
  {name:'yellow', count:12, size:0.6, color:'#ffe02a', fin:0xffe02a, tex:stripeTex('#ffe02a',[],'#000'), speed:2.3},
  {name:'angel', count:6, size:0.95, color:'#d8d8e8', fin:0xeeeeff, tex:stripeTex('#e8e8f0',[[0.2,0.06],[0.45,0.07],[0.7,0.05]],'#222233'), speed:1.6},
];
const bodyBase = new THREE.SphereGeometry(1, 20, 14); bodyBase.rotateX(-Math.PI/2);
const tailShape = new THREE.Shape(); tailShape.moveTo(0,0); tailShape.lineTo(-0.9,0.55); tailShape.quadraticCurveTo(-0.7,0,-0.9,-0.55); tailShape.lineTo(0,0);
const tailGeo = new THREE.ShapeGeometry(tailShape); tailGeo.rotateY(Math.PI/2); // lies in YZ, extends to -z
const finShape = new THREE.Shape(); finShape.moveTo(0.4,0); finShape.quadraticCurveTo(0,0.6,-0.6,0.15); finShape.lineTo(-0.5,0); finShape.lineTo(0.4,0);
const finGeo = new THREE.ShapeGeometry(finShape); finGeo.rotateY(Math.PI/2);
const eyeGeo = new THREE.SphereGeometry(0.09, 8, 6);
const eyeMat = new THREE.MeshStandardMaterial({color:0x050505, roughness:0.2});
const eyeWhiteMat = new THREE.MeshStandardMaterial({color:0xffffff, roughness:0.3});

const fish=[];
species.forEach((sp,si)=>{
  const bodyMat = new THREE.MeshStandardMaterial({map:sp.tex, roughness:0.4, metalness:0.15, emissive:new THREE.Color(sp.color), emissiveIntensity:0.12});
  const finMat = new THREE.MeshStandardMaterial({color:sp.fin, side:THREE.DoubleSide, transparent:true, opacity:0.85, roughness:0.5, emissive:sp.fin, emissiveIntensity:0.1});
  const tall = sp.name==='angel' ? 1.15 : sp.name==='tang'||sp.name==='yellow' ? 0.75 : 0.5;
  for(let i=0;i<sp.count;i++){
    const g = new THREE.Group();
    const body = new THREE.Mesh(bodyBase, bodyMat); body.scale.set(0.3, tall, 1); g.add(body);
    const tailPivot = new THREE.Group(); tailPivot.position.z = -0.85; g.add(tailPivot);
    const tail = new THREE.Mesh(tailGeo, finMat); tail.scale.set(1, tall*1.3, 0.8); tailPivot.add(tail);
    const dorsal = new THREE.Mesh(finGeo, finMat); dorsal.position.set(0, tall*0.85, -0.1); dorsal.scale.set(1, tall*(sp.name==='angel'?2.2:1), 1); g.add(dorsal);
    if(sp.name==='angel'){ const ven=dorsal.clone(); ven.rotation.z=Math.PI; ven.position.y=-tall*0.85; g.add(ven); }
    for(const s of [-1,1]){
      const w=new THREE.Mesh(eyeGeo, eyeWhiteMat); w.position.set(s*0.24, tall*0.2, 0.6); w.scale.setScalar(1.1); g.add(w);
      const e=new THREE.Mesh(eyeGeo, eyeMat); e.position.set(s*0.29, tall*0.2, 0.63); e.scale.setScalar(0.65); g.add(e);
    }
    const sz = sp.size*(0.85+Math.random()*0.3);
    g.scale.setScalar(sz);
    const pos = new THREE.Vector3((Math.random()-.5)*B.x*1.6, (Math.random()-.5)*B.y*1.4, (Math.random()-.5)*B.z*1.4);
    const vel = new THREE.Vector3(Math.random()-.5, (Math.random()-.5)*0.3, Math.random()-.5).setLength(sp.speed);
    g.position.copy(pos);
    scene.add(g);
    fish.push({g, tailPivot, pos, vel, sp:si, base:sp.speed, size:sz, phase:Math.random()*10, fear:0, wander:new THREE.Vector3()});
  }
});

// Cursor / predator indicator
const cursorMat = new THREE.MeshBasicMaterial({color:0xaef0ff, transparent:true, opacity:0.35, depthWrite:false, blending:THREE.AdditiveBlending});
const cursorBall = new THREE.Mesh(new THREE.SphereGeometry(0.25, 16, 12), cursorMat);
const ring = new THREE.Mesh(new THREE.RingGeometry(0.5, 0.6, 40), cursorMat.clone());
ring.material.side = THREE.DoubleSide;
scene.add(cursorBall, ring);

const ndc = new THREE.Vector2(); let mouseActive=false, lastMove=-10;
const raycaster = new THREE.Raycaster();
const modeEl = document.getElementById('mode');
renderer.domElement.addEventListener('pointermove', e=>{
  ndc.set(e.clientX/innerWidth*2-1, -(e.clientY/innerHeight)*2+1);
  mouseActive=true; lastMove=performance.now()/1000;
});
renderer.domElement.addEventListener('pointerleave', ()=>{mouseActive=false;});

// Boids update
const tmp=new THREE.Vector3(), acc=new THREE.Vector3(), coh=new THREE.Vector3(), ali=new THREE.Vector3(), sep=new THREE.Vector3();
const dummy=new THREE.Object3D(), closest=new THREE.Vector3(), diff=new THREE.Vector3();
const FEAR_R = 3.2;
const planeN=new THREE.Vector3(), plane=new THREE.Plane(), hit=new THREE.Vector3();

function updateFish(dt, t, ray, active){
  for(const f of fish){
    acc.set(0,0,0); coh.set(0,0,0); ali.set(0,0,0); sep.set(0,0,0);
    let n=0;
    for(const o of fish){
      if(o===f) continue;
      const d = f.pos.distanceTo(o.pos);
      if(d < 0.9*(f.size+o.size)){ tmp.subVectors(f.pos,o.pos).divideScalar(d*d+0.01); sep.add(tmp); }
      if(o.sp===f.sp && d<3){ coh.add(o.pos); ali.add(o.vel); n++; }
    }
    if(n){ coh.divideScalar(n).sub(f.pos).multiplyScalar(0.6); ali.divideScalar(n).sub(f.vel).multiplyScalar(0.8); acc.add(coh).add(ali); }
    acc.addScaledVector(sep, 2.5);
    // wander
    f.wander.set(Math.sin(t*0.7+f.phase*3), Math.sin(t*0.5+f.phase)*0.4, Math.cos(t*0.6+f.phase*2)).multiplyScalar(0.8);
    acc.add(f.wander);
    // bounds
    const m=1.8, k=6;
    const bx=B.x-1, by=B.y-0.8, bz=B.z-0.8;
    if(f.pos.x> bx-m) acc.x -= k*(f.pos.x-(bx-m)); if(f.pos.x< -bx+m) acc.x += k*((-bx+m)-f.pos.x);
    if(f.pos.y> by-m) acc.y -= k*(f.pos.y-(by-m)); if(f.pos.y< FLOOR+1.2+m*0.5) acc.y += k*((FLOOR+1.2+m*0.5)-f.pos.y);
    if(f.pos.z> bz-m) acc.z -= k*(f.pos.z-(bz-m)); if(f.pos.z< -bz+m) acc.z += k*((-bz+m)-f.pos.z);
    acc.y -= f.vel.y*0.8; // keep mostly horizontal
    // flee from cursor ray
    if(active){
      tmp.subVectors(f.pos, ray.origin);
      const tt = tmp.dot(ray.direction);
      closest.copy(ray.origin).addScaledVector(ray.direction, tt);
      diff.subVectors(f.pos, closest);
      const d = diff.length();
      if(d < FEAR_R && tt>0){
        const s = (1 - d/FEAR_R);
        diff.normalize();
        acc.addScaledVector(diff, 40*s*s + 8*s);
        f.fear = Math.min(1, f.fear + s*dt*8);
      }
    }
    f.fear = Math.max(0, f.fear - dt*0.6);
    f.vel.addScaledVector(acc, dt);
    const maxS = f.base*(1+f.fear*2.8), minS = f.base*0.45;
    const sp = f.vel.length();
    if(sp>maxS) f.vel.multiplyScalar(maxS/sp); else if(sp<minS) f.vel.multiplyScalar(minS/(sp||1));
    f.pos.addScaledVector(f.vel, dt);
    f.pos.x=THREE.MathUtils.clamp(f.pos.x,-B.x+0.4,B.x-0.4);
    f.pos.y=THREE.MathUtils.clamp(f.pos.y,FLOOR+0.6,B.y-0.4);
    f.pos.z=THREE.MathUtils.clamp(f.pos.z,-B.z+0.4,B.z-0.4);
    f.g.position.copy(f.pos);
    dummy.position.copy(f.pos); dummy.lookAt(tmp.copy(f.pos).add(f.vel));
    f.g.quaternion.slerp(dummy.quaternion, Math.min(1, dt*(5+f.fear*10)));
    const spd=f.vel.length()/f.base;
    f.phase += dt*(6+spd*7);
    f.tailPivot.rotation.y = Math.sin(f.phase)*(0.35+f.fear*0.3);
    f.g.children[0].rotation.y = Math.sin(f.phase-1)*0.05;
  }
}

const clock = new THREE.Clock();
const demoNdc = new THREE.Vector2();
function animate(){
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  const now = performance.now()/1000;
  const userCtl = mouseActive && now-lastMove < 4;
  let useNdc;
  if(userCtl){ useNdc = ndc; modeEl.style.opacity=0; }
  else{
    demoNdc.set(Math.sin(t*0.45)*0.6 + Math.sin(t*1.3)*0.1, Math.sin(t*0.7+1)*0.35);
    useNdc = demoNdc; modeEl.style.opacity=0.8;
  }
  raycaster.setFromCamera(useNdc, camera);
  const ray = raycaster.ray;
  updateFish(dt, t, ray, true);

  camera.getWorldDirection(planeN);
  plane.setFromNormalAndCoplanarPoint(planeN, controls.target);
  if(ray.intersectPlane(plane, hit)){
    cursorBall.position.copy(hit); ring.position.copy(hit);
    ring.quaternion.copy(camera.quaternion);
    const pulse = 1+Math.sin(t*5)*0.15;
    ring.scale.setScalar(pulse*2.2);
    cursorBall.visible = ring.visible = true;
  }

  // bubbles
  for(let i=0;i<BUB;i++){
    bubPos[i*3+1] += bubSpd[i]*dt;
    bubPos[i*3] += Math.sin(t*3+i)*dt*0.15;
    if(bubPos[i*3+1] > B.y) resetBubble(i,false);
  }
  bubGeo.attributes.position.needsUpdate = true;
  particles.rotation.y = Math.sin(t*0.05)*0.1;

  floorMat.uniforms.t.value = t; weedMat.uniforms.t.value = t; surfMat.uniforms.t.value = t; shaftMat.uniforms.t.value = t;
  shafts.forEach(s=>s.rotation.z = 0.25+Math.sin(t*0.3+s.userData.ph)*0.05);

  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>
Reasoning trace (2,645 tokens)
I'm putting together a full Three.js aquarium that includes boid-style fish which scatter when the mouse ray passes near them, aiming for something compact that still looks good. For the scene I'm planning a glass tank, a sand floor with rocks, animated seaweed, rising bubbles, a shimmering caustic light effect on the floor, and blue fog. I'm also starting to think about how to build the fish procedurally.

Still working out the fish: I'm planning to build each one from simple shapes like a body, tail, and fins, and to wag the tail by rotating it. Mouse interaction will use the distance from each fish to the cursor ray, with the fish steering away when it gets close.

Still working through the fish and the scene's materials: I'm planning the fish from a body, a tail pivot that wags, eyes, and a dorsal fin, and using a custom shader for the caustic sand floor and a vertex-shader sway for the seaweed.

I'm moving on to the tank's bounds, with the glass box spanning x ±10, y ±5, z ±5, and the camera set back at z 18. I'm also working out the flee logic: for each fish I find the closest point on the mouse ray, and if it falls within radius 3, I push the fish away along that offset, with a speed boost. The cursor marker will sit where the ray hits the plane facing the camera, and an idle phantom cursor will take over after three seconds without mouse movement.

Now I'm writing the update loop for the fish: each one checks nearby fish to align and cohere with its own species, steers away from anything too close, and nudges back inside the tank bounds. Flee force gets added on top, and the speed clamp allows a higher top speed when a fish is scared. Orientation is slerped toward the velocity direction, and the tail wag rate scales with speed. I'm also building the glass tank out of edge lines and transparent back faces.

Writing the caustic floor shader now, using an iterative pattern that layers sine waves over the UVs and time.

Still writing the caustic floor shader, with the mod-based UV wrap and a loop that layers the sine-based pattern. Next I'm adding dunes to the floor plane and making it a little larger than the tank bottom. Rocks come after that.

Still writing the rock and seaweed materials, along with the lighting and the resize handling for narrow screens.

Still writing the fish textures: I'm painting a small canvas stripe pattern per species, with white bands for the clownfish, and rotating the body geometry so its poles run along the length of the fish.

Still writing the fish: I'll turn the body into a stripe-textured mesh by rotating its geometry so the poles run along the length. Next up is the auto demo, where a virtual cursor traces a Lissajous path through the tank whenever the mouse goes idle.