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
- 3 min 48 s
- Cost
- $0.086
- Output tokens
- 17,168
- Spent thinking
- 7,763 tokens
- Speed
- 76.9 tok/s
- Console errors
- 0
- First token after
- 5.1 s
- First visible text after
- 2 min 22 s
- Prompt tokens
- 423
- Code size
- 26.4 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
{"effort":"xhigh"}- 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 · 608 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>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>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }
body {
font-family: Arial, Helvetica, sans-serif;
color: #e3efeb;
background:
radial-gradient(ellipse at 50% 43%, #183c3d 0%, #102b2d 35%, #091b1d 75%),
#091b1d;
}
#scene { position: absolute; inset: 0; }
#scene canvas { display: block; width: 100%; height: 100%; touch-action: none; }
.shade {
position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(to bottom, rgba(4,17,19,.48), transparent 20%, transparent 76%, rgba(4,17,19,.6));
}
.topbar {
position: absolute; top: 0; left: 0; right: 0;
display: flex; align-items: center; justify-content: space-between;
padding: 30px 48px; pointer-events: none;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 37px; height: 37px; border: 1px solid rgba(195,230,216,.42);
border-radius: 50%; display: grid; place-items: center;
}
.brand-mark svg { width: 22px; height: 22px; }
.brand-name { font-family: Georgia, serif; font-size: 22px; letter-spacing: -.8px; line-height: 1; }
.brand-name span { color: #a9d1bc; }
.brand-sub { color: #93aaa3; font-size: 8px; letter-spacing: .22em; margin-top: 5px; font-weight: 700; }
.top-right { display: flex; align-items: center; gap: 13px; pointer-events: auto; }
.live {
height: 34px; display: flex; gap: 8px; align-items: center;
padding: 0 13px; border: 1px solid rgba(186,219,205,.17);
border-radius: 20px; background: rgba(8,29,29,.35);
color: #c4d8cf; font-size: 9px; font-weight: bold; letter-spacing: .15em;
}
.live i { width: 6px; height: 6px; border-radius: 50%; background: #a5d6b7; box-shadow: 0 0 10px #a5d6b7; }
#pause {
border: 1px solid rgba(186,219,205,.17); border-radius: 20px; height: 34px;
padding: 0 13px; color: #d9e9e2; background: rgba(8,29,29,.35);
font: 700 9px Arial, sans-serif; letter-spacing: .12em; cursor: pointer;
}
#pause:hover { background: rgba(138,188,166,.18); border-color: rgba(186,219,205,.4); }
#pause:focus-visible { outline: 2px solid #b9dec8; outline-offset: 3px; }
.pause-icon { font-size: 12px; margin-right: 6px; letter-spacing: 0; }
.footer {
position: absolute; bottom: 0; left: 0; right: 0;
padding: 0 48px 35px; display: flex; justify-content: space-between;
align-items: end; gap: 20px; pointer-events: none;
}
.eyebrow { color: #9bbab0; font-size: 9px; font-weight: 700; letter-spacing: .19em; text-transform: uppercase; }
.caption-title { margin-top: 8px; font: 25px/1.1 Georgia, serif; letter-spacing: -.4px; color: #e3eee8; }
.caption-sub { margin-top: 7px; color: #8fa8a0; font-size: 11px; letter-spacing: .02em; }
.hint {
display: flex; align-items: center; gap: 13px;
padding: 13px 16px; border: 1px solid rgba(190,221,207,.18);
border-radius: 6px; background: rgba(10,33,34,.67);
box-shadow: 0 8px 24px rgba(0,0,0,.1);
}
.hint-icon {
width: 32px; height: 32px; border: 1px solid rgba(186,219,205,.27);
border-radius: 50%; display: grid; place-items: center; flex: none;
}
.hint-icon svg { width: 16px; height: 16px; }
.hint-title { font-size: 9px; font-weight: 700; letter-spacing: .16em; color: #bad2c7; }
.hint-text { font-size: 11px; color: #e0ebe6; margin-top: 5px; white-space: nowrap; }
#cursor {
position: fixed; left: 0; top: 0; width: 68px; height: 68px;
border: 1px solid rgba(220,250,235,.5); border-radius: 50%;
box-shadow: 0 0 0 7px rgba(202,242,220,.04), inset 0 0 18px rgba(210,245,227,.06);
transform: translate(-50%,-50%) scale(.8); opacity: 0;
transition: opacity .25s, transform .25s; pointer-events: none;
}
#cursor.visible { opacity: 1; transform: translate(-50%,-50%) scale(1); }
#cursor:after {
content: ""; position: absolute; left: 50%; top: 50%; width: 3px; height: 3px;
background: #d6f3e2; border-radius: 50%; transform: translate(-50%,-50%);
}
@media (max-width: 600px) {
.topbar { padding: 21px 20px; }
.brand-name { font-size: 20px; }
.brand-mark { width: 34px; height: 34px; }
.brand-sub { font-size: 7px; }
.live { padding: 0 10px; font-size: 8px; }
#pause { width: 34px; padding: 0; font-size: 0; }
.pause-icon { font-size: 12px; margin: 0; }
.footer { padding: 0 20px 25px; flex-direction: column; align-items: flex-start; gap: 18px; }
.caption-title { font-size: 22px; }
.caption-sub { font-size: 10px; }
.hint { padding: 10px 13px; }
.hint-text { font-size: 10px; }
}
</style>
</head>
<body>
<div id="scene" aria-label="A three-dimensional aquarium with swimming fish"></div>
<div class="shade"></div>
<header class="topbar">
<div class="brand">
<div class="brand-mark">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M3 12c3-4.5 8.4-5.8 12.2-1.2L20.5 8v8l-5.3-2.8C11.4 17.8 6 16.5 3 12Z" stroke="#c5e2d2" stroke-width="1.1" stroke-linejoin="round"/>
<circle cx="7.8" cy="11.3" r=".8" fill="#c5e2d2"/>
<path d="M4 19c2.2-1.1 4.4-1.1 6.6 0 2.2 1.1 4.4 1.1 6.6 0" stroke="#92bdaa" stroke-width=".8"/>
</svg>
</div>
<div><div class="brand-name">aquarium<span>.</span></div><div class="brand-sub">A LITTLE WORLD UNDERWATER</div></div>
</div>
<div class="top-right">
<div class="live"><i></i> 12 FISH · LIVE</div>
<button id="pause" type="button" aria-label="Pause motion"><span class="pause-icon">Ⅱ</span><span id="pause-label">PAUSE</span></button>
</div>
</header>
<footer class="footer">
<div>
<div class="eyebrow">No. 01 / The quiet tank</div>
<div class="caption-title">A moment beneath the surface.</div>
<div class="caption-sub">Take a breath. Stay a little while.</div>
</div>
<div class="hint">
<div class="hint-icon">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="7" y="3" width="10" height="17" rx="5" stroke="#c3dfd0" stroke-width="1.3"/>
<path d="M12 6.2v3.5" stroke="#c3dfd0" stroke-width="1.3" stroke-linecap="round"/>
</svg>
</div>
<div><div class="hint-title">MAKE A LITTLE WAVE</div><div class="hint-text">Move near the fish and watch them scatter.</div></div>
</div>
</footer>
<div id="cursor"></div>
<script type="module">
import * as THREE from 'three';
const container = document.getElementById('scene');
const cursor = document.getElementById('cursor');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.13;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, .1, 100);
camera.position.set(0, 6.8, 19.5);
camera.lookAt(0, 3.8, 0);
scene.add(new THREE.HemisphereLight(0xd5f3e5, 0x27413b, 2.0));
const sunlight = new THREE.DirectionalLight(0xffedc9, 2.25);
sunlight.position.set(-5, 11, 7);
scene.add(sunlight);
const fill = new THREE.PointLight(0x74c8b6, 30, 25);
fill.position.set(5, 6, 5);
scene.add(fill);
let seed = 42051;
function rand() { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; }
function range(a, b) { return a + (b - a) * rand(); }
function makeBackground() {
const c = document.createElement('canvas');
c.width = 1024; c.height = 512;
const x = c.getContext('2d');
let grad = x.createLinearGradient(0, 0, 0, 512);
grad.addColorStop(0, '#438d86');
grad.addColorStop(.28, '#306f6d');
grad.addColorStop(.68, '#235655');
grad.addColorStop(1, '#173b3b');
x.fillStyle = grad; x.fillRect(0, 0, 1024, 512);
const glow = x.createRadialGradient(520, 10, 10, 520, 50, 650);
glow.addColorStop(0, 'rgba(176,225,196,.22)');
glow.addColorStop(1, 'rgba(20,65,64,0)');
x.fillStyle = glow; x.fillRect(0, 0, 1024, 512);
x.save();
x.filter = 'blur(18px)';
for (let i = 0; i < 9; i++) {
const start = i * 150 - 110;
x.fillStyle = `rgba(190,235,205,${range(.025,.065)})`;
x.beginPath(); x.moveTo(start, 0); x.lineTo(start + range(35, 100), 0);
x.lineTo(start + range(150, 300), 512); x.lineTo(start + range(60, 130), 512); x.closePath(); x.fill();
}
x.restore();
for (let i = 0; i < 6500; i++) {
const a = range(.01, .055);
x.fillStyle = `rgba(212,245,224,${a})`;
const r = range(.3, 1.2);
x.fillRect(range(0, 1024), range(0, 512), r, r);
}
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const back = new THREE.Mesh(new THREE.PlaneGeometry(16, 7.6), new THREE.MeshBasicMaterial({ map: makeBackground() }));
back.position.set(0, 3.8, -3.02);
scene.add(back);
function sandTexture() {
const c = document.createElement('canvas');
c.width = 512; c.height = 512;
const ctx = c.getContext('2d');
ctx.fillStyle = '#b8b69b'; ctx.fillRect(0, 0, 512, 512);
for (let i = 0; i < 12500; i++) {
const light = rand() > .5;
ctx.fillStyle = light ? `rgba(241,225,188,${range(.08,.4)})` : `rgba(73,80,62,${range(.04,.22)})`;
const r = range(.4, 2.0);
ctx.beginPath(); ctx.ellipse(range(0,512), range(0,512), r * 1.5, r, range(0,3), 0, Math.PI * 2); ctx.fill();
}
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(3, 1.5);
return tex;
}
const floor = new THREE.Mesh(new THREE.PlaneGeometry(16, 6), new THREE.MeshStandardMaterial({ map: sandTexture(), roughness: 1 }));
floor.rotation.x = -Math.PI / 2;
floor.position.y = .025;
scene.add(floor);
const sideMat = new THREE.MeshBasicMaterial({ color: 0x92cfc2, transparent: true, opacity: .09, side: THREE.DoubleSide, depthWrite: false });
for (const side of [-1, 1]) {
const pane = new THREE.Mesh(new THREE.PlaneGeometry(6, 7.6), sideMat);
pane.rotation.y = Math.PI / 2;
pane.position.set(side * 8, 3.8, 0);
scene.add(pane);
}
const surface = new THREE.Mesh(
new THREE.PlaneGeometry(16, 6),
new THREE.MeshStandardMaterial({ color: 0x8fc7b8, transparent: true, opacity: .25, roughness: .2, metalness: .15, side: THREE.DoubleSide, depthWrite: false })
);
surface.rotation.x = -Math.PI / 2;
surface.position.y = 7.58;
scene.add(surface);
const frameMat = new THREE.MeshStandardMaterial({ color: 0x162c2b, roughness: .42, metalness: .45 });
const trimMat = new THREE.MeshStandardMaterial({ color: 0x74968b, roughness: .45, metalness: .6 });
function box(w, h, d, x, y, z, mat) {
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
m.position.set(x, y, z); scene.add(m); return m;
}
box(16.3, .32, 6.25, 0, -.14, 0, frameMat);
box(16.33, .035, .06, 0, -.04, 3.14, trimMat);
for (const z of [-3.03, 3.03]) {
box(16.28, .12, .14, 0, 7.64, z, frameMat);
box(16.27, .025, .03, 0, 7.72, z, trimMat);
}
for (const x of [-8.06, 8.06]) {
box(.14, .12, 6.15, x, 7.64, 0, frameMat);
box(.035, 7.65, .035, x, 3.82, 3.03, trimMat);
box(.025, 7.65, .025, x, 3.82, -3.03, trimMat);
}
// Fine reflections on the front glass.
const glass = new THREE.Mesh(new THREE.PlaneGeometry(16, 7.6), new THREE.ShaderMaterial({
transparent: true, depthWrite: false,
uniforms: {},
vertexShader: `varying vec2 vUv; void main(){vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
fragmentShader: `
varying vec2 vUv;
void main(){
float edge = pow(abs(vUv.x-.5)*2.0, 12.0)*.12;
float stripe = smoothstep(.055,0.0,abs(vUv.x - .12 - vUv.y*.12))*.035;
float stripe2 = smoothstep(.018,0.0,abs(vUv.x - .86 - vUv.y*.08))*.025;
gl_FragColor=vec4(.75,1.0,.91,edge+stripe+stripe2);
}`
}));
glass.position.set(0, 3.8, 3.07);
glass.renderOrder = 10;
scene.add(glass);
// Small elliptical marks on the water surface.
const rippleMat = new THREE.LineBasicMaterial({ color: 0xb6e1d0, transparent: true, opacity: .25, depthWrite: false });
for (let i = 0; i < 11; i++) {
const pts = [];
const cx = range(-7, 7), cz = range(-2.6, 2.6), rx = range(.18, .65), rz = rx * range(.2, .35);
for (let j = 0; j <= 35; j++) {
const a = j / 35 * Math.PI * 2;
pts.push(new THREE.Vector3(cx + Math.cos(a)*rx, 7.61, cz + Math.sin(a)*rz));
}
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), rippleMat));
}
// Stones nestled in the sand.
const stoneGeo = new THREE.IcosahedronGeometry(1, 2);
const stoneMats = [0x777e6c, 0x929487, 0x6d796b, 0xa29e88, 0x596a60].map(c =>
new THREE.MeshStandardMaterial({ color: c, roughness: 1 })
);
const rockClusters = [[-6.4, 1.1], [-4.9, -1.7], [6.3, .8], [4.8, -1.8], [-1.5, -2.1], [2.5, -2.3]];
for (const [cx, cz] of rockClusters) {
for (let i = 0; i < 5; i++) {
const rock = new THREE.Mesh(stoneGeo, stoneMats[Math.floor(rand() * stoneMats.length)]);
const s = range(.23, .64);
rock.scale.set(s * range(1, 1.6), s * range(.4, .7), s * range(.7, 1.1));
rock.position.set(cx + range(-.9, .9), s * .31, cz + range(-.55, .55));
rock.rotation.set(range(0,2), range(0,3), range(0,2));
scene.add(rock);
}
}
// Organic, curved blades of aquatic grass.
const leafMats = [0x315e48, 0x3c7655, 0x548a63, 0x285849, 0x6b9872, 0x396d61].map(color =>
new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide, roughness: .86 })
);
function makeLeaf(height, width, bend) {
const vertices = [], indices = [];
const steps = 12;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const center = bend * t * t + Math.sin(t * Math.PI) * bend * .1;
const w = width * Math.pow(Math.sin(Math.PI * t), .7) * (1 - .28 * t) + .006;
vertices.push(center - w, height * t, 0, center + w, height * t, 0);
if (i < steps) {
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();
return g;
}
const plantGroups = [];
function plant(x, z, size, count) {
const group = new THREE.Group();
group.position.set(x, .03, z);
for (let i = 0; i < count; i++) {
const h = size * range(.55, 1.18);
const leaf = new THREE.Mesh(makeLeaf(h, range(.07,.16) * size, range(-.55,.55) * size), leafMats[Math.floor(rand() * leafMats.length)]);
leaf.position.set(range(-.25,.25), 0, range(-.2,.2));
leaf.rotation.y = range(-.8, .8);
leaf.rotation.z = range(-.24, .24);
group.add(leaf);
}
group.userData.phase = range(0, 6);
scene.add(group); plantGroups.push(group);
}
plant(-7.2, -1.9, 3.5, 16);
plant(-6.2, -2.5, 2.4, 13);
plant(-7.3, 1.8, 2.2, 13);
plant(-4.7, -2.5, 1.55, 11);
plant(7.2, -2.0, 3.7, 17);
plant(6.1, -2.5, 2.6, 14);
plant(7.0, 1.5, 2.05, 13);
plant(4.6, -2.5, 1.6, 11);
plant(-2.5, -2.7, 1.0, 8);
plant(2.2, -2.7, 1.0, 8);
// A low piece of driftwood.
const woodMat = new THREE.MeshStandardMaterial({ color: 0x554a39, roughness: 1 });
function branch(points, radius) {
const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(...p)));
const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, 24, radius, 7, false), woodMat);
scene.add(mesh);
}
branch([[-6.3,.16,-.7],[-5.4,.27,-.8],[-4.4,.20,-1],[-3.6,.32,-1.2]], .13);
branch([[-5.2,.23,-.8],[-4.9,.48,-.9],[-4.55,.71,-1]], .065);
branch([[4.2,.16,-1.7],[5.0,.24,-1.5],[5.8,.19,-1.3],[6.6,.3,-1.1]], .11);
// Translucent bubble sprites.
function bubbleTexture() {
const c = document.createElement('canvas'); c.width = c.height = 64;
const ctx = c.getContext('2d');
ctx.beginPath(); ctx.arc(32,32,24,0,Math.PI*2);
ctx.fillStyle = 'rgba(192,245,232,.08)'; ctx.fill();
ctx.strokeStyle = 'rgba(212,255,242,.65)'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.arc(25,24,5,0,Math.PI*2);
ctx.fillStyle = 'rgba(245,255,250,.75)'; ctx.fill();
return new THREE.CanvasTexture(c);
}
const bubbleMat = new THREE.SpriteMaterial({ map: bubbleTexture(), transparent: true, opacity: .58, depthWrite: false });
const bubbles = [];
for (let i = 0; i < 37; i++) {
const sprite = new THREE.Sprite(bubbleMat);
const side = i % 2 ? -1 : 1;
sprite.position.set(side * range(5.3, 7.3), range(.4, 7.5), range(-2.1, 1.5));
const s = range(.07, .20);
sprite.scale.set(s, s, 1);
sprite.userData = { speed: range(.35, .8), phase: range(0, 6), x: sprite.position.x };
scene.add(sprite); bubbles.push(sprite);
}
// Fish bodies have a rounded profile, colored bellies and delicate fins.
function bodyGeometry(color, stripes) {
const verts = [], normals = [], colors = [], inds = [];
const rings = 32, sides = 24;
const base = new THREE.Color(color);
const stripeColor = new THREE.Color(0xf5dfb2);
for (let i = 0; i <= rings; i++) {
const t = i / rings;
const x = -.67 + 1.36 * t;
const r = .36 * Math.pow(Math.max(0, Math.sin(Math.PI * t)), .72) * (.80 + .25 * t) + .008;
for (let j = 0; j <= sides; j++) {
const a = j / sides * Math.PI * 2;
const y = Math.cos(a) * r, z = Math.sin(a) * r * .58;
verts.push(x, y, z);
const col = base.clone();
if (stripes && ((t > .36 && t < .43) || (t > .62 && t < .68))) col.lerp(stripeColor, .7);
col.multiplyScalar(y > 0 ? .91 : 1.13);
colors.push(col.r, col.g, col.b);
}
}
for (let i = 0; i < rings; i++) for (let j = 0; j < sides; j++) {
const a = i * (sides + 1) + j, b = a + sides + 1;
inds.push(a, b, a + 1, b, b + 1, a + 1);
}
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
g.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
g.setIndex(inds); g.computeVertexNormals();
return g;
}
function shapeMesh(shape, mat) { return new THREE.Mesh(new THREE.ShapeGeometry(shape, 16), mat); }
const eyeWhite = new THREE.MeshStandardMaterial({ color: 0xf1e1be, roughness: .4 });
const eyeBlack = new THREE.MeshStandardMaterial({ color: 0x142a27, roughness: .18 });
const glintMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const sphere = new THREE.SphereGeometry(1, 12, 10);
function createFish(spec, index) {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: .44, metalness: .05 });
group.add(new THREE.Mesh(bodyGeometry(spec.color, spec.stripes), bodyMat));
const finColor = new THREE.Color(spec.color).lerp(new THREE.Color(0xe6bd83), .22);
const finMat = new THREE.MeshStandardMaterial({ color: finColor, side: THREE.DoubleSide, transparent: true, opacity: .82, roughness: .55 });
const tailShape = new THREE.Shape();
tailShape.moveTo(0, 0);
tailShape.quadraticCurveTo(-.15,.18,-.57,.33);
tailShape.quadraticCurveTo(-.45,.11,-.46,0);
tailShape.quadraticCurveTo(-.45,-.11,-.57,-.33);
tailShape.quadraticCurveTo(-.15,-.18,0,0);
const tail = new THREE.Group();
tail.position.x = -.64;
tail.add(shapeMesh(tailShape, finMat));
const lineMat = new THREE.LineBasicMaterial({ color: 0xffe1b2, transparent: true, opacity: .28 });
for (let k = -2; k <= 2; k++) {
const points = [new THREE.Vector3(-.02,0,.005), new THREE.Vector3(-.48, k * .125, .005)];
tail.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), lineMat));
}
group.add(tail);
const dorsal = new THREE.Shape();
dorsal.moveTo(-.38,.24); dorsal.quadraticCurveTo(-.25,.54,-.09,.51);
dorsal.quadraticCurveTo(.08,.47,.25,.30); dorsal.quadraticCurveTo(-.05,.34,-.38,.24);
group.add(shapeMesh(dorsal, finMat));
const bottom = new THREE.Shape();
bottom.moveTo(-.34,-.23); bottom.quadraticCurveTo(-.26,-.44,-.08,-.43);
bottom.lineTo(.12,-.30); bottom.quadraticCurveTo(-.12,-.31,-.34,-.23);
group.add(shapeMesh(bottom, finMat));
const finShape = new THREE.Shape();
finShape.moveTo(.08,0); finShape.quadraticCurveTo(-.12,.06,-.30,-.13);
finShape.quadraticCurveTo(-.14,-.25,.08,0);
const fins = [];
for (const side of [-1, 1]) {
const fin = shapeMesh(finShape, finMat);
fin.position.set(.05, -.03, side * .19);
fin.rotation.y = side * .3;
group.add(fin); fins.push(fin);
const eye = new THREE.Mesh(sphere, eyeWhite);
eye.scale.setScalar(.072); eye.position.set(.42,.085,side*.148); group.add(eye);
const pupil = new THREE.Mesh(sphere, eyeBlack);
pupil.scale.setScalar(.047); pupil.position.set(.43,.087,side*.205); group.add(pupil);
const sparkle = new THREE.Mesh(sphere, glintMat);
sparkle.scale.setScalar(.014); sparkle.position.set(.442,.108,side*.245); group.add(sparkle);
const gillPoints = [];
for (let j = 0; j <= 12; j++) {
const t = j / 12;
gillPoints.push(new THREE.Vector3(.16 + Math.sin(t*Math.PI)*.05, .19 - t*.34, side * (.165 + Math.sin(t*Math.PI)*.035)));
}
const gillMat = new THREE.LineBasicMaterial({ color: new THREE.Color(spec.color).multiplyScalar(.57), transparent: true, opacity: .7 });
group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(gillPoints), gillMat));
}
group.position.set(spec.x, spec.y, spec.z);
group.scale.setScalar(spec.s);
scene.add(group);
const vx = spec.dir * range(.65, .95);
const velocity = new THREE.Vector3(vx, range(-.12,.12), range(-.15,.15));
group.rotation.y = vx < 0 ? Math.PI : 0;
return { group, tail, fins, velocity, phase: index * 1.37, speed: range(.65, .95) };
}
const specs = [
{x:-5.4,y:5.65,z:-.5,s:.88,color:0xe79459,dir:1,stripes:true},
{x:-3.25,y:5.05,z:.5,s:.83,color:0xeab776,dir:1},
{x:-1.0,y:6.15,z:-1,s:.78,color:0xd6d6b2,dir:-1},
{x:1.45,y:5.45,z:.3,s:.92,color:0xe5965c,dir:-1,stripes:true},
{x:4.05,y:5.9,z:-.8,s:.8,color:0xd7be84,dir:1},
{x:5.8,y:4.35,z:.6,s:.87,color:0xd68a63,dir:-1},
{x:-5.2,y:3.35,z:.6,s:.91,color:0xd7cda5,dir:1},
{x:-2.4,y:3.65,z:-.4,s:1.02,color:0xed9d62,dir:1,stripes:true},
{x:.45,y:3.9,z:1,s:.86,color:0xe9c27f,dir:-1},
{x:3.25,y:3.05,z:-.3,s:.97,color:0xcbd1af,dir:-1},
{x:-3.3,y:2.0,z:.4,s:.78,color:0xd79967,dir:1},
{x:1.45,y:2.05,z:-.1,s:.85,color:0xe9a775,dir:1,stripes:true}
];
const fish = specs.map(createFish);
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const waterPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const pointerWorld = new THREE.Vector3();
let pointerActive = false;
function updatePointer(e) {
mouse.x = e.clientX / window.innerWidth * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
pointerActive = !!raycaster.ray.intersectPlane(waterPlane, pointerWorld);
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
cursor.classList.add('visible');
}
renderer.domElement.addEventListener('pointermove', updatePointer);
renderer.domElement.addEventListener('pointerdown', updatePointer);
renderer.domElement.addEventListener('pointerleave', () => {
pointerActive = false; cursor.classList.remove('visible');
});
renderer.domElement.addEventListener('pointercancel', () => {
pointerActive = false; cursor.classList.remove('visible');
});
let paused = false;
document.getElementById('pause').addEventListener('click', () => {
paused = !paused;
document.getElementById('pause-label').textContent = paused ? 'PLAY' : 'PAUSE';
document.querySelector('.pause-icon').textContent = paused ? '▶' : 'Ⅱ';
document.getElementById('pause').setAttribute('aria-label', paused ? 'Resume motion' : 'Pause motion');
});
function angleLerp(a, b, amount) {
const delta = Math.atan2(Math.sin(b - a), Math.cos(b - a));
return a + delta * amount;
}
let previous = performance.now(), elapsed = 0;
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min((now - previous) / 1000, .04);
previous = now;
if (!paused) {
elapsed += dt;
for (const f of fish) {
const p = f.group.position, v = f.velocity;
v.y += Math.sin(elapsed * .63 + f.phase) * .11 * dt;
v.z += Math.cos(elapsed * .47 + f.phase) * .08 * dt;
if (p.x > 6.85) v.x -= (p.x - 6.85 + .5) * 2.0 * dt;
if (p.x < -6.85) v.x += (-6.85 - p.x + .5) * 2.0 * dt;
if (p.y > 6.75) v.y -= (p.y - 6.75 + .3) * 2.2 * dt;
if (p.y < 1.25) v.y += (1.25 - p.y + .3) * 2.2 * dt;
if (p.z > 2.0) v.z -= (p.z - 2.0 + .2) * 1.7 * dt;
if (p.z < -2.0) v.z += (-2.0 - p.z + .2) * 1.7 * dt;
let fleeing = false;
if (pointerActive) {
let dx = p.x - pointerWorld.x, dy = p.y - pointerWorld.y;
let dist = Math.hypot(dx, dy);
if (dist < 2.25) {
if (dist < .05) { dx = Math.cos(f.phase); dy = Math.sin(f.phase); dist = 1; }
const strength = (1 - dist / 2.25) * 7.5;
v.x += dx / dist * strength * dt;
v.y += dy / dist * strength * dt;
fleeing = true;
}
}
v.multiplyScalar(Math.exp(-(fleeing ? .08 : .32) * dt));
const speed = v.length();
if (speed < f.speed) v.multiplyScalar(1 + (f.speed - speed) * .7 * dt / Math.max(speed, .01));
if (speed > 2.9) v.multiplyScalar(2.9 / speed);
p.addScaledVector(v, dt);
const heading = Math.atan2(-v.z, v.x);
f.group.rotation.y = angleLerp(f.group.rotation.y, heading, Math.min(1, dt * 2.5));
const tilt = Math.atan2(v.y, Math.max(.25, Math.abs(v.x))) * (v.x < 0 ? -1 : 1);
f.group.rotation.z += (THREE.MathUtils.clamp(tilt, -.45, .45) - f.group.rotation.z) * Math.min(1, dt * 2);
f.tail.rotation.y = Math.sin(elapsed * (7 + Math.min(speed, 2)) + f.phase) * .52;
f.fins[0].rotation.y = -.3 + Math.sin(elapsed * 8 + f.phase) * .25;
f.fins[1].rotation.y = .3 - Math.sin(elapsed * 8 + f.phase) * .25;
}
for (const b of bubbles) {
b.position.y += b.userData.speed * dt;
b.position.x = b.userData.x + Math.sin(elapsed * 1.1 + b.userData.phase) * .09;
if (b.position.y > 7.45) b.position.y = .25;
}
for (const p of plantGroups) p.rotation.z = Math.sin(elapsed * .7 + p.userData.phase) * .025;
}
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
function resize() {
const w = window.innerWidth, h = window.innerHeight;
camera.aspect = w / h;
camera.fov = w / h < .7 ? 42 : 35;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
renderer.setSize(w, h);
}
window.addEventListener('resize', resize);
resize();
</script>
</body>
</html>Reasoning trace (7,763 tokens)
**Designing a 3D Aquarium** I need to produce a complete HTML Three.js design with an import map. The goal is to create a visually impressive, responsive aquarium scene where fish swim away from the mouse. I should think carefully about the design elements, starting with a beautifully painted aquarium scene. It should include real 3D objects like custom fish with animations, surrounded by plants, sand, and atmospheric lighting. I want a cohesive layout with a dark background and clear tank boundaries, ensuring the 3D tank is visible right away.**Planning the Aquarium Scene** I'm thinking about using primitive geometries and shaders for the water's caustics, which could look really nice. I need to avoid external requests except for three CDN links and implement everything inline. The scene layout is full viewport with a body background in a dark color. I want to include a header overlay with a small logo and "AQUA," plus instructions for user interaction. I could add controls like a "Reset view" button and an audio toggle. The aquarium should feel polished, with a clear canvas and structured information cards without overcrowding.**Designing the Framed Tank** I'm considering creating a nicely framed tank with a perspective camera positioned at (0, 5, 15) and focused on (0, 2.5, 0). The tank's dimensions would be 16 units wide, 7.5 high, and 6 deep, fitting within a canvas edge of a 1280x800 viewport. The UI would have a header height of 20-100 units, with the main tank spanning from 105 to 670 vertically. I also want to ensure the glass outlines and edges are visible, showcasing water ripples, a sandy floor, and a metal border.**Exploring CanvasTexture options** I'm thinking about generating a CanvasTexture for light rays and possibly for the floor too. There’s a lot to consider with textures, like how they interact with light and the overall visual effect. I want to ensure that the light rays create the desired ambiance while also checking how the floor texture complements the scene! It’s about getting that perfect balance. Let’s delve into the specifics of the textures and their applications!**Planning fish visualization** I’m thinking about the color scheme for the fish, something like a base color of orange with accents of pale orange, gold, dark teal blue, and cream. I need to label them visibly, maybe using luminous text against the background. The schooling involves around 12 fish, with most of them centrally positioned. I’m figuring out the size and scale so they display properly, ensuring they don’t appear too small against the projected height. Overall, these details are crucial for an effective visual representation.**Designing a premium background** I’m thinking about creating a background shader with undulating animated bands at the top and including 45 small motes. I might layer this with floor stones made from overlapping customized spheres. Maybe I could add driftwood rocks and some plants around the sides and back for extra detail. I want to incorporate sand textures and rocks to make it realistic while ensuring the overall appearance is visually harmonious and premium!**Calculating camera dimensions** I'm working through some math for a camera setup. I have a width of 16.3 and specific camera coordinates. For distance and field of view calculations, I find a vertical and horizontal measurement for viewing space. There's a complex relationship between the front and the back width, and I'm trying to make sense of how much of the viewport is occupied. It includes projecting points for perspective as well — it's a bit intricate, but I'm getting there!**Analyzing mobile camera view** I'm looking at the camera view when it's mobile, and I've got an aspect ratio of .469 with a field of view of 35. The horizontal view width measures 5.8, which I need to compare against the tank width of 16. This difference indicates that I'll need to crop the image to fit. It's a bit of a balancing act, but figuring out these dimensions will help ensure a proper fit!**Evaluating mobile camera setup** I’m thinking about how to adjust the camera for mobile use. Since the portrait width can’t fit everything at once, I need to render the tank mostly in the upper or middle so the fish remain visible, even if they are small. The user mentioned needing a graceful adaptation without horizontal scrollbars. Crop width should be focused on the center to make the fish prominent, tweaking the camera positioning and aspect ratio could help with that!**Adjusting fish tank display** I’m focusing on keeping the fish size between 50-100px to ensure they’re enjoyable for the viewer, while the tank’s front rim will be out of sight. For mobile, I can modify the camera to fit a width of 10.8 and adjust the vertical view so that the tank takes up about 35% of the screen. I’m also considering scaling the fish for better visibility. I think using three different colors with proper lighting will help enhance the overall appearance of the scene!**Designing custom shader for the back wall** I need to create a realistic custom shader for the back wall that considers three colors for both the working color and shader output. It’s important to ensure that the ShaderMaterial's raw output incorporates tone mapping. I should include both `<tonemapping_fragment>` and `<colorspace_fragment>` in the shader setup to ensure proper color treatment. Using a standard plane with a canvas texture gradient could be a good approach to achieving the desired effect. This should help create a visually appealing backdrop!**Improving rock textures** I'm considering how to create the rocks in my design with 24 irregular low poly shapes using IcosahedronGeometry. They'll be scaled between 0.3 and 0.9 and come in muted gray and earthy tones. I envision placing them individually on the bottom with subtle variations in their y-positions. I think I’d like to arrange larger rocks in clusters, maybe three mounds altogether. I'll also enhance the first paint of the sandbox by adding textures and hemisphere lighting with some speckles for detail.**Designing fish species** I'm considering how to create fish species with stripes and varying body colors. I think using shader geometry could allow for different colors depending on their position and angles. For example, I could use shades like coral orange, pale cream, and blue, blending colors smoothly along the x-axis for a natural look. I'll generate geometry for each species and ensure they have a cute appearance, especially focusing on the shapes of their bodies and gills. This could be a fun project!**Evaluating rotation settings** I’m looking into how to set the angle interpolation for a group rotation based on velocity. If the x-component of velocity is positive, I'll set `group.rotation.y` to around 0; if it's negative, it should be π. I need to start with an initial angle set based on the calculated value from `atan2(-vel.z, vel.x)`. I'll also think about the color variation for the fish as part of this process. Let's move forward!