Interactive 3D aquariumOne-line
Muse Ember
by MetaReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 41.4 s
- Cost
- $0.0090
- Output tokens
- 17,830
- Spent thinking
- 7,686 tokens
- Speed
- 492.9 tok/s
- Console errors
- 0
- First token after
- 5.2 s
- First visible text after
- 27.2 s
- Prompt tokens
- 441
- Code size
- 28.6 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"xhigh"}- 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 · 647 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Aquarium — Fish Flee From Your Cursor</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}
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#062a44;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#fff}
#scene-container{position:fixed;inset:0;width:100%;height:100%}
#scene-container canvas{display:block;width:100%;height:100%;cursor:crosshair}
.panel{position:absolute;background:rgba(3,32,54,.58);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid rgba(255,255,255,.18);border-radius:18px;box-shadow:0 12px 32px rgba(0,0,0,.35);z-index:10}
#title-panel{top:16px;left:16px;padding:14px 16px;max-width:360px}
#title-panel h1{margin:0;font-size:24px;line-height:1.1;letter-spacing:.2px}
#title-panel h1 span{font-size:26px}
#title-panel p{margin:7px 0 0;font-size:13px;line-height:1.45;color:#dbeafe}
#title-panel p b{color:#7dd3fc}
.chips{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.chip{font-size:11px;padding:5px 8px;border-radius:999px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);white-space:nowrap}
#stats-panel{top:16px;right:16px;padding:12px 14px;width:205px}
#stats-panel .row{display:flex;justify-content:space-between;align-items:center;font-size:12px;margin:5px 0;color:#e0f2fe}
#stats-panel .row b{font-size:14px;color:#fff}
.meter{height:9px;background:rgba(255,255,255,.16);border-radius:999px;overflow:hidden;margin-top:6px}
#panicFill{height:100%;width:0%;border-radius:999px;background:linear-gradient(90deg,#38bdf8,#facc15,#f97316,#ef4444);transition:width .15s ease}
#panicLabel{font-size:11px;color:#bae6fd;margin-top:5px;text-align:right}
#controls{position:absolute;left:50%;transform:translateX(-50%);bottom:16px;display:flex;gap:8px;flex-wrap:wrap;justify-content:center;z-index:11;width:min(94vw,760px)}
button{border:1px solid rgba(255,255,255,.25);background:linear-gradient(135deg,rgba(14,116,144,.92),rgba(8,145,178,.92));color:#fff;padding:10px 14px;border-radius:999px;font-weight:700;font-size:13px;cursor:pointer;box-shadow:0 8px 20px rgba(0,0,0,.3);transition:transform .12s ease,filter .12s ease}
button:hover{transform:translateY(-1px) scale(1.03);filter:brightness(1.12)}
button:active{transform:scale(.97)}
button.secondary{background:linear-gradient(135deg,rgba(255,255,255,.18),rgba(255,255,255,.08))}
#hint{position:absolute;left:16px;bottom:16px;z-index:9;font-size:12px;line-height:1.5;color:#e0f2fe;background:rgba(3,32,54,.45);backdrop-filter:blur(10px);padding:9px 11px;border-radius:12px;border:1px solid rgba(255,255,255,.14);max-width:260px}
#hint b{color:#fff}
.orb-legend{display:inline-block;width:11px;height:11px;border-radius:50%;background:#22d3ee;box-shadow:0 0 10px #22d3ee,0 0 0 5px rgba(34,211,238,.18);vertical-align:-1px;margin-right:5px}
#scaredTag{position:absolute;z-index:12;pointer-events:none;transform:translate(-50%,-150%);background:#fff;color:#0f172a;font-size:12px;font-weight:800;padding:7px 10px;border-radius:999px;box-shadow:0 8px 22px rgba(0,0,0,.35);display:none;white-space:nowrap}
#scaredTag:after{content:"";position:absolute;left:50%;bottom:-6px;width:12px;height:12px;background:#fff;transform:translateX(-50%) rotate(45deg)}
#loader{position:absolute;inset:0;z-index:30;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 40%,#0c4a6e,#062a44 70%);transition:opacity .6s ease}
#loader .fish{font-size:58px;animation:bob 1s ease-in-out infinite alternate}
#loader p{margin:10px 0 0;color:#bae6fd;font-weight:700;letter-spacing:.4px}
@keyframes bob{from{transform:translateY(-6px) rotate(-5deg)}to{transform:translateY(8px) rotate(6deg)}}
@media (max-width:700px){
#title-panel{max-width:250px;padding:11px 12px;top:10px;left:10px}
#title-panel h1{font-size:18px}
#title-panel p{font-size:11px}
.chips{display:none}
#stats-panel{width:150px;padding:9px 10px;top:10px;right:10px}
#hint{display:none}
#controls{bottom:10px;gap:6px}
button{padding:8px 11px;font-size:12px}
}
</style>
</head>
<body>
<div id="scene-container"></div>
<div id="loader"><div class="fish">🐠</div><p>Filling the aquarium…</p></div>
<div class="panel" id="title-panel">
<h1><span>🐠</span> 3D Aquarium</h1>
<p>Move your cursor <b>near a fish</b> and it will panic and dart away! The glowing cyan orb is your danger zone.</p>
<div class="chips"><span class="chip">🖱️ Move = scare</span><span class="chip">👆 Click = feed</span><span class="chip">␣ Space = scatter</span></div>
</div>
<div class="panel" id="stats-panel">
<div class="row"><span>🐟 Fish</span><b id="fishVal">24</b></div>
<div class="row"><span>💨 Escapes</span><b id="escapesVal">0</b></div>
<div class="row"><span>📏 Closest fish</span><b id="closestVal">—</b></div>
<div class="row"><span>😱 Panic</span><b id="panicVal">0%</b></div>
<div class="meter"><div id="panicFill"></div></div>
<div id="panicLabel">calm waters</div>
</div>
<div id="scaredTag">😱 Ahh! Too close!</div>
<div id="controls">
<button id="scatterBtn">💨 Scatter Fish</button>
<button id="feedBtn">🍤 Drop Food</button>
<button id="calmBtn" class="secondary">🌊 Calm</button>
<button id="lightsBtn" class="secondary">🌙 Night Mode</button>
</div>
<div id="hint"><span class="orb-legend"></span><b>Demo mode:</b> a ghost cursor is swimming around, so fish flee even before you move your mouse. Take over anytime!</div>
<script type="module">
import * as THREE from 'three';
const container = document.getElementById('scene-container');
const loader = document.getElementById('loader');
const escapesEl = document.getElementById('escapesVal');
const closestEl = document.getElementById('closestVal');
const panicFill = document.getElementById('panicFill');
const panicVal = document.getElementById('panicVal');
const panicLabel = document.getElementById('panicLabel');
const scaredTag = document.getElementById('scaredTag');
const rand = (a,b)=>a+Math.random()*(b-a);
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x062a44);
scene.fog = new THREE.FogExp2(0x07304d, 0.012);
const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0,3,27);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
container.appendChild(renderer.domElement);
const ambient = new THREE.AmbientLight(0x9fd8ef, 0.65);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xfff2c4, 1.35);
sun.position.set(10,22,12);
sun.castShadow = true;
sun.shadow.mapSize.set(1024,1024);
scene.add(sun);
const fill = new THREE.PointLight(0x22d3ee, 18, 45);
fill.position.set(0,6,4);
scene.add(fill);
const pinkLight = new THREE.PointLight(0xff6b9d, 10, 30);
pinkLight.position.set(-10,0,6);
scene.add(pinkLight);
/* ---------- Tank ---------- */
const TANK = {w:34,h:18,d:16,cx:0,cy:1,cz:0};
const glass = new THREE.Mesh(
new THREE.BoxGeometry(TANK.w,TANK.h,TANK.d),
new THREE.MeshPhysicalMaterial({color:0xbfefff,transparent:true,opacity:0.07,roughness:0.05,metalness:0,side:THREE.DoubleSide,depthWrite:false})
);
glass.position.set(TANK.cx,TANK.cy,TANK.cz);
scene.add(glass);
const edges = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(TANK.w,TANK.h,TANK.d)),
new THREE.LineBasicMaterial({color:0xa5f3fc,transparent:true,opacity:0.55})
);
edges.position.copy(glass.position);
scene.add(edges);
// top rim
const rimMat = new THREE.MeshStandardMaterial({color:0x0f766e,roughness:0.35,metalness:0.65});
[[0,10.05,-8,TANK.w,0.35,0.35],[0,10.05,8,TANK.w,0.35,0.35],[-17,10.05,0,0.35,0.35,TANK.d],[17,10.05,0,0.35,0.35,TANK.d]].forEach(r=>{
const m=new THREE.Mesh(new THREE.BoxGeometry(r[3],r[4],r[5]),rimMat);
m.position.set(r[0],r[1],r[2]); scene.add(m);
});
// water surface
const waterGeo = new THREE.PlaneGeometry(TANK.w,TANK.d,24,12);
waterGeo.rotateX(-Math.PI/2);
const water = new THREE.Mesh(waterGeo,new THREE.MeshStandardMaterial({color:0x38bdf8,transparent:true,opacity:0.28,roughness:0.1,metalness:0.1,side:THREE.DoubleSide}));
water.position.y = 9.6;
scene.add(water);
const waterBase = waterGeo.attributes.position.array.slice();
// sand
const sandGeo = new THREE.PlaneGeometry(36,18,64,32);
sandGeo.rotateX(-Math.PI/2);
{
const pos = sandGeo.attributes.position;
const colors = [];
const c1 = new THREE.Color(0xfde68a), c2 = new THREE.Color(0xd97706), tmp = new THREE.Color();
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), z=pos.getZ(i);
const y = Math.sin(x*0.45)*0.22 + Math.cos(z*0.6)*0.18 + Math.sin((x+z)*0.3)*0.12 - Math.abs(z)*0.02;
pos.setY(i,y);
tmp.copy(c1).lerp(c2, clamp(0.5+y*0.9,0,1)*0.55);
colors.push(tmp.r,tmp.g,tmp.b);
}
sandGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors,3));
sandGeo.computeVertexNormals();
}
const sand = new THREE.Mesh(sandGeo,new THREE.MeshStandardMaterial({vertexColors:true,roughness:1}));
sand.position.y = -7.9;
sand.receiveShadow = true;
scene.add(sand);
/* ---------- Decor ---------- */
const seaweeds=[];
function makeSeaweed(x,z,h,color){
const pts=[];
for(let i=0;i<=4;i++) pts.push(new THREE.Vector3(Math.sin(i*1.1)*0.35, (h/4)*i, Math.cos(i*0.8)*0.22));
const curve=new THREE.CatmullRomCurve3(pts);
const geo=new THREE.TubeGeometry(curve,20,0.14,7,false);
const mesh=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color,roughness:0.6}));
mesh.castShadow=true;
const g=new THREE.Group(); g.add(mesh); g.position.set(x,-7.6,z);
g.userData.phase=rand(0,Math.PI*2);
scene.add(g); seaweeds.push(g);
}
makeSeaweed(-15.5,-5,7,0x22c55e); makeSeaweed(-14,-6.5,8.5,0x16a34a); makeSeaweed(-12,5,6,0x4ade80);
makeSeaweed(14,-6,8,0x22c55e); makeSeaweed(15.5,4,6.5,0x16a34a); makeSeaweed(11,-2,5,0x86efac);
makeSeaweed(-5,-6.8,6,0x22c55e); makeSeaweed(6,-6.8,7,0x16a34a); makeSeaweed(0,6,5,0x4ade80);
function makeRock(x,z,s){
const geo=new THREE.IcosahedronGeometry(s,1);
const p=geo.attributes.position;
for(let i=0;i<p.count;i++){ p.setXYZ(i,p.getX(i)*rand(0.85,1.15),p.getY(i)*rand(0.6,0.9),p.getZ(i)*rand(0.85,1.15)); }
geo.computeVertexNormals();
const m=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0x64748b,roughness:0.95,flatShading:true}));
m.position.set(x,-7.4,z); m.castShadow=true; m.receiveShadow=true; scene.add(m);
}
makeRock(-10,-4,2.2); makeRock(-8.5,-5.5,1.5); makeRock(10,-5,2.6); makeRock(12,-3,1.6); makeRock(3,-6.5,1.4); makeRock(-3,5.5,1.2);
function makeCoral(x,z,color,scale=1){
const g=new THREE.Group(); g.position.set(x,-7.6,z); g.scale.setScalar(scale);
const mat=new THREE.MeshStandardMaterial({color,roughness:0.55,emissive:color,emissiveIntensity:0.12});
const branches=[[0,1,0,0,2.6,0.22],[0.5,1.7,0.2,0.5,1.6,0.16],[-0.5,1.7,-0.2,-0.6,1.5,0.16],[0.2,2.4,-0.1,0.2,1.2,0.12],[-0.2,2.5,0.2,-0.3,1.1,0.12]];
branches.forEach(b=>{
const len=Math.hypot(b[3],b[4],b[2]);
const geo=new THREE.CylinderGeometry(0.08,0.2,len,8);
const m=new THREE.Mesh(geo,mat);
m.position.set(b[0]+b[3]/2,b[1]+b[4]/2,b[2]+b[5]/2);
m.lookAt(new THREE.Vector3(b[3],b[4],b[2]));
m.rotateX(Math.PI/2);
m.castShadow=true; g.add(m);
const tip=new THREE.Mesh(new THREE.SphereGeometry(0.22,10,8),mat);
tip.position.set(b[0]+b[3],b[1]+b[4],b[2]+b[5]); g.add(tip);
});
scene.add(g);
}
makeCoral(-13,3,0xff6b9d,1.1); makeCoral(13,2,0xff8c42,1.2); makeCoral(-6,-6,0xa78bfa,0.9); makeCoral(7,-5.5,0xfb7185,0.85);
// starfish
function makeStar(x,z,color){
const shape=new THREE.Shape();
for(let i=0;i<10;i++){
const r=i%2===0?1:0.42, a=(i/10)*Math.PI*2 - Math.PI/2;
const px=Math.cos(a)*r, py=Math.sin(a)*r;
i===0?shape.moveTo(px,py):shape.lineTo(px,py);
}
shape.closePath();
const geo=new THREE.ExtrudeGeometry(shape,{depth:0.22,bevelEnabled:true,bevelSize:0.06,bevelThickness:0.06});
const m=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color,roughness:0.7}));
m.rotation.x=-Math.PI/2; m.rotation.z=rand(0,3); m.position.set(x,-7.55,z); m.scale.setScalar(1.4); m.castShadow=true; scene.add(m);
}
makeStar(6,3.2,0xfb923c); makeStar(-9,4.5,0xf472b6); makeStar(2,-2,0xfacc15);
// treasure chest
{
const g=new THREE.Group(); g.position.set(-8,-7.2,-2.5); g.rotation.y=0.5;
const wood=new THREE.MeshStandardMaterial({color:0x92400e,roughness:0.8});
const base=new THREE.Mesh(new THREE.BoxGeometry(2.6,1.3,1.7),wood); base.position.y=0.65; base.castShadow=true; g.add(base);
const lid=new THREE.Mesh(new THREE.CylinderGeometry(0.85,0.85,2.6,16,1,false,0,Math.PI),wood);
lid.rotation.z=Math.PI/2; lid.rotation.y=Math.PI/2; lid.position.y=1.3; g.add(lid);
const gold=new THREE.MeshStandardMaterial({color:0xfacc15,metalness:0.9,roughness:0.25});
for(let i=0;i<7;i++){ const c=new THREE.Mesh(new THREE.CylinderGeometry(0.32,0.32,0.1,14),gold); c.position.set(rand(-0.8,0.8),1.45+rand(0,0.35),rand(-0.4,0.4)); c.rotation.set(rand(0,0.5),rand(0,3),rand(0,0.5)); g.add(c); }
scene.add(g);
}
// god rays
const rays=[];
for(let i=0;i<4;i++){
const geo=new THREE.ConeGeometry(rand(2,3.4),16,16,1,true);
const m=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:0x7dd3fc,transparent:true,opacity:0.06,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide}));
m.position.set(rand(-10,10),5,rand(-4,2)); m.rotation.z=rand(-0.25,0.25);
scene.add(m); rays.push(m);
}
// plankton
const planktonGeo=new THREE.BufferGeometry();
{
const arr=[];
for(let i=0;i<350;i++) arr.push(rand(-16,16),rand(-7,9),rand(-7,7));
planktonGeo.setAttribute('position',new THREE.Float32BufferAttribute(arr,3));
}
const plankton=new THREE.Points(planktonGeo,new THREE.PointsMaterial({color:0xe0f2fe,size:0.08,transparent:true,opacity:0.55}));
scene.add(plankton);
/* ---------- Bubbles ---------- */
const bubbles=[];
const bubbleGeo=new THREE.SphereGeometry(0.12,8,6);
const bubbleMat=new THREE.MeshBasicMaterial({color:0xdff6ff,transparent:true,opacity:0.55});
for(let i=0;i<55;i++){
const m=new THREE.Mesh(bubbleGeo,bubbleMat);
m.position.set(rand(-15,15),rand(-7,9),rand(-6,6));
m.scale.setScalar(rand(0.5,1.6));
scene.add(m);
bubbles.push({m,speed:rand(0.6,1.8),phase:rand(0,6)});
}
const burstBubbles=[];
function burst(pos){
for(let i=0;i<4;i++){
const m=new THREE.Mesh(bubbleGeo,bubbleMat.clone());
m.position.copy(pos); m.position.x+=rand(-0.3,0.3); m.scale.setScalar(rand(0.7,1.4));
scene.add(m); burstBubbles.push({m,life:rand(0.8,1.4),vy:rand(1,2)});
}
}
/* ---------- Fish ---------- */
function tailGeometry(){
const s=new THREE.Shape();
s.moveTo(0,0); s.lineTo(1.15,0.62); s.lineTo(1.45,0); s.lineTo(1.15,-0.62); s.closePath();
const g=new THREE.ShapeGeometry(s);
g.rotateY(Math.PI/2);
return g;
}
const sharedTailGeo = tailGeometry();
function makeFish(color,size,stripeColor=null){
const group=new THREE.Group();
const mat=new THREE.MeshStandardMaterial({color,roughness:0.45,metalness:0.12});
const lightMat=new THREE.MeshStandardMaterial({color:new THREE.Color(color).lerp(new THREE.Color(0xffffff),0.35),roughness:0.5,side:THREE.DoubleSide});
const body=new THREE.Mesh(new THREE.SphereGeometry(0.62,22,16),mat);
body.scale.set(0.78,1,1.9); body.castShadow=true; group.add(body);
const tailPivot=new THREE.Group(); tailPivot.position.set(0,0,-1.05);
const tail=new THREE.Mesh(sharedTailGeo,lightMat); tailPivot.add(tail); group.add(tailPivot);
const dorsal=new THREE.Mesh(sharedTailGeo,lightMat);
dorsal.scale.setScalar(0.65); dorsal.position.set(0,0.52,-0.25); group.add(dorsal);
[-1,1].forEach(side=>{
const eye=new THREE.Mesh(new THREE.SphereGeometry(0.085,10,8),new THREE.MeshStandardMaterial({color:0x111827,roughness:0.2}));
eye.position.set(side*0.38,0.18,0.82); group.add(eye);
const glint=new THREE.Mesh(new THREE.SphereGeometry(0.03,6,6),new THREE.MeshBasicMaterial({color:0xffffff}));
glint.position.set(side*0.43,0.24,0.88); group.add(glint);
const fin=new THREE.Mesh(new THREE.SphereGeometry(0.32,10,8),lightMat);
fin.scale.set(0.18,0.7,1.1); fin.position.set(side*0.48,-0.15,0.25); fin.rotation.z=side*0.55; group.add(fin);
});
if(stripeColor){
const sm=new THREE.MeshStandardMaterial({color:stripeColor,roughness:0.6});
[0.35,-0.35].forEach(z=>{
const ring=new THREE.Mesh(new THREE.TorusGeometry(0.46,0.065,8,22),sm);
ring.scale.set(0.82,1.08,1); ring.position.z=z; group.add(ring);
});
}
group.scale.setScalar(size);
return {group,tailPivot};
}
const fishes=[];
const palette=[0xff8c42,0x3ec1f3,0xff6b9d,0xffd93d,0xa78bfa,0x2dd4bf,0xef476f,0x60a5fa,0xf97316,0x34d399];
for(let i=0;i<24;i++){
const isTiny = i>=18;
const color = isTiny ? 0xcbd5e1 : palette[i%palette.length];
const size = isTiny ? rand(0.38,0.52) : rand(0.75,1.25);
const {group,tailPivot}=makeFish(color,size, isTiny?null:(i%3===0?0xffffff:(i%3===1?0x1e293b:null)));
group.position.set(rand(-13,13),rand(-5,7),rand(-5,5));
scene.add(group);
fishes.push({
group,tailPivot,
vel:new THREE.Vector3(rand(-1,1),rand(-0.3,0.3),rand(-1,1)).normalize().multiplyScalar(rand(1.2,2.2)),
wander:new THREE.Vector3(rand(-12,12),rand(-4,7),rand(-5,5)),
wanderTimer:rand(0,3),
panic:0, scared:false, phase:rand(0,Math.PI*2),
baseSpeed:rand(1.4,2.6)*(isTiny?1.2:1),
isTiny
});
}
/* ---------- Cursor / Danger Orb ---------- */
const mousePoint=new THREE.Vector3(0,0,0);
const realNDC=new THREE.Vector2(0,0);
let hasReal=false, lastRealMove=-10000;
const raycaster=new THREE.Raycaster();
const zPlane=new THREE.Plane(new THREE.Vector3(0,0,1),0);
const orb=new THREE.Group();
const orbCore=new THREE.Mesh(new THREE.SphereGeometry(0.34,16,12),new THREE.MeshBasicMaterial({color:0x22d3ee}));
orb.add(orbCore);
const orbRing=new THREE.Mesh(new THREE.TorusGeometry(0.62,0.045,8,28),new THREE.MeshBasicMaterial({color:0x67e8f9}));
orb.add(orbRing);
const danger=new THREE.Mesh(new THREE.SphereGeometry(7,22,14),new THREE.MeshBasicMaterial({color:0x22d3ee,transparent:true,opacity:0.04,wireframe:true,depthWrite:false}));
orb.add(danger);
const orbLight=new THREE.PointLight(0x22d3ee,8,14);
orb.add(orbLight);
scene.add(orb);
const ripples=[];
function spawnRipple(pos){
const m=new THREE.Mesh(new THREE.RingGeometry(0.4,0.55,32),new THREE.MeshBasicMaterial({color:0xa5f3fc,transparent:true,opacity:0.8,side:THREE.DoubleSide,depthWrite:false}));
m.position.copy(pos); m.lookAt(camera.position); scene.add(m);
ripples.push({m,life:0.7});
}
/* ---------- Food ---------- */
const foods=[];
const foodGeo=new THREE.SphereGeometry(0.17,10,8);
const foodMat=new THREE.MeshStandardMaterial({color:0xd97706,roughness:0.8});
function spawnFood(center){
spawnRipple(center);
for(let i=0;i<6;i++){
const m=new THREE.Mesh(foodGeo,foodMat);
m.position.copy(center); m.position.x+=rand(-1.4,1.4); m.position.z+=rand(-1,1); m.position.y+=rand(-0.3,0.3);
scene.add(m);
foods.push({m,life:rand(7,10),vy:rand(-0.45,-0.25)});
}
}
/* ---------- Events ---------- */
window.addEventListener('pointermove',e=>{
realNDC.x=(e.clientX/innerWidth)*2-1;
realNDC.y=-(e.clientY/innerHeight)*2+1;
hasReal=true; lastRealMove=performance.now();
},{passive:true});
window.addEventListener('pointerleave',()=>{hasReal=false;});
renderer.domElement.addEventListener('pointerdown',e=>{
realNDC.x=(e.clientX/innerWidth)*2-1;
realNDC.y=-(e.clientY/innerHeight)*2+1;
hasReal=true; lastRealMove=performance.now();
raycaster.setFromCamera(realNDC,camera);
const hit=new THREE.Vector3();
if(raycaster.ray.intersectPlane(zPlane,hit)){
hit.x=clamp(hit.x,-14,14); hit.y=clamp(hit.y,-6,8); hit.z=0;
spawnFood(hit);
}
});
let escapes=0;
function scatter(){
fishes.forEach(f=>{
const dir=new THREE.Vector3(rand(-1,1),rand(-0.4,0.8),rand(-1,1)).normalize();
f.vel.add(dir.multiplyScalar(9)); f.panic=1; f.scared=true;
});
escapes+=3; escapesEl.textContent=escapes;
spawnRipple(mousePoint);
}
document.getElementById('scatterBtn').addEventListener('click',scatter);
document.getElementById('feedBtn').addEventListener('click',()=>spawnFood(new THREE.Vector3(rand(-5,5),7,rand(-2,2))));
document.getElementById('calmBtn').addEventListener('click',()=>{
fishes.forEach(f=>{f.panic=0;f.scared=false;});
foods.forEach(f=>scene.remove(f.m)); foods.length=0;
});
let night=false;
document.getElementById('lightsBtn').addEventListener('click',e=>{
night=!night;
sun.intensity=night?0.35:1.35;
ambient.intensity=night?0.3:0.65;
scene.background.set(night?0x020617:0x062a44);
scene.fog.color.set(night?0x020617:0x07304d);
e.target.textContent=night?'☀️ Day Mode':'🌙 Night Mode';
});
window.addEventListener('keydown',e=>{
if(e.code==='Space'){e.preventDefault();scatter();}
if(e.code==='KeyF') spawnFood(new THREE.Vector3(rand(-5,5),7,0));
if(e.code==='KeyC') fishes.forEach(f=>f.panic=0);
});
window.addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;
camera.fov=camera.aspect<1?72:55;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
camera.fov=camera.aspect<1?72:55; camera.updateProjectionMatrix();
/* ---------- Animation ---------- */
const clock=new THREE.Clock();
const tmpV=new THREE.Vector3(), tmpV2=new THREE.Vector3(), steer=new THREE.Vector3();
let hudTimer=0;
const BOUNDS={x:15,yMin:-6.2,yMax:7.8,z:6};
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05);
const t=clock.elapsedTime;
// --- pointer (real or ghost demo) ---
const useReal = hasReal && (performance.now()-lastRealMove < 9000);
let pxNDC, pyNDC;
if(useReal){
raycaster.setFromCamera(realNDC,camera);
const hit=new THREE.Vector3();
if(raycaster.ray.intersectPlane(zPlane,hit)){
mousePoint.copy(hit);
mousePoint.x=clamp(mousePoint.x,-16,16);
mousePoint.y=clamp(mousePoint.y,-7,8.5);
mousePoint.z=clamp(mousePoint.z,-6,6);
}
pxNDC=realNDC.x; pyNDC=realNDC.y;
}else{
mousePoint.set(Math.sin(t*0.55)*11, Math.sin(t*0.9)*4.2, Math.sin(t*0.35)*1.8);
pxNDC=mousePoint.x/17; pyNDC=mousePoint.y/10;
}
orb.position.copy(mousePoint);
orbRing.lookAt(camera.position);
orbCore.scale.setScalar(1+Math.sin(t*6)*0.12);
danger.material.opacity=0.035+Math.sin(t*3)*0.012;
// camera parallax
camera.position.x += ((pxNDC*2.6)-camera.position.x)*0.04;
camera.position.y += ((3+pyNDC*1.6)-camera.position.y)*0.04;
camera.lookAt(0,1,0);
// water wave
{
const p=waterGeo.attributes.position;
for(let i=0;i<p.count;i++){
const x=waterBase[i*3], z=waterBase[i*3+2];
p.setY(i, Math.sin(x*0.7+t*1.6)*0.14 + Math.cos(z*0.9+t)*0.1);
}
p.needsUpdate=true;
}
seaweeds.forEach(s=>{ s.rotation.z=Math.sin(t*0.9+s.userData.phase)*0.16; s.rotation.x=Math.cos(t*0.7+s.userData.phase)*0.09; });
rays.forEach((r,i)=>{ r.material.opacity=0.045+Math.sin(t*0.6+i)*0.02; r.rotation.y=t*0.05+i; });
plankton.rotation.y=t*0.015; plankton.position.y=Math.sin(t*0.3)*0.3;
// bubbles
bubbles.forEach(b=>{
b.m.position.y+=b.speed*dt;
b.m.position.x+=Math.sin(t*2+b.phase)*0.15*dt;
if(b.m.position.y>9.4){ b.m.position.y=-7.2; b.m.position.x=rand(-15,15); b.m.position.z=rand(-6,6); }
});
for(let i=burstBubbles.length-1;i>=0;i--){
const b=burstBubbles[i]; b.life-=dt; b.m.position.y+=b.vy*dt; b.m.material.opacity=Math.max(0,b.life*0.5);
if(b.life<=0){ scene.remove(b.m); burstBubbles.splice(i,1); }
}
for(let i=ripples.length-1;i>=0;i--){
const r=ripples[i]; r.life-=dt; r.m.scale.multiplyScalar(1+dt*5); r.m.material.opacity=Math.max(0,r.life*1.1);
if(r.life<=0){ scene.remove(r.m); ripples.splice(i,1); }
}
// food
for(let i=foods.length-1;i>=0;i--){
const f=foods[i]; f.life-=dt;
f.m.position.y+=f.vy*dt + Math.sin(t*4+i)*0.02;
if(f.m.position.y<-7){ f.m.position.y=-7; f.vy=0; }
if(f.life<1.5) f.m.scale.setScalar(Math.max(0.01,f.life/1.5));
if(f.life<=0){ scene.remove(f.m); foods.splice(i,1); }
}
// --- fish AI ---
let panicSum=0, closestDist=Infinity, closestFish=null, maxPanicFish=null;
fishes.forEach(f=>{
const pos=f.group.position;
tmpV.copy(pos).sub(mousePoint);
const distMouse=tmpV.length();
if(distMouse<closestDist){closestDist=distMouse; closestFish=f;}
const panicTarget = distMouse<8 ? (1-distMouse/8) : 0;
f.panic += (panicTarget-f.panic)*(panicTarget>f.panic?0.32:0.05);
panicSum+=f.panic;
if(!maxPanicFish || f.panic>maxPanicFish.panic) maxPanicFish=f;
if(panicTarget>0.45 && !f.scared){
f.scared=true; escapes++; escapesEl.textContent=escapes; burst(pos);
}
if(panicTarget<0.08) f.scared=false;
if(f.panic>0.55 && Math.random()<0.02) burst(pos);
steer.set(0,0,0);
// wander
f.wanderTimer-=dt;
if(f.wanderTimer<=0){ f.wander.set(rand(-12,12),rand(-4,7),rand(-5,5)); f.wanderTimer=rand(2,5); }
tmpV2.copy(f.wander).sub(pos).normalize();
steer.add(tmpV2.multiplyScalar(0.55));
// seek food (unless panicking)
let nearestFood=null, nd=Infinity;
foods.forEach(fd=>{ const d=pos.distanceTo(fd.m.position); if(d<nd){nd=d; nearestFood=fd;} });
if(nearestFood && nd<13 && f.panic<0.55){
tmpV2.copy(nearestFood.m.position).sub(pos).normalize();
steer.add(tmpV2.multiplyScalar(1.6));
if(nd<0.85){ nearestFood.life-=dt*6; if(nearestFood.life<=0){ scene.remove(nearestFood.m); foods.splice(foods.indexOf(nearestFood),1);} }
}
// FLEE from cursor — the star of the show
if(distMouse<8.5){
tmpV.copy(pos).sub(mousePoint).normalize();
tmpV.y+=0.25; tmpV.normalize();
const strength=(8.5-distMouse)*1.35*(0.6+f.panic);
steer.add(tmpV.multiplyScalar(strength));
// jitter sideways when scared
steer.x+=Math.sin(t*22+f.phase)*f.panic*0.7;
steer.y+=Math.cos(t*19+f.phase)*f.panic*0.5;
}
// separation
fishes.forEach(o=>{
if(o===f) return;
const d=pos.distanceTo(o.group.position);
if(d<2.3 && d>0.01){
tmpV.copy(pos).sub(o.group.position).normalize();
steer.add(tmpV.multiplyScalar((2.3-d)*0.9));
}
});
// walls
if(pos.x>BOUNDS.x-2) steer.x-=(pos.x-(BOUNDS.x-2))*1.4;
if(pos.x<-BOUNDS.x+2) steer.x+=(-(BOUNDS.x-2)-pos.x)*1.4;
if(pos.y>BOUNDS.yMax-1.5) steer.y-=(pos.y-(BOUNDS.yMax-1.5))*1.6;
if(pos.y<BOUNDS.yMin+1.5) steer.y+=((BOUNDS.yMin+1.5)-pos.y)*1.6;
if(pos.z>BOUNDS.z-1.5) steer.z-=(pos.z-(BOUNDS.z-1.5))*1.4;
if(pos.z<-BOUNDS.z+1.5) steer.z+=((-(BOUNDS.z-1.5))-pos.z)*1.4;
f.vel.add(steer.multiplyScalar(dt*6));
const maxSpeed=f.baseSpeed*(1+f.panic*2.6);
if(f.vel.length()>maxSpeed) f.vel.setLength(maxSpeed);
if(f.vel.length()<0.7) f.vel.setLength(0.7);
f.vel.multiplyScalar(0.985);
pos.add(tmpV2.copy(f.vel).multiplyScalar(dt));
pos.x=clamp(pos.x,-BOUNDS.x,BOUNDS.x);
pos.y=clamp(pos.y,BOUNDS.yMin,BOUNDS.yMax);
pos.z=clamp(pos.z,-BOUNDS.z,BOUNDS.z);
if(f.vel.lengthSq()>0.01){
tmpV.copy(pos).add(f.vel);
f.group.lookAt(tmpV);
f.group.rotateZ(Math.sin(t*3+f.phase)*0.08);
}
const wiggleFreq=6+f.panic*16+f.vel.length()*1.5;
f.tailPivot.rotation.y=Math.sin(t*wiggleFreq+f.phase)*(0.5+f.panic*0.55);
f.group.rotation.y+=Math.sin(t*wiggleFreq*0.5+f.phase)*0.06;
});
// orb color reacts to panic
const avgPanic=panicSum/fishes.length;
orbCore.material.color.setHSL(0.52-avgPanic*0.52,0.9,0.6);
orbLight.color.copy(orbCore.material.color);
orbLight.intensity=8+avgPanic*22;
danger.material.color.copy(orbCore.material.color);
// HUD (throttled)
hudTimer+=dt;
if(hudTimer>0.15){
hudTimer=0;
const pct=Math.round(clamp(avgPanic*100,0,100));
panicFill.style.width=pct+'%';
panicVal.textContent=pct+'%';
panicLabel.textContent = pct>55?'😱 total panic!':pct>25?'😰 fish are nervous':'calm waters';
closestEl.textContent=closestDist===Infinity?'—':closestDist.toFixed(1)+' u';
}
// scared tag follows most panicked fish
if(maxPanicFish && maxPanicFish.panic>0.35){
tmpV.copy(maxPanicFish.group.position).project(camera);
if(tmpV.z<1){
scaredTag.style.display='block';
scaredTag.style.left=((tmpV.x*0.5+0.5)*innerWidth)+'px';
scaredTag.style.top=((-tmpV.y*0.5+0.5)*innerHeight)+'px';
}else scaredTag.style.display='none';
}else scaredTag.style.display='none';
renderer.render(scene,camera);
}
animate();
loader.style.opacity='0';
setTimeout(()=>loader.style.display='none',700);
</script>
</body>
</html>Reasoning trace (7,686 tokens)
Implementing a self-contained Threejs aquarium with import map, procedural fish boids fleeing via raycasted mouse position and 3D scene elements.Extending the boid logic with tank bounds, separation, and raycasted mouse fleeing plus scene props and UI.Adding mouse trail visuals, HUD controls, food interaction, and camera parallax to the aquarium demo.Defining fish geometry orientation and tail fin rotation to align +Z as forward.Extending the fish model with fins, eyes, stripes, colors, sizes, and a small schooling group, then adding caustics, god rays, fog, UI, and an auto-demo pointer.Adding input-driven pointer logic with idle fallback, plus geometry and animation details for props and fish behavior.Extending the scene with bubble bursts, decor, caustic lights, UI/HUD elements, and fish panic and steering logic.Adding steering, wall avoidance, panic-driven speed, tail wiggle, food decay, and pointer-driven mouse control.Extending the scene with sand displacement, coral and seaweed generation, tank framing, water surface animation, and UI styling.Adding fish flocking, bubble and plankton systems, coral generation, and final HTML export with importmap.