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
- 2 min 37 s
- Cost
- $0.064
- Output tokens
- 12,754
- Spent thinking
- 4,359 tokens
- Speed
- 86.4 tok/s
- Console errors
- 0
- First token after
- 9.0 s
- First visible text after
- 1 min 17 s
- Prompt tokens
- 423
- Code size
- 23.8 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
{"effort":"high"}- 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 · 479 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aqua Atelier — The Reef</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>
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; background: #081719; color: #edf6ee; min-height: 100vh; }
body::before { content: ""; position: fixed; inset: 0; pointer-events: none; background: radial-gradient(ellipse at 50% 42%, #173c3a 0%, transparent 60%); opacity: .35; }
.page { position: relative; max-width: 1500px; margin: 0 auto; padding: 0 42px; }
header { height: 72px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(201, 231, 216, .12); }
.brand { display: flex; align-items: center; gap: 11px; font-size: 12px; font-weight: 750; letter-spacing: .19em; }
.brand-mark { width: 27px; height: 27px; border: 1px solid #8cc9b2; border-radius: 50%; display: grid; place-items: center; }
.brand-mark svg { width: 16px; height: 16px; }
.brand span { color: #7eaaa0; font-weight: 400; }
.header-right { display: flex; align-items: center; gap: 23px; font-size: 10px; font-weight: 650; letter-spacing: .15em; color: #91a9a0; }
.live { display: flex; align-items: center; gap: 8px; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: #a5e3b6; box-shadow: 0 0 10px #8ddda8; }
.intro { height: 117px; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.eyebrow { color: #8cb6a4; font-size: 10px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase; margin-bottom: 9px; }
h1 { margin: 0; font-family: Georgia, "Times New Roman", serif; font-weight: 400; font-size: clamp(26px, 3vw, 36px); letter-spacing: -.035em; line-height: 1; }
.intro-note { color: #93aaa2; font-size: 12px; line-height: 1.6; text-align: right; }
.stage { height: clamp(420px, calc(100vh - 263px), 620px); min-height: 420px; position: relative; border: 1px solid rgba(183, 225, 205, .19); border-radius: 14px; overflow: hidden; background: radial-gradient(ellipse at 50% 40%, #123f43, #092327 70%, #071a1d); box-shadow: 0 24px 80px rgba(0,0,0,.24), inset 0 0 70px rgba(0,0,0,.2); }
#aquarium { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
.stage::after { content: ""; pointer-events: none; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(2,17,20,.22), transparent 17%, transparent 72%, rgba(3,18,20,.5)); }
.stage-top, .stage-bottom { position: absolute; z-index: 2; left: 26px; right: 26px; pointer-events: none; }
.stage-top { top: 23px; display: flex; justify-content: space-between; align-items: center; }
.tag { display: flex; gap: 9px; align-items: center; color: #c7e2d4; font-size: 9px; font-weight: 700; letter-spacing: .17em; }
.tag .dot { width: 5px; height: 5px; }
.stage-count { color: #abc6bc; font-size: 10px; letter-spacing: .13em; }
.stage-bottom { bottom: 25px; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.scene-label .small { color: #a5c9b6; font-size: 9px; font-weight: 700; letter-spacing: .19em; margin-bottom: 6px; }
.scene-label .name { font-family: Georgia, serif; font-size: 27px; letter-spacing: -.03em; }
.interaction { display: flex; align-items: center; gap: 10px; color: #c2d8cc; font-size: 11px; line-height: 1.4; text-align: left; }
.interaction svg { flex: none; width: 34px; height: 34px; padding: 8px; border: 1px solid rgba(200,231,213,.3); border-radius: 50%; }
.interaction strong { display: block; color: #ecf6ed; font-size: 11px; font-weight: 600; }
.cursor-ring { position: absolute; z-index: 3; left: 0; top: 0; width: 46px; height: 46px; margin: -23px; border: 1px solid rgba(206,246,222,.65); border-radius: 50%; box-shadow: 0 0 18px rgba(143,235,197,.2), inset 0 0 15px rgba(143,235,197,.12); pointer-events: none; opacity: 0; transition: opacity .25s; }
.cursor-ring.visible { opacity: 1; }
.cursor-ring::after { content: ""; position: absolute; width: 4px; height: 4px; border-radius: 50%; background: #d2f7df; left: 20px; top: 20px; }
.below { min-height: 70px; display: flex; align-items: center; justify-content: space-between; gap: 20px; color: #819d92; font-size: 11px; }
.below-left { display: flex; align-items: center; gap: 10px; }
.below-left svg { width: 16px; height: 16px; color: #90b5a5; }
.controls { display: flex; align-items: center; gap: 18px; }
.ambient { font-size: 9px; font-weight: 700; letter-spacing: .14em; color: #6f9084; }
button { font: inherit; color: #cce0d4; border: 1px solid rgba(183, 225, 205, .22); border-radius: 7px; padding: 9px 13px; background: rgba(255,255,255,.025); cursor: pointer; display: flex; gap: 8px; align-items: center; }
button:hover { background: rgba(167, 224, 194, .1); border-color: rgba(183, 225, 205, .45); }
button:focus-visible { outline: 2px solid #9ddbbb; outline-offset: 3px; }
button svg { width: 12px; height: 12px; }
@media (max-width: 650px) {
.page { padding: 0 16px; }
header { height: 62px; }
.header-right { gap: 0; }
.header-right .edition { display: none; }
.intro { height: 105px; }
.intro-note { display: none; }
.stage { height: 480px; min-height: 0; }
.stage-top, .stage-bottom { left: 16px; right: 16px; }
.stage-top { top: 17px; }
.stage-bottom { bottom: 18px; }
.scene-label .name { font-size: 23px; }
.interaction { max-width: 175px; font-size: 10px; }
.interaction strong { font-size: 10px; }
.below { align-items: flex-start; padding-top: 17px; }
.below-left { max-width: 190px; line-height: 1.5; }
.ambient { display: none; }
}
@media (max-width: 390px) {
.stage { height: 430px; }
.stage-count { font-size: 9px; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="brand">
<div class="brand-mark"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M3 8c2.3-2.1 4.7-2.1 7 0s4.7 2.1 7 0M3 12c2.3-2.1 4.7-2.1 7 0s4.7 2.1 7 0" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
AQUA <span>ATELIER</span>
</div>
<div class="header-right"><span class="edition">A LITTLE SPACE TO BREATHE</span><span class="live"><i class="dot"></i> LIVE HABITAT</span></div>
</header>
<section class="intro">
<div><div class="eyebrow">Your window to the underwater</div><h1>A moment beneath the surface.</h1></div>
<div class="intro-note">A small, peaceful world.<br>Take your time here.</div>
</section>
<main class="stage" id="stage">
<canvas id="aquarium" aria-label="A 3D aquarium with fish that swim away from your cursor"></canvas>
<div class="stage-top"><div class="tag"><i class="dot"></i> THE AQUARIUM IS ALIVE</div><div class="stage-count">01 / THE REEF</div></div>
<div class="stage-bottom">
<div class="scene-label"><div class="small">EXPLORE THE HABITAT</div><div class="name">The Reef</div></div>
<div class="interaction">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 3l13 9-6 .7-2.7 5.4L5 3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
<div><strong>Make a little ripple</strong>Move your cursor near the fish</div>
</div>
</div>
<div class="cursor-ring" id="cursor"></div>
</main>
<div class="below">
<div class="below-left"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 3c-2.1 3.3-6 7.4-6 11.2a6 6 0 0 0 12 0C18 10.4 14.1 6.3 12 3Z" stroke="currentColor" stroke-width="1.3"/><path d="M9 15c.1 1.4 1 2.3 2.3 2.5" stroke="currentColor" stroke-linecap="round"/></svg> A tiny world, moving at its own pace.</div>
<div class="controls"><span class="ambient">STAY A WHILE · BREATHE</span><button id="pause" type="button" aria-label="Pause animation"><svg id="pauseIcon" viewBox="0 0 12 12" fill="currentColor"><path d="M2.5 1.5h2v9h-2zm5 0h2v9h-2z"/></svg><span id="pauseText">Pause</span></button></div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
const canvas = document.getElementById('aquarium');
const stage = document.getElementById('stage');
const ring = document.getElementById('cursor');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, alpha:true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(34, 1, .1, 100);
camera.position.set(0, 5.3, 16);
camera.lookAt(0, 2.85, 0);
scene.add(new THREE.HemisphereLight(0xd2fff1, 0x254a41, 2.0));
const sun = new THREE.DirectionalLight(0xffefcd, 2.5);
sun.position.set(-5, 10, 7);
scene.add(sun);
const fill = new THREE.DirectionalLight(0x73dbd4, 1.1);
fill.position.set(6, 4, -5);
scene.add(fill);
let seed = 11928;
function rand(){ seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; }
function range(a,b){ return a + (b-a)*rand(); }
function mat(color, roughness=.65, extra={}) { return new THREE.MeshStandardMaterial({color, roughness, ...extra}); }
// A softly illuminated back of the tank, with moving, underwater light.
const waterUniforms = { uTime: {value:0} };
const waterBack = new THREE.Mesh(
new THREE.PlaneGeometry(16, 6.3),
new THREE.ShaderMaterial({
uniforms: waterUniforms,
vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
fragmentShader: `
varying vec2 vUv; uniform float uTime;
void main(){
vec3 low=vec3(0.016,0.16,0.18);
vec3 high=vec3(0.035,0.33,0.34);
vec3 col=mix(low,high,smoothstep(0.,1.,vUv.y));
float glow=exp(-pow((vUv.x-.52)*2.0,2.0))*vUv.y;
col+=vec3(.015,.045,.04)*glow;
float a=sin(vUv.x*35. + sin(vUv.y*13.+uTime*.18)*1.4 + uTime*.14);
float b=sin(vUv.x*54. - vUv.y*20. + uTime*.1);
float caustic=pow(max(0., a*b), 7.)*.025;
col+=vec3(.55,1.,.86)*caustic*(.25+vUv.y*.75);
float beam=pow(max(0.,sin(vUv.x*22.+vUv.y*3.)),10.)*.014*vUv.y;
col+=vec3(.5,1.,.88)*beam;
gl_FragColor=vec4(col,1.);
}`
})
);
waterBack.position.set(0, 3.15, -2.65);
scene.add(waterBack);
// Glass, surface, and the fine edges of the tank.
const sideMat = new THREE.MeshBasicMaterial({color:0x2c8e88, transparent:true, opacity:.13, side:THREE.DoubleSide, depthWrite:false});
for (const x of [-8,8]) {
const side = new THREE.Mesh(new THREE.PlaneGeometry(5.3,6.3), sideMat);
side.rotation.y = Math.PI/2;
side.position.set(x,3.15,0);
scene.add(side);
}
const surface = new THREE.Mesh(new THREE.PlaneGeometry(16,5.3), new THREE.MeshBasicMaterial({color:0x8ed7c6,transparent:true,opacity:.10,side:THREE.DoubleSide,depthWrite:false}));
surface.rotation.x = -Math.PI/2;
surface.position.y = 6.3;
scene.add(surface);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(16,6.3,5.3)), new THREE.LineBasicMaterial({color:0x9ad5c2,transparent:true,opacity:.42}));
edges.position.y=3.15;
scene.add(edges);
const waterline = new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(-8,6.19,2.65),new THREE.Vector3(8,6.19,2.65)]), new THREE.LineBasicMaterial({color:0xc4f3d5,transparent:true,opacity:.55}));
scene.add(waterline);
// Sandy floor with gently uneven surface.
const sandGeo = new THREE.PlaneGeometry(16,5.3,64,24);
sandGeo.rotateX(-Math.PI/2);
const pos=sandGeo.attributes.position;
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), z=pos.getZ(i);
pos.setY(i, .16 + .045*Math.sin(x*1.1+z*2) + .035*Math.sin(x*2.3-z*1.3));
}
sandGeo.computeVertexNormals();
const sand = new THREE.Mesh(sandGeo,mat(0xb4b391,1));
scene.add(sand);
const base = new THREE.Mesh(new THREE.BoxGeometry(16.08,.27,5.4),mat(0x315c54,.8));
base.position.y=-.08;
scene.add(base);
const baseLip = new THREE.Mesh(new THREE.BoxGeometry(16.1,.055,.08),mat(0x9ab6a1,.65));
baseLip.position.set(0,.03,2.71);
scene.add(baseLip);
// Stones and tiny flecks in the sand.
const stoneColors=[0x66847b,0x718e7e,0x829586,0x9ca38b,0x6a8075];
const rockGeo=new THREE.IcosahedronGeometry(1,2);
function rock(x,z,sx,sy,sz,color){
const m=new THREE.Mesh(rockGeo,mat(color,1));
m.scale.set(sx,sy,sz); m.position.set(x,.17+sy*.47,z); m.rotation.set(range(-.2,.2),range(0,6),range(-.2,.2)); scene.add(m);
}
[
[-6.5,-.6,.82,.48,.64],[-5.45,-1.35,.55,.32,.45],[-7.1,1,.5,.28,.5],
[5.7,-1.1,.95,.52,.65],[6.7,.15,.68,.38,.58],[4.8,1.4,.43,.24,.39],
[-3.8,1.7,.37,.20,.33],[3.9,-1.8,.4,.24,.4]
].forEach((r,i)=>rock(...r,stoneColors[i%stoneColors.length]));
for(let i=0;i<95;i++){
const x=range(-7.7,7.7), z=range(-2.4,2.4);
const s=range(.025,.075);
rock(x,z,s*1.4,s*.55,s,stoneColors[Math.floor(rand()*stoneColors.length)]);
}
// Tapered ribbons of seagrass.
const plantMaterials=[0x296f5a,0x3d8b66,0x4a9675,0x72a57d,0x357d72].map(c=>mat(c,.85,{side:THREE.DoubleSide}));
const leaves=[];
function makeLeaf(x,z,h,w,bend,material){
const vertices=[], indices=[];
const n=14;
for(let i=0;i<=n;i++){
const t=i/n;
const cx=bend*t*t + Math.sin(t*Math.PI)*bend*.13;
const width=w*Math.pow(Math.sin(Math.PI*t),.72)+.006;
const zz=Math.sin(t*Math.PI)*.09;
vertices.push(cx-width/2,t*h,zz, cx+width/2,t*h,zz);
if(i<n){const k=i*2; indices.push(k,k+1,k+2,k+1,k+3,k+2);}
}
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(vertices,3));
g.setIndex(indices); g.computeVertexNormals();
const leaf=new THREE.Mesh(g,material);
leaf.position.set(x,.18,z);
leaf.rotation.y=range(-.6,.6);
leaf.userData.phase=range(0,6.28);
scene.add(leaf); leaves.push(leaf);
}
function plantCluster(cx,cz,count,height){
for(let i=0;i<count;i++){
makeLeaf(cx+range(-.48,.48),cz+range(-.35,.35),range(height*.55,height),range(.12,.27),range(-.6,.6),plantMaterials[Math.floor(rand()*plantMaterials.length)]);
}
}
plantCluster(-7,-1.6,18,2.8);
plantCluster(-5.9,-2,15,2.25);
plantCluster(-4.45,-2.15,11,1.45);
plantCluster(7,-1.7,19,2.6);
plantCluster(5.9,-2.05,14,2.15);
plantCluster(4.4,-2.1,9,1.5);
plantCluster(-6.8,1.7,8,1.15);
plantCluster(7.2,1.4,8,1.2);
// A small branching coral.
const coralMat=mat(0xd28e75,.85);
function coralBranch(points,radius){
const curve=new THREE.CatmullRomCurve3(points.map(p=>new THREE.Vector3(...p)));
const mesh=new THREE.Mesh(new THREE.TubeGeometry(curve,12,radius,6,false),coralMat);
scene.add(mesh);
const tip=new THREE.Mesh(new THREE.SphereGeometry(radius*1.1,8,6),coralMat);
tip.position.copy(curve.getPoint(1)); scene.add(tip);
}
const coralX=5.15, coralZ=.35;
coralBranch([[coralX,.18,coralZ],[5.18,.58,.35],[5.12,1.05,.35],[5.18,1.4,.35]],.07);
coralBranch([[5.15,.53,.35],[4.87,.76,.35],[4.76,1.18,.35]],.05);
coralBranch([[5.14,.78,.35],[5.48,1.02,.35],[5.54,1.38,.35]],.045);
coralBranch([[4.88,.76,.35],[4.62,.91,.35],[4.57,1.1,.35]],.036);
coralBranch([[5.48,1.02,.35],[5.72,1.14,.35],[5.76,1.35,.35]],.034);
// Fish with rounded bodies, banded coloring, fins, and eyes.
function bodyGeometry(baseColor, stripe){
const verts=[], colors=[], indices=[];
const base=new THREE.Color(baseColor);
const pale=new THREE.Color(stripe || baseColor);
const edge=new THREE.Color(0x344c40);
const n=36, m=24;
for(let i=0;i<=n;i++){
const t=i/n*2-1;
const x=t*.78;
const profile=Math.pow(Math.max(0,1-t*t),.57);
for(let j=0;j<=m;j++){
const a=j/m*Math.PI*2;
verts.push(x,Math.sin(a)*profile*.34,Math.cos(a)*profile*.245);
let col=base.clone();
if(stripe){
const d=Math.min(Math.abs(x+.30),Math.abs(x-.18));
if(d<.045) col.copy(pale);
else if(d<.065) col.lerp(edge,.6);
}
col.multiplyScalar(.88 + .12*(Math.sin(a)+1)/2 + .06*(t+1)/2);
colors.push(col.r,col.g,col.b);
if(i<n && j<m){
const k=i*(m+1)+j;
indices.push(k,k+m+1,k+1,k+1,k+m+1,k+m+2);
}
}
}
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));
g.setAttribute('color',new THREE.Float32BufferAttribute(colors,3));
g.setIndex(indices); g.computeVertexNormals();
return g;
}
function shapeMesh(points, material){
const shape=new THREE.Shape();
shape.moveTo(points[0][0],points[0][1]);
points.slice(1).forEach(p=>shape.lineTo(p[0],p[1]));
shape.closePath();
return new THREE.Mesh(new THREE.ShapeGeometry(shape),material);
}
const eyeWhite=mat(0xf4e8c9,.4);
const eyeBlack=mat(0x102c2a,.28);
const shine=new THREE.MeshBasicMaterial({color:0xffffff});
const fish=[];
function createFish(spec){
const root=new THREE.Group();
const body=new THREE.Mesh(bodyGeometry(spec.color,spec.stripe),new THREE.MeshStandardMaterial({vertexColors:true,roughness:.55,metalness:.03,side:THREE.DoubleSide}));
root.add(body);
const finMaterial=mat(spec.fin || spec.color,.7,{side:THREE.DoubleSide});
const tail=new THREE.Group();
tail.position.x=-.67;
tail.add(shapeMesh([[0,0],[-.55,.37],[-.46,.08],[-.48,0],[-.46,-.08],[-.55,-.37]],finMaterial));
root.add(tail);
const dorsal=shapeMesh([[-.53,.22],[-.34,.46],[-.12,.43],[.16,.30],[.23,.23]],finMaterial);
root.add(dorsal);
const lower=shapeMesh([[-.48,-.22],[-.31,-.34],[.02,-.31],[.17,-.22]],finMaterial);
root.add(lower);
const fins=[];
for(const side of [-1,1]){
const fin=shapeMesh([[.04,0],[-.25,-.06],[-.32,-.25],[-.08,-.17]],finMaterial);
fin.position.set(.03,-.025,side*.22);
fin.rotation.y=side*.28;
root.add(fin); fins.push(fin);
const eye=new THREE.Mesh(new THREE.SphereGeometry(.066,12,8),eyeWhite);
eye.position.set(.49,.105,side*.165); root.add(eye);
const pupil=new THREE.Mesh(new THREE.SphereGeometry(.038,12,8),eyeBlack);
pupil.position.set(.503,.108,side*.218); root.add(pupil);
const glint=new THREE.Mesh(new THREE.SphereGeometry(.012,8,6),shine);
glint.position.set(.514,.125,side*.25); root.add(glint);
}
root.scale.setScalar(spec.scale);
root.position.set(spec.x,spec.y,spec.z);
const dir=spec.dir;
const velocity=new THREE.Vector3(dir*range(.43,.68),range(-.08,.08),range(-.13,.13));
root.rotation.y=dir===1?0:Math.PI;
scene.add(root);
fish.push({root,tail,fins,velocity,phase:range(0,6.28), speed:range(.5,.8), turn:range(-1,1)});
}
const specs=[
{x:-5.7,y:4.65,z:.0,scale:.70,color:0xf0925d,stripe:0xffe5b7,dir:1},
{x:-3.8,y:3.45,z:1.0,scale:.82,color:0xf4a55d,stripe:0xffedc9,dir:1},
{x:-1.6,y:5.05,z:-.8,scale:.67,color:0x86cbb4,fin:0x66b39d,dir:-1},
{x:.15,y:3.8,z:.55,scale:.92,color:0xf3bb6c,fin:0xe89b5e,dir:-1},
{x:2.4,y:4.95,z:-.5,scale:.65,color:0xf08f6d,stripe:0xffdfb4,dir:1},
{x:4.65,y:3.7,z:.5,scale:.79,color:0x8cd0bc,fin:0x6ab5a5,dir:-1},
{x:6.15,y:5.3,z:-1,scale:.54,color:0xf5c77d,dir:-1},
{x:-5.3,y:2.15,z:-.9,scale:.58,color:0x91cfb8,fin:0x68a992,dir:1},
{x:-2.15,y:2.1,z:-.2,scale:.62,color:0xe9846b,stripe:0xfbe0bd,dir:-1},
{x:1.0,y:2.0,z:-1.15,scale:.57,color:0xf5ba73,dir:1},
{x:3.45,y:2.45,z:1.1,scale:.76,color:0xed956a,stripe:0xffe8c5,dir:1},
{x:6.2,y:2.45,z:-.8,scale:.57,color:0xedd38b,fin:0xe5b96c,dir:-1}
];
specs.forEach(createFish);
// Suspended bubbles and motes.
const bubbles=[];
const bubbleMat=new THREE.MeshBasicMaterial({color:0xc3f4e3,transparent:true,opacity:.36,depthWrite:false});
const bubbleGeo=new THREE.SphereGeometry(1,10,8);
for(let i=0;i<33;i++){
const m=new THREE.Mesh(bubbleGeo,bubbleMat);
const s=range(.025,.075); m.scale.setScalar(s);
const column=i%2===0?-5.95:5.85;
m.position.set(column+range(-.45,.45),range(.4,6),range(-1.6,.6));
m.userData={baseX:m.position.x,speed:range(.22,.52),phase:range(0,6.28)};
scene.add(m); bubbles.push(m);
}
const particles=[];
for(let i=0;i<130;i++) particles.push(range(-7.8,7.8),range(.35,6.1),range(-2.5,2.2));
const particleGeo=new THREE.BufferGeometry();
particleGeo.setAttribute('position',new THREE.Float32BufferAttribute(particles,3));
scene.add(new THREE.Points(particleGeo,new THREE.PointsMaterial({color:0xafe4d1,size:.026,transparent:true,opacity:.45,depthWrite:false})));
const pointer=new THREE.Vector2();
const raycaster=new THREE.Raycaster();
let pointerActive=false;
function updatePointer(e){
const rect=canvas.getBoundingClientRect();
pointer.x=((e.clientX-rect.left)/rect.width)*2-1;
pointer.y=-((e.clientY-rect.top)/rect.height)*2+1;
pointerActive=true;
ring.style.left=(e.clientX-rect.left)+'px';
ring.style.top=(e.clientY-rect.top)+'px';
ring.classList.add('visible');
}
canvas.addEventListener('pointermove',updatePointer);
canvas.addEventListener('pointerdown',updatePointer);
canvas.addEventListener('pointerleave',()=>{pointerActive=false;ring.classList.remove('visible');});
canvas.addEventListener('pointerup',e=>{if(e.pointerType==='touch'){pointerActive=false;ring.classList.remove('visible');}});
function resize(){
const w=stage.clientWidth, h=stage.clientHeight;
renderer.setSize(w,h,false);
camera.aspect=w/h;
camera.updateProjectionMatrix();
}
new ResizeObserver(resize).observe(stage);
resize();
let paused=false;
document.getElementById('pause').addEventListener('click',()=>{
paused=!paused;
document.getElementById('pauseText').textContent=paused?'Play':'Pause';
document.getElementById('pause').setAttribute('aria-label',paused?'Play animation':'Pause animation');
document.getElementById('pauseIcon').innerHTML=paused?'<path d="M3 1.5 10 6l-7 4.5z"/>':'<path d="M2.5 1.5h2v9h-2zm5 0h2v9h-2z"/>';
});
const clock=new THREE.Clock();
let time=0;
const temp=new THREE.Vector3();
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),.05);
if(!paused){
time+=dt;
waterUniforms.uTime.value=time;
if(pointerActive) raycaster.setFromCamera(pointer,camera);
fish.forEach((f,i)=>{
const p=f.root.position, v=f.velocity;
// Smooth steering keeps the fish within the water.
const force=new THREE.Vector3();
if(p.x>6.8) force.x-=(p.x-6.8)*1.1;
if(p.x< -6.8) force.x+=(-6.8-p.x)*1.1;
if(p.y>5.7) force.y-=(p.y-5.7)*1.4;
if(p.y<1.0) force.y+=(1.0-p.y)*1.4;
if(p.z>1.65) force.z-=(p.z-1.65);
if(p.z< -1.9) force.z+=(-1.9-p.z);
force.y+=Math.sin(time*.6+f.phase)*.05;
force.z+=Math.sin(time*.35+f.phase)*.07;
if(pointerActive && Math.abs(raycaster.ray.direction.z)>.0001){
const t=(p.z-raycaster.ray.origin.z)/raycaster.ray.direction.z;
if(t>0){
raycaster.ray.at(t,temp);
const dx=p.x-temp.x, dy=p.y-temp.y;
const d=Math.sqrt(dx*dx+dy*dy);
if(d<2.1 && d>.001){
const strength=(1-d/2.1)*3.8;
force.x+=dx/d*strength;
force.y+=dy/d*strength;
}
}
}
v.addScaledVector(force,dt);
v.y=THREE.MathUtils.clamp(v.y,-.6,.6);
v.z=THREE.MathUtils.clamp(v.z,-.45,.45);
const speed=v.length();
if(speed>.001) v.multiplyScalar(THREE.MathUtils.lerp(speed,f.speed,Math.min(1,dt*.7))/speed);
p.addScaledVector(v,dt);
let target=Math.atan2(-v.z,v.x);
let angle=f.root.rotation.y;
const delta=Math.atan2(Math.sin(target-angle),Math.cos(target-angle));
f.root.rotation.y+=delta*Math.min(1,dt*2.3);
f.root.rotation.z=THREE.MathUtils.lerp(f.root.rotation.z,Math.atan2(v.y,Math.sqrt(v.x*v.x+v.z*v.z))*.5,dt*2);
f.tail.rotation.y=Math.sin(time*8+f.phase)*.38;
f.fins[0].rotation.y=-.28+Math.sin(time*8+f.phase)*.2;
f.fins[1].rotation.y=.28-Math.sin(time*8+f.phase)*.2;
});
leaves.forEach(l=>{l.rotation.z=Math.sin(time*.7+l.userData.phase)*.045;});
bubbles.forEach(b=>{
b.position.y+=b.userData.speed*dt;
if(b.position.y>6.12) b.position.y=.3;
b.position.x=b.userData.baseX+Math.sin(time*1.2+b.userData.phase)*.11;
});
}
renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>Reasoning trace (4,359 tokens)
**Creating 3D Aquarium Designs** I’m working on building a 3D aquarium with transparent walls, a water surface, and textured sand, all without using external files. I want to create visually appealing organic fish, plants, and coral. For the fish geometry, I’ll use custom shapes for their orientation, emphasizing the swim direction. The body needs to be slightly tapered, using spheres scaled to specific values, and I plan to create fun variations in colors while ensuring the silhouettes are recognizable.**Designing Custom Fish Meshes** I'm thinking about using custom fish meshes created with parametric geometry, which allows the body to be anatomically tapered. A sphere is suitable for this, and I plan to make the body elliptical with a small snout and tail. To add distinct features, I could create highlights or stripes, possibly using three curves or a torus around the body. For the texture, I’ll use a canvas-generated map that includes patterns, stripes, and a palette carefully mapped to the fish's surface.**Designing a visual layout** I’m thinking about a canvas within a large scene section featuring a border radius of 20 and a dark gradient. Outside the canvas, I want to place a header at the top with the wordmark "AQUA / ATELIER" and a right-side navigation pill labeled "● LIVE HABITAT." There's also a rounded sound toggle to consider. Inside the bottom left, I envision a label saying "THE REEF / No. 01," and a central/right status prompt that reads "MOVE YOUR CURSOR ...". It feels aesthetic and editorial.**Designing an aquarium scene** I'm considering creating a page with a background color of #07181c and a top header at 70px. The heading area should be about 90px above the main scene. I envision a large stage of 560px and a footer around 60px. I want to depict a visible tank with a marine environment in the center and establish a camera perspective with specific dimensions and positions. It’s crucial to adjust dimensions and camera settings to create an effective 3D aquarium look!**Refining the aquarium design** I'm thinking about adjusting the tank dimensions to a width of 16 and height of 6.3, while setting the camera's field of view to 33 at a distance of 14.5. This should give me a height of about 8.6 and a width of 19, making it occupy 84% of the stage. I'm also considering color gradients for the tank’s background and walls, including adding procedural sand texture at the bottom. This all needs to look natural and not too distracting for the viewer!**Considering tank design elements** I'm thinking about my tank design with a backdrop that's dark outside the tank while having lightly luminous edges. The water line at the top and bottom should be highlighted, and I want the stage UI overlay to anchor the corners of the canvas. For lighting, I might use hemisphere directional ambient without shadows, as they're costly. Then I want to add fish in colors like tangerine, coral, yellow, aquamarine, and peach, scaling them around 0.7 to 1.0.**Designing fish movement dynamics** I’m thinking about how to manage fish movement in relation to the user’s mouse without unnecessary interactions. I want a visual cursor ring that disappears when the mouse leaves. It’s important to set a velocity vector for the fish that accounts for center attraction and periodic turns. I need to maintain consistent movement and avoid sudden jumps, ensuring smooth rotations. I’d also like to start with 13 fish placed randomly but deterministically, making sure they don’t overlap.**Optimizing mobile canvas** I'm considering how to make the canvas responsive for mobile devices with a width of 375 and a height of 500. If the camera maintains the horizontal aspect ratio, the scene height will make the tank look tiny. To solve this, I'll set the mobile height to 500 and use an orthographic camera. I need to adjust the field of view based on the aspect ratio (375/500) and ensure the tank fills the width appropriately, even if the height is smaller. The desired outcome is a clear visual at around 350 in height.**Considering UI design** I'm thinking about the design for my interface. The "Pause" button should effectively toggle the animation, and I'm looking at three states: static, plus hover for interaction. Maybe I'll include a scatter button too, labeled "Tap to interact." It's important to make the paused motion state clear with a good icon and inline SVG. I'd like to add instructions in an overlay at the bottom left while ensuring the visuals aren’t obstructive. A linear gradient backdrop could enhance the immersion.