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.7 s
- Cost
- $0.010
- Output tokens
- 20,358
- Spent thinking
- 9,392 tokens
- Speed
- 517.5 tok/s
- Console errors
- 0
- First token after
- 2.3 s
- First visible text after
- 28.5 s
- Prompt tokens
- 441
- Code size
- 30.4 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"max"}- 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 · 497 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coral Cove — 3D Aquarium</title>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;overflow:hidden;background:#062a44;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#fff}
#scene{position:fixed;inset:0}
#scene canvas{display:block}
.overlay{position:fixed;inset:0;pointer-events:none;z-index:10}
.panel{pointer-events:auto;background:rgba(6,32,52,.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 40px rgba(0,0,0,.35)}
#info{position:absolute;top:14px;left:14px;padding:14px 16px;max-width:340px}
#info h1{margin:0;font-size:22px;letter-spacing:.02em;line-height:1.1}
#info p{margin:6px 0 0;font-size:12.5px;line-height:1.45;color:#d6f0ff}
.live{display:inline-flex;align-items:center;gap:6px;margin-top:9px;font-size:10.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;background:rgba(46,204,113,.18);border:1px solid rgba(46,204,113,.45);color:#aef0c6;padding:5px 9px;border-radius:999px}
.dot{width:7px;height:7px;border-radius:50%;background:#2ee66b;box-shadow:0 0 10px #2ee66b;animation:pulse 1.4s infinite}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.5)}}
.stats{display:flex;gap:8px;margin-top:11px;flex-wrap:wrap}
.stat{flex:1;min-width:78px;background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:7px 9px;text-align:center}
.stat b{display:block;font-size:18px;line-height:1}
.stat span{font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:#bfe8ff;font-weight:700}
.legend{display:flex;gap:10px;flex-wrap:wrap;margin-top:11px;font-size:11px;color:#dff4ff}
.legend i{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:-1px;border:1px solid rgba(255,255,255,.5)}
.sliderRow{margin-top:12px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:9px 10px}
.sliderRow label{display:flex;justify-content:space-between;font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:#cdefff}
.sliderRow input{width:100%;margin-top:7px;accent-color:#ffb84d;cursor:pointer}
#mode{margin-top:8px;font-size:11px;color:#ffe9b0}
#controls{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);display:flex;gap:9px;align-items:center;padding:10px;pointer-events:auto;max-width:94vw;flex-wrap:wrap;justify-content:center}
button{pointer-events:auto;cursor:pointer;border:1px solid rgba(255,255,255,.22);background:linear-gradient(180deg,#2ea8e0,#1477b8);color:#fff;font-weight:800;font-size:13px;padding:11px 16px;border-radius:999px;box-shadow:0 8px 20px rgba(0,0,0,.28);transition:transform .15s,filter .15s;white-space:nowrap}
button:hover{transform:translateY(-2px);filter:brightness(1.15)}
button:active{transform:translateY(0) scale(.97)}
button.secondary{background:linear-gradient(180deg,#ff9a3d,#e05e1a)}
button.dark{background:linear-gradient(180deg,#5b6cff,#2b2f7a)}
#hint{position:absolute;left:14px;bottom:14px;font-size:11.5px;line-height:1.5;color:#e6f7ff;background:rgba(6,32,52,.5);border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:9px 11px;backdrop-filter:blur(10px);max-width:300px}
#toast{position:absolute;top:16px;left:50%;transform:translate(-50%,-16px);background:rgba(255,184,77,.96);color:#3a2100;font-weight:900;font-size:13px;padding:10px 16px;border-radius:999px;opacity:0;transition:all .25s;box-shadow:0 10px 30px rgba(0,0,0,.35);pointer-events:none}
#toast.show{opacity:1;transform:translate(-50%,0)}
#cursorRing{position:fixed;width:46px;height:46px;border:2.5px solid rgba(255,255,255,.95);border-radius:50%;pointer-events:none;z-index:20;transform:translate(-50%,-50%);box-shadow:0 0 0 6px rgba(255,255,255,.12),0 0 22px rgba(255,255,255,.45);transition:border-color .2s}
#cursorRing::after{content:"";position:absolute;left:50%;top:50%;width:7px;height:7px;background:#fff;border-radius:50%;transform:translate(-50%,-50%)}
#cursorRing.demo{border-color:#ffb84d;box-shadow:0 0 0 6px rgba(255,184,77,.16),0 0 24px rgba(255,184,77,.7)}
#cursorRing.demo::after{background:#ffb84d}
@media(max-width:640px){
#info{max-width:calc(100vw - 28px);padding:11px 12px}
#info h1{font-size:18px}
#info p{display:none}
.legend{display:none}
#hint{display:none}
#controls{bottom:10px;padding:8px;gap:6px}
button{padding:9px 12px;font-size:12px}
#toast{top:150px;font-size:12px}
}
</style>
</head>
<body>
<div id="scene"></div>
<div id="cursorRing" class="demo"></div>
<div class="overlay">
<div id="info" class="panel">
<h1>🐠 Coral Cove<br><span style="font-size:13px;color:#9be7ff;letter-spacing:.14em;text-transform:uppercase">3D Aquarium</span></h1>
<p>Move your cursor <b>near a fish</b> — it senses you and darts away! Click the tank to drop food. Drag to orbit the aquarium.</p>
<div class="live"><span class="dot"></span> Live • Flee Mode</div>
<div class="stats">
<div class="stat"><b id="fishCount">20</b><span>Fish</span></div>
<div class="stat"><b id="fleeCount">0</b><span>Startled</span></div>
<div class="stat"><b id="foodCount">0</b><span>Fed</span></div>
</div>
<div class="legend">
<span><i style="background:#ff7b24"></i>Clown</span>
<span><i style="background:#2a7fff"></i>Blue Tang</span>
<span><i style="background:#ffd93b"></i>Yellow</span>
<span><i style="background:#ff6b9d"></i>Anthias</span>
<span><i style="background:#dbeafe"></i>School</span>
</div>
<div class="sliderRow">
<label><span>⚡ Scare radius</span><span id="radiusVal">2.3 m</span></label>
<input id="radius" type="range" min="10" max="42" value="23">
</div>
<div id="mode">🤖 Demo cursor active — move mouse to take over</div>
</div>
<div id="toast">🐠 A fish darted away!</div>
<div id="controls" class="panel">
<button id="feedBtn">🍤 Drop Food</button>
<button id="scatterBtn" class="secondary">💨 Scatter All</button>
<button id="nightBtn" class="dark">🌙 Night Lights</button>
</div>
<div id="hint"><b>How it works:</b> Fish detect your cursor in 3D (and on screen). Get close and they burst away with bubbles. Drop food and watch them calmly come back to eat.</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container=document.getElementById('scene');
const ringEl=document.getElementById('cursorRing');
const fleeEl=document.getElementById('fleeCount');
const foodEl=document.getElementById('foodCount');
const toastEl=document.getElementById('toast');
const modeEl=document.getElementById('mode');
const radiusInput=document.getElementById('radius');
const radiusVal=document.getElementById('radiusVal');
const rand=(a,b)=>a+Math.random()*(b-a);
const V3=(x,y,z)=>new THREE.Vector3(x,y,z);
let fleeRadius=2.3, fleeCount=0, eaten=0, isNight=false, lastToast=0;
// Scene
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x0b4a73);
scene.fog=new THREE.FogExp2(0x0b4a73,0.026);
const camera=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,0.1,100);
camera.position.set(0,0.9,15.6);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.15;
container.appendChild(renderer.domElement);
const controls=new OrbitControls(camera,renderer.domElement);
controls.target.set(0,0.1,0);
controls.enableDamping=true; controls.dampingFactor=0.06;
controls.enablePan=false;
controls.minDistance=9; controls.maxDistance=20;
controls.minAzimuthAngle=-0.55; controls.maxAzimuthAngle=0.55;
controls.minPolarAngle=1.15; controls.maxPolarAngle=1.95;
// Lights
const ambient=new THREE.AmbientLight(0x88ccee,0.95); scene.add(ambient);
const sun=new THREE.DirectionalLight(0xffffff,1.6); sun.position.set(6,11,8); scene.add(sun);
const fill=new THREE.PointLight(0xff9a5a,18,22); fill.position.set(-6,2,5); scene.add(fill);
const mouseLight=new THREE.PointLight(0xffffff,6,9); scene.add(mouseLight);
const backLight=new THREE.PointLight(0x66d9ff,14,20); backLight.position.set(0,4,-4); scene.add(backLight);
// Textures
function causticTexture(){
const c=document.createElement('canvas');c.width=c.height=256;const g=c.getContext('2d');
g.fillStyle='black';g.fillRect(0,0,256,256);
for(let i=0;i<90;i++){const x=Math.random()*256,y=Math.random()*256,r=rand(12,42);const gr=g.createRadialGradient(x,y,0,x,y,r);gr.addColorStop(0,'rgba(255,255,255,0.85)');gr.addColorStop(1,'rgba(255,255,255,0)');g.fillStyle=gr;g.beginPath();g.arc(x,y,r,0,7);g.fill();}
const t=new THREE.CanvasTexture(c);t.wrapS=t.wrapT=THREE.RepeatWrapping;t.repeat.set(3,2);return t;
}
function rayTexture(){
const c=document.createElement('canvas');c.width=64;c.height=256;const g=c.getContext('2d');
const gr=g.createLinearGradient(0,0,0,256);gr.addColorStop(0,'rgba(255,255,255,0.9)');gr.addColorStop(1,'rgba(255,255,255,0)');
g.fillStyle=gr;g.fillRect(0,0,64,256);
const t=new THREE.CanvasTexture(c);return t;
}
const caustic=causticTexture();
const rayTex=rayTexture();
// Tank & environment
const tankGroup=new THREE.Group(); scene.add(tankGroup);
// back wall gradient
{
const c=document.createElement('canvas');c.width=16;c.height=256;const g=c.getContext('2d');
const gr=g.createLinearGradient(0,0,0,256);gr.addColorStop(0,'#1e88c7');gr.addColorStop(0.5,'#0b4a73');gr.addColorStop(1,'#052a44');
g.fillStyle=gr;g.fillRect(0,0,16,256);
const tex=new THREE.CanvasTexture(c);
const wall=new THREE.Mesh(new THREE.PlaneGeometry(30,16),new THREE.MeshBasicMaterial({map:tex,fog:false}));
wall.position.set(0,1,-6); tankGroup.add(wall);
}
// glass
{
const glass=new THREE.Mesh(new THREE.BoxGeometry(22,9,8),new THREE.MeshPhysicalMaterial({color:0xbfefff,transparent:true,opacity:0.07,roughness:0.05,metalness:0,side:THREE.DoubleSide,depthWrite:false}));
glass.position.y=0; tankGroup.add(glass);
const edges=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(22,9,8)),new THREE.LineBasicMaterial({color:0x1a2f3f}));
edges.position.y=0; tankGroup.add(edges);
}
// water surface + caustics
const water=new THREE.Mesh(new THREE.PlaneGeometry(22,8),new THREE.MeshBasicMaterial({map:caustic,transparent:true,opacity:0.28,side:THREE.DoubleSide,depthWrite:false,blending:THREE.AdditiveBlending}));
water.rotation.x=-Math.PI/2; water.position.y=4.35; scene.add(water);
// sand
{
const geo=new THREE.PlaneGeometry(22,8,64,24); geo.rotateX(-Math.PI/2);
const pos=geo.attributes.position;
for(let i=0;i<pos.count;i++){const x=pos.getX(i),z=pos.getZ(i);pos.setY(i,Math.sin(x*0.7)*0.14+Math.cos(z*1.1)*0.1+rand(-0.05,0.05)-Math.abs(x)*0.01);}
geo.computeVertexNormals();
const sand=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0xe8d9a3,roughness:1}));
sand.position.y=-4.35; scene.add(sand);
const sandLight=new THREE.Mesh(new THREE.PlaneGeometry(22,8),new THREE.MeshBasicMaterial({map:caustic,transparent:true,opacity:0.16,depthWrite:false,blending:THREE.AdditiveBlending}));
sandLight.rotation.x=-Math.PI/2; sandLight.position.y=-4.12; scene.add(sandLight);
// speckles
const pts=[]; for(let i=0;i<600;i++) pts.push(rand(-10.5,10.5),-4.1,rand(-3.8,3.8));
const pg=new THREE.BufferGeometry(); pg.setAttribute('position',new THREE.Float32BufferAttribute(pts,3));
scene.add(new THREE.Points(pg,new THREE.PointsMaterial({color:0xfff6c9,size:0.07})));
}
// light rays
const rays=[];
for(let i=0;i<4;i++){
const m=new THREE.Mesh(new THREE.PlaneGeometry(rand(2.5,4.5),11),new THREE.MeshBasicMaterial({map:rayTex,transparent:true,opacity:0.11,side:THREE.DoubleSide,depthWrite:false,blending:THREE.AdditiveBlending}));
m.position.set(rand(-7,7),3,rand(-3,1)); m.rotation.z=rand(0.15,0.4); m.rotation.y=rand(-0.2,0.2); scene.add(m); rays.push(m);
}
// plankton
const planktonGeo=new THREE.BufferGeometry();
{
const pts=[]; for(let i=0;i<320;i++) pts.push(rand(-10,10),rand(-3.8,3.8),rand(-3.5,3.5));
planktonGeo.setAttribute('position',new THREE.Float32BufferAttribute(pts,3));
}
const plankton=new THREE.Points(planktonGeo,new THREE.PointsMaterial({color:0xffffff,size:0.06,transparent:true,opacity:0.45}));
scene.add(plankton);
// Rocks, corals, plants, castle
const coralMats=[], weeds=[], anemones=[];
function rock(x,y,z,s){
const m=new THREE.Mesh(new THREE.DodecahedronGeometry(s,0),new THREE.MeshStandardMaterial({color:0x7a7d7a,roughness:0.95,flatShading:true}));
m.position.set(x,y,z); m.rotation.set(rand(0,3),rand(0,3),rand(0,3)); m.scale.y=0.65; scene.add(m); return m;
}
rock(-9,-4,-2,1.5); rock(-8,-4.1,-1,1.1); rock(8.5,-4,-2.5,1.6); rock(9,-4.1,-0.5,1.0); rock(5,-4.2,2.5,0.9); rock(-4,-4.2,2.8,1.0); rock(0,-4.3,-3.5,1.2); rock(-6.5,-4.1,1.8,0.7); rock(7,-4.1,1.5,0.8);
// castle
{
const g=new THREE.Group(); g.position.set(-7.6,-4.2,-2.8);
const mat=new THREE.MeshStandardMaterial({color:0x9aa3ab,roughness:0.9});
const base=new THREE.Mesh(new THREE.BoxGeometry(2.2,1.3,1.2),mat); base.position.y=0.65; g.add(base);
[[-0.9,0],[0.9,0]].forEach(p=>{const t=new THREE.Mesh(new THREE.CylinderGeometry(0.42,0.42,2.1,10),mat);t.position.set(p[0],1.05,0);g.add(t);const r=new THREE.Mesh(new THREE.ConeGeometry(0.55,0.6,10),new THREE.MeshStandardMaterial({color:0xff6b4a}));r.position.set(p[0],2.4,0);g.add(r);});
scene.add(g);
}
// chest + coins
{
const g=new THREE.Group(); g.position.set(6.8,-4.05,-1.2); g.rotation.y=-0.5;
const chest=new THREE.Mesh(new THREE.BoxGeometry(1.1,0.55,0.7),new THREE.MeshStandardMaterial({color:0x7a4a22,roughness:0.8})); chest.position.y=0.27; g.add(chest);
const lid=new THREE.Mesh(new THREE.BoxGeometry(1.1,0.22,0.7),new THREE.MeshStandardMaterial({color:0x9a6130})); lid.position.y=0.62; lid.rotation.z=0.25; g.add(lid);
for(let i=0;i<9;i++){const c=new THREE.Mesh(new THREE.SphereGeometry(0.11,10,8),new THREE.MeshStandardMaterial({color:0xffd23f,metalness:0.9,roughness:0.25,emissive:0x664400}));c.scale.y=0.45;c.position.set(rand(-0.4,0.4),rand(0.55,0.75),rand(-0.25,0.25));g.add(c);}
scene.add(g);
}
function coral(x,z,color,scale=1){
const mat=new THREE.MeshStandardMaterial({color,roughness:0.6,emissive:color,emissiveIntensity:0.08}); coralMats.push(mat);
const g=new THREE.Group(); g.position.set(x,-4.2,z);
function branch(parent,pos,dir,len,rad,depth){
const geo=new THREE.CylinderGeometry(rad*0.65,rad,len,6); geo.translate(0,len/2,0);
const m=new THREE.Mesh(geo,mat); m.position.copy(pos); m.quaternion.setFromUnitVectors(V3(0,1,0),dir.clone().normalize()); parent.add(m);
if(depth<=0) return;
const end=pos.clone().add(dir.clone().multiplyScalar(len));
for(let i=0;i<2;i++){const nd=dir.clone(); nd.x+=rand(-0.6,0.6); nd.z+=rand(-0.6,0.6); nd.y+=rand(0.1,0.5); branch(parent,end,nd,len*0.68,rad*0.7,depth-1);}
}
branch(g,V3(0,0,0),V3(0,1,0),0.9*scale,0.14*scale,3);
scene.add(g);
}
coral(-3.5,-2.8,0xff6b9d,1.1); coral(3.5,-3,0xff9f43,0.95); coral(8,-3.2,0xc084fc,0.8); coral(-9,0.5,0x4ade80,0.7); coral(0.5,-3.4,0xf472b6,0.75);
function seaweed(x,z,h){
const geo=new THREE.PlaneGeometry(0.28,h,1,6); geo.translate(0,h/2,0);
const m=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0x1fa85a,side:THREE.DoubleSide,roughness:0.7}));
m.position.set(x,-4.2,z); m.rotation.y=rand(0,3); scene.add(m); weeds.push({m,ph:rand(0,6),h});
}
for(let i=0;i<18;i++) seaweed(rand(-10,10),rand(-3.5,-1),rand(1.4,3.2));
for(let i=0;i<6;i++) seaweed(rand(-10,10),rand(1,3.2),rand(1,2));
function anemone(x,z,color){
const g=new THREE.Group(); g.position.set(x,-4.15,z);
const base=new THREE.Mesh(new THREE.SphereGeometry(0.45,12,10),new THREE.MeshStandardMaterial({color,roughness:0.6,emissive:color,emissiveIntensity:0.15})); base.scale.y=0.6; g.add(base); coralMats.push(base.material);
for(let i=0;i<22;i++){const a=rand(0,Math.PI*2),r=rand(0.1,0.38);const t=new THREE.Mesh(new THREE.ConeGeometry(0.045,0.55,5),new THREE.MeshStandardMaterial({color:0x7ef0a8,emissive:0x1a5c33,emissiveIntensity:0.3}));t.position.set(Math.cos(a)*r,0.25,Math.sin(a)*r);t.rotation.set(rand(-0.4,0.4),0,rand(-0.4,0.4));g.add(t);}
scene.add(g); anemones.push({g,ph:rand(0,6)});
}
anemone(4.8,2.2,0xff8fab); anemone(-5,2.6,0xffb3c6); anemone(1.5,3,0xc77dff);
function starfish(x,z,color){
const s=new THREE.Shape(); for(let i=0;i<10;i++){const a=i*Math.PI/5-Math.PI/2,r=i%2?0.2:0.42;const px=Math.cos(a)*r,py=Math.sin(a)*r;i?s.lineTo(px,py):s.moveTo(px,py);} s.closePath();
const m=new THREE.Mesh(new THREE.ExtrudeGeometry(s,{depth:0.08,bevelEnabled:true,bevelSize:0.03,bevelThickness:0.03}),new THREE.MeshStandardMaterial({color,roughness:0.8}));
m.rotation.x=-Math.PI/2; m.position.set(x,-4.25,z); scene.add(m);
}
starfish(2,-1.8,0xff8c42); starfish(-2.5,1.2,0xff5d8f);
// mouse ripple ring in 3D
const ripple=new THREE.Mesh(new THREE.RingGeometry(0.55,0.68,40),new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:0.65,side:THREE.DoubleSide,depthWrite:false}));
scene.add(ripple);
const ripple2=new THREE.Mesh(new THREE.RingGeometry(0.9,0.96,40),new THREE.MeshBasicMaterial({color:0xffb84d,transparent:true,opacity:0.4,side:THREE.DoubleSide,depthWrite:false}));
scene.add(ripple2);
// Fish
function tailGeo(){
const s=new THREE.Shape(); s.moveTo(0,0); s.lineTo(0.55,0.42); s.lineTo(0.72,0); s.lineTo(0.55,-0.42); s.lineTo(0,0);
const g=new THREE.ShapeGeometry(s); g.rotateY(Math.PI/2); return g;
}
function finGeo(){
const s=new THREE.Shape(); s.moveTo(0,0); s.lineTo(0.42,0.22); s.lineTo(0.5,0); s.lineTo(0.42,-0.22); s.lineTo(0,0);
const g=new THREE.ShapeGeometry(s); g.rotateY(Math.PI/2); return g;
}
const sharedTail=tailGeo(), sharedFin=finGeo();
const fishes=[];
function createFish(cfg){
const group=new THREE.Group(); const bodyG=new THREE.Group(); group.add(bodyG);
const mat=new THREE.MeshStandardMaterial({color:cfg.color,roughness:0.35,metalness:cfg.metal?0.55:0.05,emissive:cfg.color,emissiveIntensity:0.06});
const body=new THREE.Mesh(new THREE.SphereGeometry(0.5,22,16),mat); body.scale.set(0.55,0.78,1.55); bodyG.add(body);
const belly=new THREE.Mesh(new THREE.SphereGeometry(0.42,16,12),new THREE.MeshStandardMaterial({color:cfg.belly||0xffe9c9,roughness:0.5})); belly.scale.set(0.42,0.5,1.1); belly.position.set(0,-0.12,0.08); bodyG.add(belly);
if(cfg.stripes){
[0.28,-0.18].forEach(z=>{const t=new THREE.Mesh(new THREE.TorusGeometry(0.28,0.075,8,24),new THREE.MeshStandardMaterial({color:0xffffff,roughness:0.4}));t.position.z=z;t.scale.set(1,1.32,1);bodyG.add(t);});
}
const eyeMat=new THREE.MeshStandardMaterial({color:0x111111,roughness:0.2});
[-1,1].forEach(s=>{const e=new THREE.Mesh(new THREE.SphereGeometry(0.085,10,10),eyeMat);e.position.set(0.22*s,0.14,0.52);bodyG.add(e);const w=new THREE.Mesh(new THREE.SphereGeometry(0.035,8,8),new THREE.MeshBasicMaterial({color:0xffffff}));w.position.set(0.27*s,0.18,0.58);bodyG.add(w);});
const tailPivot=new THREE.Group(); tailPivot.position.z=-0.68; bodyG.add(tailPivot);
const tailMat=new THREE.MeshStandardMaterial({color:cfg.tail||cfg.color,side:THREE.DoubleSide,roughness:0.45,emissive:cfg.tail||cfg.color,emissiveIntensity:0.1});
const tail=new THREE.Mesh(sharedTail,tailMat); tailPivot.add(tail);
const dorsal=new THREE.Mesh(sharedFin,new THREE.MeshStandardMaterial({color:cfg.tail||cfg.color,side:THREE.DoubleSide,roughness:0.45})); dorsal.position.set(0,0.34,-0.05); dorsal.rotation.x=-0.2; bodyG.add(dorsal);
const pecs=[];
[-1,1].forEach(s=>{const p=new THREE.Mesh(new THREE.SphereGeometry(0.22,10,8),tailMat);p.scale.set(0.7,0.12,1);p.position.set(0.32*s,-0.12,0.18);bodyG.add(p);pecs.push(p);});
group.scale.setScalar(cfg.scale);
group.position.set(rand(-7,7),rand(-2,2.5),rand(-2.5,2.5));
scene.add(group);
const f={group,bodyG,tailPivot,pecs,name:cfg.name,scale:cfg.scale,baseSpeed:cfg.speed||rand(0.9,1.4),vel:V3(rand(-1,1),0,rand(-1,1)).normalize().multiplyScalar(1),wander:V3(rand(-8,8),rand(-2.5,3),rand(-2.5,2.5)),fleeDir:V3(1,0,0),scared:0,cool:0,phase:rand(0,6),isSchool:!!cfg.school};
fishes.push(f); return f;
}
const cfgs=[
{name:'Clownfish',color:0xff7b24,tail:0xffb03a,scale:0.95,stripes:true},
{name:'Clownfish',color:0xff7b24,tail:0xffb03a,scale:0.85,stripes:true},
{name:'Clownfish',color:0xff8c3a,tail:0xffc04a,scale:1.05,stripes:true},
{name:'Blue Tang',color:0x2a7fff,tail:0xffd93b,scale:1.1,belly:0x7ec8ff},
{name:'Blue Tang',color:0x3a8dff,tail:0xffd93b,scale:0.95,belly:0x7ec8ff},
{name:'Blue Tang',color:0x1f6fe0,tail:0xffe066,scale:1.0,belly:0x7ec8ff},
{name:'Yellow Fish',color:0xffd93b,tail:0xffb703,scale:0.92},
{name:'Yellow Fish',color:0xffe066,tail:0xffa500,scale:1.0},
{name:'Yellow Fish',color:0xffd23f,tail:0xff9e00,scale:0.85},
{name:'Pink Anthias',color:0xff6b9d,tail:0xff8fab,scale:0.82},
{name:'Pink Anthias',color:0xff85a8,tail:0xffa8c5,scale:0.9},
{name:'Pink Anthias',color:0xf94d8a,tail:0xff7ba9,scale:0.78},
{name:'Purple Fish',color:0x9b5de5,tail:0xc77dff,scale:0.95},
{name:'Purple Fish',color:0x7b4bc4,tail:0xb388ff,scale:0.88},
];
cfgs.forEach(c=>createFish(c));
for(let i=0;i<6;i++) createFish({name:'Silver School Fish',color:0xdbeafe,tail:0xbfe3ff,scale:0.42,metal:true,belly:0xffffff,school:true,speed:1.7});
document.getElementById('fishCount').textContent=fishes.length;
// Bubbles
const bubbles=[];
const bubbleGeo=new THREE.SphereGeometry(1,10,8);
const bubbleMat=new THREE.MeshStandardMaterial({color:0xffffff,transparent:true,opacity:0.35,roughness:0.1});
const vents=[V3(-8,-3.8,-1),V3(8.5,-3.8,-1),V3(0,-3.8,-3)];
for(let i=0;i<65;i++){
const m=new THREE.Mesh(bubbleGeo,bubbleMat); const s=rand(0.04,0.13); m.scale.setScalar(s);
const v=vents[i%vents.length]; m.position.set(v.x+rand(-0.3,0.3),rand(-4,4),v.z+rand(-0.3,0.3));
scene.add(m); bubbles.push({m,speed:rand(0.5,1.1),ph:rand(0,6),vent:v});
}
function burst(pos){
for(let i=0;i<4;i++){const b=bubbles[Math.floor(Math.random()*bubbles.length)];b.m.position.copy(pos);b.m.position.x+=rand(-0.3,0.3);b.m.position.y+=rand(-0.2,0.2);b.m.scale.setScalar(rand(0.06,0.16));}
}
// Food
const foods=[];
const foodMat=new THREE.MeshStandardMaterial({color:0xff8c42,roughness:0.8});
function dropFood(cx){
for(let i=0;i<14;i++){
const m=new THREE.Mesh(new THREE.SphereGeometry(rand(0.06,0.1),8,8),foodMat);
m.position.set((cx??rand(-5,5))+rand(-1.2,1.2),rand(3.2,4.3),rand(-2,2)); scene.add(m); foods.push({m,ph:rand(0,6)});
}
}
// Mouse / demo cursor
const mouseNDC=new THREE.Vector2(0,0);
const raycaster=new THREE.Raycaster();
const planeZ=new THREE.Plane(V3(0,0,1),0);
let lastMouse=-9999, clientX=innerWidth/2, clientY=innerHeight/2, downX=0,downY=0;
let demoActive=true;
window.addEventListener('pointermove',e=>{
clientX=e.clientX; clientY=e.clientY;
mouseNDC.x=(e.clientX/innerWidth)*2-1; mouseNDC.y=-(e.clientY/innerHeight)*2+1;
lastMouse=performance.now()/1000; demoActive=false;
});
renderer.domElement.addEventListener('pointerdown',e=>{downX=e.clientX;downY=e.clientY;});
renderer.domElement.addEventListener('pointerup',e=>{
if(Math.hypot(e.clientX-downX,e.clientY-downY)<6){
raycaster.setFromCamera(mouseNDC,camera); const p=new THREE.Vector3();
if(raycaster.ray.intersectPlane(planeZ,p)) dropFood(THREE.MathUtils.clamp(p.x,-9,9));
}
});
radiusInput.addEventListener('input',()=>{fleeRadius=radiusInput.value/10; radiusVal.textContent=fleeRadius.toFixed(1)+' m';});
function showToast(txt){
const now=performance.now(); if(now-lastToast<700) return; lastToast=now;
toastEl.textContent=txt; toastEl.classList.add('show'); setTimeout(()=>toastEl.classList.remove('show'),1400);
}
document.getElementById('feedBtn').addEventListener('click',()=>{dropFood(rand(-6,6)); showToast('🍤 Food dropped!');});
document.getElementById('scatterBtn').addEventListener('click',()=>{
fishes.forEach(f=>{f.fleeDir.copy(f.group.position).setY(0).add(V3(rand(-1,1),rand(-0.3,0.5),rand(-1,1))).normalize(); f.scared=1.3; f.cool=1; burst(f.group.position);});
fleeCount++; fleeEl.textContent=fleeCount; showToast('💨 Scatter! All fish flee!');
});
document.getElementById('nightBtn').addEventListener('click',e=>{
isNight=!isNight; e.target.textContent=isNight?'☀️ Day Lights':'🌙 Night Lights';
scene.background.set(isNight?0x04172e:0x0b4a73); scene.fog.color.set(isNight?0x04172e:0x0b4a73);
ambient.intensity=isNight?0.35:0.95; sun.intensity=isNight?0.25:1.6; fill.intensity=isNight?28:18;
rays.forEach(r=>r.material.opacity=isNight?0.04:0.11);
coralMats.forEach(m=>m.emissiveIntensity=isNight?0.55:0.08);
showToast(isNight?'🌙 Night in the reef':'☀️ Day lights on');
});
// Animation
const clock=new THREE.Clock(); let elapsed=0;
const tmpV=new THREE.Vector3(), tmpV2=new THREE.Vector3(), tmpM=new THREE.Matrix4(), tmpQ=new THREE.Quaternion();
const schoolTarget=V3(0,1,0); let schoolTimer=0;
const mouseWorld=V3(0,0,0);
function pickWander(f){f.wander.set(rand(-8.5,8.5),rand(-2.8,3.0),rand(-3,3));}
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05); elapsed+=dt; const t=elapsed;
controls.update();
// demo cursor
const now=performance.now()/1000;
if(now-lastMouse>4){
demoActive=true;
mouseNDC.x=Math.sin(t*0.45)*0.62; mouseNDC.y=Math.sin(t*0.82+1)*0.38;
clientX=(mouseNDC.x*0.5+0.5)*innerWidth; clientY=(-mouseNDC.y*0.5+0.5)*innerHeight;
}
ringEl.style.left=clientX+'px'; ringEl.style.top=clientY+'px'; ringEl.classList.toggle('demo',demoActive);
modeEl.textContent=demoActive?'🤖 Demo cursor active — move mouse to take over':'🖱️ You are scaring the fish — get close!';
raycaster.setFromCamera(mouseNDC,camera);
let hasPoint=false;
if(raycaster.ray.intersectPlane(planeZ,mouseWorld)){hasPoint=Math.abs(mouseWorld.x)<11&&Math.abs(mouseWorld.y)<4.5;}
ripple.visible=ripple2.visible=hasPoint;
if(hasPoint){
ripple.position.copy(mouseWorld); ripple2.position.copy(mouseWorld);
ripple.lookAt(camera.position); ripple2.lookAt(camera.position);
const s=1+Math.sin(t*5)*0.15; ripple.scale.setScalar(s); ripple2.scale.setScalar(1+Math.sin(t*3)*0.2);
mouseLight.position.copy(mouseWorld); mouseLight.intensity=isNight?9:5;
} else { mouseLight.intensity=0; }
schoolTimer-=dt; if(schoolTimer<=0){schoolTarget.set(rand(-6,6),rand(-1.5,2.5),rand(-2,2)); schoolTimer=rand(3,6);}
// fish
for(const f of fishes){
const pos=f.group.position;
if(f.scared>0)f.scared-=dt; if(f.cool>0)f.cool-=dt;
// flee detection: distance to cursor ray + screen distance
const closest=new THREE.Vector3(); raycaster.ray.closestPointToPoint(pos,closest);
const dRay=pos.distanceTo(closest);
const proj=pos.clone().project(camera);
const dScreen=Math.hypot(proj.x-mouseNDC.x,proj.y-mouseNDC.y);
const near=(dRay < fleeRadius*f.scale+0.7) || (dScreen<0.16 && proj.z<1);
if(near){
let away=pos.clone().sub(closest);
if(away.lengthSq()<0.02) away.set(rand(-1,1),rand(-0.3,0.6),rand(-1,1));
away.normalize(); f.fleeDir.copy(away);
if(f.scared<=0 && f.cool<=0){f.scared=0.55; f.cool=0.9; fleeCount++; fleeEl.textContent=fleeCount; showToast('🐠 '+f.name+' darted away!'); burst(pos);}
else f.scared=Math.max(f.scared,0.35);
}
// target
let target=f.isSchool?schoolTarget:f.wander;
let nearestFood=null, nd=1e9;
if(f.scared<=0){ for(const fd of foods){const d=pos.distanceTo(fd.m.position); if(d<nd){nd=d;nearestFood=fd;}} if(nearestFood&&nd<7) target=nearestFood.m.position; }
let toTarget=tmpV.copy(target).sub(pos); const distT=toTarget.length();
if(distT<0.6 && !f.isSchool && f.scared<=0) pickWander(f);
toTarget.normalize();
const desired=new THREE.Vector3();
if(f.scared>0){ desired.copy(f.fleeDir).multiplyScalar(f.baseSpeed*3.3); desired.add(tmpV2.copy(toTarget).multiplyScalar(0.4)); }
else { desired.copy(toTarget).multiplyScalar(f.baseSpeed*(nearestFood&&nd<7?1.5:1)); }
// separation
for(const o of fishes){ if(o===f)continue; const d=pos.distanceTo(o.group.position); if(d<0.85&&d>0.001){ desired.add(tmpV2.copy(pos).sub(o.group.position).normalize().multiplyScalar((0.85-d)*2)); } }
// bounds
if(pos.x>9)desired.x-=(pos.x-9)*2; if(pos.x<-9)desired.x-=(pos.x+9)*2;
if(pos.y>3.2)desired.y-=(pos.y-3.2)*2; if(pos.y<-3.2)desired.y-=(pos.y+3.2)*2;
if(pos.z>3)desired.z-=(pos.z-3)*2; if(pos.z<-3)desired.z-=(pos.z+3)*2;
const maxS=f.baseSpeed*3.6; if(desired.length()>maxS)desired.setLength(maxS);
f.vel.lerp(desired,1-Math.exp(-(f.scared>0?6.5:2.6)*dt));
pos.addScaledVector(f.vel,dt);
pos.x=THREE.MathUtils.clamp(pos.x,-10,10); pos.y=THREE.MathUtils.clamp(pos.y,-3.8,3.8); pos.z=THREE.MathUtils.clamp(pos.z,-3.6,3.6);
// orientation
tmpM.lookAt(pos,tmpV2.copy(pos).add(f.vel),V3(0,1,0)); tmpQ.setFromRotationMatrix(tmpM);
f.group.quaternion.slerp(tmpQ,1-Math.exp(-8*dt));
// tail + wiggle
const speed=f.vel.length(); const freq=5+speed*3.2;
f.tailPivot.rotation.y=Math.sin(t*freq+f.phase)*0.6;
f.bodyG.rotation.y=Math.sin(t*freq+f.phase)*0.13;
f.bodyG.rotation.z=Math.sin(t*freq*0.5+f.phase)*0.06;
f.pecs.forEach((p,i)=>{p.rotation.z=(i? -1:1)*Math.sin(t*8+f.phase)*0.4;});
}
// food
for(let i=foods.length-1;i>=0;i--){
const fd=foods[i]; fd.m.position.y-=0.38*dt; fd.m.position.x+=Math.sin(t*2+fd.ph)*0.15*dt;
let eatenNow=false;
for(const f of fishes){ if(f.group.position.distanceTo(fd.m.position)<0.55*f.scale+0.25){eatenNow=true;break;} }
if(eatenNow){scene.remove(fd.m); foods.splice(i,1); eaten++; foodEl.textContent=eaten; continue;}
if(fd.m.position.y<-4.1){scene.remove(fd.m); foods.splice(i,1);}
}
// bubbles
for(const b of bubbles){ b.m.position.y+=b.speed*dt; b.m.position.x+=Math.sin(t*3+b.ph)*0.18*dt; if(b.m.position.y>4.2){b.m.position.set(b.vent.x+rand(-0.3,0.3),-3.9,b.vent.z+rand(-0.3,0.3));} }
// plants
weeds.forEach(w=>{w.m.rotation.z=Math.sin(t*0.9+w.ph)*0.16; w.m.rotation.x=Math.cos(t*0.7+w.ph)*0.08;});
anemones.forEach(a=>{a.g.rotation.z=Math.sin(t*1.1+a.ph)*0.08; a.g.rotation.x=Math.cos(t*0.9+a.ph)*0.06;});
plankton.rotation.y=t*0.015; plankton.position.y=Math.sin(t*0.4)*0.15;
caustic.offset.x=t*0.015; caustic.offset.y=t*0.01;
rays.forEach((r,i)=>{r.material.opacity=(isNight?0.04:0.11)+Math.sin(t*0.8+i)*0.025;});
water.position.y=4.35+Math.sin(t*0.8)*0.04;
renderer.render(scene,camera);
}
animate();
window.addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>Reasoning trace (9,392 tokens)
Generating a self-contained Threejs web demo with import map for a 3D aquarium featuring fish boid movement and mouse-triggered flee behavior.Adding responsive controls, fish geometry, and screen-space mouse flee logic to the boid simulation.Extending the boid simulation with underwater visuals, tank geometry, and fish AI behaviors.Adding interactive flee logic, demo cursor, and UI controls for the underwater scene.Integrating OrbitControls with limited rotation and damping while handling parallax conflicts and adding scene toggles.Extending the scene with fish orientation fixes, schooling behavior, flee detection, and particle effects.Implementing UI controls, lighting toggles, counters, and performance-aware shadow settings.Adding sand displacement, instanced speckles, scene props, and fish scare and animation logic.Adding demo cursor pathing, caustic and light ray visuals, UI overlays, and toast notifications.Adjusting fish tail and fin geometry orientation and adding seaweed, coral, and anemone meshes.