Interactive 3D aquariumOne-line
GPT-6.1 Terra
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 56 s
- Cost
- $0.046
- Output tokens
- 9,097
- Spent thinking
- 1,954 tokens
- Speed
- 83.3 tok/s
- Console errors
- 0
- First token after
- 7.1 s
- First visible text after
- 38.8 s
- Prompt tokens
- 423
- Code size
- 20.4 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
model default- Max output
- 128,000 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 411 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Aquarium — A little world underwater</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; }
:root { color-scheme: dark; }
body {
margin: 0;
min-height: 100vh;
color: #edf8f5;
background: radial-gradient(ellipse at 50% 42%, #143a43 0%, #0b242d 43%, #07171e 100%);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.page { max-width: 1200px; margin: 0 auto; padding: 25px 32px 18px; }
header { display: flex; align-items: center; justify-content: space-between; }
.brand { display: flex; gap: 11px; align-items: center; }
.brand-icon {
width: 35px; height: 35px; display: grid; place-items: center;
border: 1px solid #527a7b; border-radius: 50%; color: #9fe3ce;
}
.brand-name { font-size: 11px; letter-spacing: .22em; font-weight: 750; }
.brand-sub { margin-top: 4px; font-size: 8px; letter-spacing: .19em; color: #81a8a8; }
.live { display: flex; align-items: center; gap: 8px; font-size: 9px; font-weight: 700; letter-spacing: .16em; color: #b4d4cc; }
.dot { width: 6px; height: 6px; background: #9ce6be; border-radius: 50%; box-shadow: 0 0 12px #76eab1; }
.intro { text-align: center; margin: 25px 0 21px; }
.eyebrow { color: #91c4b9; font-size: 9px; font-weight: 700; letter-spacing: .23em; text-transform: uppercase; }
h1 { margin: 8px 0 5px; font: 400 clamp(25px, 3vw, 36px)/1.1 Georgia, serif; letter-spacing: -.025em; }
.intro p { margin: 0; color: #9ab9b9; font-size: 12px; line-height: 1.5; }
.tank {
position: relative; overflow: hidden;
height: min(565px, calc(100vh - 236px)); min-height: 400px;
border: 1px solid rgba(159, 217, 210, .24);
border-radius: 12px;
background: radial-gradient(ellipse at 50% 15%, #1b7880 0%, #0c505e 43%, #082d3b 100%);
box-shadow: 0 24px 70px #0007, inset 0 0 40px #8ddbd00a;
}
canvas { display: block; width: 100%; height: 100%; touch-action: none; }
.tank::after {
content: ""; position: absolute; inset: 0; pointer-events: none;
border-radius: inherit; box-shadow: inset 0 0 60px #00202b66;
}
.overlay { position: absolute; z-index: 2; pointer-events: none; }
.top-left { top: 22px; left: 25px; }
.tag { display: flex; align-items: center; gap: 7px; color: #c2e9df; font-size: 9px; letter-spacing: .19em; font-weight: 700; }
.tag .dot { width: 5px; height: 5px; }
.tank-title { margin-top: 7px; font: 20px Georgia, serif; color: #f0faf5; }
.top-right { top: 23px; right: 25px; color: #b9ded6; font-size: 9px; letter-spacing: .16em; font-weight: 700; }
.bottom-left { bottom: 21px; left: 25px; display: flex; gap: 10px; align-items: center; }
.cursor-icon {
width: 31px; height: 31px; border: 1px solid #acd7cf72; border-radius: 50%;
display: grid; place-items: center; color: #c6eee3;
}
.hint-title { font-size: 11px; font-weight: 600; color: #e7faf3; }
.hint-sub { margin-top: 3px; font-size: 9px; color: #a8d3cb; letter-spacing: .03em; }
.bottom-right { bottom: 27px; right: 25px; color: #b7d9d1; font-size: 9px; letter-spacing: .16em; }
footer { display: flex; justify-content: space-between; align-items: center; padding: 16px 2px 0; color: #789b9c; font-size: 10px; letter-spacing: .04em; }
footer span:last-child { letter-spacing: .14em; font-size: 9px; }
@media (max-width: 600px) {
.page { padding: 19px 16px; }
.intro { margin: 30px 0 21px; }
.intro p { max-width: 270px; margin: auto; }
.tank { height: 530px; min-height: 0; }
.top-left { top: 19px; left: 18px; }
.top-right { top: 21px; right: 17px; }
.bottom-left { left: 18px; bottom: 18px; }
.bottom-right { display: none; }
footer span:last-child { display: none; }
.brand-sub { font-size: 7px; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="brand">
<div class="brand-icon">
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M3 13c3-5 8-6 13-2l4-2v7l-4-2c-5 4-10 3-13-1Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="8" cy="12" r=".9" fill="currentColor"/><path d="M4 19c3-1.4 5 1.4 8 0s5 1.4 8 0" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/></svg>
</div>
<div><div class="brand-name">THE AQUARIUM</div><div class="brand-sub">A LITTLE MOMENT OF CALM</div></div>
</div>
<div class="live"><span class="dot"></span> LIVE ECOSYSTEM</div>
</header>
<section class="intro">
<div class="eyebrow">Take a breath · Stay a while</div>
<h1>A little world, underwater.</h1>
<p>A peaceful pocket of ocean, right at your fingertips.</p>
</section>
<main class="tank" id="tank">
<div class="overlay top-left"><div class="tag"><span class="dot"></span> LIVE VIEW</div><div class="tank-title">The coral reef</div></div>
<div class="overlay top-right">12 LITTLE RESIDENTS</div>
<div class="overlay bottom-left">
<div class="cursor-icon"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 3l13 10-6 1-3 6L5 3Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></div>
<div><div class="hint-title">Come say hello</div><div class="hint-sub">Move your cursor near the fish</div></div>
</div>
<div class="overlay bottom-right">A GENTLE INTERACTION ✳</div>
</main>
<footer><span>Made for a moment of stillness.</span><span>DRIFT A LITTLE · BREATHE A LITTLE</span></footer>
</div>
<script type="module">
import * as THREE from 'three';
const container = document.getElementById('tank');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
container.prepend(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-9, 9, 4.7, -4.7, .1, 80);
camera.position.set(0, 5.2, 19);
camera.lookAt(0, 3.35, 0);
scene.add(new THREE.HemisphereLight(0xc6faff, 0x234a43, 2.0));
const sun = new THREE.DirectionalLight(0xfff5d6, 2.3);
sun.position.set(-5, 10, 8);
scene.add(sun);
const fill = new THREE.PointLight(0x4fd4d3, 45, 22);
fill.position.set(5, 5, 5);
scene.add(fill);
let seed = 3287;
function rand() { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; }
function between(a,b) { return a + (b-a)*rand(); }
function mat(color, extra = {}) { return new THREE.MeshStandardMaterial({color, roughness: .75, ...extra}); }
// Painted water at the back of the glass.
const waterCanvas = document.createElement('canvas');
waterCanvas.width = 1024; waterCanvas.height = 512;
const ctx = waterCanvas.getContext('2d');
const grad = ctx.createLinearGradient(0,0,0,512);
grad.addColorStop(0, '#438f94'); grad.addColorStop(.34, '#247681'); grad.addColorStop(1, '#0b4655');
ctx.fillStyle = grad; ctx.fillRect(0,0,1024,512);
for (let i=0; i<90; i++) {
const x=between(0,1024), y=between(0,450), w=between(20,130);
ctx.beginPath(); ctx.moveTo(x,y);
ctx.bezierCurveTo(x+w*.25,y-between(2,12),x+w*.65,y+between(2,10),x+w,y-between(0,7));
ctx.strokeStyle = `rgba(195,255,240,${between(.015,.06)})`;
ctx.lineWidth = between(.6,2); ctx.stroke();
}
const waterTex = new THREE.CanvasTexture(waterCanvas);
waterTex.colorSpace = THREE.SRGBColorSpace;
const back = new THREE.Mesh(new THREE.PlaneGeometry(16,7.3), new THREE.MeshBasicMaterial({map:waterTex}));
back.position.set(0,3.65,-2.7); scene.add(back);
// Soft light streaming through the water.
const beamMat = new THREE.MeshBasicMaterial({color:0xb6fff1, transparent:true, opacity:.035, side:THREE.DoubleSide, depthWrite:false});
for(let i=0;i<5;i++){
const shape = new THREE.Shape();
const x=-7+i*3.1;
shape.moveTo(x,7.25); shape.lineTo(x+1.1,7.25); shape.lineTo(x-1.3,0); shape.lineTo(x-2.4,0);
const beam = new THREE.Mesh(new THREE.ShapeGeometry(shape), beamMat);
beam.position.z=-2.58; scene.add(beam);
}
const sand = new THREE.Mesh(new THREE.BoxGeometry(16,.36,5.4), mat(0xcbbd8c));
sand.position.set(0,.23,0); scene.add(sand);
const sandTop = new THREE.Mesh(new THREE.PlaneGeometry(16,5.4), mat(0xe2d6a8));
sandTop.rotation.x=-Math.PI/2; sandTop.position.y=.42; scene.add(sandTop);
// Glass edges describe the tank without covering the view.
const edgeMaterial = new THREE.LineBasicMaterial({color:0xb7f0e8, transparent:true, opacity:.32});
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(16,7.3,5.4)), edgeMaterial);
edges.position.y=3.65; scene.add(edges);
const glassSideMat = new THREE.MeshBasicMaterial({color:0xb3f7ed, transparent:true, opacity:.045, side:THREE.DoubleSide, depthWrite:false});
for (const x of [-8,8]) {
const side = new THREE.Mesh(new THREE.PlaneGeometry(5.4,7.3), glassSideMat);
side.rotation.y=Math.PI/2; side.position.set(x,3.65,0); scene.add(side);
}
// Pebbles and stones.
const pebbleGeo = new THREE.SphereGeometry(1, 9, 7);
const pebbleMats = [mat(0x968f78),mat(0xb9a987),mat(0x807f73),mat(0xd2c49e)];
for(let i=0;i<100;i++){
const p = new THREE.Mesh(pebbleGeo, pebbleMats[Math.floor(rand()*pebbleMats.length)]);
const s=between(.025,.09);
p.scale.set(s*1.5,s*.5,s); p.position.set(between(-7.7,7.7),.44,between(-2.4,2.4)); scene.add(p);
}
function rock(x,z,s,color){
const r = new THREE.Mesh(new THREE.IcosahedronGeometry(1,2),mat(color));
r.scale.set(s*1.35,s*.7,s*.85); r.position.set(x,.42+s*.34,z); r.rotation.set(between(-.2,.2),between(0,3),between(-.2,.2)); scene.add(r);
}
rock(-6.3,-.7,.72,0x657e75); rock(-5.2,-1.4,.45,0x829285); rock(-4.8,1.25,.43,0x7a8e81);
rock(5.6,-1.1,.8,0x72897b); rock(6.7,.5,.55,0x8d9d89); rock(3.9,1.5,.33,0x9c9f84);
// Curved ribbons of seagrass.
const plants = [];
const leafColors=[0x248c72,0x37a27f,0x176b62,0x61ad84,0x337b67];
function makeBlade(height,width,lean,color) {
const vertices=[], indices=[];
const segments=10;
for(let i=0;i<=segments;i++){
const t=i/segments;
const center=lean*t*t + Math.sin(t*Math.PI)*.08;
const w=width*(1-t)*(.8+.2*Math.sin(t*Math.PI));
vertices.push(center-w/2,t*height,0, center+w/2,t*height,0);
if(i<segments){const n=i*2; indices.push(n,n+1,n+2,n+1,n+3,n+2);}
}
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.Float32BufferAttribute(vertices,3)); geo.setIndex(indices); geo.computeVertexNormals();
return new THREE.Mesh(geo,mat(color,{side:THREE.DoubleSide}));
}
function plantCluster(x,z,count,h){
const group=new THREE.Group(); group.position.set(x,.43,z);
for(let i=0;i<count;i++){
const blade=makeBlade(between(h*.55,h),between(.13,.28),between(-.65,.65),leafColors[Math.floor(rand()*leafColors.length)]);
blade.position.set(between(-.35,.35),0,between(-.18,.18));
blade.rotation.y=between(-.6,.6); group.add(blade);
}
scene.add(group); plants.push({group,phase:between(0,6)});
}
plantCluster(-7,-1.8,9,2.7); plantCluster(-6.1,1.1,9,1.8); plantCluster(-4.3,-2.1,8,1.8);
plantCluster(7,-1.6,10,2.9); plantCluster(6.1,1.3,8,2.1); plantCluster(4.4,-2.1,8,1.7);
plantCluster(-2.7,-2.1,6,1.25); plantCluster(2.6,-2.1,6,1.4);
// Branching coral.
function branch(points, radius, material, group) {
const curve=new THREE.CatmullRomCurve3(points.map(p=>new THREE.Vector3(...p)));
const mesh=new THREE.Mesh(new THREE.TubeGeometry(curve,12,radius,7,false),material);
group.add(mesh);
const end=points[points.length-1];
const tip=new THREE.Mesh(new THREE.SphereGeometry(radius,8,6),material);
tip.position.set(...end); group.add(tip);
}
function coral(x,z,scale,color){
const g=new THREE.Group(); g.position.set(x,.42,z); g.scale.setScalar(scale);
const m=mat(color);
branch([[0,0,0],[0,.45,0],[.04,1,0],[.12,1.45,0]],.105,m,g);
branch([[0,.45,0],[-.3,.65,0],[-.47,1.05,.03],[-.45,1.35,.03]],.085,m,g);
branch([[0,.65,0],[.35,.85,0],[.52,1.2,.02],[.53,1.48,.02]],.085,m,g);
branch([[.35,.85,0],[.68,1.02,.02],[.75,1.24,.02]],.065,m,g);
branch([[-.27,.65,0],[-.65,.78,0],[-.75,1.02,0]],.065,m,g);
scene.add(g);
}
coral(-5.1,-.2,.85,0xf29d77); coral(5.05,-.1,.8,0xe88277);
coral(-3.3,-1.7,.52,0xe9b575); coral(3.25,-1.75,.58,0xd88083);
// A tiny starfish on the sand.
const starShape=new THREE.Shape();
for(let i=0;i<10;i++){const a=i*Math.PI/5-Math.PI/2, r=i%2===0?.31:.13; const x=Math.cos(a)*r,y=Math.sin(a)*r; if(i===0) starShape.moveTo(x,y); else starShape.lineTo(x,y);}
starShape.closePath();
const star=new THREE.Mesh(new THREE.ShapeGeometry(starShape),mat(0xe78d70,{side:THREE.DoubleSide}));
star.rotation.x=-Math.PI/2; star.rotation.z=.3; star.position.set(3,.45,1.6); scene.add(star);
// Fish are built with rounded bodies, fins, eyes, and curved pale bands.
const fishList=[];
const bodyGeo=new THREE.SphereGeometry(1,24,16);
const eyeGeo=new THREE.SphereGeometry(1,12,8);
const finShape=new THREE.Shape();
finShape.moveTo(0,0); finShape.lineTo(-.65,.37); finShape.quadraticCurveTo(-.53,0,-.65,-.37); finShape.lineTo(0,0);
const tailGeo=new THREE.ShapeGeometry(finShape);
const dorsalShape=new THREE.Shape();
dorsalShape.moveTo(-.36,.17); dorsalShape.quadraticCurveTo(-.24,.52,.05,.43); dorsalShape.lineTo(.27,.23); dorsalShape.closePath();
const dorsalGeo=new THREE.ShapeGeometry(dorsalShape);
function bandGeometry(x1,x2){
const verts=[],idx=[], n=28;
for(let j=0;j<=n;j++){
const a=j/n*Math.PI*2;
for(const x of [x1,x2]){
const r=Math.sqrt(1-x*x);
verts.push(x*.64, Math.cos(a)*r*.34*1.012, Math.sin(a)*r*.235*1.012);
}
if(j<n){const k=j*2;idx.push(k,k+1,k+2,k+1,k+3,k+2);}
}
const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));g.setIndex(idx);g.computeVertexNormals();return g;
}
const band1=bandGeometry(-.34,-.21), band2=bandGeometry(.15,.25);
const white=mat(0xf8f1da), dark=mat(0x102c32), glint=new THREE.MeshBasicMaterial({color:0xffffff});
function createFish(color, finColor, bands, scale, pos){
const group=new THREE.Group();
const body=new THREE.Mesh(bodyGeo,mat(color)); body.scale.set(.64,.34,.235); group.add(body);
const tailPivot=new THREE.Group(); tailPivot.position.x=-.53;
const tail=new THREE.Mesh(tailGeo,mat(finColor,{side:THREE.DoubleSide})); tailPivot.add(tail); group.add(tailPivot);
const dorsal=new THREE.Mesh(dorsalGeo,mat(finColor,{side:THREE.DoubleSide})); group.add(dorsal);
const lower=new THREE.Mesh(dorsalGeo,mat(finColor,{side:THREE.DoubleSide})); lower.scale.set(.75,-.65,1); lower.position.x=-.07; group.add(lower);
if(bands){
const bm=mat(bands,{side:THREE.DoubleSide});
group.add(new THREE.Mesh(band1,bm)); group.add(new THREE.Mesh(band2,bm));
}
for(const side of [-1,1]){
const eye=new THREE.Mesh(eyeGeo,white); eye.scale.set(.075,.075,.035); eye.position.set(.38,.10,side*.19); group.add(eye);
const pupil=new THREE.Mesh(eyeGeo,dark); pupil.scale.set(.038,.042,.02); pupil.position.set(.395,.105,side*.222); group.add(pupil);
const shine=new THREE.Mesh(eyeGeo,glint); shine.scale.set(.013,.013,.009); shine.position.set(.405,.12,side*.241); group.add(shine);
const sideFin=new THREE.Mesh(dorsalGeo,mat(finColor,{side:THREE.DoubleSide,transparent:true,opacity:.8}));
sideFin.scale.set(.45,.42,.45);sideFin.rotation.z=-1.3;sideFin.position.set(-.08,-.02,side*.22);group.add(sideFin);
}
group.scale.setScalar(scale); group.position.copy(pos); scene.add(group);
const direction=rand()>.5?1:-1;
const fish={group,tailPivot, velocity:new THREE.Vector3(direction*between(.4,.7),between(-.12,.12),between(-.12,.12)), target:new THREE.Vector3(direction,0,0), timer:between(1,4), phase:between(0,6), angle:direction===1?0:Math.PI};
group.rotation.y=fish.angle; fishList.push(fish);
}
const colors=[
[0xffa35d,0xf1784a,0xfff1d4],
[0x72cfca,0x3baaa8,0xd6f4df],
[0xf6c965,0xe9a94d,null],
[0xf39083,0xdf6d72,0xffe6cb],
[0x93cbd7,0x58a9bd,null],
[0xffba74,0xef8756,0xfff0d7],
[0xead679,0xc6ae56,null],
[0x73c7b9,0x45a591,0xe6f5d7],
[0xf3a5a0,0xd67c82,null],
[0xf6c86e,0xe9a255,0xfff0cf],
[0x88c8df,0x5aaac2,0xe2f2eb],
[0xf6a269,0xec7d50,null]
];
const positions=[
[-5.6,5.55,.1],[-3.8,4.3,1],[-1.6,5.55,-.8],[.8,4.8,.7],[3.4,5.7,-.5],[5.6,4.25,.7],
[-6.3,2.8,-.3],[-3,2.7,.8],[-.6,3.45,-.3],[1.8,2.4,1],[4.15,3.15,-.5],[6.45,5.65,-1]
];
colors.forEach((c,i)=>createFish(c[0],c[1],c[2],between(.75,1.12),new THREE.Vector3(...positions[i])));
// Bubbles, with a small highlight on each.
const bubbleGeo=new THREE.SphereGeometry(1,12,8);
const bubbleMat=new THREE.MeshBasicMaterial({color:0xc4f8ed,transparent:true,opacity:.3,wireframe:true,depthWrite:false});
const bubbles=[];
for(let i=0;i<34;i++){
const mesh=new THREE.Mesh(bubbleGeo,bubbleMat);
const size=between(.035,.105); mesh.scale.setScalar(size);
const baseX=i%2===0?-5.8:5.8;
mesh.position.set(baseX+between(-.5,.5),between(.5,7),between(-1.7,1.2));
scene.add(mesh); bubbles.push({mesh,baseX:mesh.position.x,speed:between(.35,.8),phase:between(0,6)});
}
// Fine suspended particles.
const dustPositions=[];
for(let i=0;i<130;i++) dustPositions.push(between(-7.8,7.8),between(.6,7),between(-2.4,2));
const dustGeo=new THREE.BufferGeometry(); dustGeo.setAttribute('position',new THREE.Float32BufferAttribute(dustPositions,3));
scene.add(new THREE.Points(dustGeo,new THREE.PointsMaterial({color:0xc3f8e8,size:.025,transparent:true,opacity:.45})));
const pointer = new THREE.Vector2();
const pointerWorld = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
const pointerPlane = new THREE.Plane(new THREE.Vector3(0,0,1),0);
let pointerActive=false;
function updatePointer(e){
const r=renderer.domElement.getBoundingClientRect();
pointer.set((e.clientX-r.left)/r.width*2-1,-((e.clientY-r.top)/r.height)*2+1);
raycaster.setFromCamera(pointer,camera);
pointerActive=!!raycaster.ray.intersectPlane(pointerPlane,pointerWorld);
}
renderer.domElement.addEventListener('pointermove',updatePointer);
renderer.domElement.addEventListener('pointerdown',updatePointer);
renderer.domElement.addEventListener('pointerleave',()=>{pointerActive=false;});
renderer.domElement.addEventListener('pointercancel',()=>{pointerActive=false;});
function resize(){
const w=container.clientWidth,h=container.clientHeight, aspect=w/h;
const viewHeight=9.4;
camera.left=-viewHeight*aspect/2;camera.right=viewHeight*aspect/2;
camera.top=viewHeight/2;camera.bottom=-viewHeight/2;
camera.updateProjectionMatrix();renderer.setSize(w,h,false);
}
window.addEventListener('resize',resize); resize();
const clock=new THREE.Clock();
let elapsed=0;
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),.04); elapsed+=dt;
for(const f of fishList){
f.timer-=dt;
if(f.timer<=0){
f.timer=between(2,5);
f.target.set((f.velocity.x>=0?1:-1)*between(.65,1),between(-.35,.35),between(-.2,.2)).normalize();
if(rand()<.15) f.target.x*=-1;
}
const p=f.group.position;
const desired=f.target.clone();
if(p.x>7) desired.x-=1.8;
if(p.x< -7) desired.x+=1.8;
if(p.y>6.35) desired.y-=1.4;
if(p.y<1.45) desired.y+=1.4;
if(p.z>1.8) desired.z-=1;
if(p.z< -1.8) desired.z+=1;
let speed=.63;
if(pointerActive){
const dx=p.x-pointerWorld.x,dy=p.y-pointerWorld.y;
const d=Math.hypot(dx,dy);
if(d<2.5 && d>.001){
const power=(2.5-d)/2.5;
desired.x+=dx/d*power*4; desired.y+=dy/d*power*4;
speed=1.15+power*1.2;
}
}
desired.normalize().multiplyScalar(speed);
f.velocity.lerp(desired,Math.min(1,dt*1.5));
p.addScaledVector(f.velocity,dt);
p.x=THREE.MathUtils.clamp(p.x,-7.35,7.35); p.y=THREE.MathUtils.clamp(p.y,1.15,6.6); p.z=THREE.MathUtils.clamp(p.z,-2,2);
const targetAngle=f.velocity.x>=0 ? 0 : Math.PI;
let diff=((targetAngle-f.angle+Math.PI*3)%(Math.PI*2))-Math.PI;
f.angle+=diff*Math.min(1,dt*3);
f.group.rotation.y=f.angle;
f.group.rotation.z=THREE.MathUtils.lerp(f.group.rotation.z,Math.max(-.22,Math.min(.22,f.velocity.y*.3))*(f.velocity.x>=0?1:-1),dt*2);
f.tailPivot.rotation.y=Math.sin(elapsed*8+f.phase)*.36;
f.group.position.y+=Math.sin(elapsed*2+f.phase)*dt*.035;
}
for(const b of bubbles){
b.mesh.position.y+=b.speed*dt;
b.mesh.position.x=b.baseX+Math.sin(elapsed*1.1+b.phase)*.12;
if(b.mesh.position.y>7.1)b.mesh.position.y=.5;
}
for(const p of plants)p.group.rotation.z=Math.sin(elapsed*.8+p.phase)*.035;
renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>Reasoning trace (1,954 tokens)
**Designing 3D Aquarium** I need to create a polished 3D aquarium using Three.js. The design will feature a dark navy page with a centered header and dashboard frame. The aquarium scene should include transparent glass edges, a sandy bottom, rocks, plants, coral, and fish that flee when interacted with. I'll maintain robust geometry for visuals and ensure a screenshot is taken at 3 seconds. The main title will be "A little world, underwater," set against a dark blue background. The camera setup needs specific dimensions too.**Setting Up 3D Tank** I'm working on the dimensions for the tank, which has bounds of x ±7, y from 0.7 to 6.5, and z ±2.5. For the camera, the position needs to be better optimized, aiming for a perspective with a field of view (FOV) of around 34 and specific dimensions for height and width compatibility with the tank. I’ll also incorporate a dark teal background and fog for added atmosphere, ensuring the tank is framed well within the scene. It’s crucial to balance all elements effectively!**Breaking down fish simulation** I'm thinking about creating a detailed 3D scene with transparent triangles and a raycaster for pointer projection. The idea is to simulate fish that swim away from the mouse if they're within a certain distance. Each fish can have distinct body shapes and colors, like orange, blue, yellow, and pink, using ellipsoids for their bodies and triangulated shapes for tails. I want their textures to be applied procedurally, perhaps using spheres for stripes on their bodies. I’m exploring this mesh structure to ensure everything looks dynamic.**Exploring UV mapping for fish** I’m considering how to map UV stripes that wrap around a sphere in a way that isn’t too obvious. I think using bands to create a segmented sphere might simplify things. For the fish's body, I'll focus on stylized shapes for the ellipsoid and fins, adding white dots for eyes. An accent stripe could be a thin, curved shape along the sides. I’ll generate the sphere mesh at specific angles along the x-axis to achieve a nice wrap effect, using ellipses for a clean surface.**Considering camera settings** I'm thinking about using an orthographic camera with the right aspect ratio. I need to make sure the width is at least 18 and the height is at least 9.7. If I set the view height to the maximum of 9.7 or 18 divided by the aspect ratio, that could help. For mobile devices, the height may be significantly larger, which could make the tank appear tiny with extra whitespace. It's crucial to strike the right balance!