Interactive 3D aquariumOne-line
MiniMax M3.1 Flash
by MiniMaxReasoningClean
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.