Interactive 3D aquariumOne-line
Claude Haiku 5.5
by AnthropicReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 5 s
- Cost
- $0.029
- Output tokens
- 57,500
- Spent thinking
- 45,309 tokens
- Speed
- 236.5 tok/s
- Console errors
- 0
- First token after
- 2.0 s
- First visible text after
- 3 min 30 s
- Prompt tokens
- 649
- Code size
- 22.5 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"high"}- Max output
- 128,000 tokens
- Generated
- Nov 10, 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 · 624 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Reef Aquarium · fish scatter from your cursor</title>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;overflow:hidden;background:#042a38;touch-action:none;user-select:none;-webkit-user-select:none}
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#eafcff}
#scene{position:fixed;inset:0;width:100vw;height:100vh;display:block}
.vignette{position:fixed;inset:0;pointer-events:none;
background:radial-gradient(ellipse at 50% 45%,rgba(0,0,0,0) 55%,rgba(2,18,28,.6) 100%)}
header{position:fixed;top:20px;left:24px;pointer-events:none;text-shadow:0 2px 14px rgba(0,20,30,.5)}
h1{margin:0;font-size:24px;font-weight:650;letter-spacing:.2px}
header p{margin:4px 0 0;font-size:13px;opacity:.78}
#hint{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);padding:10px 18px;border-radius:999px;
font-size:14px;background:rgba(3,34,46,.6);border:1px solid rgba(170,240,255,.22);
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);white-space:nowrap;
transition:opacity .8s ease;pointer-events:none}
#hint.dim{opacity:.45}
#hud{position:fixed;right:20px;bottom:24px;font-size:12px;opacity:.8;font-variant-numeric:tabular-nums;
text-shadow:0 1px 6px rgba(0,0,0,.4);pointer-events:none}
@media (max-width:600px){
h1{font-size:19px}
header{top:14px;left:16px}
header p{font-size:12px}
#hint{font-size:12px;white-space:normal;text-align:center;width:max-content;max-width:88vw;bottom:14px;padding:8px 14px}
#hud{right:14px;bottom:auto;top:16px}
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div class="vignette"></div>
<header>
<h1>Reef Aquarium</h1>
<p>Three.js · flocking fish that flee your cursor</p>
</header>
<div id="hint">Move your mouse near the fish and they scatter</div>
<div id="hud"><span id="count">0</span> fish · <span id="scared">0</span> startled</div>
<script type="module">
import * as THREE from 'three';
/* ---------- renderer / scene / camera ---------- */
const canvas = document.getElementById('scene');
const hintEl = document.getElementById('hint');
const countEl = document.getElementById('count');
const scaredEl = document.getElementById('scared');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = makeSkyGradient();
scene.fog = new THREE.FogExp2(0x146b7c, 0.03);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 300);
const LOOK_AT = new THREE.Vector3(0, 0.6, 0);
let camDist = 17.5;
/* ---------- constants ---------- */
const FLOOR_Y = -3;
const B = { x: 9.4, y0: -2.5, y1: 4.6, z: 2.6 }; // tank bounds
const FLEE_R = 5.5; // how far the cursor affects fish
const SEP_R = 1.9; // fish keep this distance
const NEIGH_R = 5.0; // flocking neighbourhood
const AX = new THREE.Vector3(1, 0, 0);
const rand = (a, b) => a + Math.random() * (b - a);
const floorHeight = (x, z) =>
FLOOR_Y + Math.sin(x * 0.45 + 1.3) * 0.12 + Math.cos(z * 0.7 - x * 0.2) * 0.1 + Math.sin(x * 1.9 + z * 1.3) * 0.04;
const sectionScale = (x) => Math.sqrt(Math.max(0, 1 - x * x)) * (x < 0 ? 1 + x * 0.62 : 1);
/* ---------- lights ---------- */
scene.add(new THREE.HemisphereLight(0xcdf6ff, 0x24503f, 2.0));
const sun = new THREE.DirectionalLight(0xeafcff, 2.2);
sun.position.set(-4, 12, 6);
scene.add(sun);
/* ---------- sandy floor ---------- */
const floorGeo = new THREE.PlaneGeometry(120, 60, 90, 45);
floorGeo.rotateX(-Math.PI / 2);
{
const p = floorGeo.attributes.position;
for (let i = 0; i < p.count; i++) p.setY(i, floorHeight(p.getX(i), p.getZ(i)));
floorGeo.computeVertexNormals();
}
const floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: '#cdb580', roughness: 0.95 }));
scene.add(floor);
/* ---------- caustic light pattern on the floor (additive, animated) ---------- */
function makeCausticTexture() {
const S = 256;
const c = document.createElement('canvas');
c.width = c.height = S;
const g = c.getContext('2d');
const img = g.createImageData(S, S);
const d = img.data;
for (let y = 0; y < S; y++) {
for (let x = 0; x < S; x++) {
const u = (x / S) * Math.PI * 2, v = (y / S) * Math.PI * 2;
const a = Math.sin(u * 3 + Math.sin(v * 2) * 1.7);
const b = Math.sin(v * 3 + Math.sin(u * 2 + 1) * 1.7);
const val = Math.pow(1 - Math.min(1, Math.abs(a * b)), 10);
const k = Math.round(val * 255);
const i = (y * S + x) * 4;
d[i] = d[i + 1] = d[i + 2] = k;
d[i + 3] = 255;
}
}
g.putImageData(img, 0, 0);
const tex = new THREE.CanvasTexture(c);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(12, 6);
return tex;
}
const causticTex = makeCausticTexture();
const caustics = new THREE.Mesh(
new THREE.PlaneGeometry(120, 60),
new THREE.MeshBasicMaterial({
map: causticTex, color: 0x9ff3ff, transparent: true, opacity: 0.45,
blending: THREE.AdditiveBlending, depthWrite: false, fog: false
})
);
caustics.rotation.x = -Math.PI / 2;
caustics.position.y = FLOOR_Y + 0.3;
scene.add(caustics);
/* ---------- light shafts ---------- */
function makeRayTexture() {
const c = document.createElement('canvas');
c.width = 32; c.height = 256;
const g = c.getContext('2d');
const grad = g.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, 'rgba(255,255,255,0.9)');
grad.addColorStop(0.6, 'rgba(255,255,255,0.25)');
grad.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = grad;
g.fillRect(0, 0, 32, 256);
g.globalCompositeOperation = 'destination-in';
const h = g.createLinearGradient(0, 0, 32, 0);
h.addColorStop(0, 'rgba(0,0,0,0)');
h.addColorStop(0.5, 'rgba(0,0,0,1)');
h.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = h;
g.fillRect(0, 0, 32, 256);
return new THREE.CanvasTexture(c);
}
const rayTex = makeRayTexture();
const rays = [];
for (let i = 0; i < 7; i++) {
const m = new THREE.MeshBasicMaterial({
map: rayTex, color: 0xbff7ff, transparent: true, opacity: 0.07,
blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, fog: false
});
const r = new THREE.Mesh(new THREE.PlaneGeometry(rand(1.4, 2.6), 14), m);
r.position.set(-13 + i * 4.1 + rand(-0.6, 0.6), 4, rand(-6, -2));
r.userData.baseRot = rand(-0.28, -0.16);
r.userData.seed = rand(0, 10);
r.rotation.z = r.userData.baseRot;
scene.add(r);
rays.push(r);
}
/* ---------- rocks ---------- */
const rockMats = ['#6e6a66', '#7a7065', '#5d6168'].map(c =>
new THREE.MeshStandardMaterial({ color: c, roughness: 0.9, flatShading: true }));
for (let i = 0; i < 12; i++) {
const geo = new THREE.IcosahedronGeometry(1, 2);
const p = geo.attributes.position;
const seed = rand(0, 10);
const v = new THREE.Vector3();
for (let k = 0; k < p.count; k++) {
v.fromBufferAttribute(p, k);
// deterministic displacement so shared vertices stay welded
const n = 0.82 + 0.25 * Math.sin(v.x * 3.1 + seed) * Math.cos(v.y * 2.7 + v.z * 2.3 + seed * 0.7)
+ 0.08 * Math.sin(v.z * 7.1 + seed);
p.setXYZ(k, v.x * n, v.y * n * 0.7, v.z * n);
}
geo.computeVertexNormals();
const rock = new THREE.Mesh(geo, rockMats[i % rockMats.length]);
const s = rand(0.5, 1.6);
rock.scale.set(s * rand(1, 1.6), s * 0.7, s);
const x = rand(-10, 10), z = rand(-3.4, 0.6);
rock.position.set(x, floorHeight(x, z) + s * 0.15, z);
rock.rotation.y = rand(0, Math.PI);
scene.add(rock);
}
/* ---------- seaweed (vertex-animated ribbons) ---------- */
const weedMats = ['#2f8a57', '#3aa368', '#24704a', '#4bb178'].map(c =>
new THREE.MeshStandardMaterial({ color: c, roughness: 0.7, side: THREE.DoubleSide }));
const weeds = [];
for (let i = 0; i < 28; i++) {
const h = rand(2.2, 4.8);
const w = rand(0.35, 0.6);
const geo = new THREE.PlaneGeometry(w, h, 1, 12);
geo.translate(0, h / 2, 0);
const base = Float32Array.from(geo.attributes.position.array);
const mesh = new THREE.Mesh(geo, weedMats[i % weedMats.length]);
const x = rand(-10, 10), z = rand(-4.2, -1.4);
mesh.position.set(x, floorHeight(x, z) + 0.02, z);
mesh.rotation.y = rand(-0.4, 0.4);
scene.add(mesh);
weeds.push({ geo, base, h, speed: rand(0.8, 1.4), phase: rand(0, 6) });
}
/* ---------- bubbles (instanced) ---------- */
const BUBBLES = 46;
const bubbleMesh = new THREE.InstancedMesh(
new THREE.SphereGeometry(1, 8, 6),
new THREE.MeshStandardMaterial({ color: '#e8fdff', roughness: 0.05, metalness: 0.1, transparent: true, opacity: 0.38 }),
BUBBLES
);
bubbleMesh.frustumCulled = false;
scene.add(bubbleMesh);
const bubbles = [];
for (let i = 0; i < BUBBLES; i++) {
const x = rand(-9, 9), z = rand(-2.2, 2.2);
bubbles.push({ x, z, y: rand(FLOOR_Y, 4.5), r: rand(0.04, 0.12), sp: rand(0.5, 1.1), ph: rand(0, 10) });
}
const dummy = new THREE.Object3D();
/* ---------- marine snow (drifting particles) ---------- */
const SNOW = 260;
const snowPos = new Float32Array(SNOW * 3);
const snowSeed = new Float32Array(SNOW);
for (let i = 0; i < SNOW; i++) {
snowPos[i * 3] = rand(-10, 10);
snowPos[i * 3 + 1] = rand(FLOOR_Y, 5);
snowPos[i * 3 + 2] = rand(-3, 3);
snowSeed[i] = rand(0, 10);
}
const snowGeo = new THREE.BufferGeometry();
snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPos, 3));
const snow = new THREE.Points(snowGeo, new THREE.PointsMaterial({
color: '#e6fbff', size: 0.07, transparent: true, opacity: 0.45, depthWrite: false
}));
snow.frustumCulled = false;
scene.add(snow);
/* ---------- fish ---------- */
const SPECIES = [
{ body: '#ff7a2b', fin: '#ffb36b', stripes: [0.25, -0.32], stripe: '#f7f7f7' }, // clown
{ body: '#2d6fe6', fin: '#6ea4ff' }, // blue tang
{ body: '#ffd23f', fin: '#ffe88a' }, // lemon
{ body: '#c7d7e0', fin: '#eef6fa' }, // silver
{ body: '#9d5cff', fin: '#c9a2ff' }, // violet
{ body: '#ff4f5e', fin: '#ff8e99' }, // ruby
];
function buildFishTemplate(sp) {
const g = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: sp.body, roughness: 0.35, metalness: 0.05 });
const finMat = new THREE.MeshStandardMaterial({ color: sp.fin, roughness: 0.5, side: THREE.DoubleSide, transparent: true, opacity: 0.9 });
const whiteMat = new THREE.MeshStandardMaterial({ color: '#ffffff', roughness: 0.25 });
const pupilMat = new THREE.MeshStandardMaterial({ color: '#0b1014', roughness: 0.2 });
const stripeMat = new THREE.MeshStandardMaterial({ color: sp.stripe || '#ffffff', roughness: 0.4 });
// body: tapered ellipsoid along +X (head at +X)
const bodyGeo = new THREE.SphereGeometry(1, 28, 18);
const bp = bodyGeo.attributes.position;
for (let i = 0; i < bp.count; i++) {
const x = bp.getX(i);
if (x < 0) {
const t = 1 + x * 0.62;
bp.setY(i, bp.getY(i) * t);
bp.setZ(i, bp.getZ(i) * t);
}
}
bodyGeo.scale(1, 0.38, 0.27);
bodyGeo.computeVertexNormals();
g.add(new THREE.Mesh(bodyGeo, bodyMat));
// forked tail (rotates around Y to wag side to side)
const tailShape = new THREE.Shape();
tailShape.moveTo(0.05, 0);
tailShape.quadraticCurveTo(-0.3, 0.22, -0.6, 0.5);
tailShape.quadraticCurveTo(-0.46, 0, -0.6, -0.5);
tailShape.quadraticCurveTo(-0.3, -0.22, 0.05, 0);
const tail = new THREE.Group();
tail.name = 'tail';
tail.position.set(-0.96, 0, 0);
tail.add(new THREE.Mesh(new THREE.ShapeGeometry(tailShape, 8), finMat));
g.add(tail);
// dorsal fin
const dorsal = new THREE.Shape();
dorsal.moveTo(0.4, 0.22);
dorsal.quadraticCurveTo(0.0, 0.5, -0.45, 0.2);
dorsal.lineTo(-0.4, 0.05);
dorsal.lineTo(0.4, 0.05);
g.add(new THREE.Mesh(new THREE.ShapeGeometry(dorsal, 6), finMat));
// pectoral fins
for (const s of [-1, 1]) {
const pf = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 8), finMat);
pf.scale.set(1, 0.25, 0.6);
pf.position.set(0.25, -0.05, s * 0.26);
pf.rotation.x = s * 0.3;
g.add(pf);
}
// eyes
for (const s of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.085, 14, 10), whiteMat);
eye.position.set(0.56, 0.1, s * 0.2);
g.add(eye);
const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.04, 10, 8), pupilMat);
pupil.position.set(0.6, 0.1, s * 0.27);
g.add(pupil);
}
// optional stripes (wrapped rings around the body)
if (sp.stripes) {
for (const x of sp.stripes) {
const f = sectionScale(x);
const geo = new THREE.TorusGeometry(1, 0.065, 8, 40);
geo.rotateY(Math.PI / 2);
geo.scale(1, 0.38 * f * 1.03, 0.27 * f * 1.03);
geo.translate(x, 0, 0);
g.add(new THREE.Mesh(geo, stripeMat));
}
}
return g;
}
const templates = SPECIES.map(buildFishTemplate);
const fishes = [];
const FISH_COUNT = 60;
for (let i = 0; i < FISH_COUNT; i++) {
const obj = templates[Math.floor(Math.random() * templates.length)].clone(true);
const size = rand(0.6, 1.1);
obj.scale.setScalar(size);
obj.position.set(rand(-8, 8), rand(-1.8, 3.2), rand(-2, 2));
scene.add(obj);
const speed = rand(0.9, 1.6);
fishes.push({
obj,
tail: obj.getObjectByName('tail'),
v: new THREE.Vector3(rand(-1, 1), rand(-0.3, 0.3), rand(-0.5, 0.5)).normalize().multiplyScalar(speed),
speed,
seed: rand(0, 100),
tailPhase: rand(0, 6),
panic: 0,
force: new THREE.Vector3(),
qT: new THREE.Quaternion()
});
}
/* ---------- pointer (cursor in 3D, plus a self-playing demo pointer) ---------- */
const raycaster = new THREE.Raycaster();
const ndc = new THREE.Vector2();
const zPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const hit = new THREE.Vector3();
const pointerTarget = new THREE.Vector3();
const pointerPos = new THREE.Vector3();
const demoTmp = new THREE.Vector3();
let lastRealMove = -Infinity;
let elapsed = 0;
function demoPointer(t) {
return demoTmp.set(
Math.sin(t * 0.42) * 7.5 + Math.sin(t * 0.9) * 1.2,
0.8 + Math.sin(t * 0.63) * 2.4,
Math.sin(t * 0.29) * 1.2
);
}
function onPointer(e) {
ndc.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
raycaster.setFromCamera(ndc, camera);
if (raycaster.ray.intersectPlane(zPlane, hit)) {
pointerTarget.set(
THREE.MathUtils.clamp(hit.x, -12, 12),
THREE.MathUtils.clamp(hit.y, -3, 6),
0
);
lastRealMove = elapsed;
hintEl.classList.add('dim');
}
}
window.addEventListener('pointermove', onPointer, { passive: true });
window.addEventListener('pointerdown', onPointer, { passive: true });
window.addEventListener('mouseout', (e) => { if (!e.relatedTarget) lastRealMove = -Infinity; });
window.addEventListener('blur', () => { lastRealMove = -Infinity; });
// pointer visual: soft glow sprite + ring facing the camera
function makeGlowTexture() {
const c = document.createElement('canvas');
c.width = c.height = 128;
const g = c.getContext('2d');
const grad = g.createRadialGradient(64, 64, 0, 64, 64, 64);
grad.addColorStop(0, 'rgba(255,255,255,0.9)');
grad.addColorStop(0.4, 'rgba(255,255,255,0.25)');
grad.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = grad;
g.fillRect(0, 0, 128, 128);
return new THREE.CanvasTexture(c);
}
const pointerGroup = new THREE.Group();
const glow = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeGlowTexture(), color: 0x9ff0ff, transparent: true, opacity: 0.35,
blending: THREE.AdditiveBlending, depthWrite: false, fog: false
}));
glow.scale.set(5, 5, 1);
const ringMat = new THREE.MeshBasicMaterial({ color: '#d8fbff', transparent: true, opacity: 0.5, depthWrite: false, fog: false });
const ring = new THREE.Mesh(new THREE.RingGeometry(0.3, 0.38, 48), ringMat);
pointerGroup.add(glow, ring);
scene.add(pointerGroup);
/* ---------- flocking + fleeing ---------- */
const tmpD = new THREE.Vector3(), tmpA = new THREE.Vector3(), tmpB = new THREE.Vector3();
const avgV = new THREE.Vector3(), avgP = new THREE.Vector3(), sepV = new THREE.Vector3();
function updateFish(dt, t) {
const n = fishes.length;
for (let i = 0; i < n; i++) {
const f = fishes[i];
const p = f.obj.position;
const F = f.force.set(0, 0, 0);
avgV.set(0, 0, 0); avgP.set(0, 0, 0); sepV.set(0, 0, 0);
let cnt = 0;
for (let j = 0; j < n; j++) {
if (j === i) continue;
const g = fishes[j];
const q = g.obj.position;
tmpD.subVectors(p, q);
const d2 = tmpD.lengthSq();
if (d2 < NEIGH_R * NEIGH_R) {
cnt++;
avgV.add(g.v);
avgP.add(q);
if (d2 < SEP_R * SEP_R) {
const d = Math.sqrt(d2) + 1e-4;
sepV.addScaledVector(tmpD, (SEP_R - d) / (SEP_R * d));
}
}
}
F.addScaledVector(sepV, 5);
if (cnt > 0) {
avgV.multiplyScalar(1 / cnt);
avgP.multiplyScalar(1 / cnt);
F.addScaledVector(tmpA.subVectors(avgV, f.v), 0.8); // alignment
F.addScaledVector(tmpA.subVectors(avgP, p), 0.25); // cohesion
}
// gentle wandering
const w = t * 0.5 + f.seed;
F.x += Math.sin(w * 1.3) * 0.6;
F.y += Math.sin(w * 0.9 + 1.7) * 0.35;
F.z += Math.cos(w * 1.1) * 0.4;
// soft walls of the tank
const mX = 1.8, mY = 1.6, mZ = 0.9, k = 6;
if (p.x > B.x - mX) F.x -= (p.x - (B.x - mX)) * k;
if (p.x < -(B.x - mX)) F.x += (-(B.x - mX) - p.x) * k;
if (p.y > B.y1 - mY) F.y -= (p.y - (B.y1 - mY)) * k;
if (p.y < B.y0 + mY) F.y += (B.y0 + mY - p.y) * k;
if (p.z > B.z - mZ) F.z -= (p.z - (B.z - mZ)) * k;
if (p.z < -(B.z - mZ)) F.z += (-(B.z - mZ) - p.z) * k;
// flee the pointer
f.panic *= Math.exp(-dt * 1.3);
tmpB.subVectors(p, pointerPos);
const dist = tmpB.length();
if (dist < FLEE_R) {
const s = 1 - dist / FLEE_R;
f.panic = Math.max(f.panic, s);
if (dist > 1e-3) F.addScaledVector(tmpB, (40 * s * s) / dist);
}
// integrate
f.v.addScaledVector(F, dt);
const maxS = f.speed * (1 + f.panic * 2.4);
const minS = f.speed * 0.7;
let sp = f.v.length();
if (sp < 1e-3) {
f.v.set(rand(-1, 1), rand(-0.3, 0.3), rand(-0.5, 0.5)).normalize().multiplyScalar(minS);
sp = minS;
}
if (sp > maxS) { f.v.multiplyScalar(maxS / sp); sp = maxS; }
else if (sp < minS) { f.v.multiplyScalar(minS / sp); sp = minS; }
p.addScaledVector(f.v, dt);
p.x = THREE.MathUtils.clamp(p.x, -B.x, B.x);
p.y = THREE.MathUtils.clamp(p.y, B.y0, B.y1 + 0.5);
p.z = THREE.MathUtils.clamp(p.z, -B.z, B.z);
// face the direction of travel (smooth)
tmpA.copy(f.v).normalize();
f.qT.setFromUnitVectors(AX, tmpA);
f.obj.quaternion.slerp(f.qT, 1 - Math.exp(-dt * 7));
// tail beat speeds up when startled
f.tailPhase += dt * (6 + sp * 2.5 + f.panic * 5);
f.tail.rotation.y = Math.sin(f.tailPhase) * (0.35 + f.panic * 0.25);
}
}
/* ---------- layout / resize ---------- */
function layout() {
const w = window.innerWidth, h = window.innerHeight;
const aspect = w / h;
renderer.setSize(w, h, false);
camera.aspect = aspect;
camera.fov = aspect < 1 ? 58 : 45;
camera.updateProjectionMatrix();
const tanH = Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));
camDist = Math.max(17.5, 10.5 / (tanH * aspect));
}
layout();
window.addEventListener('resize', layout);
/* ---------- sky gradient (screen-space background) ---------- */
function makeSkyGradient() {
const c = document.createElement('canvas');
c.width = 4; c.height = 256;
const g = c.getContext('2d');
const grad = g.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#5cc3d4');
grad.addColorStop(0.35, '#1b7e92');
grad.addColorStop(0.75, '#0a4a5e');
grad.addColorStop(1, '#042a38');
g.fillStyle = grad;
g.fillRect(0, 0, 4, 256);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
/* ---------- main loop ---------- */
let last = performance.now();
let hudTimer = 0;
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
elapsed += dt;
const t = elapsed;
// real cursor wins for 2.5s after the last move; otherwise a demo pointer sweeps the tank
const isReal = (t - lastRealMove) < 2.5;
const target = isReal ? pointerTarget : demoPointer(t);
pointerPos.lerp(target, 1 - Math.exp(-dt * (isReal ? 14 : 3)));
updateFish(dt, t);
// pointer visuals
pointerGroup.position.copy(pointerPos);
pointerGroup.quaternion.copy(camera.quaternion);
const pulse = 1 + Math.sin(t * 3) * 0.06;
ring.scale.setScalar(pulse);
ringMat.opacity = isReal ? 0.8 : 0.35;
glow.material.opacity = isReal ? 0.45 : 0.2;
// weeds sway
for (const w of weeds) {
const p = w.geo.attributes.position;
const b = w.base;
for (let i = 0; i < p.count; i++) {
const bx = b[i * 3], by = b[i * 3 + 1];
const y = by / w.h;
const ph = t * w.speed + w.phase + y * 2.4;
p.setX(i, bx + Math.sin(ph) * 0.45 * y * y);
p.setZ(i, Math.cos(ph * 0.8) * 0.2 * y * y);
}
p.needsUpdate = true;
}
// bubbles rise
for (let i = 0; i < BUBBLES; i++) {
const b = bubbles[i];
b.y += b.sp * dt;
b.x += Math.sin(t * 1.3 + b.ph) * 0.15 * dt;
if (b.y > 4.8) {
b.y = floorHeight(b.x, b.z) + 0.2;
b.x = rand(-9, 9);
b.z = rand(-2.2, 2.2);
}
dummy.position.set(b.x, b.y, b.z);
dummy.scale.setScalar(b.r);
dummy.updateMatrix();
bubbleMesh.setMatrixAt(i, dummy.matrix);
}
bubbleMesh.instanceMatrix.needsUpdate = true;
// marine snow drifts slowly
for (let i = 0; i < SNOW; i++) {
snowPos[i * 3 + 1] -= dt * 0.05;
snowPos[i * 3] += Math.sin(t * 0.3 + snowSeed[i]) * dt * 0.05;
if (snowPos[i * 3 + 1] < FLOOR_Y) snowPos[i * 3 + 1] = 5;
}
snowGeo.attributes.position.needsUpdate = true;
// caustics and light shafts animate
causticTex.offset.set(t * 0.012, t * 0.007);
for (const r of rays) r.rotation.z = r.userData.baseRot + Math.sin(t * 0.3 + r.userData.seed) * 0.03;
// camera gently follows the pointer for parallax
camera.position.set(pointerPos.x * 0.05, 1.2 + pointerPos.y * 0.03, camDist);
camera.lookAt(LOOK_AT);
// HUD
hudTimer -= dt;
if (hudTimer <= 0) {
hudTimer = 0.25;
countEl.textContent = fishes.length;
scaredEl.textContent = fishes.reduce((a, f) => a + (f.panic > 0.35 ? 1 : 0), 0);
}
renderer.render(scene, camera);
}
renderer.setAnimationLoop(frame);
</script>
</body>
</html>Reasoning trace (45,309 tokens)
I'm working out a 3D aquarium in Three.js where the fish swim away from the mouse, using the import map exactly as given in a single self-contained HTML file. The fish need to be visible on first paint, and I'm weighing whether to start in a self-playing demo behind a "Click or press a key to start" prompt, since this is more of an interactive toy than a full game. I'm going back and forth on whether the overlay is needed, since the aquarium only responds to the mouse and nothing needs a key press. I think the fish can just swim on their own while the mouse pushes them away, and when the pointer goes quiet for a few seconds, a virtual pointer can wander so the scatter still shows up in the view. Next I'm sketching the scene: a water-tinted gradient with fog, a procedural sandy floor with rocks, swaying seaweed, rising bubbles, and maybe a light caustic effect. For the fish, I'm thinking of building each one from simple pieces, a scaled sphere for the body, a flattened cone for the tail that wags, and eyes, with a few species. I'm settling on the fish and their behavior now: a few colored species, with boids-style separation, alignment, and cohesion, plus wandering and tank bounds. Each fish gets pushed away from the pointer's position inside the tank, and I'm adding a faint glowing ring there so the interaction reads visually. I'm sketching the camera as a fixed perspective view with a slight parallax that follows the mouse, skipping orbit controls so they don't fight the fleeing behavior. Next I'm planning for performance and seaweed: about 60 simple fish are fine, and the seaweed can be a small number of strands whose vertices get displaced each frame. I'm laying out the rest of the scene: seaweed as tall strips with vertices displaced each frame, bubbles as instanced spheres rising through the water, and a sandy floor with rocks built from displaced icosahedrons. For the lighting I'm considering a hemisphere light plus a directional light, with a faked caustic effect on a plane. I'm settling the lighting and water look now: a faked caustic layer made from a procedurally drawn canvas texture, added over the sandy floor and drifting slowly, with fog colored to match the teal gradient behind the transparent canvas. I'm settling the background as a solid deep teal and matching the fog to it, with a CSS vignette on top for depth. Next I'm working out the fish behavior, starting with how each fish tracks its position and velocity every frame and how the neighbor checks keep the cost manageable with around sixty fish. I'm setting the boundaries: the fish stay inside a tank box and get nudged back when they drift past the edges. The pointer pushes them away with a force that fades out with distance, and a fish that gets pushed gets a temporary speed boost that decays. For the orientation, the fish model is built along the X axis, so each fish's rotation should slerp toward the direction it's moving. Writing orientation code... I'm setting up the pointer as a 3D point by intersecting the mouse ray with a plane at z=0, and I'm keeping its position smooth and marking it inactive when the mouse leaves the window. Fish within about 5 units of that point will be pushed away. When the mouse goes idle for a few seconds, I'm planning a virtual pointer that traces a slow Lissajous path across the tank so the scatter keeps happening. I'm working out the camera framing next: a fixed perspective view at z=16, with the tank's half-width of 9 fitting inside the visible width on a wide screen. On a narrow portrait screen the visible width shrinks a lot, so I need to push the camera back further based on the aspect ratio. I'm settling the camera distance for narrow screens: I want the tank's half-width of 10 to fit, and I'm keeping the distance at least 17 so the height still works. On a portrait screen that pushes the camera back to around 45, which leaves the tank looking small vertically, but that's an acceptable tradeoff. I'm weighing a larger field of view on narrow portrait screens, since that lets the tank stay readable without pushing the camera back so far, and I'm settling on the basic distance approach for now. Next I'm setting the renderer's pixel ratio to the device value, capped at 2. I'm settling the lighting next: a hemisphere light for the overall water tint plus a directional light from above. For the light shafts, I'm thinking of a few tilted, faint additive planes with a gradient texture, slowly pulsing in opacity. I'm also starting to think about how to make the caustic texture look convincingly like moving light. I'm working out a procedural caustic texture for the floor, and the sine-based pattern I'm sketching isn't giving me a clean look, so I'm considering a cheaper approach using a canvas with soft blurred blobs. Writing caustic pixel pattern... I'm now writing the caustic texture as a pixel pattern: a product of two offset sine waves, sharpened and thresholded into bright lines. I'll draw it onto a repeating additive overlay and lay that over the floor, slightly above it. Now I'm adding a faint caustic overlay: a black canvas with bright sine-pattern lines, laid additively over the floor so the dark areas add nothing. For the seaweed, I'm building about 22 ribbon-like strands, each with its own geometry, and sliding the vertices side to side so the tops sway more than the bases. I'm placing the rocks as displaced icosahedrons with flat shading in gray-brown tones, scattered near the edges of the floor. I'm also adding bubbles as an instanced mesh of 40 small translucent spheres that rise and wobble, resetting to the bottom once they reach the top, and a 40-by-20 floor plane that I'll displace vertically for some unevenness. I'm settling the floor and the surface: the sandy floor sits at y=-3 with a little noise for unevenness, and I'm skipping extra walls since the fog should give enough depth. I'm also leaving out a water-surface shimmer plane, since the camera is looking level and the surface would likely sit out of view. Now I'm turning to how to build each fish model. Still writing the fish body shape... Still working on the tail: it's a flat triangular fin in the XY plane, and I'm weighing whether it should sway around the Y axis so its tips move sideways, since a fish wiggles laterally along Z. Writing tail and fin shapes... Still working on the fish's eyes and the side markings: I'm placing a pair of small white eyes with black pupils near the front of the body, slightly off the centerline. For the stripes, I'm weighing a thin torus ring, but I'm leaning toward skipping it for now. Still working on the stripes: I'm weighing a torus ring scaled to match the body's cross-section at each stripe position, but I'm unsure whether the tube thickness will stay uniform once the group is scaled non-uniformly. I'm weighing whether the stripe ring's thickness will stay right once the group is scaled, since scaling the torus shifts the tube's width depending on the axis. I'm leaning toward keeping the x scale at 1 so the band stays thin, and checking that its radial thickness still looks correct against the body's cross-section. I'm weighing whether to scale the body geometry directly, so each part is built in fixed local coordinates, instead of scaling the whole fish group. That would keep the stripe torus and the tail aligned without the distortion that a non-uniform group scale causes. Still writing the fish body: I'm tapering the sphere toward the tail, then scaling it to length, height and width. Still working on the fish body: I'm scaling the tail end of the sphere down so it tapers, then I'll add the triangular tail fin with a curved edge. Still working on the tail fin: I'm building it as a forked shape with curved edges, placed at the narrow back end of the body, and I'll set its size so it roughly matches the body's height. Still writing the fish's dorsal fin and pectoral fins, placing them along the body's top and sides. Now I'm setting the eyes on the front of the body, slightly above the centerline and offset to each side. Still writing the fish's eyes: I'm placing each one on the front of the body, a little above the centerline and set outward, with a black pupil just in front of it. Still working on the stripes: I'm placing each one as a thin torus ring lying in the YZ plane, scaled to match the body's cross-section, and checking whether the ring sits cleanly on the surface. Still working on the stripes: I'm placing them at positive x values, around 0.1 and 0.4, where the body is at full cross-section, so the ring can sit just outside the surface as a visible band. Now I'm working out the stripe rings, checking how the ring's radius should shrink with its position along the body. Still working on the fish species: I'm settling on six colors, from orange clown fish with white stripes to blue, yellow, silver, purple and red, and creating one shared material for each. Still working on the fish templates: I'm building one per species and cloning it for each fish, since clones share geometry and materials. I'll look up the tail group by name so I can animate its rotation, and I'm considering a small body sway as well. Now I'm settling the flocking rules: each fish looks at the neighbors within a set radius, averaging their positions and velocities while pushing away from any that get too close. Still working on the flocking forces: I'm weighing separation against alignment and cohesion, and adding a bounds term that pushes fish back as they approach the tank walls. Still working on the flocking forces: I'm adding a pointer-flee push that grows stronger the closer a fish gets to the cursor, and easing speed up when a fish is panicking. Still working on the pointer flee force: I'm checking how strong the push is at close range versus farther out, and making sure the bounds term keeps fish from being shoved through the tank walls. Now I'm checking the virtual pointer for the demo, which kicks in after about 2.5 seconds without real mouse movement, so the fish still look lively in the first few seconds. Now I'm looking at whether the pointer should start right at load, with a hint to move the mouse near the fish, or only after a second of idle time. I'm settling on a one-second idle delay and a swooping demo path through the tank, plus a semi-transparent glow ring at the pointer position, billboarded toward the camera and fading with activity. For real mouse input, I'll smooth the pointer position toward a raycast hit on the z=0 plane. I'm settling on using a real mouse position projected onto the z=0 plane, with a fallback to the demo pointer once the mouse has been idle for a moment, so the fish always have something to react to. I'm settling the input handling now: the fish react to pointer events, and for touch I'll set touch-action to none on the body and treat a finger lifting as the mouse going idle. Still working on the seaweed: I'm making each strand a tall plane anchored at the floor, and swaying its vertices so the tips move more than the base. Still writing the seaweed: I'm placing each strand as a tall plane anchored at the floor, with its base fixed and its vertices swaying so the tips move the most. Next I'm adding the light shafts and the building the HTML skeleton. Still writing the page markup: I'm laying out the document head, styles, and a translucent vignette over the canvas, with a header overlay at the top. Still writing the page styles: I'm setting up the header, the hint pill at the bottom of the screen, and the HUD in the top right corner. Still writing the page styles: I'm finishing the HUD in the bottom right and adding responsive rules so the header shrinks and the hint wraps on small screens. Next I'm setting up the Three.js renderer on the canvas with a capped pixel ratio. Still writing the scene setup: I'm configuring the renderer with ACES tone mapping and sRGB output, and adding fog in the water color so distant fish fade into the tank. Next I'm creating the perspective camera and fixing its look-at target near the middle of the tank. Still writing the scene setup: I'm adding hemisphere and directional lights, with the sun coming in from above and to the side so the fish get lit from the surface. Still writing the floor: I'm building a large ground plane and displacing its vertices so the sea floor isn't flat. Still writing the floor: I'm rotating the plane flat and checking which axis the vertex displacement ends up on. Still working on the floor: I'm checking how the sand plane's edges sit against the camera's view, since the far corners could show through the fog. Still writing the rock shapes: I'm building each one from a detailed icosahedron and jittering its vertices so they look irregular. Still working on the rocks: I'm building each one from a detailed icosahedron and scaling its vertices by random amounts so each rock comes out lumpy and irregular. Still writing the rock geometry: I'm displacing each icosahedron vertex by a function of its position, so shared corners move the same way and the faces don't crack apart. Still writing the seaweed: I'm generating the seaweed material and sway logic, with each strand's vertices moving more toward the tips. Still working on the floor: I'm keeping the caustic overlay's displacement small so it doesn't clip through the sand bumps, and weighing whether an additive overlay plane sitting slightly above the floor will look right. Still writing the floor: I'm putting the caustic overlay plane a bit above the sand, with its displacement kept small, and checking whether the additive light spilling over the visible water behind looks acceptable. Still writing the caustic texture: I'm generating it procedurally on a small canvas from overlapping sine waves, with the bright ridges pushed sharper so they read as light patterns. Still writing the caustic texture: I'm building it procedurally on a canvas from crossing sine waves, with the bright lines pushed sharper so they read as light patterns. Still writing the god rays: I'm building their gradient texture on a small canvas, with a bright top fading to transparent at the bottom. Still working on the sky: I'm thinking about the god ray planes, keeping their opacity low so the overlapping additive light doesn't blow out, and wondering whether the water surface at the top of the tank would show as a bright band from the camera's angle. I'm setting the background to a vertical teal gradient texture so the tank doesn't read as a flat color, and I'm choosing a mid-tone fog color so distant fish blend into it. Still writing the fish: I'm defining a species list with body, fin, and stripe colors for each fish type. Still writing the fish: I'm defining the species templates, mostly with flat body and fin colors, and I'm building each fish's geometry from a group. Still writing the fish body: I'm shaping the sphere so the tail half tapers toward the back. Still writing the fish body: I'm scaling the tapered sphere flatter on the vertical and depth axes so it reads as a fish silhouette, then recomputing its normals so the lighting stays smooth. Still writing the fish body: I'm finishing the tail half of the body, then building the fish's tail fin as a tapered shape. Still writing the fish body: I'm finishing the tail mesh and placing it at the rear of the body, then adding a dorsal fin shaped with a curved top edge. Still writing the dorsal fin: I'm shaping it with a curved top edge and a flat base set inside the body, so it sticks out above the back while staying visible from both sides. Still writing the fish: I'm adding the pectoral fins as small flattened spheres on each side, angled slightly outward. Still writing the fish: I'm placing the eyes and pupils on each side of the head, checking that the eye sits on the body surface. Still writing the eyes: I'm checking that each pupil sits on the surface of its eye sphere, so it doesn't float off. Still writing the stripes: I'm placing torus rings across the body at set positions, scaling each ring to match the body's taper. Still writing the stripe rings: I'm converting each torus into a vertical ring by rotating it, then scaling it to match the body's cross-section at that point. Still writing the stripes: I'm sizing each torus ring so its tube thickness roughly matches the stripe width after the scale. Still writing the stripe rings: I'm sizing each ring's tube so its thickness reads as a stripe without looking too heavy along the body. Still writing the stripes: I'm making each stripe a torus ring that's translated to its spot along the body, and I'm sizing the rings' materials to match the white. Next I'm building the eyes and pupils, placing the pupils on the eye surfaces so they don't float off. Still writing the fish: I'm setting each fish's scale from a random range so the school has varied sizes, then planning the heading to follow its velocity. Still writing the fish: I'm working out the heading so the fish turns smoothly toward its velocity, using a minimal rotation that keeps it from rolling around its length. Still writing the fish movement: I'm keeping the fish upright by using the minimal rotation toward their velocity, and adding a tail wiggle whose speed rises with how fast each fish swims. Still writing the pointer handling: I'm setting up a raycaster and a ground plane so the mouse position maps to a point in the tank. Still writing the pointer code: I'm mapping the mouse onto the tank's z=0 plane with a raycaster, and I'm noting that a flee radius of about 5.5 in 3D should suit the pointer. Still writing the pointer setup: I'm making the demo pointer follow a slow Lissajous path through the tank, with the real mouse taking over for about 2.5 seconds after it moves. I'm also adding a ring marker that shows the current pointer position, dimmer in demo mode. Still working on the pointer: I'm smoothing the pointer position toward its target each frame, and letting the flee force read from that smoothed position. I'm also checking whether the fish should react when the real mouse sits idle or leaves the window. Still writing the bubbles: I'm making them an instanced sphere mesh with a pale, slightly transparent material, and giving each one its own speed, wobble, and radius. I'm moving them upward each frame, wrapping them back to the floor when they pass the top, and then setting their matrices. Still writing the particles: I'm adding marine snow as a points cloud with a faint, mostly transparent material, and I'm planning to nudge its positions with a slow sine drift each frame. Then I'm setting up the camera layout so its distance depends on the aspect ratio and field of view. I'm checking the camera distance for a 1600-wide aspect: the visible half-width at distance 17 comes out to about 11.3, which covers the tank with room to spare. The vertical span also fits the tank's height around the look-at point. I'm wondering whether to pull the camera in a little closer for a more immersive feel. Still writing the camera and resize code: I'm having the camera drift slightly with the pointer's horizontal position for some parallax, even in demo mode. Then I'm updating the renderer size on resize. Still writing the render loop: I'm switching the canvas to CSS-sized full-window rendering and driving the animation frames with the renderer's loop, using a time source for the movement. I'm also adding a small HUD that counts how many fish are startled. Next I'm hooking up the hint text: it starts at full opacity and drops to a dimmer level once the first real mouse move happens. I'm also sizing the header and hint smaller for mobile screens, and I'm placing the fish at random spots across the tank so they don't stack on each other at startup. Still writing the fish update: I'm building a per-fish loop that accumulates separation, alignment, and cohesion forces against the other fish, plus the flee push from the pointer. Still writing the fish update: I'm computing a separation push for each neighbor that gets stronger the closer it is, using the distance relative to the separation radius. Still writing the bounds: I'm pushing each fish back toward the tank when it gets near an edge. Still writing the fish update: I'm defining the bounds so each fish is nudged back in toward the tank when it gets near an edge. Still writing the pointer flee: I'm pushing each fish away from the pointer with a force that grows as it gets closer, and I'm recording how startled it is. Still writing the pointer flee: I'm damping the panic level each frame so it fades out, then integrating each fish's velocity from the accumulated force. Still writing the fish speed limits: I'm capping each fish's speed by its panic level, with a floor so it never stalls completely, and then moving it along its velocity. Still writing the fish movement: I'm turning each fish toward its velocity with a smooth slerp, and driving its tail wag from an accumulated phase that speeds up with how fast it swims and how panicked it is. Still writing the fish bounds: I'm clamping each position back inside the tank and reflecting its velocity when it strays outside. Still writing the HTML page: I'm setting up the import map for Three.js and the document head, then starting the inline styles. Still writing the HTML body: I'm laying out the canvas, a vignette overlay, the header with a short subtitle, and the hint and counter elements, then wiring the module script to the page. Still writing the fish: I'm wiring up the flee and bounds handling in the update loop, then moving on to the rest of the scene setup. Still writing the scene setup: I'm setting up the fog, the gradient backdrop, and the camera, with the look-at point at the middle of the tank. Still writing the backdrop: I'm setting the gradient stops for the background texture and filling the canvas with it. Still writing the floor: I'm building a large plane and warping its surface with a few sine waves so the sand looks uneven. Still writing the caustics: I'm generating a caustic texture and laying it over the floor as an additive overlay. I'm checking how the caustic overlay sits relative to the floor, since it's a horizontal plane just above the sand and could show up in places where the floor dips above it. I'm checking whether the caustic overlay can show above the floor anywhere: the plane sits just above the lowest floor dips, so it should only ever be visible over the sand. Still writing the god rays: I'm building a set of translucent light shafts from a shared ray texture, each with a slightly different tint. Still writing the god rays: I'm placing each light shaft at a random x offset and tilting it slightly, with the planes sized to run from just above the floor up past the top of the view. I'm checking the ray texture: a vertical white gradient fading from opaque at the top to transparent at the bottom, so the additive shafts stay subtle. Still writing the seaweed: I'm building a set of swaying plants in a few shades of green, each with a random height and width. Still writing the seaweed: I'm placing each plant on the floor at a random spot toward the back, sampling the floor's height at that point so the base sits on the sand. Still writing the seaweed sway: I'm offsetting each vertex sideways by a phase-driven sine that grows toward the top of each plant. Still writing the seaweed sway: I'm adding a sideways offset and a smaller depth offset to each vertex, scaled up toward the top of the plant. Still writing the rocks: I'm generating a set of lumpy stones from a subdivided icosahedron, displacing each vertex with a seeded noise so no two look the same. Still writing the rocks: I'm placing each stone on the sand at a random spot, lifting it slightly so it sits on the floor, and giving it a random rotation. Still writing the floor height: I'm defining it as a base level plus a few sine waves over x and z, so it matches the uneven sand the seaweed and rocks sit on. Still writing the fish: I'm building a set of fish templates from the species list and cloning one for each of the sixty fish, giving each a random size. Still writing the fish: I'm setting each fish's scale, placing it at a random spot in the tank, and giving it a speed, velocity, and tail phase. Still writing the fish: I'm adding each fish to the scene and tuning its speed range so the movement feels lively but not too fast. Still writing the snow: I'm laying out a set of falling particles with a light drift, and I'm planning to wrap each one back to the top once it sinks below the floor. Still writing the pointer handling: I'm setting up a raycaster and normalized device coordinates for the mouse, plus a plane at z=0 to project the pointer onto the tank. Still writing the pointer handling: I'm defining the target and position vectors and a mouse handler that converts the pointer to normalized coordinates and projects it onto the z=0 plane, clamping the hit point to the tank bounds. Still writing the pointer handling: I'm wiring the pointer events to the projection and adding a blur listener so the target resets when the mouse leaves the page. Still writing the pointer handling: I'm switching the leave detection to a window mouseout check on the related target, and picking the target each frame between the real pointer and a demo path, easing toward whichever applies. Still writing the pointer: I'm defining the demo path that sweeps the target across the tank, with the ring dimmed when no real pointer has moved recently. Still writing the pointer ring: I'm defining a ring mesh and a faint additive halo around it, both set to ignore fog. Still writing the pointer group: I'm placing the ring and a faint halo under one group that follows the pointer and turns to face the camera each frame, and I'm nudging the camera sideways with the pointer for a bit of parallax. Still writing the layout: I'm sizing the renderer to the window, setting the camera's field of view based on whether the screen is wide or narrow, and computing a base distance so the tank fits the view in either orientation. I'm weighing a narrow-screen layout, where the tank would look small at the current camera distance, and I'm considering whether to scale the fish or shift the camera instead. I'm working out the pointer code now: a raycaster projects the mouse onto the z=0 plane and clamps the hit to the tank, with a blur or mouseout reset so the target clears when the cursor leaves. Each frame I'm easing toward the real pointer if it moved recently, otherwise toward a slow demo path that sweeps across the tank. Then I'm adding a ring and a faint halo as a group that follows the pointer and faces the camera, with a small parallax nudge. Last, I'm setting the layout so the camera's field of view and distance change with the screen's aspect ratio, and turning to the resize and animation loop. Still writing the HUD: I'm setting up the fish count and how many are currently scared, refreshing that readout a few times a second. Still writing the fish: I'm checking that the tail sits right against the body's rear end, and that the fins and eyes read as a fish from the front. Still writing the stripes: I'm building a torus-based ring for each body band, scaling it to match the fish's tapered profile and translating it along the body. Still writing the stripes: I'm checking the scale on the stripe rings, since a torus squeezed along the body changes how thick each band looks. Still writing the stripe spacing: I'm defining a section-scale helper that shrinks the body's cross-section toward the tail, so the stripe rings can match the taper. Still writing the stripes: I'm placing two bands on the body at equal distances from the center, and using a thinner tube for each one. Checking the lighting now: the intensities should work with the ACES tone mapping, so I'm bumping the hemisphere light up a bit and the directional light too, since the tone mapping tends to wash things out. I'm also setting the floor material to respond to the lights. Next I'm writing the caustic texture generator, drawing it onto a canvas. Still writing the caustic texture: I'm filling each pixel of a 256×256 canvas with a grayscale value from overlapping sine waves, raised to a high power so only thin bright lines remain. Still writing the caustic texture: I'm setting the repeat so the pattern tiles across the floor at a size that suits the lines. Still writing the caustic texture: I'm checking that the bright lines only show where the two wave terms cross zero, since the falloff is steep enough that most of the canvas stays dark. Still writing the caustic texture: I'm setting the offset so the pattern drifts slowly over time, and I'm making a ray texture from a vertical gradient on a narrow canvas. Still writing the ray texture: I'm drawing a vertical white gradient onto a narrow canvas, then applying a horizontal fade so the rays are brightest in the middle and vanish at the edges. Still writing the ray texture: I'm placing the rays at staggered positions across the back of the tank, each one slightly rotated with a gentle sway over time. Still writing the bubbles: I'm creating an instanced mesh of 45 small translucent spheres for the bubble particles. Still writing the bubbles: I'm updating each bubble's position every frame so it drifts upward and gently sways, resetting it near the floor once it passes the top of the tank. I'm tuning the flocking forces now: alignment, cohesion, and separation each get a weight so their accelerations stay in a sensible range, and the wander and boundary terms are set to match. Still writing the boundaries: I'm clamping the fish to hard walls so none of them can leave the tank, reflecting their velocity when they hit one. Still writing the fish boundaries: I'm setting hard limits on x, y and z with a stiff soft-margin force pushing fish back before they reach the walls. I'm also checking that the tails don't dip too far into the floor. Still writing the CSS: I'm setting the page to fill the screen with no scrolling or touch panning, and turning off text selection so dragging over the tank doesn't highlight anything. Still writing the CSS: I'm styling the HUD, title and vignette overlay, with the header pinned to the top-left and ignoring pointer events. Still writing the CSS: I'm styling the hint bar at the bottom center with a soft blur and a dimmed state, and the HUD readout in the bottom-right corner. Still writing the mobile layout: I'm shrinking the title and hint text, and moving the readout to the top right so it doesn't collide with the hint bar at the bottom. Next I'm assembling the full JavaScript in one pass, starting with the Three.js import and the basic renderer setup with ACES tone mapping and capped pixel ratio. Still writing the scene setup: I'm configuring the renderer and fog, then placing the camera and the floor bounds the fish have to stay within. Still writing the scene constants: I'm defining the floor height, the tank bounds, and the radii for fleeing, separation and neighbour checks. Still writing the scene pointer: I'm positioning the pointer group at the smoothed pointer position and orienting its ring to face the camera. Still writing the camera and animation loop: I'm offsetting the camera slightly with the pointer position and aiming it at the tank's center, then setting up the update order for fish, weeds and bubbles. Still writing the fish update: I'm setting up the steering and flocking math in a shared vector so each fish can compute its separation, alignment and cohesion without allocating new objects every frame. Still writing the fish update: I'm looping over every fish and comparing it against the others to gather their velocities and positions for the flocking forces. Still writing the flocking loop: I'm accumulating the separation, alignment and cohesion contributions from each neighbour and scaling them into the force. Still writing the fish boundaries: I'm adding soft walls that push each fish back as it nears the edge of the tank. Still writing the fish boundaries: I'm pushing fish back from the x walls with a stiff force, and doing the same for the floor, ceiling and the depth limits, with a thinner margin on z. Still writing the fish steering: I'm adding a speed clamp so each fish stays within a range that grows when it's startled, and I'm handling the case where a fish's velocity has nearly stalled. Still writing the fish steering: I'm clamping each fish's speed between a minimum and a maximum that grows with its panic, rescaling the velocity when it falls outside that range. Still writing the fish steering: I'm integrating each fish's velocity and position, then hard-clamping its position to the tank bounds. Still writing the fish steering: I'm capping each fish's speed between a minimum and a maximum that rises with its panic, then working out the weed positions on the floor. Still writing the ring and halo: I'm checking that the geometry calls are valid and the ring and halo sizes line up with the flee radius. Still writing the halo: I'm using a soft radial glow sprite with additive blending instead of a flat disc, and keeping the ring as a separate mesh on the fish's flee zone. Still writing the fish glow: I'm using a soft radial glow sprite with additive blending for the pointer's flee zone, with the ring kept as a separate child mesh. Still writing the fish body: I'm building the materials and the template for each species. Still writing the fish body: I'm building the species materials and a tapered body shape by pulling the head side of a sphere into a more streamlined form. Still writing the fish body: I'm tapering the head end of the body sphere, then flattening it to a streamlined shape. Still writing the fish body: I'm tapering the head end of the body sphere and then adding the tail, dorsal and pectoral fins along with the eyes. Still writing the species templates: I'm defining the body material and fin and eye materials for each fish, then tapering the body sphere and adding stripes. Still writing the species templates: I'm adding the stripes and tapered body shape for each fish type.