Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
43.8 s
Cost
$0.0047
Output tokens
7,800
Spent thinking
1,275 tokens
Speed
180.1 tok/s
Console errors
0
First token after
500 ms
First visible text after
10.9 s
Prompt tokens
613
Code size
18.6 KB
Finish reason
stop
Served by
Fireworks
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"low"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 503 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;padding:0;height:100%;background:#03151f;overflow:hidden;
    font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#dff3ff;}
  canvas{display:block}
  #hud{position:fixed;left:0;right:0;top:0;padding:18px 22px;pointer-events:none;
    text-shadow:0 2px 10px rgba(0,0,0,.6)}
  #hud h1{margin:0;font-size:20px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;color:#a8e8ff}
  #hud p{margin:6px 0 0;font-size:13px;color:#8fc3d9;letter-spacing:.04em}
  #tag{position:fixed;right:18px;bottom:16px;font-size:11px;letter-spacing:.18em;
    color:#4e8ba5;text-transform:uppercase;pointer-events:none}
  #start{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;
    background:radial-gradient(ellipse at center,rgba(2,22,34,.15),rgba(2,16,26,.85));
    transition:opacity .8s ease;cursor:pointer;z-index:10}
  #start .box{text-align:center;padding:26px 40px;border:1px solid rgba(120,210,255,.28);
    border-radius:14px;background:rgba(4,32,48,.55);backdrop-filter:blur(6px);
    box-shadow:0 20px 60px rgba(0,0,0,.45)}
  #start h2{margin:0 0 8px;font-size:22px;letter-spacing:.14em;text-transform:uppercase;color:#bfeaff}
  #start span{font-size:13px;color:#8fc3d9;letter-spacing:.08em}
  #start .pulse{animation:p 1.6s ease-in-out infinite}
  @keyframes p{0%,100%{opacity:.45}50%{opacity:1}}
</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">
  <h1>Reef Tank</h1>
  <p>Move your mouse through the water — the fish scatter away from your pointer.</p>
</div>
<div id="tag">three.js · boids + flee</div>
<div id="start"><div class="box">
  <h2>Click or press a key to start</h2>
  <span class="pulse">the tank runs on its own — move the mouse to tease the fish</span>
</div></div>

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

/* ---------- renderer / scene ---------- */
const renderer = new THREE.WebGLRenderer({antialias:true, alpha:false});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = false;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x072a3d, 0.028);

// background gradient
{
  const c = document.createElement('canvas'); c.width=8; c.height=256;
  const g = c.getContext('2d').createLinearGradient(0,0,0,256);
  g.addColorStop(0,'#0d5b7a'); g.addColorStop(.35,'#073c56'); g.addColorStop(1,'#02121d');
  const ctx=c.getContext('2d'); ctx.fillStyle=g; ctx.fillRect(0,0,8,256);
  const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
  scene.background = t;
}

const camera = new THREE.PerspectiveCamera(48, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0, 1.5, 24);
camera.lookAt(0, 0.5, 0);

/* ---------- lighting ---------- */
scene.add(new THREE.AmbientLight(0x2e6f96, 1.1));
const hemi = new THREE.HemisphereLight(0x9fe4ff, 0x0a2230, 0.85); scene.add(hemi);
const sun = new THREE.DirectionalLight(0xcfeeff, 1.5);
sun.position.set(-6, 18, 8); scene.add(sun);
const spot = new THREE.PointLight(0x66d9ff, 55, 60, 2);
spot.position.set(7, 6, 6); scene.add(spot);
const spot2 = new THREE.PointLight(0x39a0ff, 40, 60, 2);
spot2.position.set(-9, -2, -4); scene.add(spot2);

/* ---------- tank bounds ---------- */
const TANK = { x:12, yTop:6.0, yBottom:-5.2, z:7.2 };

/* ---------- sand floor ---------- */
function sandTexture(){
  const s=512, c=document.createElement('canvas'); c.width=c.height=s;
  const g=c.getContext('2d');
  g.fillStyle='#c8b189'; g.fillRect(0,0,s,s);
  for(let i=0;i<9000;i++){
    const x=Math.random()*s, y=Math.random()*s, r=Math.random()*2.4+0.3;
    const v=Math.random();
    g.fillStyle = v<.5? 'rgba(255,245,220,'+(0.05+Math.random()*0.2)+')' : 'rgba(120,100,70,'+(0.05+Math.random()*0.18)+')';
    g.beginPath(); g.arc(x,y,r,0,7); g.fill();
  }
  // ripples
  g.globalAlpha=0.12;
  for(let i=0;i<40;i++){
    g.strokeStyle='#7a6a4a'; g.lineWidth=1+Math.random()*2;
    g.beginPath();
    const y=Math.random()*s;
    g.moveTo(0,y);
    for(let x=0;x<=s;x+=16) g.lineTo(x, y+Math.sin(x*0.05+i)*6);
    g.stroke();
  }
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
  t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(3,3);
  return t;
}
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(TANK.x*2, TANK.z*2, 60, 40),
  new THREE.MeshStandardMaterial({map:sandTexture(), roughness:0.95, metalness:0})
);
floor.rotation.x = -Math.PI/2;
floor.position.y = TANK.yBottom;
// gentle dunes
{
  const pos = floor.geometry.attributes.position;
  for(let i=0;i<pos.count;i++){
    const x=pos.getX(i), y=pos.getY(i);
    pos.setZ(i, Math.sin(x*0.35)*0.25 + Math.cos(y*0.5)*0.18 + Math.sin(x*0.12+y*0.2)*0.4);
  }
  floor.geometry.computeVertexNormals();
}
scene.add(floor);

/* ---------- rocks ---------- */
const rockMat = new THREE.MeshStandardMaterial({color:0x6b6a63, roughness:0.9});
function rock(x,z,s){
  const g = new THREE.IcosahedronGeometry(s, 1);
  const p=g.attributes.position;
  for(let i=0;i<p.count;i++){
    p.setXYZ(i, p.getX(i)*(0.8+Math.random()*0.5), p.getY(i)*(0.55+Math.random()*0.5), p.getZ(i)*(0.8+Math.random()*0.5));
  }
  g.computeVertexNormals();
  const m=new THREE.Mesh(g, rockMat);
  m.position.set(x, TANK.yBottom + s*0.28, z);
  m.rotation.set(Math.random(),Math.random()*6,Math.random());
  scene.add(m);
  return m;
}
rock(-7.5,-4,1.7); rock(-6.2,-2.6,1.0); rock(8.2,-4.6,1.9); rock(6.6,-3.2,1.1);
rock(1.5,5.0,1.3); rock(-3.0,5.2,0.9);

/* ---------- plants ---------- */
const plants=[];
function plant(x,z,height,color){
  const grp=new THREE.Group();
  const mat=new THREE.MeshStandardMaterial({color, roughness:0.6, metalness:0, side:THREE.DoubleSide});
  const n=6+Math.floor(Math.random()*5);
  for(let i=0;i<n;i++){
    const h=height*(0.55+Math.random()*0.6);
    const blade=new THREE.Mesh(new THREE.PlaneGeometry(0.32,h,1,8), mat);
    const p=blade.geometry.attributes.position;
    for(let k=0;k<p.count;k++){ const t=p.getY(k)/h+0.5; p.setX(k, p.getX(k)*(1-t*0.8)); }
    blade.geometry.computeVertexNormals();
    const holder=new THREE.Group();
    holder.add(blade);
    holder.position.set((Math.random()-0.5)*1.4, h/2, (Math.random()-0.5)*1.4);
    holder.rotation.y=Math.random()*Math.PI;
    blade.position.y=h/2;
    holder.rotation.z=(Math.random()-0.5)*0.2;
    grp.add(holder);
    plants.push({holder, phase:Math.random()*6.28, amp:0.09+Math.random()*0.08});
  }
  grp.position.set(x, TANK.yBottom, z);
  scene.add(grp);
}
plant(-9,-1.5,3.4,0x2f7a4a); plant(-8.4,-0.6,2.6,0x3c9155);
plant(9.2,-1.0,3.8,0x2b6f43); plant(10.0,-2.2,2.8,0x39884f);
plant(-3.2,4.6,2.2,0x2a6b46); plant(4.4,4.8,2.6,0x347f4c);
plant(0.4,4.4,1.8,0x2f7a52);

/* ---------- water surface ---------- */
{
  const surf = new THREE.Mesh(
    new THREE.PlaneGeometry(TANK.x*2, TANK.z*2, 40, 24),
    new THREE.MeshPhysicalMaterial({
      color:0x8fd8ff, transparent:true, opacity:0.28, roughness:0.25,
      metalness:0, transmission:0.4, side:THREE.DoubleSide, depthWrite:false
    })
  );
  surf.rotation.x = Math.PI/2;
  surf.position.y = TANK.yTop;
  scene.add(surf);
  window.__surf = surf;
}

/* ---------- glass walls ---------- */
{
  const box = new THREE.BoxGeometry(TANK.x*2, (TANK.yTop-TANK.yBottom), TANK.z*2);
  const glass = new THREE.Mesh(box, new THREE.MeshStandardMaterial({
    color:0x9fe6ff, transparent:true, opacity:0.055, roughness:0.1, metalness:0,
    side:THREE.BackSide, depthWrite:false
  }));
  glass.position.y=(TANK.yTop+TANK.yBottom)/2;
  scene.add(glass);
  const edges = new THREE.LineSegments(
    new THREE.EdgesGeometry(box),
    new THREE.LineBasicMaterial({color:0x7fd8ff, transparent:true, opacity:0.28})
  );
  edges.position.y=(TANK.yTop+TANK.yBottom)/2;
  scene.add(edges);
}

/* ---------- fish ---------- */
const FISH_COLORS = [0xff8a3d,0xffc93c,0x4fd8e8,0xff5f7e,0x9d7bff,0x7df07a,0xffffff,0x54a8ff];
const fishes=[];

function makeFish(colorIdx){
  const grp = new THREE.Group();
  const col = FISH_COLORS[colorIdx % FISH_COLORS.length];
  const bodyMat = new THREE.MeshStandardMaterial({color:col, roughness:0.35, metalness:0.12});
  const body = new THREE.Mesh(new THREE.SphereGeometry(1, 26, 18), bodyMat);
  body.scale.set(0.85,0.42,0.32);
  grp.add(body);

  // lighter belly
  const belly = new THREE.Mesh(new THREE.SphereGeometry(1,20,14), new THREE.MeshStandardMaterial({color:0xffffff,roughness:0.4}));
  belly.scale.set(0.78,0.36,0.33); belly.position.y=-0.08;
  grp.add(belly);

  // tail
  const tail = new THREE.Group();
  const tailMesh = new THREE.Mesh(new THREE.ConeGeometry(0.34,0.85,3,1), new THREE.MeshStandardMaterial({color:col, roughness:0.4, side:THREE.DoubleSide}));
  tailMesh.rotation.z = Math.PI/2; tailMesh.rotation.y = Math.PI/2;
  tailMesh.scale.set(1,1,0.35);
  tailMesh.position.x = -0.32;
  tail.add(tailMesh);
  tail.position.x = -0.78;
  grp.add(tail);

  // dorsal + side fins
  const finMat = new THREE.MeshStandardMaterial({color:col, transparent:true, opacity:0.85, side:THREE.DoubleSide, roughness:0.4});
  const dorsal = new THREE.Mesh(new THREE.ConeGeometry(0.2,0.45,3), finMat);
  dorsal.scale.set(1,1,0.3); dorsal.position.set(0,0.4,0);
  grp.add(dorsal);
  const finL = new THREE.Mesh(new THREE.ConeGeometry(0.14,0.35,3), finMat);
  finL.scale.set(1,1,0.25); finL.position.set(0.05,-0.1,0.3); finL.rotation.x=Math.PI/2.4;
  grp.add(finL);
  const finR = finL.clone(); finR.position.z=-0.3; finR.rotation.x=-Math.PI/2.4;
  grp.add(finR);

  // eyes
  const eyeW = new THREE.Mesh(new THREE.SphereGeometry(0.12,12,10), new THREE.MeshStandardMaterial({color:0xffffff, roughness:0.2}));
  const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.06,10,8), new THREE.MeshStandardMaterial({color:0x101820, roughness:0.2}));
  eyeW.position.set(0.62,0.14,0.22); pupil.position.set(0.68,0.14,0.28);
  const eyeW2=eyeW.clone(); eyeW2.position.z=-0.22; const pupil2=pupil.clone(); pupil2.position.z=-0.28;
  grp.add(eyeW,eyeW2,pupil,pupil2);

  const s = 0.7 + Math.random()*0.55;
  grp.scale.setScalar(s);
  scene.add(grp);
  return {grp, tail, dorsal, s, phase:Math.random()*6.28, speed:0.7+Math.random()*0.6};
}

const COUNT = 16;
for(let i=0;i<COUNT;i++){
  const f = makeFish(i);
  f.grp.position.set((Math.random()-0.5)*18, (Math.random()-0.5)*6, (Math.random()-0.5)*8);
  f.vel = new THREE.Vector3(Math.random()-0.5, (Math.random()-0.5)*0.3, (Math.random()-0.5)).normalize().multiplyScalar(1.4);
  fishes.push(f);
}
// a couple of big lonely ones
for(let i=0;i<3;i++){
  const f = makeFish(i+3);
  f.s = 1.6 + Math.random()*0.6; f.grp.scale.setScalar(f.s);
  f.grp.position.set((Math.random()-0.5)*16, (Math.random()-0.5)*5, (Math.random()-0.5)*8);
  f.vel = new THREE.Vector3(Math.random()-0.5,0,(Math.random()-0.5)).normalize().multiplyScalar(1.0);
  fishes.push(f);
}

/* ---------- bubbles ---------- */
function bubbleTexture(){
  const c=document.createElement('canvas'); c.width=c.height=64;
  const g=c.getContext('2d');
  const rg=g.createRadialGradient(32,32,2,32,32,30);
  rg.addColorStop(0,'rgba(255,255,255,0.85)');
  rg.addColorStop(0.5,'rgba(200,240,255,0.18)');
  rg.addColorStop(1,'rgba(255,255,255,0)');
  g.fillStyle=rg; g.fillRect(0,0,64,64);
  return new THREE.CanvasTexture(c);
}
const bTex = bubbleTexture();
const bubbles=[];
{
  const N=110;
  const pos=new Float32Array(N*3), sizes=new Float32Array(N);
  for(let i=0;i<N;i++){
    pos[i*3]=(Math.random()-0.5)*22;
    pos[i*3+1]=TANK.yBottom+Math.random()*(TANK.yTop-TANK.yBottom);
    pos[i*3+2]=(Math.random()-0.5)*12;
    sizes[i]=0.12+Math.random()*0.28;
    bubbles.push({vy:0.5+Math.random()*0.9, sway:Math.random()*6.28, base:pos[i*3], baseZ:pos[i*3+2]});
  }
  const geo=new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(pos,3));
  geo.setAttribute('size', new THREE.BufferAttribute(sizes,1));
  const mat=new THREE.PointsMaterial({map:bTex, transparent:true, depthWrite:false, size:0.4, sizeAttenuation:true, color:0xdff6ff, opacity:0.85, blending:THREE.AdditiveBlending});
  const pts=new THREE.Points(geo,mat);
  scene.add(pts);
  window.__bubbles = {pts, geo};
}

/* ---------- plankton (drifting motes) ---------- */
{
  const N=260, pos=new Float32Array(N*3);
  for(let i=0;i<N;i++){
    pos[i*3]=(Math.random()-0.5)*24; pos[i*3+1]=TANK.yBottom+Math.random()*10; pos[i*3+2]=(Math.random()-0.5)*13;
  }
  const g=new THREE.BufferGeometry(); g.setAttribute('position', new THREE.BufferAttribute(pos,3));
  const m=new THREE.PointsMaterial({color:0xbfe9ff, size:0.055, transparent:true, opacity:0.5, depthWrite:false, blending:THREE.AdditiveBlending});
  scene.add(new THREE.Points(g,m));
  window.__plankton = g;
}

/* ---------- pointer ---------- */
const pointer = new THREE.Vector2(0,0);       // NDC
const pointerWorld = new THREE.Vector3(0,0,0);
const pointerActive = false;
let pointerInside=false;
const raycaster = new THREE.Raycaster();
const interactionPlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);
const hitPoint = new THREE.Vector3();

addEventListener('pointermove', e=>{
  pointer.x = (e.clientX/innerWidth)*2-1;
  pointer.y = -(e.clientY/innerHeight)*2+1;
  pointerInside = true;
});
addEventListener('pointerleave', ()=>{ pointerInside=false; });

/* ---------- start overlay ---------- */
const startEl = document.getElementById('start');
let started=false;
function begin(){
  if(started) return; started=true;
  startEl.style.opacity='0';
  setTimeout(()=>startEl.remove(), 900);
}
addEventListener('pointerdown', begin);
addEventListener('keydown', begin);
setTimeout(begin, 3200); // auto-dismiss so the tank is visible in screenshots

/* ---------- loop ---------- */
const clock = new THREE.Clock();
const tmp = new THREE.Vector3(), sep=new THREE.Vector3(), ali=new THREE.Vector3(), coh=new THREE.Vector3(), flee=new THREE.Vector3();
const X_AXIS = new THREE.Vector3(1,0,0);
const q = new THREE.Quaternion();

function updateFishes(dt, t){
  // pointer world position on plane z = 0
  if(pointerInside){
    raycaster.setFromCamera(pointer, camera);
    if(raycaster.ray.intersectPlane(interactionPlane, hitPoint)){
      pointerWorld.copy(hitPoint);
    }
  }
  const usePointer = pointerInside;

  for(const f of fishes){
    const p = f.grp.position, v = f.vel;
    sep.set(0,0,0); ali.set(0,0,0); coh.set(0,0,0);
    let n=0;
    for(const o of fishes){
      if(o===f) continue;
      tmp.subVectors(p, o.grp.position);
      const d2 = tmp.lengthSq();
      if(d2 < 9){
        // separation
        sep.addScaledVector(tmp.normalize(), (3-d2)*0.9);
        ali.add(o.vel); coh.add(o.grp.position); n++;
      }
    }
    if(n>0){
      ali.divideScalar(n).sub(v).multiplyScalar(0.6);
      coh.divideScalar(n).sub(p).multiplyScalar(0.35);
    }
    // flee from pointer
    flee.set(0,0,0);
    if(usePointer){
      tmp.subVectors(p, pointerWorld);
      const d = tmp.length();
      const R = 7.5;
      if(d < R && d > 0.0001){
        flee.copy(tmp.divideScalar(d)).multiplyScalar((1-d/R)*(1-d/R)*34);
      }
    }
    // gentle wander
    const w = Math.sin(t*0.7 + f.phase)*0.5;
    const w2 = Math.cos(t*0.53 + f.phase*1.7)*0.5;
    const wander = tmp.set(w, w2*0.5, Math.sin(t*0.41+f.phase*0.6)*0.5);

    v.addScaledVector(sep, 0.9*dt);
    v.addScaledVector(ali, dt);
    v.addScaledVector(coh, dt);
    v.addScaledVector(wander, dt*1.2);
    v.addScaledVector(flee, dt*3.2);

    // speed limits
    const minS = 0.9*f.speed, maxS = 6.0*f.speed;
    const sp = v.length();
    if(sp>maxS) v.multiplyScalar(maxS/sp);
    if(sp<minS) v.multiplyScalar(minS/Math.max(sp,0.0001));

    p.addScaledVector(v, dt);

    // bounds
    const b = TANK;
    if(p.x < -b.x+1.2){ p.x=-b.x+1.2; v.x = Math.abs(v.x); }
    if(p.x >  b.x-1.2){ p.x=  b.x-1.2; v.x = -Math.abs(v.x); }
    if(p.y <  b.yBottom+0.8){ p.y=b.yBottom+0.8; v.y = Math.abs(v.y); }
    if(p.y >  b.yTop-0.9){ p.y=b.yTop-0.9; v.y = -Math.abs(v.y); }
    if(p.z < -b.z+1.0){ p.z=-b.z+1.0; v.z = Math.abs(v.z); }
    if(p.z >  b.z-1.0){ p.z= b.z-1.0; v.z = -Math.abs(v.z); }

    // orientation (+X is forward)
    tmp.copy(v).normalize();
    q.setFromUnitVectors(X_AXIS, tmp);
    f.grp.quaternion.slerp(q, Math.min(1, dt*4.5));

    // tail wag & fin flutter
    const wag = Math.sin(t*9*f.speed + f.phase);
    f.tail.rotation.y = wag*0.55;
    f.dorsal.rotation.z = wag*0.06;
  }
}

function updateBubbles(dt){
  const {pts, geo} = window.__bubbles;
  const pos = geo.attributes.position.array;
  for(let i=0;i<bubbles.length;i++){
    const b=bubbles[i];
    pos[i*3+1] += b.vy*dt;
    if(pos[i*3+1] > TANK.yTop-0.1){
      pos[i*3+1] = TANK.yBottom+0.1;
      pos[i*3] = (Math.random()-0.5)*22;
      pos[i*3+2] = (Math.random()-0.5)*12;
    }
    const sway = Math.sin(clock.elapsedTime*1.6 + b.sway)*0.25;
    pos[i*3] += (sway - (pos[i*3]-b.base)*0.5)*dt*0.8;
    pos[i*3+2] += (Math.cos(clock.elapsedTime*1.2+b.sway)*0.2)*dt*0.8;
  }
  geo.attributes.position.needsUpdate = true;
}

function updatePlants(t){
  for(const p of plants){
    p.holder.rotation.x = Math.sin(t*1.1 + p.phase)*p.amp;
    p.holder.rotation.z = Math.cos(t*0.9 + p.phase*1.3)*p.amp;
  }
}

function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  updateFishes(dt, t);
  updateBubbles(dt);
  updatePlants(t);

  // gentle camera parallax
  const tx = pointer.x*1.6, ty = pointer.y*0.9;
  camera.position.x += (tx - camera.position.x)*0.035;
  camera.position.y += (1.5+ty - camera.position.y)*0.035;
  camera.position.z += (24 - camera.position.z)*0.02;
  camera.lookAt(0, 0.4, 0);

  // water surface undulation
  if(window.__surf){
    const g = window.__surf.geometry, p = g.attributes.position;
    for(let i=0;i<p.count;i++){
      const x=p.getX(i), y=p.getY(i);
      p.setZ(i, Math.sin(x*0.6+t*1.1)*0.12 + Math.cos(y*0.8+t*0.9)*0.1);
    }
    p.needsUpdate = true;
  }

  // plankton drift
  if(window.__plankton){
    const p = window.__plankton.attributes.position.array;
    for(let i=0;i<p.length;i+=3){
      p[i+1] += 0.06*dt;
      p[i] += Math.sin(t*0.5 + i)*0.002;
      if(p[i+1] > TANK.yTop) p[i+1] = TANK.yBottom;
    }
    window.__plankton.attributes.position.needsUpdate = true;
  }

  renderer.render(scene, camera);
}
animate();

addEventListener('resize', ()=>{
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Reasoning trace (1,275 tokens)
Build a nice 3D aquarium with Three.js, fish with boids-ish behavior, flee from mouse ray. Include import map, module script. Add bubbles, sand, plants, caustics-ish lighting, god rays maybe. Keep it performant.

I'll write a single HTML file.

Key parts:
- Scene, fog, water tank glass box, sand floor with rocks, plants (cones/cylinders), fish (groups with body ellipsoid, tail fin, dorsal fin) animated swimming.
- Mouse raycast into plane at fish depth; fish compute repulsion vector if close to mouse world point; flee.
- Also boids cohesion/alignment/separation for natural movement.
- Lighting: directional light from above, ambient, point light.
- Bubbles particles rising.
- Camera slight parallax with mouse.
- Overlay text "Click or press a key to start" — but demo should self-play. Instructions say games must start in self-playing demo mode behind a "Click or press a key to start" message. I'll show a fading overlay that auto-hides after a couple seconds? Better: show the message overlay with "Click or press any key to start" but also auto-start after 3s. Actually the rule: behind message, demo plays. I'll show message, auto-dismiss after 2.5s so screenshot shows... hmm screenshot at 3s would show message overlay and aquarium. Let's make overlay semi-transparent and auto-fade at ~2s so the screenshot shows the aquarium clearly. Keep message visible 1.5s then fade.

Fish flee from mouse — pointer moves needed. Also make fish flee from a slow-moving "pointer" default? Fine.

Let me write fish mesh: body = scaled sphere (SphereGeometry), tail = ConeGeometry flattened, fins = small cones/planes. Use MeshStandardMaterial with color. Animate tail wiggle by rotating tail group around y with sin.

Fish movement: velocity, wander + boids + flee. Orient using lookAt along velocity.

Let me write the code carefully.

Fish body along X axis (nose +X). Then lookAt won't align. Use quaternion from direction: build object with local +X forward; compute target quaternion using matrix.lookAt(new Vector3(), dir, up) gives -Z forward. Instead construct: quaternion.setFromUnitVectors(new Vector3(1,0,0), dir) — works but no roll/up control; fine for fish, plus add slight banking.

Simplest: use Object3D group "fish" positioned; child mesh oriented with +X forward. Set group.quaternion from setFromUnitVectors(xAxis, vel.normalized).

Boids: O(n^2) with ~14 fish = fine.

Mouse: raycast to a plane z = 0 (fish swim in a slab around z in [-6,6]). Better: raycast against fish? Use raycaster from camera through pointer, intersect a plane through origin perpendicular to camera direction at distance; that gives world point; fish flee from that point. Do both: nearest fish intersection via raycaster on fish meshes gives direct hit point. I'll use plane intersection at z=0 plus raycaster intersectObjects of fish groups for direct hit.

Tank: box size 24 x 12 x 14 (x,y,z). Fish within bounds, bounce off walls.

Let me add caustics? skip; use a soft light with a moving "water" surface plane above with transparent material and animated texture? Could use a shader on the floor for caustic-like pattern. I'll do a custom ShaderMaterial floor with animated caustic lines—nice touch but risk. I'll do simple: floor with MeshStandardMaterial and an added overlay plane with procedural canvas texture? Keep simple: sand color with noise via canvas texture.

Plants: use cone geometry bunches swaying via vertex? Use Group with several thin cylinders, rotate slightly with time.

Bubbles: sprites/points rising.

Water surface plane at top with translucent material.

Let's write.

Canvas texture for sand: create 512x512 canvas with noise dots. Fine.

Glass walls: MeshPhysicalMaterial transmission might be heavy; use MeshStandardMaterial transparent low opacity, side BackSide? I'll use a BoxGeometry with transparent material opacity 0.06, side: THREE.DoubleSide, depthWrite false — plus edge lines? Add EdgesGeometry lines for tank edges for definition.

Fog: THREE.FogExp2(0x0a3a5a, 0.03).

Background: gradient via scene.background color 0x062a3f and a large sphere? Use a canvas gradient texture as background.

Let's code.

Fish flee strength: for each fish compute d = fish.pos - mouseWorld; if dist < fleeRadius (6), add acceleration = normalize(d) * (1-d/R)^2 * 40.

Also fish flee from clicking.

Include UI: title, hint "Move your mouse — the fish scatter". Also counter? Not needed.

Write code now.

Bubbles: THREE.Points with custom sprite texture (canvas radial gradient), positions updated in JS.

Also add small "plankton" particles drifting.

Let me produce code.

Fish creation function:

function makeFish(color, scale) { group with body mesh SphereGeometry(1,24,16) scaled (0.9,0.45,0.35)... } Use geometry scaled via mesh.scale.

Tail: ConeGeometry(0.28,0.7,3) rotated so it extends -X, at x=-0.85, plus a flat triangle fin (ConeGeometry with radialSegments 4 scaled flat in z).

Dorsal fin: small cone on top.

Eye: small white sphere + dark pupil.

Animation: tailGroup.rotation.y = sin(t*8 + phase)*0.5; also body slight sine.

Write it.

Also depth: fish z range -5..5.

Camera at (0,2,20) looking at (0,0,0). Slight mouse parallax.

Tank dims: x ±12, y -5..5, z ±7.

Fish keep within x ±10.5, y -4..4, z ±5.5.

Sand floor at y=-5, top plane.

Let's write final HTML.