Interactive 3D aquariumOne-line
Gemini 3.8 Flash
by GoogleReasoning18 console errors
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 18 s
- Cost
- $0.154
- Output tokens
- 40,919
- Spent thinking
- 23,497 tokens
- Speed
- 160.2 tok/s
- Console errors
- 18
- First token after
- 2.6 s
- First visible text after
- 2 min 51 s
- Prompt tokens
- 448
- Code size
- 46.1 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"high"}- Max output
- 65,536 tokens
- Generated
- Nov 12, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values. PlaneGeometry
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 · 1402 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>3D Aquarium - Interactive Fish Ecosystem</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #020b14;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #fff;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* UI Overlay */
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 18px 24px;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.brand {
background: rgba(4, 24, 44, 0.65);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(80, 200, 255, 0.25);
border-radius: 14px;
padding: 10px 18px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.brand h1 {
font-size: 19px;
font-weight: 700;
letter-spacing: 1px;
background: linear-gradient(135deg, #70e1ff 0%, #00b4d8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 8px;
}
.brand h1::before {
content: "";
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
background: #00f0ff;
box-shadow: 0 0 10px #00f0ff;
}
.brand p {
font-size: 12px;
color: #9cd5ea;
margin-top: 3px;
}
.species-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
pointer-events: auto;
}
.badge {
background: rgba(6, 26, 46, 0.65);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px;
padding: 6px 13px;
font-size: 11.5px;
display: flex;
align-items: center;
gap: 7px;
color: #c9e6f2;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot-clown { background: #ff6819; box-shadow: 0 0 6px #ff6819; }
.dot-tang { background: #185adb; box-shadow: 0 0 6px #185adb; }
.dot-yellow { background: #ffd000; box-shadow: 0 0 6px #ffd000; }
.dot-tetra { background: #00f7ff; box-shadow: 0 0 6px #00f7ff; }
/* Controls Bar */
.controls-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
}
.instructions {
background: rgba(4, 24, 44, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(80, 200, 255, 0.25);
border-radius: 12px;
padding: 9px 16px;
font-size: 12.5px;
color: #aae0f7;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
.instructions span.icon {
font-size: 15px;
}
.btn-group {
display: flex;
gap: 8px;
pointer-events: auto;
}
.btn {
background: rgba(7, 34, 60, 0.75);
backdrop-filter: blur(10px);
border: 1px solid rgba(80, 200, 255, 0.35);
color: #e2f4fd;
border-radius: 10px;
padding: 8px 14px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.btn:hover {
background: rgba(14, 60, 105, 0.9);
border-color: #00d2ff;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 195, 255, 0.3);
color: #ffffff;
}
.btn:active {
transform: translateY(0);
}
/* Interactive Cursor Glow */
#cursor-status {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #00ffaa;
box-shadow: 0 0 8px #00ffaa;
animation: pulse 1.6s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.2); }
}
@media (max-width: 680px) {
.ui-layer { padding: 12px; }
.brand h1 { font-size: 16px; }
.brand p { font-size: 11px; }
.badge { font-size: 10.5px; padding: 4px 9px; }
.instructions { font-size: 11px; padding: 7px 12px; }
.btn { font-size: 11px; padding: 6px 11px; }
}
</style>
<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>
</head>
<body>
<div id="canvas-container"></div>
<div class="ui-layer">
<div class="header">
<div class="brand">
<h1>AQUARIUM 3D</h1>
<p>Dynamic Interactive Underwater Biosphere</p>
</div>
<div class="species-badges">
<div class="badge"><div class="dot dot-clown"></div>Clownfish (8)</div>
<div class="badge"><div class="dot dot-tang"></div>Blue Tang (6)</div>
<div class="badge"><div class="dot dot-yellow"></div>Yellow Tang (6)</div>
<div class="badge"><div class="dot dot-tetra"></div>Neon Tetra (16)</div>
</div>
</div>
<div class="controls-bar">
<div class="instructions">
<div id="cursor-status"></div>
<span><strong>Move cursor near fish to scare them away</strong> • Click in water to feed • Drag to orbit</span>
</div>
<div class="btn-group">
<button class="btn" id="btn-feed">🍴 Feed Fish</button>
<button class="btn" id="btn-scatter">⚡ Scatter All</button>
<button class="btn" id="btn-cam">📷 View</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- Tank Dimensions ---
const TANK = {
w: 64, // X: -32 .. 32
h: 36, // Y: -18 .. 18
d: 32, // Z: -16 .. 16
sandY: -17.5,
surfaceY: 17.5
};
// --- Scene Setup ---
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x062840, 0.016);
const camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 1, 56);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 95;
controls.minDistance = 15;
controls.maxPolarAngle = Math.PI * 0.53;
controls.minPolarAngle = Math.PI * 0.18;
controls.target.set(0, 0, 0);
// --- Lighting ---
const ambientLight = new THREE.AmbientLight(0x0a324d, 1.4);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xdcf6ff, 2.8);
sunLight.position.set(8, 38, 10);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 70;
sunLight.shadow.camera.left = -34;
sunLight.shadow.camera.right = 34;
sunLight.shadow.camera.top = 22;
sunLight.shadow.camera.bottom = -22;
sunLight.shadow.bias = -0.001;
scene.add(sunLight);
const blueFill = new THREE.DirectionalLight(0x0088cc, 1.2);
blueFill.position.set(-20, 20, -10);
scene.add(blueFill);
const pointGlow = new THREE.PointLight(0x38e1ff, 1.0, 45, 1.4);
pointGlow.position.set(0, 8, 0);
scene.add(pointGlow);
// --- Procedural Canvas Textures for Fish ---
function createClownfishTexture() {
const cv = document.createElement('canvas');
cv.width = 512; cv.height = 256;
const ctx = cv.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#f9570c');
grad.addColorStop(0.5, '#ff7315');
grad.addColorStop(1, '#db4502');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 512, 256);
// Stripes with black borders
function drawStripe(xc, w) {
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.roundRect(xc - w/2 - 4, 0, w + 8, 256, 4);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.roundRect(xc - w/2, 0, w, 256, 3);
ctx.fill();
}
drawStripe(512 * 0.32, 30);
drawStripe(512 * 0.60, 26);
drawStripe(512 * 0.86, 18);
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
function createBlueTangTexture() {
const cv = document.createElement('canvas');
cv.width = 512; cv.height = 256;
const ctx = cv.getContext('2d');
ctx.fillStyle = '#0f4ceb';
ctx.fillRect(0, 0, 512, 256);
// Black palette loop
ctx.fillStyle = '#080812';
ctx.beginPath();
ctx.ellipse(260, 128, 170, 75, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0f4ceb';
ctx.beginPath();
ctx.ellipse(280, 128, 90, 40, 0, 0, Math.PI * 2);
ctx.fill();
// Yellow tail wedge
ctx.fillStyle = '#ffe014';
ctx.beginPath();
ctx.moveTo(390, 70);
ctx.lineTo(512, 10);
ctx.lineTo(512, 246);
ctx.lineTo(390, 186);
ctx.closePath();
ctx.fill();
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
function createYellowTangTexture() {
const cv = document.createElement('canvas');
cv.width = 512; cv.height = 256;
const ctx = cv.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#ffd800');
grad.addColorStop(0.5, '#fff033');
grad.addColorStop(1, '#ffaa00');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 512, 256);
// White defensive spine scalpel
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(430, 128, 22, 6, 0, 0, Math.PI * 2);
ctx.fill();
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
function createNeonTetraTexture() {
const cv = document.createElement('canvas');
cv.width = 512; cv.height = 256;
const ctx = cv.getContext('2d');
// Silvery base
ctx.fillStyle = '#a6c6d3';
ctx.fillRect(0, 0, 512, 256);
// Brilliant glowing electric cyan line
ctx.fillStyle = '#00f7ff';
ctx.shadowColor = '#00f7ff';
ctx.shadowBlur = 10;
ctx.fillRect(10, 40, 360, 28);
ctx.fillRect(10, 188, 360, 28);
ctx.shadowBlur = 0;
// Ruby red lower half and tail
ctx.fillStyle = '#ff113a';
ctx.fillRect(240, 80, 272, 96);
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const fishTextures = {
clown: createClownfishTexture(),
blueTang: createBlueTangTexture(),
yellowTang: createYellowTangTexture(),
tetra: createNeonTetraTexture()
};
// --- Fish Mesh Generator ---
function buildFishBodyGeometry(species) {
const lSegs = 16;
const rSegs = 14;
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
for (let i = 0; i <= lSegs; i++) {
const u = i / lSegs; // 0 at nose, 1 at peduncle
const z = (0.5 - u) * 2.2; // +1.1 at nose to -1.1 at tail
let h, w;
if (species === 'clown') {
h = Math.sin(Math.pow(u, 0.75) * Math.PI) * 0.75;
w = h * 0.42;
} else if (species === 'blueTang') {
h = Math.sin(Math.pow(u, 0.65) * Math.PI) * 0.95;
w = h * 0.28;
} else if (species === 'yellowTang') {
const s = u < 0.25 ? u / 0.25 : Math.sin((u - 0.25) / 0.75 * Math.PI);
h = s * 1.05;
w = h * 0.22;
} else { // tetra
h = Math.sin(u * Math.PI) * 0.38;
w = h * 0.52;
}
if (i === 0) { h = 0.02; w = 0.01; }
if (i === lSegs) { h = 0.09; w = 0.03; }
for (let j = 0; j <= rSegs; j++) {
const v = j / rSegs;
const theta = v * Math.PI * 2;
const x = Math.cos(theta) * w;
const y = Math.sin(theta) * h;
positions.push(x, y, z);
normals.push(x, y, 0);
uvs.push(v, u);
}
}
for (let i = 0; i < lSegs; i++) {
for (let j = 0; j < rSegs; j++) {
const a = i * (rSegs + 1) + j;
const b = a + rSegs + 1;
indices.push(a, b, a + 1);
indices.push(b, b + 1, a + 1);
}
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geom.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geom.setIndex(indices);
geom.computeVertexNormals();
return geom;
}
function createFinGeometry(pts) {
const shape = new THREE.Shape();
shape.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < pts.length; i++) {
shape.lineTo(pts[i][0], pts[i][1]);
}
shape.closePath();
return new THREE.ShapeGeometry(shape);
}
// --- Fish Assembly & Class ---
class Fish {
constructor(type, config) {
this.type = type;
this.config = config;
this.group = new THREE.Group();
this.inner = new THREE.Group();
// Point model forward along -Z for Three.js lookAt convention
this.inner.rotation.y = Math.PI;
this.group.add(this.inner);
// Body
const bodyGeom = buildFishBodyGeometry(type);
const bodyMat = new THREE.MeshStandardMaterial({
map: fishTextures[type],
roughness: 0.35,
metalness: 0.1,
bumpScale: 0.02
});
this.bodyMesh = new THREE.Mesh(bodyGeom, bodyMat);
this.bodyMesh.castShadow = true;
this.inner.add(this.bodyMesh);
// Eyes
const eyeGeom = new THREE.SphereGeometry(config.eyeSize, 12, 12);
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x070707, roughness: 0.1 });
const eyeL = new THREE.Mesh(eyeGeom, eyeMat);
const eyeR = new THREE.Mesh(eyeGeom, eyeMat);
eyeL.position.set(config.eyeW, config.eyeY, 0.72);
eyeR.position.set(-config.eyeW, config.eyeY, 0.72);
this.inner.add(eyeL, eyeR);
// Fin Material
const finMat = new THREE.MeshStandardMaterial({
color: config.finColor,
roughness: 0.3,
metalness: 0.05,
transparent: true,
opacity: config.finOpacity,
side: THREE.DoubleSide
});
// Tail Hierarchy: inner -> tailBase -> tailFin
this.tailBase = new THREE.Group();
this.tailBase.position.set(0, 0, -1.05);
this.inner.add(this.tailBase);
const tailGeom = createFinGeometry(config.tailPts);
this.tailFin = new THREE.Mesh(tailGeom, finMat);
this.tailFin.rotation.y = Math.PI / 2;
this.tailFin.position.set(0, 0, -0.2);
this.tailFin.castShadow = true;
this.tailBase.add(this.tailFin);
// Pectoral Fins (Left & Right)
const pecGeom = createFinGeometry([[0,0], [0.45, 0.25], [0.65, 0.05], [0.35, -0.2]]);
this.pecL = new THREE.Mesh(pecGeom, finMat);
this.pecR = new THREE.Mesh(pecGeom, finMat);
this.pecL.position.set(config.eyeW * 0.9, -0.05, 0.3);
this.pecL.rotation.set(0.1, 0.4, 0.3);
this.pecR.position.set(-config.eyeW * 0.9, -0.05, 0.3);
this.pecR.rotation.set(0.1, -0.4, -0.3);
this.pecR.scale.set(1, -1, 1);
this.inner.add(this.pecL, this.pecR);
// Dorsal Fin
if (config.dorsalPts) {
const dorsalGeom = createFinGeometry(config.dorsalPts);
const dorsal = new THREE.Mesh(dorsalGeom, finMat);
dorsal.rotation.y = Math.PI / 2;
dorsal.position.set(0, config.dorsalY, 0.1);
this.inner.add(dorsal);
}
// Apply species scale
this.group.scale.setScalar(config.scale);
// Physics State
this.position = this.group.position;
this.position.set(
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 22,
(Math.random() - 0.5) * 24
);
this.velocity = new THREE.Vector3(
(Math.random() - 0.5) * config.speed,
(Math.random() - 0.5) * (config.speed * 0.3),
(Math.random() - 0.5) * config.speed
);
if (this.velocity.lengthSq() < 0.1) this.velocity.set(1, 0, 0);
this.velocity.normalize().multiplyScalar(config.speed);
this.accel = new THREE.Vector3();
this.isFleeing = false;
this.fleeTimer = 0;
this.fleeIntensity = 0;
this.swimPhase = Math.random() * Math.PI * 2;
this.rollAngle = 0;
}
update(dt, allFish, mouseRay, foodPellets) {
this.accel.set(0, 0, 0);
// 1. Mouse Evasion (CRITICAL: Swim away from mouse)
if (mouseRay) {
const closest = new THREE.Vector3();
mouseRay.closestPointToPoint(this.position, closest);
const distToRay = this.position.distanceTo(closest);
const fleeRadius = this.config.fleeRadius;
if (distToRay < fleeRadius) {
const repel = new THREE.Vector3().subVectors(this.position, closest);
if (repel.lengthSq() < 0.001) {
repel.set((Math.random() - 0.5), (Math.random() - 0.5), (Math.random() - 0.5));
}
repel.normalize();
// Nonlinear urgency curve: closer = dramatically higher force
const urgency = Math.pow(1.0 - (distToRay / fleeRadius), 1.5);
this.accel.addScaledVector(repel, urgency * this.config.fleeForce);
this.isFleeing = true;
this.fleeTimer = 0.9; // remain excited briefly
this.fleeIntensity = Math.max(this.fleeIntensity, urgency);
// Trigger flee bubbles occasionally
if (urgency > 0.4 && Math.random() < 0.25) {
spawnMicroBubble(this.position);
}
}
}
if (this.fleeTimer > 0) {
this.fleeTimer -= dt;
if (this.fleeTimer <= 0) {
this.isFleeing = false;
this.fleeIntensity = 0;
}
}
// 2. Food Seeking (when not fleeing)
if (!this.isFleeing && foodPellets.length > 0) {
let closestPellet = null;
let minDist = 22;
for (let p of foodPellets) {
if (p.active) {
const d = this.position.distanceTo(p.position);
if (d < minDist) {
minDist = d;
closestPellet = p;
}
}
}
if (closestPellet) {
const seek = new THREE.Vector3().subVectors(closestPellet.position, this.position).normalize();
this.accel.addScaledVector(seek, 8.0);
if (minDist < 1.4) {
closestPellet.eat();
}
}
}
// 3. Flocking & Schooling with same species
const sep = new THREE.Vector3();
const ali = new THREE.Vector3();
const coh = new THREE.Vector3();
let neighborCount = 0;
for (let other of allFish) {
if (other === this) continue;
const d = this.position.distanceTo(other.position);
// Separation from all nearby fish
if (d < this.config.sepDist && d > 0.001) {
sep.add(new THREE.Vector3().subVectors(this.position, other.position).normalize().divideScalar(d));
}
// Schooling alignment & cohesion with own species
if (other.type === this.type && d < this.config.schoolDist) {
ali.add(other.velocity);
coh.add(other.position);
neighborCount++;
}
}
if (sep.lengthSq() > 0) this.accel.addScaledVector(sep.normalize(), this.config.sepWeight);
if (neighborCount > 0) {
ali.divideScalar(neighborCount).normalize();
this.accel.addScaledVector(ali, this.config.aliWeight);
coh.divideScalar(neighborCount).sub(this.position).normalize();
this.accel.addScaledVector(coh, this.config.cohWeight);
}
// 4. Soft Boundaries (Turn gently inside aquarium)
const margin = 4.5;
const bForce = 18.0;
if (this.position.x < -TANK.w/2 + margin) this.accel.x += Math.pow((-TANK.w/2 + margin - this.position.x) / margin, 2) * bForce;
if (this.position.x > TANK.w/2 - margin) this.accel.x -= Math.pow((this.position.x - (TANK.w/2 - margin)) / margin, 2) * bForce;
if (this.position.y < TANK.sandY + margin) this.accel.y += Math.pow((TANK.sandY + margin - this.position.y) / margin, 2) * bForce;
if (this.position.y > TANK.surfaceY - margin) this.accel.y -= Math.pow((this.position.y - (TANK.surfaceY - margin)) / margin, 2) * bForce;
if (this.position.z < -TANK.d/2 + margin) this.accel.z += Math.pow((-TANK.d/2 + margin - this.position.z) / margin, 2) * bForce;
if (this.position.z > TANK.d/2 - margin) this.accel.z -= Math.pow((this.position.z - (TANK.d/2 - margin)) / margin, 2) * bForce;
// Hard clamp inside glass
this.position.x = THREE.MathUtils.clamp(this.position.x, -TANK.w/2 + 0.8, TANK.w/2 - 0.8);
this.position.y = THREE.MathUtils.clamp(this.position.y, TANK.sandY + 0.8, TANK.surfaceY - 0.8);
this.position.z = THREE.MathUtils.clamp(this.position.z, -TANK.d/2 + 0.8, TANK.d/2 - 0.8);
// 5. Physics Integration
this.velocity.addScaledVector(this.accel, dt);
const curSpeedTarget = this.isFleeing ? this.config.fleeSpeed : this.config.speed;
const currentSpeed = this.velocity.length();
const blendedSpeed = THREE.MathUtils.lerp(currentSpeed, curSpeedTarget, dt * 3.0);
this.velocity.normalize().multiplyScalar(blendedSpeed);
this.position.addScaledVector(this.velocity, dt);
// 6. Smooth Orientation & Agile Banking
const lookTarget = this.position.clone().add(this.velocity);
const dummy = new THREE.Object3D();
dummy.position.copy(this.position);
dummy.lookAt(lookTarget);
this.group.quaternion.slerp(dummy.quaternion, Math.min(1.0, dt * 8.0));
// Banking roll when turning
const lateralTurn = this.accel.x * 0.04;
this.rollAngle = THREE.MathUtils.lerp(this.rollAngle, -lateralTurn, dt * 5.0);
this.inner.rotation.z = this.rollAngle;
// 7. Dynamic Swimming Animations
const wagFreq = this.isFleeing ? (14.0 + this.fleeIntensity * 8.0) : (4.0 + currentSpeed * 1.2);
const wagAmp = this.isFleeing ? 0.65 : 0.32;
this.swimPhase += wagFreq * dt;
// Articulated tail bending
this.tailBase.rotation.y = Math.sin(this.swimPhase) * wagAmp;
this.tailFin.rotation.y = Math.PI / 2 + Math.sin(this.swimPhase - 0.7) * (wagAmp * 1.3);
// Fluttering pectoral fins
const pecSpeed = wagFreq * 1.3;
this.pecL.rotation.y = 0.4 + Math.sin(this.swimPhase * 1.5) * 0.35;
this.pecR.rotation.y = -0.4 - Math.sin(this.swimPhase * 1.5) * 0.35;
}
}
// --- Species Configurations ---
const SPECIES_CONFIGS = {
clown: {
scale: 1.4,
speed: 4.8,
fleeSpeed: 14.5,
fleeRadius: 13.0,
fleeForce: 36.0,
sepDist: 3.2,
schoolDist: 10.0,
sepWeight: 4.5,
aliWeight: 2.2,
cohWeight: 1.8,
eyeSize: 0.08, eyeW: 0.28, eyeY: 0.12,
finColor: 0xff6611, finOpacity: 0.9,
tailPts: [[0, -0.2], [0.65, -0.55], [0.8, -0.2], [0.8, 0.2], [0.65, 0.55], [0, 0.2]],
dorsalY: 0.6,
dorsalPts: [[0, 0], [0.3, 0.35], [0.9, 0.25], [1.1, 0]]
},
blueTang: {
scale: 1.5,
speed: 5.6,
fleeSpeed: 16.5,
fleeRadius: 14.0,
fleeForce: 42.0,
sepDist: 3.8,
schoolDist: 12.0,
sepWeight: 5.0,
aliWeight: 2.6,
cohWeight: 2.0,
eyeSize: 0.09, eyeW: 0.24, eyeY: 0.16,
finColor: 0xffe214, finOpacity: 0.95,
tailPts: [[0, -0.15], [0.85, -0.6], [0.75, 0], [0.85, 0.6], [0, 0.15]],
dorsalY: 0.8,
dorsalPts: [[0, 0], [0.2, 0.3], [1.2, 0.25], [1.4, 0]]
},
yellowTang: {
scale: 1.45,
speed: 5.2,
fleeSpeed: 15.0,
fleeRadius: 13.5,
fleeForce: 38.0,
sepDist: 3.5,
schoolDist: 11.0,
sepWeight: 4.8,
aliWeight: 2.4,
cohWeight: 2.1,
eyeSize: 0.085, eyeW: 0.22, eyeY: 0.18,
finColor: 0xffd900, finOpacity: 0.9,
tailPts: [[0, -0.2], [0.75, -0.65], [0.85, 0], [0.75, 0.65], [0, 0.2]],
dorsalY: 0.9,
dorsalPts: [[0, 0], [0.4, 0.5], [1.3, 0.4], [1.5, 0]]
},
tetra: {
scale: 0.85,
speed: 6.2,
fleeSpeed: 18.5,
fleeRadius: 15.0,
fleeForce: 48.0,
sepDist: 2.0,
schoolDist: 14.0,
sepWeight: 3.8,
aliWeight: 4.2,
cohWeight: 3.5,
eyeSize: 0.06, eyeW: 0.16, eyeY: 0.05,
finColor: 0xd6f7ff, finOpacity: 0.65,
tailPts: [[0, -0.1], [0.7, -0.45], [0.5, 0], [0.7, 0.45], [0, 0.1]],
dorsalY: 0.32,
dorsalPts: [[0, 0], [0.2, 0.3], [0.5, 0.2], [0.6, 0]]
}
};
// Instantiate Fish Population
const fishes = [];
function spawnSchool(type, count) {
for (let i = 0; i < count; i++) {
const fish = new Fish(type, SPECIES_CONFIGS[type]);
scene.add(fish.group);
fishes.push(fish);
}
}
spawnSchool('clown', 8);
spawnSchool('blueTang', 6);
spawnSchool('yellowTang', 6);
spawnSchool('tetra', 16);
// --- Hardscape: Sand Bed with Live Caustic Shader ---
const sandGeom = new THREE.PlaneGeometry(TANK.w, TANK.d, 48, 32);
// Subtle sand dunes
const posAttr = sandGeom.attributes.position;
for (let i = 0; i < posAttr.count; i++) {
const x = posAttr.getX(i);
const y = posAttr.getY(i);
const d = Math.sin(x * 0.12) * 0.9 + Math.cos(y * 0.16) * 0.7 + Math.sin(x * 0.28 + y * 0.2) * 0.35;
posAttr.setZ(i, d);
}
sandGeom.computeVertexNormals();
const sandMat = new THREE.MeshStandardMaterial({
color: 0xdec69a,
roughness: 0.85,
metalness: 0.05
});
// Custom Caustic shader hook on standard material
sandMat.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 };
sandMat.userData.shader = shader;
shader.vertexShader = `
varying vec3 vWorldPosCustom;
` + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`#include <begin_vertex>
vWorldPosCustom = (modelMatrix * vec4(position, 1.0)).xyz;`
);
shader.fragmentShader = `
varying vec3 vWorldPosCustom;
uniform float uTime;
float getCaustics(vec2 p, float t) {
vec2 v1 = p * 0.18 + vec2(sin(p.y * 0.38 + t * 1.1), cos(p.x * 0.38 + t * 0.9)) * 0.38;
vec2 v2 = p * 0.18 + vec2(cos(p.y * 0.42 - t * 0.8), sin(p.x * 0.42 - t * 1.2)) * 0.38;
float c1 = sin(v1.x * 4.2 + v1.y * 4.2 + t * 1.3);
float c2 = cos(v2.x * 5.2 - v2.y * 3.8 - t * 1.1);
return pow(clamp((c1 + c2) * 0.5 + 0.5, 0.0, 1.0), 3.6) * 2.2;
}
` + shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
`#include <dithering_fragment>
float c = getCaustics(vWorldPosCustom.xz, uTime);
gl_FragColor.rgb += vec3(0.35, 0.85, 0.98) * c * 0.38;`
);
};
const sandMesh = new THREE.Mesh(sandGeom, sandMat);
sandMesh.rotation.x = -Math.PI / 2;
sandMesh.position.y = TANK.sandY;
sandMesh.receiveShadow = true;
scene.add(sandMesh);
// --- Hardscape: Craggy Rocks ---
const rockMat = new THREE.MeshStandardMaterial({
color: 0x48585e,
roughness: 0.9,
metalness: 0.1
});
// Add caustics to rocks too
rockMat.onBeforeCompile = sandMat.onBeforeCompile;
function createOrganicRock(radius, sx, sy, sz) {
const geom = new THREE.DodecahedronGeometry(radius, 2);
const pos = geom.attributes.position;
const v = new THREE.Vector3();
for (let i = 0; i < pos.count; i++) {
v.fromBufferAttribute(pos, i);
const noise = Math.sin(v.x * 2.2) * Math.cos(v.y * 2.2) * Math.sin(v.z * 2.2);
v.multiplyScalar(1.0 + noise * 0.28);
pos.setXYZ(i, v.x, v.y, v.z);
}
geom.computeVertexNormals();
const mesh = new THREE.Mesh(geom, rockMat);
mesh.scale.set(sx, sy, sz);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
const rockGroup = new THREE.Group();
// Left cluster
const r1 = createOrganicRock(7.5, 1.2, 1.6, 1.1);
r1.position.set(-20, TANK.sandY + 7, -6);
r1.rotation.set(0.3, 0.8, -0.2);
const r2 = createOrganicRock(4.5, 1.4, 1.1, 1.3);
r2.position.set(-13, TANK.sandY + 3.5, 2);
// Right cluster
const r3 = createOrganicRock(8.0, 1.3, 1.8, 1.2);
r3.position.set(21, TANK.sandY + 8, -5);
r3.rotation.set(-0.2, -0.6, 0.4);
const r4 = createOrganicRock(5.0, 1.2, 1.0, 1.4);
r4.position.set(14, TANK.sandY + 4, 3);
// Center back accent
const r5 = createOrganicRock(3.8, 1.4, 0.9, 1.1);
r5.position.set(2, TANK.sandY + 2.5, -9);
rockGroup.add(r1, r2, r3, r4, r5);
scene.add(rockGroup);
// --- Aquatic Seaweed Plants ---
const seaweedClusters = [];
const seaweedMat = new THREE.MeshStandardMaterial({
color: 0x1d753b,
roughness: 0.4,
metalness: 0.1,
side: THREE.DoubleSide
});
function createSeaweedStrand(height, width, baseY) {
const segs = 12;
const geom = new THREE.PlaneGeometry(width, height, 1, segs);
// Shift anchor to base
geom.translate(0, height / 2, 0);
const mesh = new THREE.Mesh(geom, seaweedMat);
mesh.position.y = baseY;
mesh.castShadow = true;
return {
mesh,
height,
phase: Math.random() * Math.PI * 2,
speed: 1.2 + Math.random() * 0.8,
origPos: geom.attributes.position.clone()
};
}
const plantLocations = [
{ x: -22, z: -10, count: 6 },
{ x: -16, z: -4, count: 4 },
{ x: 18, z: -9, count: 7 },
{ x: 25, z: 2, count: 5 },
{ x: 0, z: -12, count: 6 }
];
plantLocations.forEach((loc) => {
for (let i = 0; i < loc.count; i++) {
const h = 14 + Math.random() * 14;
const w = 0.5 + Math.random() * 0.35;
const strand = createSeaweedStrand(h, w, TANK.sandY);
strand.mesh.position.x = loc.x + (Math.random() - 0.5) * 4.5;
strand.mesh.position.z = loc.z + (Math.random() - 0.5) * 4.5;
strand.mesh.rotation.y = Math.random() * Math.PI;
scene.add(strand.mesh);
seaweedClusters.push(strand);
}
});
// --- Water Aerator Bubble Column ---
const BUBBLE_COUNT = 130;
const bubbleGeom = new THREE.SphereGeometry(0.2, 8, 8);
const bubbleMat = new THREE.MeshStandardMaterial({
color: 0xe0f7ff,
roughness: 0.1,
metalness: 0.8,
transparent: true,
opacity: 0.65
});
const bubbleInst = new THREE.InstancedMesh(bubbleGeom, bubbleMat, BUBBLE_COUNT);
const bubbleData = [];
const dummyMat4 = new THREE.Matrix4();
for (let i = 0; i < BUBBLE_COUNT; i++) {
bubbleData.push({
x: 21 + (Math.random() - 0.5) * 2.5,
y: TANK.sandY + Math.random() * (TANK.h - 2),
z: -6 + (Math.random() - 0.5) * 2.5,
speed: 4.5 + Math.random() * 4.0,
wobble: Math.random() * Math.PI * 2,
scale: 0.5 + Math.random() * 1.0
});
}
scene.add(bubbleInst);
// --- Micro Bubble Pool for Fleeing Fish ---
const FLEE_BUBBLES = 60;
const microInst = new THREE.InstancedMesh(bubbleGeom, bubbleMat, FLEE_BUBBLES);
const microData = [];
for (let i = 0; i < FLEE_BUBBLES; i++) {
microData.push({
x: 0, y: -999, z: 0,
speed: 6.0,
life: 0,
maxLife: 1.2
});
}
scene.add(microInst);
let nextMicroIdx = 0;
function spawnMicroBubble(pos) {
const b = microData[nextMicroIdx];
b.x = pos.x + (Math.random() - 0.5) * 0.4;
b.y = pos.y + (Math.random() - 0.5) * 0.4;
b.z = pos.z + (Math.random() - 0.5) * 0.4;
b.life = b.maxLife;
nextMicroIdx = (nextMicroIdx + 1) % FLEE_BUBBLES;
}
// --- God Rays (Sunlight Shafts) ---
function createGodRays() {
const cv = document.createElement('canvas');
cv.width = 128; cv.height = 512;
const ctx = cv.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 0, 512);
grad.addColorStop(0, 'rgba(210, 245, 255, 0.45)');
grad.addColorStop(0.3, 'rgba(160, 230, 255, 0.22)');
grad.addColorStop(1, 'rgba(0, 160, 230, 0.0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 128, 512);
const tex = new THREE.CanvasTexture(cv);
const rayGeom = new THREE.PlaneGeometry(16, 42);
const rayMat = new THREE.MeshBasicMaterial({
map: tex,
transparent: true,
opacity: 0.28,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide
});
const rays = new THREE.Group();
for (let i = 0; i < 4; i++) {
const m = new THREE.Mesh(rayGeom, rayMat);
m.position.set(-18 + i * 12, 4, -4 + i * 2);
m.rotation.z = -0.22;
m.rotation.y = 0.35 * (i % 2 === 0 ? 1 : -1);
rays.add(m);
}
scene.add(rays);
return rays;
}
const godRaysGroup = createGodRays();
// --- Suspended Water Dust / Plankton Motes ---
const MOTE_COUNT = 320;
const moteGeom = new THREE.BufferGeometry();
const motePos = new Float32Array(MOTE_COUNT * 3);
for (let i = 0; i < MOTE_COUNT; i++) {
motePos[i*3] = (Math.random() - 0.5) * TANK.w;
motePos[i*3+1] = TANK.sandY + Math.random() * TANK.h;
motePos[i*3+2] = (Math.random() - 0.5) * TANK.d;
}
moteGeom.setAttribute('position', new THREE.BufferAttribute(motePos, 3));
const moteMat = new THREE.PointsMaterial({
color: 0x7be8ff,
size: 0.35,
transparent: true,
opacity: 0.45,
blending: THREE.AdditiveBlending
});
const motes = new THREE.Points(moteGeom, moteMat);
scene.add(motes);
// --- Aquarium Tank Structure & Water Surface ---
// Back panel gradient
const backGeom = new THREE.PlaneGeometry(TANK.w, TANK.h);
const cvBack = document.createElement('canvas');
cvBack.width = 16; cvBack.height = 256;
const ctxB = cvBack.getContext('2d');
const gradB = ctxB.createLinearGradient(0, 0, 0, 256);
gradB.addColorStop(0, '#001c38');
gradB.addColorStop(0.4, '#043259');
gradB.addColorStop(1, '#011526');
ctxB.fillStyle = gradB;
ctxB.fillRect(0, 0, 16, 256);
const backTex = new THREE.CanvasTexture(cvBack);
const backMat = new THREE.MeshBasicMaterial({ map: backTex });
const backMesh = new THREE.Mesh(backGeom, backMat);
backMesh.position.set(0, 0, -TANK.d / 2);
scene.add(backMesh);
// Minimalist Tank Rim Bevels
const frameMat = new THREE.MeshStandardMaterial({ color: 0x091520, roughness: 0.2, metalness: 0.8 });
const rimTop = new THREE.Mesh(new THREE.BoxGeometry(TANK.w + 0.8, 0.8, TANK.d + 0.8), frameMat);
rimTop.position.y = TANK.surfaceY + 0.4;
const rimBot = new THREE.Mesh(new THREE.BoxGeometry(TANK.w + 0.8, 0.8, TANK.d + 0.8), frameMat);
rimBot.position.y = TANK.sandY - 0.4;
scene.add(rimTop, rimBot);
// Water Surface
const waterSurfGeom = new THREE.PlaneGeometry(TANK.w, TANK.d, 32, 16);
const waterSurfMat = new THREE.MeshStandardMaterial({
color: 0x1f8ab2,
roughness: 0.1,
metalness: 0.3,
transparent: true,
opacity: 0.45
});
const waterSurface = new THREE.Mesh(waterSurfGeom, waterSurfMat);
waterSurface.rotation.x = -Math.PI / 2;
waterSurface.position.y = TANK.surfaceY;
scene.add(waterSurface);
// --- Interactive 3D Cursor Ring ---
const cursorRingGeom = new THREE.RingGeometry(1.2, 1.45, 32);
const cursorRingMat = new THREE.MeshBasicMaterial({
color: 0x00f0ff,
transparent: true,
opacity: 0.75,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
const cursorRing = new THREE.Mesh(cursorRingGeom, cursorRingMat);
cursorRing.visible = false;
scene.add(cursorRing);
// --- Food Pellets System ---
const foodPellets = [];
const foodGeom = new THREE.SphereGeometry(0.22, 10, 10);
const foodMat = new THREE.MeshStandardMaterial({ color: 0xbf7b26, roughness: 0.6 });
class FoodPellet {
constructor(x, y, z) {
this.mesh = new THREE.Mesh(foodGeom, foodMat);
this.mesh.position.set(x, y, z);
this.position = this.mesh.position;
this.active = true;
this.sinkSpeed = 1.8 + Math.random() * 0.8;
this.seed = Math.random() * 10;
scene.add(this.mesh);
foodPellets.push(this);
}
update(dt, t) {
if (!this.active) return;
this.position.y -= this.sinkSpeed * dt;
this.position.x += Math.sin(t * 3.0 + this.seed) * 0.015;
this.position.z += Math.cos(t * 2.5 + this.seed) * 0.015;
// Bottom reached
if (this.position.y <= TANK.sandY + 0.3) {
this.active = false;
scene.remove(this.mesh);
}
}
eat() {
if (!this.active) return;
this.active = false;
spawnMicroBubble(this.position);
scene.remove(this.mesh);
}
}
function dropFood(x, z) {
for (let i = 0; i < 4; i++) {
const px = THREE.MathUtils.clamp(x + (Math.random() - 0.5) * 5, -TANK.w/2 + 2, TANK.w/2 - 2);
const pz = THREE.MathUtils.clamp(z + (Math.random() - 0.5) * 5, -TANK.d/2 + 2, TANK.d/2 - 2);
new FoodPellet(px, TANK.surfaceY - 0.5, pz);
}
}
// --- Mouse & Raycasting System ---
const raycaster = new THREE.Raycaster();
const mouseNDC = new THREE.Vector2(-999, -999);
let mouseActive = false;
let lastMouseMoveTime = 0;
const interactionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); // Z = 0 plane
let downPos = { x: 0, y: 0 };
let downTime = 0;
window.addEventListener('pointerdown', (e) => {
downPos = { x: e.clientX, y: e.clientY };
downTime = performance.now();
});
window.addEventListener('pointermove', (e) => {
mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
mouseActive = true;
lastMouseMoveTime = performance.now();
});
window.addEventListener('pointerup', (e) => {
const dist = Math.hypot(e.clientX - downPos.x, e.clientY - downPos.y);
const duration = performance.now() - downTime;
// If short click without dragging camera: drop food at clicked position
if (dist < 6 && duration < 320) {
raycaster.setFromCamera(mouseNDC, camera);
const target = new THREE.Vector3();
raycaster.ray.intersectPlane(interactionPlane, target);
if (target) {
dropFood(target.x, target.z);
}
}
});
// Touch support for mobile
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0) {
mouseNDC.x = (e.touches[0].clientX / window.innerWidth) * 2 - 1;
mouseNDC.y = -(e.touches[0].clientY / window.innerHeight) * 2 + 1;
mouseActive = true;
lastMouseMoveTime = performance.now();
}
}, { passive: true });
// UI Buttons
document.getElementById('btn-feed').addEventListener('click', (e) => {
e.stopPropagation();
dropFood((Math.random() - 0.5) * 20, (Math.random() - 0.5) * 10);
});
document.getElementById('btn-scatter').addEventListener('click', (e) => {
e.stopPropagation();
// Scatter all fish violently from center
const center = new THREE.Vector3(0, 0, 0);
fishes.forEach(f => {
const dir = f.position.clone().sub(center).normalize();
if (dir.lengthSq() < 0.01) dir.set(1, 0, 0);
f.accel.addScaledVector(dir, 70.0);
f.isFleeing = true;
f.fleeTimer = 1.4;
f.fleeIntensity = 1.0;
spawnMicroBubble(f.position);
});
});
const cameraPresets = [
{ pos: [0, 1, 56], target: [0, 0, 0] }, // Front View
{ pos: [0, 35, 42], target: [0, -3, 0] }, // High Angle
{ pos: [-36, 6, 36], target: [-5, 0, 0] } // Side Perspective
];
let camIdx = 0;
document.getElementById('btn-cam').addEventListener('click', (e) => {
e.stopPropagation();
camIdx = (camIdx + 1) % cameraPresets.length;
const p = cameraPresets[camIdx];
// Smooth lerp can happen, or quick shift:
camera.position.set(...p.pos);
controls.target.set(...p.target);
controls.update();
});
// --- Responsive Framing ---
function handleResize() {
const w = window.innerWidth;
const h = window.innerHeight;
const aspect = w / h;
camera.aspect = aspect;
// Adjust camera distance for mobile narrow aspect ratios
if (aspect < 1.0) {
camera.position.z = 78 / Math.max(0.55, aspect);
} else if (aspect < 1.35) {
camera.position.z = 64;
} else {
camera.position.z = 56;
}
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
window.addEventListener('resize', handleResize);
handleResize();
// --- Main Animation Loop ---
const clock = new THREE.Clock();
let simTime = 0;
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.06);
simTime += dt;
// 1. Mouse Ray Update
let currentRay = null;
// Inactive after 3 seconds of no mouse movement
if (mouseActive && (performance.now() - lastMouseMoveTime > 3000)) {
mouseActive = false;
}
if (mouseActive) {
raycaster.setFromCamera(mouseNDC, camera);
currentRay = raycaster.ray;
// Position 3D Cursor Ring
const hit = new THREE.Vector3();
currentRay.intersectPlane(interactionPlane, hit);
if (hit) {
cursorRing.visible = true;
cursorRing.position.copy(hit);
cursorRing.lookAt(camera.position);
const pulse = 1.0 + Math.sin(simTime * 8.0) * 0.15;
cursorRing.scale.set(pulse, pulse, 1);
}
} else {
cursorRing.visible = false;
}
// 2. Update Fish Population
for (let i = 0; i < fishes.length; i++) {
fishes[i].update(dt, fishes, currentRay, foodPellets);
}
// 3. Update Food Pellets
for (let i = foodPellets.length - 1; i >= 0; i--) {
foodPellets[i].update(dt, simTime);
if (!foodPellets[i].active) {
foodPellets.splice(i, 1);
}
}
// 4. Update Seaweed Current Sway
for (let s of seaweedClusters) {
const pos = s.mesh.geometry.attributes.position;
const orig = s.origPos;
for (let i = 0; i < pos.count; i++) {
const y = orig.getY(i);
const normY = y / s.height;
const sway = Math.sin(simTime * s.speed + s.phase + normY * 1.8) * Math.pow(normY, 1.8) * 1.8;
pos.setX(i, orig.getX(i) + sway);
pos.setZ(i, orig.getZ(i) + sway * 0.4);
}
s.mesh.geometry.attributes.position.needsUpdate = true;
}
// 5. Update Bubble Column
for (let i = 0; i < BUBBLE_COUNT; i++) {
const b = bubbleData[i];
b.y += b.speed * dt;
b.x += Math.sin(simTime * 3.5 + b.wobble) * 0.02;
if (b.y > TANK.surfaceY - 0.5) {
b.y = TANK.sandY + 0.5;
b.x = 21 + (Math.random() - 0.5) * 2.2;
b.z = -6 + (Math.random() - 0.5) * 2.2;
}
dummyMat4.identity();
dummyMat4.makeTranslation(b.x, b.y, b.z);
dummyMat4.scale(new THREE.Vector3(b.scale, b.scale, b.scale));
bubbleInst.setMatrixAt(i, dummyMat4);
}
bubbleInst.instanceMatrix.needsUpdate = true;
// 6. Update Fleeing Micro-Bubbles
for (let i = 0; i < FLEE_BUBBLES; i++) {
const mb = microData[i];
if (mb.life > 0) {
mb.life -= dt;
mb.y += mb.speed * dt;
const s = (mb.life / mb.maxLife) * 0.5;
dummyMat4.identity();
dummyMat4.makeTranslation(mb.x, mb.y, mb.z);
dummyMat4.scale(new THREE.Vector3(s, s, s));
} else {
dummyMat4.identity();
dummyMat4.makeTranslation(0, -999, 0);
}
microInst.setMatrixAt(i, dummyMat4);
}
microInst.instanceMatrix.needsUpdate = true;
// 7. Update Floating Plankton Motes
const mPos = moteGeom.attributes.position.array;
for (let i = 0; i < MOTE_COUNT; i++) {
mPos[i * 3 + 1] += Math.sin(simTime * 0.8 + i) * 0.006;
mPos[i * 3] += Math.cos(simTime * 0.5 + i) * 0.004;
}
moteGeom.attributes.position.needsUpdate = true;
// 8. Shimmering Caustics & Sunbeam Opacity
if (sandMat.userData.shader) {
sandMat.userData.shader.uniforms.uTime.value = simTime;
}
for (let i = 0; i < godRaysGroup.children.length; i++) {
godRaysGroup.children[i].material.opacity = 0.22 + Math.sin(simTime * 0.9 + i) * 0.08;
}
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>Reasoning trace (23,497 tokens)
**Implementing Interactive Fish Behavior** I'm currently refining the Three.js aquarium scene, focusing on the fish's reactive movement. My goal is to have them dynamically adjust their swimming patterns, specifically moving away from the cursor's proximity, enhancing the user's interactive experience with the aquatic environment. **Updating Code Implementation** I'm now consolidating the Three.js aquarium code into a single, self-contained HTML file, ensuring all CSS and JavaScript are inlined. The focus is on integrating the interactive fish scattering behavior and the visual mouse cursor effect, aiming for a seamless presentation. **Designing Procedural Elements** I'm working on integrating procedural generation for environmental assets like rocks and aquatic plants, aiming to add depth and realism to the aquarium scene. This includes developing shaders for waving seaweed and dynamic water effects like caustics and god rays, while also planning a robust bubble particle system. **Animating Aquatic Life** I'm focusing on bringing the fish models to life with sophisticated animations. My current thought process involves developing a segmented mesh or vertex shader approach to achieve fluid, wave-like swimming motions, and I'm also exploring skeletal animation for more complex movements across various species. **Deforming Fish Movement** I'm implementing dynamic vertex deformation in Three.js shaders, leveraging fish velocity and time to create incredibly lifelike swimming motions. This shader-based approach is key to achieving the fluid, natural animation I envision. **Improving Dynamic Effects** I'm now concentrating on enhancing the realism of the water's surface and the light interactions. My current focus is on implementing procedural caustics using custom shaders, aiming for that breathtakingly realistic visual. I'm also exploring real-time Voronoi or layered sine wave patterns for the sand bed. **Locking Camera Orientation** I'm working on ensuring the camera remains stably positioned within the aquarium view, preventing any disorienting flips. My focus is on clamping the viewing angles to maintain an attractive, immersive perspective of the underwater scene. **Modeling Diverse Fish** I'm now defining the geometric structure for various fish species, sketching out how their bodies will deform along the Z-axis from a rounded nose to a narrow peduncle. I'm planning custom buffer geometries or combined meshes to create four distinct species: Clownfish, Blue Tang, Yellow Tang, and Neon Tetra, each with unique color patterns and fin shapes. **Crafting Fish Textures** I'm currently generating procedural canvas textures for each fish species, meticulously defining their vibrant color palettes and unique markings. These textures, once applied to the 3D models, will truly bring them to life with a professional polish. **Deforming Fish Meshes** I'm detailing the mathematical approach for generating watertight fish geometry, focusing on defining nose-to-tail profiles and applying UV mapping. My current thought is to implement a vertex shader for animating the fins and tail, ensuring a cohesive and performant solution. **Modifying Fish Shaders** I'm enhancing the vertex shader to incorporate sine wave deformation for both the body and fins, driven by time and speed. I'm also exploring instance-specific material attributes to manage the approximately 30-45 fish efficiently. **Animating Tail Fins** I'm leaning towards a hierarchical animation approach for the fish tails and fins. This seems the most robust method for achieving lifelike motion and ensuring compatibility across various GPUs, avoiding complex shader issues while allowing for excellent visual results. **Animating Fleeing Reactions** I'm refining the fish's fleeing animation, linking body orientation and tail/fin flutter to speed. High `fleeFactor` and rapid wagging with increased swim speed create vivid darting bursts, making their reactions to the cursor palpable. **Simulating Fish Reactions** I'm detailing the logic for fish fleeing the cursor, including bubble trails and returning to normal. I'm also refining the 3D mouse position calculation using raycasting against the aquarium bounds for precise interaction. **Targeting Interactive Fish** I'm now calculating the distance from each fish to the 3D mouse ray, identifying those near the cursor. I can project a 3D cursor point for visual feedback and use the ray's closest point to the fish to determine precise repulsion direction. **Responding to Mouse Rays** I'm currently implementing a direct repulsion force, pushing fish away from the mouse ray whenever they're within a defined threshold. This approach feels incredibly intuitive and responsive for both desktop and mobile interactions, triggering immediate fleeing behavior. **Responding to Mouse Rays** I'm focusing on disabling fleeing when the mouse is inactive and adding a visual indicator for cursor interaction. I'm also defining the aquarium's dimensions and planning the sand bed and rock formations, aiming for a natural, immersive environment. **Exploring Ambient Lighting** I'm refining the underwater ambient light, aiming for a deep ocean blue `0x0`, to create a more immersive atmosphere. This complements the directional sunlight and the projected caustics for realistic light interactions. **Generating GPU Caustics** I'm now focusing on implementing real-time caustics directly within GLSL shaders for the sand and rock materials. My current thought is to leverage overlapping wave patterns with time and position to achieve a dynamic, GPU-accelerated effect that minimizes CPU overhead, as detailed in the provided formula. **Enhancing Visual Effects** I'm integrating a GPU-accelerated caustics system using GLSL, simulating shimmering networks on surfaces. I'm also refining the bubble system for a rising column and fish-generated puffs, adding floating motes for depth and volumetric god rays to enhance immersion. **Responding to Mouse Rays** I'm focused on disabling fleeing when the mouse is inactive and adding a visual indicator for cursor interaction. I'm also defining the aquarium's dimensions and planning the sand bed and rock formations, aiming for a natural, immersive environment. **Refining Fish Anatomy** I'm focused on defining the precise geometric details for each fish species. My current task involves generating custom buffer geometries, meticulously crafting nose-to-tail profiles and incorporating UV mapping for accurate texturing. **Generating Fish Geometry** **Generating Custom Fish Geometry** I'm meticulously detailing the mathematical approach for generating watertight fish geometry, focusing on defining nose-to-tail profiles and applying UV mapping. My current thought is to implement a vertex shader for animating the fins and tail, ensuring a cohesive and performant solution. **Crafting Fin Geometry** I'm now defining the geometry for each fish's fins, focusing on distinct shapes for tail, dorsal, pectoral, and eye elements. This involves using `Shape` and `ShapeGeometry` or custom buffer geometries to create unique appearances for each species, ensuring they contribute to individual character. **Defining Fish Textures** I'm now detailing the procedural canvas texture generation for each species. I'm outlining specific color gradients, stripe placements with black borders for the Clownfish, and the iconic black "palette" for the Blue Tang. I'm also planning accents like yellow wedges and cyan trim. **Texturing Fish Models** I'm currently defining the precise UV mapping and canvas drawing logic for each fish species' textures. This involves mapping canvas coordinates to body positions and angles, allowing for precise placement of stripes, gradients, and markings, which I'll then implement using JavaScript and `<canvas>`. **Painting Clownfish Textures** I'm now implementing the procedural texture for the Clownfish, focusing on gradients and distinct white stripes with black outlines. This involves carefully defining canvas drawing logic and color palettes to achieve a vibrant and recognizable look. **Generating Neon Tetra Textures** I'm now detailing the precise UV mapping and canvas drawing logic for the Neon Tetra's textures. This involves mapping canvas coordinates to body positions and angles, allowing for precise placement of its signature cyan bands and ruby red rear, which I'll implement using JavaScript and `<canvas>`. **Guiding Fish Behavior** I'm refining the flocking simulation, implementing separation, alignment, and cohesion rules for fish. I'm also adding tank boundary avoidance, ensuring the fish stay within the scene, and detailing their fleeing logic based on proximity to the mouse. **Responding to Mouse Rays** I'm now refining the fish's fleeing logic, specifically how they react to the mouse's proximity. I've implemented a `fleeFactor` and a `fleeRadius` to control the intensity and range of their panicked darting, making their reactions more dynamic and believable. **Animating Swimming Motions** I'm currently refining the fish's swimming animations, linking tail wagging and fin fluttering to their speed and fleeing behavior. I'm also implementing smooth banking and pitching for turns, making their movements more dynamic and lifelike. **Refining Fleeing and Feeding** I'm enhancing the fish's fleeing response, incorporating smooth banking and pitch for graceful 3D arc veering away from the cursor, and developing a food pellet drop feature triggered by short clicks, with pellets that sink and wobble. **Adding Interactive Features** I'm implementing food pellet interaction, enabling fish to steer towards and consume them, triggering a satisfied animation. I'm also adding an expanding ripple effect when the user clicks, making the aquarium even more delightful. **Developing Environmental Details** I'm now detailing the procedural generation for aquatic plants like seaweed, aiming for gentle swaying motions. My focus is also on integrating a realistic air stone bubble column and simulating sunlight god rays with softly animated opacity planes. **Generating Underwater Shimmer** I'm implementing a procedural caustic shader for the sand plane using `onBeforeCompile` with `MeshStandardMaterial`. This will create dynamic, shimmering light effects without external textures, enhancing the underwater atmosphere. I'm currently integrating time uniforms and world position varying for the shader. **Generating Dynamic Caustics** I'm refining the GLSL shader for generating real-time caustics on the sand plane. My current work involves integrating time uniforms and world position to create dynamic, shimmering light patterns that enhance the underwater atmosphere without relying on external textures. **Enhancing Visual Realism** I'm refining the caustics shader for both sand and rocks using `onBeforeCompile`, integrating time uniforms for dynamic shimmering light. I'm also solidifying the tank's dimensions and designing its minimalist frame and subtle glass panels, all to enhance immersion. **Detailing Fish Interactions** I'm defining the precise logic for fish fleeing the cursor, including dynamic speed changes and bubble trails. I'm also working on projecting a 3D cursor point for visual feedback and identifying the closest point on the ray to each fish for accurate repulsion. **Generating Fish Geometries** I'm detailing the construction for each fish species, outlining specific geometric and textural elements. This involves defining body shapes, fin configurations, and eye details for Clownfish, Blue Tang, and Yellow Tang, aiming for distinct visual characteristics for each. **Developing Fish Anatomy** I'm refining the geometric construction for each fish species. My current focus involves defining their distinct body profiles, fin configurations, and eye details for the Clownfish, Blue Tang, and Yellow Tang to ensure unique visual characteristics. **Animating Tail Fins** I'm implementing a hierarchical animation system for fish tails and fins. This approach allows for realistic, multi-jointed movement, much like a real fish spine, and I'm fine-tuning materials for translucent fins, especially for species like Neon Tetras, to enhance their delicate appearance. **Designing Environmental Details** I'm currently working on procedural seaweed generation using ribbons, anchored low and bending gracefully with time and depth. I'm also detailing the creation of unique, craggy rocks using perturbed Dodecahedron geometries with organic noise displacement. **Arranging Rock Formations** I'm finalizing the placement of intricate rock formations. My current focus is on meticulously arranging a large pinnacle, support stones, an arched ledge, and a back cave, ensuring they create natural swimming channels and visually appealing compositions within the aquarium. I'm also detailing the design for a rising air stone bubble column. **Adding Micro-Bubbles and Dust** I'm implementing micro-bubble trails for fleeing fish and a subtle, glowing plankton effect to enhance underwater atmosphere and visual depth. This includes a particle system for gentle drift and light interaction. **Dropping Interactive Food** I'm implementing a food pellet system that fish will react to. They'll now steer towards and consume these pellets when not actively fleeing, with pellets sinking and dissolving on the sand. I'm also detailing the raycaster setup for mouse interaction. **Implementing Fleeing Logic** I'm refining how fish react to the mouse. They'll now become inactive and return to schooling if the cursor hasn't moved for a bit, with a visual ripple indicator appearing when active. Each fish species has a unique flee distance for more natural reactions. **Applying Repulsion Forces** I'm implementing strong repulsion forces to make fish actively dart away from the mouse with rapid tail wags and burst acceleration, ensuring immediate and noticeable reactions to cursor proximity. This logic is now robust for the prompt's requirements. **Showcasing Interactive Elements** I'm now ensuring all engaging elements are visible from the start. This means fish are actively schooling and cruising, caustics are shimmering, plants are swaying, and bubbles are rising immediately upon load. Even if idle, gentle natural movements or a subtle demo cursor will keep the scene dynamic. **Balancing Interaction and Orbit** I'm refining how clicks are distinguished from camera drags. My current thought is to implement logic that checks pointer movement distance and duration on `pointerup`. If a short, small drag, it's a click to drop food and create a ripple; otherwise, it's handled as camera orbit. **Framing the Aquarium View** I'm now calculating the optimal camera framing to encompass the entire aquarium. My goal is to ensure that at a Field of View of 48 degrees and a Z-depth of 58 units, the full scene is visible, providing a beautiful, immersive perspective across different aspect ratios, including mobile. **Adjusting Camera Framing** I'm adapting the camera's field of view and position based on screen aspect ratio to ensure the aquarium is perfectly framed on all devices. I'm also setting limits for orbit controls to prevent disorienting camera angles and designing a minimalist UI overlay with interactive prompts. **Structuring Quick Actions** I'm defining the UI for quick actions, focusing on intuitive controls like feeding, scattering fish, and camera presets. I'm also detailing species counts and ensuring the UI is stylish, semi-transparent, and responsive across devices, allowing clicks to seamlessly pass through to the canvas. **Tuning Fish Behavior** I'm now fine-tuning the Boids flocking algorithm parameters for each fish species. I'm adjusting their `maxSpeed`, `fleeSpeed`, `maxForce`, `neighborRadius`, and `separationDist` to create distinct and natural-looking movement patterns, ensuring each species behaves uniquely within the schooling dynamics. **Refining Fish Movement** I'm focusing on integrating flocking behaviors like alignment and cohesion with a soft wandering force for natural cruising. Additionally, I'm implementing strong repulsion from tank boundaries and rock formations, alongside a food-seeking behavior that drives them towards pellets when available. **Steering Fish Behavior** I'm refining the steering logic for fish, balancing their natural cruising with new behaviors like seeking dropped food pellets and fleeing the cursor. I'm also implementing a `fleeTimer` for decay and dynamically adjusting their maximum speed based on whether they're fleeing, ensuring smooth integration of these actions. **Orienting Fish Movement** I'm determining the correct orientation for fish models in Three.js, specifically how their local Z-axis aligns with their velocity for accurate `lookAt` behavior. I'm exploring rotating the mesh container 180 degrees to ensure the head points correctly into the swimming direction, and I'm also planning to implement dynamic rolling into turns. **Refining Fish Animation** I'm focusing on enhancing the fish's reactive swimming, specifically their agile, hydrodynamic darting behavior when fleeing. I'm also detailing the implementation of a segmented ribbon mesh for the seaweed, designed to sway realistically with current-driven motion updated directly on the CPU. **Generating Shimmering Surfaces** I'm refining the GLSL shader for dynamic caustics on the sand and rock materials. My focus is on integrating time uniforms and world position to ensure shimmering light patterns are generated efficiently and realistically across all platforms, enhancing the underwater atmosphere without external textures. **Updating Caustics and Fish Geometry** I've successfully integrated a dynamic caustic shader for both sand and rock materials using GLSL, driven by time uniforms for realistic shimmering light. Concurrently, I'm refining the geometric construction of each fish species, detailing their distinct body profiles, fin configurations, and eye details for the Clownfish, Blue Tang, and Yellow Tang to ensure unique visual characteristics. **Defining Fish Anatomy** I'm now detailing the specific geometric properties and visual characteristics for each fish species. This includes outlining eye construction with distinct iris colors and pupil placement, and defining precise dimensions and color palettes for Clownfish, Blue Tang, Yellow Tang, and Neon Tetras to ensure unique appearances. **Refining Fleeing Animations** I'm intensifying the fish's fleeing animations by significantly increasing their tail wag speed and overall acceleration when threatened. This creates more dramatic and visually engaging darting reactions, enhancing the interactivity of the aquarium. **Enhancing Reactive Fleeing** I'm amplifying the fish's fleeing response with furious tail fluttering, greatly increased amplitude, and rapid acceleration away from the cursor. They now emit micro-bubble trails and dynamic banking, returning to normal cruising after a short timer. I'm also developing a subtle water glow visualizer for mouse interaction. **Dropping Food Pellets** I'm now implementing the food pellet drop and consumption logic. Pellets will have physics, fade out, and trigger fish to swim towards them. Successful consumption will create a sparkle, ripple, and pellet disappearance, enhancing the interactive feeding experience with visual feedback. **Detailing Visual Depth** I'm integrating atmospheric fog with a deep underwater blue for gradual falloff, enhancing the sense of 3D depth. Concurrently, I'm finalizing the UI layout, including action buttons, interactive tips, and ensuring mobile responsiveness, while also confirming the geometry and rotation details for the tank floor and rock formations. **Integrating Shader Details** I'm meticulously checking GLSL shader compilation, specifically how `vCausticWorldPos` is defined when using `onBeforeCompile` with `MeshStandardMaterial`. To ensure robustness against Three.js updates and potential `#if` block variations, I'm devising strategies to safely compute or pass world position directly within the fragment shader for my caustics. **Verifying Shader Logic** I'm now focusing on the GLSL shader, specifically how to reliably access world position for my caustics calculation within `MeshStandardMaterial` and `onBeforeCompile`. I've discovered that `position` and `modelMatrix` are directly available in the vertex shader, allowing me to compute `vCustomWorldPos` without relying on potentially variable uniforms like `vCausticWorldPos`. **Verifying Shader Logic** I've successfully bypassed potential Three.js internal conflicts by computing world position directly within the vertex shader. This ensures my GLSL caustics shader remains robust and universally compatible, generating shimmering light effects without external dependencies. I'm now testing the integration of god rays. **Defining Fish Anatomy** I'm meticulously defining the geometric and visual characteristics for each fish species. This includes their distinct body shapes, fin configurations, eye details for Clownfish, Blue Tang, and Yellow Tang, and precise color palettes and stripe placements for Neon Tetras, ensuring each is unique and visually appealing. **Refining Interaction Logic** I'm solidifying the logic for how fish react to the mouse and environment. This includes strong repulsion forces from the cursor, gentle nudges away from tank walls and rocks, and dynamic speed adjustments. I'm also optimizing performance by grouping meshes and particle systems to maintain smooth 60 FPS. **Enhancing Fleeing Dynamics** I'm refining the fish's escape response, calculating precise repulsion vectors and applying intense acceleration when near the cursor. I've also implemented a "Scatter All" feature with a shockwave impulse and a food pellet drop mechanism triggered by clicks, complete with fish consumption animations. **Sculpting Sand Dunes** I'm adding organic contours to the sand plane using trigonometric functions to create soft dunes. This involves manipulating vertex positions in `PlaneGeometry` and displacing them along the Z-axis, which will translate to world Y after rotation, to achieve the desired undulating effect for the seabed. **Generating Bubble Systems** I'm currently focused on optimizing the bubble systems. I'm implementing an efficient `InstancedMesh` for a large, rising air stone bubble column and a separate `InstancedMesh` for micro-bubble trails emitted by fleeing fish. This approach minimizes garbage collection and maximizes performance for these dynamic visual elements. **Designing Underwater Atmosphere** I'm integrating a particle system for dust motes with a slow, sine-wave drift to add depth. Simultaneously, I'm developing a dynamic 3D water cursor with a pulsing cyan glow that triggers fish fleeing behavior when they are within its radius, and I'm also planning a subtle, automated ripple effect for idle periods to capture engaging screenshots. **Finalizing UI and Sandbox Compliance** I'm now meticulously reviewing and implementing the UI overlay, focusing on responsive HUD elements like title badges, fish count tags, and floating action buttons for scattering and feeding. I'm also ensuring strict sandbox compliance, eradicating any use of local storage, cookies, or external assets, and confirming Three.js is solely imported via the provided import map. My thoughts are on the final polish to make this a seamless, self-contained experience. **Refining Fish Behavior** I'm now tuning the specific animations and reactions for each fish species. Clownfish will have a characteristic bob, Blue Tangs will exhibit agile sweeping tails, Yellow Tangs will swim regally, and Neon Tetras will school with high-frequency synchronous movements. I'm also ensuring they all react dynamically to the mouse, ramping up tail wag speed and acceleration.