Interactive 3D aquariumOne-line
Gemini 3.8 Flash
by GoogleReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 21 s
- Cost
- $0.043
- Output tokens
- 11,309
- Spent thinking
- 796 tokens
- Speed
- 148.6 tok/s
- Console errors
- 0
- First token after
- 4.6 s
- First visible text after
- 14.2 s
- Prompt tokens
- 448
- Code size
- 35.2 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"medium"}- Max output
- 65,536 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 907 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Interactive Aquarium</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
overflow: hidden;
background: #020b14;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #d8f3dc;
}
#webgl-canvas {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: 1;
}
#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: 24px;
}
.header {
background: rgba(3, 20, 36, 0.45);
backdrop-filter: blur(10px);
border: 1px solid rgba(82, 182, 154, 0.25);
border-radius: 12px;
padding: 12px 20px;
max-width: 380px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
pointer-events: auto;
}
h1 {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: 0.5px;
color: #74c69d;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.subtitle {
font-size: 0.82rem;
color: #b7e4c7;
opacity: 0.9;
line-height: 1.35;
}
.badge {
display: inline-block;
background: rgba(45, 106, 79, 0.5);
border: 1px solid #52b788;
padding: 2px 7px;
border-radius: 6px;
font-size: 0.72rem;
color: #d8f3dc;
font-weight: bold;
}
.stats {
align-self: flex-end;
background: rgba(3, 20, 36, 0.4);
backdrop-filter: blur(8px);
border: 1px solid rgba(82, 182, 154, 0.2);
border-radius: 10px;
padding: 10px 16px;
font-size: 0.78rem;
color: #95d5b2;
display: flex;
gap: 16px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-val {
font-weight: 700;
color: #e8f5e9;
font-size: 1.05rem;
}
</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>
<canvas id="webgl-canvas"></canvas>
<div id="ui-layer">
<div class="header">
<h1><span>🐠</span> Bio-Luminescent Tank</h1>
<p class="subtitle">
Move cursor near fish to <strong>scare them away</strong>.<br>
<span class="badge">Click</span> anywhere to drop fish food flakes!
</p>
</div>
<div class="stats">
<div class="stat-item">
<span class="stat-val" id="fish-count">48</span>
<span>Fish</span>
</div>
<div class="stat-item">
<span class="stat-val" id="scared-count">0</span>
<span>Fleeing</span>
</div>
<div class="stat-item">
<span class="stat-val" id="food-count">0</span>
<span>Food Pellets</span>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
// --- Tank Dimensions ---
const TANK = {
width: 44,
height: 24,
depth: 26,
minX: -22, maxX: 22,
minY: -11, maxY: 11,
minZ: -12.5, maxZ: 12.5
};
// --- Setup Scene, Camera, Renderer ---
const canvas = document.getElementById('webgl-canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x02131d);
scene.fog = new THREE.FogExp2(0x021926, 0.022);
const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 120);
camera.position.set(0, 1.5, 34);
// --- Lights ---
const ambientLight = new THREE.AmbientLight(0x093a4a, 1.8);
scene.add(ambientLight);
// Overhead sunlight filtered by water
const sunLight = new THREE.SpotLight(0x71e5f5, 5.0);
sunLight.position.set(0, 28, 4);
sunLight.angle = Math.PI / 2.6;
sunLight.penumbra = 0.8;
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 40;
scene.add(sunLight);
// Underwater bottom bounce light
const bounceLight = new THREE.DirectionalLight(0x0a4247, 1.2);
bounceLight.position.set(0, -15, 5);
scene.add(bounceLight);
// Glowing cursor threat sphere & light
const threatLight = new THREE.PointLight(0x00f2fe, 3.5, 14);
scene.add(threatLight);
const threatGeo = new THREE.SphereGeometry(0.35, 16, 16);
const threatMat = new THREE.MeshBasicMaterial({
color: 0x48cae4,
transparent: true,
opacity: 0.6,
wireframe: true
});
const threatMesh = new THREE.Mesh(threatGeo, threatMat);
scene.add(threatMesh);
// Ripples / Sensor Ring around pointer
const ringGeo = new THREE.RingGeometry(1.2, 1.35, 32);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x48cae4,
transparent: true,
opacity: 0.35,
side: THREE.DoubleSide
});
const threatRing = new THREE.Mesh(ringGeo, ringMat);
scene.add(threatRing);
// --- Aquarium Glass Tank & Frame ---
function createAquariumStructure() {
// Tank wireframe / glass edges
const boxGeo = new THREE.BoxGeometry(TANK.width, TANK.height, TANK.depth);
const edges = new THREE.EdgesGeometry(boxGeo);
const edgeMat = new THREE.LineBasicMaterial({ color: 0x1f6f8b, transparent: true, opacity: 0.45 });
const edgeMesh = new THREE.LineSegments(edges, edgeMat);
scene.add(edgeMesh);
// Water surface plane with shimmer
const waterPlaneGeo = new THREE.PlaneGeometry(TANK.width, TANK.depth, 32, 32);
waterPlaneGeo.rotateX(-Math.PI / 2);
const waterPlaneMat = new THREE.MeshStandardMaterial({
color: 0x3a86c8,
roughness: 0.1,
metalness: 0.8,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide
});
const waterPlane = new THREE.Mesh(waterPlaneGeo, waterPlaneMat);
waterPlane.position.y = TANK.maxY;
scene.add(waterPlane);
// Sand floor with dunes
const floorGeo = new THREE.PlaneGeometry(TANK.width + 4, TANK.depth + 4, 48, 48);
floorGeo.rotateX(-Math.PI / 2);
const pos = floorGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
const dune = Math.sin(x * 0.25) * Math.cos(z * 0.3) * 0.8 + Math.sin(x * 0.1) * 0.5;
pos.setY(i, dune);
}
floorGeo.computeVertexNormals();
// Procedural sand canvas texture
const canvasTex = document.createElement('canvas');
canvasTex.width = 512;
canvasTex.height = 512;
const ctx = canvasTex.getContext('2d');
ctx.fillStyle = '#173338';
ctx.fillRect(0, 0, 512, 512);
for (let i = 0; i < 20000; i++) {
ctx.fillStyle = Math.random() > 0.5 ? '#1f444b' : '#112529';
ctx.fillRect(Math.random() * 512, Math.random() * 512, 1.5, 1.5);
}
const sandTex = new THREE.CanvasTexture(canvasTex);
sandTex.wrapS = THREE.RepeatWrapping;
sandTex.wrapT = THREE.RepeatWrapping;
sandTex.repeat.set(4, 3);
const floorMat = new THREE.MeshStandardMaterial({
map: sandTex,
color: 0x1b434a,
roughness: 0.9,
metalness: 0.1
});
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.position.y = TANK.minY;
floorMesh.receiveShadow = true;
scene.add(floorMesh);
// Background subtle light wall for depth
const backGeo = new THREE.PlaneGeometry(TANK.width * 1.4, TANK.height * 1.3);
const backMat = new THREE.MeshBasicMaterial({
color: 0x01121d
});
const backMesh = new THREE.Mesh(backGeo, backMat);
backMesh.position.set(0, 0, TANK.minZ - 4);
scene.add(backMesh);
}
createAquariumStructure();
// --- Coral / Sea Rock Clusters ---
function createRocksAndCorals() {
const rockMat = new THREE.MeshStandardMaterial({
color: 0x112d32,
roughness: 0.85,
metalness: 0.1
});
// Clusters in corners
const clusters = [
{ x: -16, z: -7, count: 6, scale: 2.2 },
{ x: 16, z: -6, count: 5, scale: 2.0 },
{ x: -10, z: 6, count: 4, scale: 1.5 },
{ x: 12, z: 5, count: 4, scale: 1.6 }
];
clusters.forEach(c => {
for (let i = 0; i < c.count; i++) {
const geo = new THREE.DodecahedronGeometry(c.scale * (0.6 + Math.random() * 0.7), 1);
const rock = new THREE.Mesh(geo, rockMat);
rock.position.set(
c.x + (Math.random() - 0.5) * 6,
TANK.minY + Math.random() * 1.5,
c.z + (Math.random() - 0.5) * 5
);
rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
rock.scale.set(1 + Math.random() * 0.5, 0.8 + Math.random() * 0.8, 1 + Math.random() * 0.5);
rock.receiveShadow = true;
rock.castShadow = true;
scene.add(rock);
}
});
}
createRocksAndCorals();
// --- Swaying Seaweed / Plants ---
const kelpSegments = [];
function createKelpField() {
const kelpPositions = [
[-18, -8], [-16, -9], [-15, -6], [-19, -5],
[17, -7], [18, -9], [15, -8], [19, -6],
[-11, 4], [-12, 6], [13, 4], [14, 6]
];
const kelpMat = new THREE.MeshStandardMaterial({
color: 0x1b4332,
roughness: 0.4,
metalness: 0.1,
side: THREE.DoubleSide
});
kelpPositions.forEach(([bx, bz]) => {
const height = 10 + Math.random() * 8;
const joints = 7;
const segHeight = height / joints;
let parent = scene;
const chain = [];
let baseY = TANK.minY;
for (let j = 0; j < joints; j++) {
const width = 0.55 * (1 - (j / joints) * 0.6);
const leafGeo = new THREE.PlaneGeometry(width, segHeight);
const leaf = new THREE.Mesh(leafGeo, kelpMat);
leaf.position.y = (j === 0) ? baseY + segHeight / 2 : segHeight;
leaf.position.x = (j === 0) ? bx + (Math.random() - 0.5) : 0;
leaf.position.z = (j === 0) ? bz + (Math.random() - 0.5) : 0;
leaf.rotation.y = Math.random() * Math.PI;
parent.add(leaf);
chain.push(leaf);
parent = leaf;
}
kelpSegments.push(chain);
});
}
createKelpField();
// --- Rising Bubbles System ---
const BUBBLE_COUNT = 90;
const bubbleGeo = new THREE.SphereGeometry(0.12, 8, 8);
const bubbleMat = new THREE.MeshPhysicalMaterial({
color: 0xade8f4,
transmission: 0.8,
opacity: 0.85,
transparent: true,
roughness: 0.1,
ior: 1.15
});
const bubbleInstanced = new THREE.InstancedMesh(bubbleGeo, bubbleMat, BUBBLE_COUNT);
const bubbleData = [];
const dummy = new THREE.Object3D();
for (let i = 0; i < BUBBLE_COUNT; i++) {
const data = {
x: (Math.random() - 0.5) * TANK.width * 0.85,
y: TANK.minY + Math.random() * TANK.height,
z: (Math.random() - 0.5) * TANK.depth * 0.85,
speedY: 2.2 + Math.random() * 2.5,
wobbleSpeed: 2 + Math.random() * 3,
wobbleAmp: 0.15 + Math.random() * 0.25,
phase: Math.random() * Math.PI * 2,
scale: 0.4 + Math.random() * 0.85
};
bubbleData.push(data);
}
scene.add(bubbleInstanced);
// --- Caustic Light Shafts ---
const causticsGroup = new THREE.Group();
const shaftMat = new THREE.MeshBasicMaterial({
color: 0x48cae4,
transparent: true,
opacity: 0.08,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false
});
for (let i = 0; i < 7; i++) {
const shaftGeo = new THREE.CylinderGeometry(0.8 + Math.random() * 1.2, 3.5 + Math.random() * 2.5, TANK.height * 1.1, 12, 1, true);
const shaft = new THREE.Mesh(shaftGeo, shaftMat);
shaft.position.set((i - 3) * 6 + (Math.random() - 0.5) * 3, 1, (Math.random() - 0.5) * 8);
shaft.rotation.z = (Math.random() - 0.5) * 0.25;
shaft.rotation.x = (Math.random() - 0.5) * 0.25;
causticsGroup.add(shaft);
}
scene.add(causticsGroup);
// --- Procedural 3D Fish Geometry Generator ---
const fishPalette = [
{ name: 'Clownfish', body: 0xff7b00, stripe: 0xffffff, accent: 0x111111, length: 1.5, height: 0.8 },
{ name: 'Blue Tang', body: 0x1976d2, stripe: 0x0d47a1, accent: 0xffeb3b, length: 1.7, height: 0.95 },
{ name: 'Yellow Tang', body: 0xffd100, stripe: 0xffaa00, accent: 0xffffff, length: 1.4, height: 1.0 },
{ name: 'Neon Tetra', body: 0x00f5d4, stripe: 0xf72585, accent: 0x00b4d8, length: 1.2, height: 0.45 },
{ name: 'Angel Fish', body: 0x3a0ca3, stripe: 0x4cc9f0, accent: 0x7209b7, length: 1.8, height: 1.25 }
];
function buildFishModel(palette) {
const root = new THREE.Group();
// Fish body: elongated tapered shape
const bodyGeo = new THREE.ConeGeometry(palette.height * 0.6, palette.length, 12);
bodyGeo.rotateZ(-Math.PI / 2); // points along +X
bodyGeo.scale(1, 0.4, 1);
const bodyMat = new THREE.MeshStandardMaterial({
color: palette.body,
roughness: 0.35,
metalness: 0.2
});
const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.castShadow = true;
root.add(bodyMesh);
// Striped banding on body
const stripeGeo = new THREE.TorusGeometry(palette.height * 0.42, 0.05, 8, 16);
stripeGeo.scale(0.5, 1, 0.8);
const stripeMat = new THREE.MeshStandardMaterial({
color: palette.stripe,
roughness: 0.3
});
const stripeMesh = new THREE.Mesh(stripeGeo, stripeMat);
stripeMesh.position.x = -palette.length * 0.1;
root.add(stripeMesh);
// Eyes
const eyeGeo = new THREE.SphereGeometry(0.1, 8, 8);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x050505 });
const eyeLeft = new THREE.Mesh(eyeGeo, eyeMat);
eyeLeft.position.set(palette.length * 0.28, palette.height * 0.12, 0.22);
const eyeRight = eyeLeft.clone();
eyeRight.position.z = -0.22;
root.add(eyeLeft);
root.add(eyeRight);
// Pectoral Fins (slight angle)
const finGeo = new THREE.BufferGeometry();
const finVerts = new Float32Array([
0, 0, 0,
-0.35, 0.2, 0.45,
-0.45, -0.15, 0.35
]);
finGeo.setAttribute('position', new THREE.BufferAttribute(finVerts, 3));
finGeo.computeVertexNormals();
const finMat = new THREE.MeshStandardMaterial({
color: palette.accent,
roughness: 0.3,
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide
});
const finLeft = new THREE.Mesh(finGeo, finMat);
finLeft.position.set(0.1, -0.05, 0.15);
const finRight = finLeft.clone();
finRight.scale.set(1, 1, -1);
finRight.position.z = -0.15;
root.add(finLeft);
root.add(finRight);
// Hinged Tail (for natural wagging)
const tailPivot = new THREE.Group();
tailPivot.position.x = -palette.length * 0.45;
const tailFinGeo = new THREE.BufferGeometry();
const tailVerts = new Float32Array([
0, 0, 0,
-0.7, palette.height * 0.65, 0,
-0.5, 0, 0,
-0.7, -palette.height * 0.65, 0
]);
const tailIndices = [
0, 1, 2,
0, 2, 3
];
tailFinGeo.setIndex(tailIndices);
tailFinGeo.setAttribute('position', new THREE.BufferAttribute(tailVerts, 3));
tailFinGeo.computeVertexNormals();
const tailMesh = new THREE.Mesh(tailFinGeo, finMat);
tailPivot.add(tailMesh);
root.add(tailPivot);
return { root, tailPivot, finLeft, finRight };
}
// --- Fish Boid / Swarm Class ---
class Fish {
constructor(id) {
this.id = id;
this.palette = fishPalette[id % fishPalette.length];
const model = buildFishModel(this.palette);
this.mesh = model.root;
this.tailPivot = model.tailPivot;
this.finLeft = model.finLeft;
this.finRight = model.finRight;
// Initial transform
this.position = new THREE.Vector3(
(Math.random() - 0.5) * TANK.width * 0.7,
(Math.random() - 0.5) * TANK.height * 0.7,
(Math.random() - 0.5) * TANK.depth * 0.7
);
this.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 1.5,
(Math.random() - 0.5) * 3
).normalize().multiplyScalar(2 + Math.random() * 2);
this.acceleration = new THREE.Vector3();
this.mesh.position.copy(this.position);
scene.add(this.mesh);
// Movement personality
this.normalSpeed = 2.4 + Math.random() * 1.6;
this.maxSpeed = this.normalSpeed;
this.maxForce = 4.2;
this.isScared = false;
this.scaredTimer = 0;
this.fleeDirection = new THREE.Vector3();
// Tail oscillation
this.wagFrequency = 6;
this.wagPhase = Math.random() * Math.PI * 2;
this.scale = 0.85 + Math.random() * 0.35;
this.mesh.scale.setScalar(this.scale);
}
update(delta, time, allFish, mousePos, mouseActive, foodPellets) {
this.acceleration.set(0, 0, 0);
// Check distance to mouse cursor threat
if (mouseActive) {
const distToMouse = this.position.distanceTo(mousePos);
const scareRadius = 8.5;
if (distToMouse < scareRadius) {
this.isScared = true;
this.scaredTimer = 1.4; // seconds of frantic flee
// Burst force directly away from threat
const fleeForce = new THREE.Vector3().subVectors(this.position, mousePos);
fleeForce.normalize();
const urgency = (1 - distToMouse / scareRadius);
fleeForce.multiplyScalar(26 * (urgency + 0.3));
this.acceleration.add(fleeForce);
// Spawn small startled bubble at fish mouth occasionally
if (Math.random() < 0.25) {
triggerBurstBubble(this.position);
}
}
}
// Handle Scared state countdown
if (this.scaredTimer > 0) {
this.scaredTimer -= delta;
this.maxSpeed = this.normalSpeed * 3.2; // super swim boost
this.wagFrequency = 18; // frantic tail wagging
if (this.scaredTimer <= 0) {
this.isScared = false;
}
} else {
this.maxSpeed = this.normalSpeed;
this.wagFrequency = 6;
// If not scared, search for food flakes!
let closestFood = null;
let minDist = 14;
for (let f of foodPellets) {
const d = this.position.distanceTo(f.position);
if (d < minDist) {
minDist = d;
closestFood = f;
}
}
if (closestFood) {
const seekFood = new THREE.Vector3().subVectors(closestFood.position, this.position);
seekFood.normalize().multiplyScalar(4.5);
this.acceleration.add(seekFood);
// Eat food if close
if (minDist < 1.0) {
closestFood.eaten = true;
}
} else {
// Flocking behavior (Separation, Alignment, Cohesion)
this.flock(allFish);
}
}
// Tank boundary avoidance
this.boundaryRepulsion();
// Physics integration
this.velocity.addScaledVector(this.acceleration, delta);
const currentSpeedLimit = this.isScared ? this.normalSpeed * 3.0 : this.normalSpeed;
this.velocity.clampLength(0.8, currentSpeedLimit);
this.position.addScaledVector(this.velocity, delta);
this.mesh.position.copy(this.position);
// Smooth rotation towards movement direction
if (this.velocity.lengthSq() > 0.01) {
const targetForward = this.velocity.clone().normalize();
const targetRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), targetForward);
this.mesh.quaternion.slerp(targetRotation, delta * 7);
}
// Animate tail fin and pectoral fins
const currentWagFreq = this.isScared ? 20 : (4 + this.velocity.length() * 2);
const wagAngle = Math.sin(time * currentWagFreq + this.wagPhase) * (this.isScared ? 0.65 : 0.4);
this.tailPivot.rotation.y = wagAngle;
this.finLeft.rotation.y = Math.cos(time * currentWagFreq) * 0.3;
this.finRight.rotation.y = -Math.cos(time * currentWagFreq) * 0.3;
}
boundaryRepulsion() {
const margin = 3.5;
const turnForce = 8.0;
if (this.position.x < TANK.minX + margin) this.acceleration.x += turnForce;
if (this.position.x > TANK.maxX - margin) this.acceleration.x -= turnForce;
if (this.position.y < TANK.minY + margin * 0.8) this.acceleration.y += turnForce;
if (this.position.y > TANK.maxY - margin * 0.8) this.acceleration.y -= turnForce;
if (this.position.z < TANK.minZ + margin) this.acceleration.z += turnForce;
if (this.position.z > TANK.maxZ - margin) this.acceleration.z -= turnForce;
// Hard clamp just in case
this.position.x = THREE.MathUtils.clamp(this.position.x, TANK.minX + 0.5, TANK.maxX - 0.5);
this.position.y = THREE.MathUtils.clamp(this.position.y, TANK.minY + 0.5, TANK.maxY - 0.5);
this.position.z = THREE.MathUtils.clamp(this.position.z, TANK.minZ + 0.5, TANK.maxZ - 0.5);
}
flock(allFish) {
const sepDist = 2.2;
const neighborDist = 6.0;
const sep = new THREE.Vector3();
const ali = new THREE.Vector3();
const coh = new THREE.Vector3();
let sepCount = 0;
let neighborCount = 0;
for (let other of allFish) {
if (other === this) continue;
const d = this.position.distanceTo(other.position);
if (d < sepDist && d > 0.001) {
const diff = new THREE.Vector3().subVectors(this.position, other.position);
diff.normalize().divideScalar(d);
sep.add(diff);
sepCount++;
}
if (d < neighborDist) {
ali.add(other.velocity);
coh.add(other.position);
neighborCount++;
}
}
if (sepCount > 0) {
sep.divideScalar(sepCount);
sep.normalize().multiplyScalar(4.0);
this.acceleration.add(sep);
}
if (neighborCount > 0) {
ali.divideScalar(neighborCount);
ali.normalize().multiplyScalar(1.5);
this.acceleration.add(ali);
coh.divideScalar(neighborCount);
const cohDir = new THREE.Vector3().subVectors(coh, this.position);
cohDir.normalize().multiplyScalar(1.2);
this.acceleration.add(cohDir);
}
}
}
// --- Create School of Fish ---
const FISH_COUNT = 48;
const fishSchool = [];
for (let i = 0; i < FISH_COUNT; i++) {
fishSchool.push(new Fish(i));
}
// --- Food Pellets System ---
const foodPellets = [];
const foodGeo = new THREE.DodecahedronGeometry(0.2, 0);
const foodMat = new THREE.MeshStandardMaterial({
color: 0xffaa00,
emissive: 0x994400,
roughness: 0.3
});
function spawnFood(worldX, worldZ) {
const mesh = new THREE.Mesh(foodGeo, foodMat);
mesh.position.set(worldX, TANK.maxY - 0.5, worldZ);
scene.add(mesh);
foodPellets.push({
mesh,
position: mesh.position,
velocity: new THREE.Vector3((Math.random() - 0.5) * 0.4, -1.8 - Math.random() * 0.8, (Math.random() - 0.5) * 0.4),
eaten: false
});
}
// Dynamic Burst Bubbles when fish dart away
const burstBubbles = [];
function triggerBurstBubble(pos) {
if (burstBubbles.length > 25) return;
const geo = new THREE.SphereGeometry(0.12 + Math.random() * 0.1, 6, 6);
const m = new THREE.Mesh(geo, bubbleMat);
m.position.copy(pos);
scene.add(m);
burstBubbles.push({
mesh: m,
vy: 3.5 + Math.random() * 2,
life: 1.2
});
}
// --- Mouse / Raycasting Target Plane ---
const mouse2D = new THREE.Vector2(-999, -999);
const raycaster = new THREE.Raycaster();
const interactionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); // Z=0 mid-tank plane
const mouse3D = new THREE.Vector3(0, 0, 0);
let mouseActive = false;
function updateMouse3D(clientX, clientY) {
mouse2D.x = (clientX / window.innerWidth) * 2 - 1;
mouse2D.y = -(clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse2D, camera);
const hitPoint = new THREE.Vector3();
if (raycaster.ray.intersectPlane(interactionPlane, hitPoint)) {
// Clamp within tank boundaries
hitPoint.x = THREE.MathUtils.clamp(hitPoint.x, TANK.minX + 2, TANK.maxX - 2);
hitPoint.y = THREE.MathUtils.clamp(hitPoint.y, TANK.minY + 2, TANK.maxY - 2);
mouse3D.copy(hitPoint);
mouseActive = true;
}
}
window.addEventListener('pointermove', (e) => {
updateMouse3D(e.clientX, e.clientY);
});
window.addEventListener('pointerleave', () => {
mouseActive = false;
});
// Click to drop food
window.addEventListener('pointerdown', (e) => {
updateMouse3D(e.clientX, e.clientY);
spawnFood(mouse3D.x + (Math.random() - 0.5) * 1.5, mouse3D.z + (Math.random() - 0.5) * 1.5);
// Drop a couple extra for fun
if (Math.random() > 0.4) {
setTimeout(() => spawnFood(mouse3D.x + (Math.random() - 0.5) * 2, mouse3D.z + (Math.random() - 0.5) * 2), 120);
}
});
// Window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- Animation Loop ---
const clock = new THREE.Clock();
const scaredCountEl = document.getElementById('scared-count');
const foodCountEl = document.getElementById('food-count');
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.06);
const time = clock.getElapsedTime();
// Self-patrolling idle threat when mouse is not active so demo is lively on initial load
let effectiveThreatPos = mouse3D;
let effectiveThreatActive = mouseActive;
if (!mouseActive) {
const autoX = Math.sin(time * 0.9) * 12;
const autoY = Math.cos(time * 0.7) * 5 + 1;
effectiveThreatPos = new THREE.Vector3(autoX, autoY, 0);
effectiveThreatActive = true; // gently drives movement in demo mode!
}
// Update glowing pointer & visual sensor rings
threatLight.position.copy(effectiveThreatPos);
threatMesh.position.copy(effectiveThreatPos);
threatRing.position.copy(effectiveThreatPos);
threatRing.rotation.z = time * 2;
const ringScale = 1.0 + Math.sin(time * 6) * 0.15;
threatRing.scale.set(ringScale, ringScale, ringScale);
// Update Kelp plant sway
kelpSegments.forEach((chain, idx) => {
const swayOffset = idx * 0.8;
chain.forEach((seg, sIdx) => {
const wave = Math.sin(time * 2.2 + swayOffset + sIdx * 0.4) * 0.08;
seg.rotation.z = wave;
seg.rotation.x = Math.cos(time * 1.6 + swayOffset + sIdx * 0.3) * 0.05;
});
});
// Update Rising Bubbles
for (let i = 0; i < BUBBLE_COUNT; i++) {
const b = bubbleData[i];
b.y += b.speedY * delta;
b.x += Math.sin(time * b.wobbleSpeed + b.phase) * (b.wobbleAmp * delta * 2.5);
// Reset at bottom when reaching surface
if (b.y > TANK.maxY) {
b.y = TANK.minY + 0.5;
b.x = (Math.random() - 0.5) * TANK.width * 0.85;
b.z = (Math.random() - 0.5) * TANK.depth * 0.85;
}
dummy.position.set(b.x, b.y, b.z);
dummy.scale.setScalar(b.scale);
dummy.updateMatrix();
bubbleInstanced.setMatrixAt(i, dummy.matrix);
}
bubbleInstanced.instanceMatrix.needsUpdate = true;
// Update Burst Bubbles
for (let i = burstBubbles.length - 1; i >= 0; i--) {
const bb = burstBubbles[i];
bb.life -= delta;
bb.mesh.position.y += bb.vy * delta;
bb.mesh.scale.multiplyScalar(0.98);
if (bb.life <= 0 || bb.mesh.position.y > TANK.maxY) {
scene.remove(bb.mesh);
bb.mesh.geometry.dispose();
burstBubbles.splice(i, 1);
}
}
// Update Food Pellets
for (let i = foodPellets.length - 1; i >= 0; i--) {
const f = foodPellets[i];
f.position.addScaledVector(f.velocity, delta);
f.mesh.rotation.x += delta * 2;
f.mesh.rotation.y += delta * 1.5;
// Stop at bottom or remove if eaten
if (f.position.y <= TANK.minY + 0.2 || f.eaten) {
scene.remove(f.mesh);
f.mesh.geometry.dispose();
foodPellets.splice(i, 1);
}
}
// Update Fish School
let fleeingCount = 0;
fishSchool.forEach(fish => {
fish.update(delta, time, fishSchool, effectiveThreatPos, effectiveThreatActive, foodPellets);
if (fish.isScared) fleeingCount++;
});
// Gentle light shaft drift
causticsGroup.rotation.y = Math.sin(time * 0.25) * 0.12;
// UI counts
scaredCountEl.textContent = fleeingCount;
foodCountEl.textContent = foodPellets.length;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>Reasoning trace (796 tokens)
**Crafting 3D Aquarium** I'm currently focused on building a dynamic 3D aquarium environment using Three.js. The core of my work involves implementing realistic water physics and subtle reflections for the tank edges. I'm also beginning to integrate the fish AI, ensuring they react realistically to user interaction. **Designing Underwater Elements** I'm now concentrating on populating the aquarium. My focus has shifted to creating distinct fish species, animating their movements, and implementing schooling behaviors with obstacle avoidance. I'm also beginning to integrate interactive elements like food dropping and mouse-based startling mechanics. **Detailing Aquatic Scenery** I'm now detailing the aquarium's physical elements. My efforts are concentrated on developing a textured sandy floor with subtle undulations, and placing coral formations at the corners. I'm also implementing swaying seaweed and a bubbling mechanism for visual depth.