Interactive 3D aquariumOne-line
Dots3-Note Preview (free)
by Dots StudioReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 1 s
- Cost
- $0
- Output tokens
- 10,537
- Spent thinking
- 7,751 tokens
- Speed
- 88 tok/s
- Console errors
- 0
- First token after
- 807 ms
- First visible text after
- 1 min 38 s
- Prompt tokens
- 427
- Code size
- 9.9 KB
- Finish reason
- stop
- Served by
- AtlasCloud
- Model id
dots-studio/dots-3-note-preview:free- Reasoning sent
model default- Max output
- 32,000 tokens
- Generated
- Nov 9, 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 · 276 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Aquarium</title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
color: #fff;
z-index: 100;
pointer-events: none;
text-shadow: 0 0 5px #000;
}
</style>
</head>
<body>
<div id="info">Move your mouse near the fish to make them swim away</div>
<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>
<script type="module">
import * as THREE from 'three';
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x003366);
scene.fog = new THREE.FogExp2(0x003366, 0.02);
// Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 12);
camera.lookAt(0, -1, 0);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15);
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0x80d8ff, 0.5, 100);
pointLight.position.set(0, 5, 0);
scene.add(pointLight);
// Water surface (semi-transparent)
const waterGeometry = new THREE.PlaneGeometry(200, 200);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x1e90ff,
transparent: true,
opacity: 0.25,
roughness: 0.1,
metalness: 0.8,
side: THREE.DoubleSide
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
scene.add(water);
// Sea floor
const floorGeometry = new THREE.PlaneGeometry(200, 200);
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x3a2f1f, roughness: 1 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -3;
scene.add(floor);
// Seaweed
const seaweedGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 8);
const seaweedMaterial = new THREE.MeshStandardMaterial({ color: 0x2e7d32 });
for (let i = 0; i < 15; i++) {
const seaweed = new THREE.Mesh(seaweedGeometry, seaweedMaterial);
seaweed.position.set(
(Math.random() - 0.5) * 20,
-2,
(Math.random() - 0.5) * 20
);
seaweed.scale.y = 0.5 + Math.random() * 0.5;
scene.add(seaweed);
}
// Fish factory
function createFish() {
const fishGroup = new THREE.Group();
// Body (elongated sphere)
const bodyGeometry = new THREE.SphereGeometry(0.3, 16, 16);
const bodyColor = new THREE.Color().setHSL(Math.random(), 0.7, 0.6);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: bodyColor,
roughness: 0.4,
metalness: 0.3
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.scale.set(1.5, 0.8, 0.8);
fishGroup.add(body);
// Tail (cone pointing backwards)
const tailGeometry = new THREE.ConeGeometry(0.2, 0.4, 16);
const tailMaterial = new THREE.MeshStandardMaterial({ color: bodyColor });
const tail = new THREE.Mesh(tailGeometry, tailMaterial);
tail.position.x = -0.55;
tail.rotation.z = Math.PI / 2;
fishGroup.add(tail);
// Dorsal fin
const dorsalGeometry = new THREE.PlaneGeometry(0.2, 0.3);
const dorsalMaterial = new THREE.MeshStandardMaterial({
color: bodyColor,
side: THREE.DoubleSide
});
const dorsal = new THREE.Mesh(dorsalGeometry, dorsalMaterial);
dorsal.position.set(0.1, 0.3, 0);
dorsal.rotation.z = Math.PI / 2;
fishGroup.add(dorsal);
// Pectoral fins
const pectoralGeometry = new THREE.PlaneGeometry(0.2, 0.1);
const pectoralMaterial = new THREE.MeshStandardMaterial({
color: bodyColor,
side: THREE.DoubleSide
});
const pectoralLeft = new THREE.Mesh(pectoralGeometry, pectoralMaterial);
pectoralLeft.position.set(0.2, 0, 0.2);
pectoralLeft.rotation.y = Math.PI / 4;
fishGroup.add(pectoralLeft);
const pectoralRight = new THREE.Mesh(pectoralGeometry, pectoralMaterial);
pectoralRight.position.set(0.2, 0, -0.2);
pectoralRight.rotation.y = -Math.PI / 4;
fishGroup.add(pectoralRight);
// Initial position and velocity
fishGroup.position.set(
(Math.random() - 0.5) * 15,
Math.random() * 2 - 1,
(Math.random() - 0.5) * 15
);
fishGroup.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.04,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.04
),
maxSpeed: 0.08,
fleeRadius: 2.5,
mouseWorld: new THREE.Vector3(0, 100, 0) // start far away
};
return fishGroup;
}
// Create fish
const fishArray = [];
for (let i = 0; i < 25; i++) {
const fish = createFish();
scene.add(fish);
fishArray.push(fish);
}
// Mouse interaction
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
const waterPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
let mouseWorld = new THREE.Vector3(0, 100, 0);
let mouseMoved = false;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(waterPlane, mouseWorld);
mouseMoved = true;
// Update fish's mouseWorld reference
for (const fish of fishArray) {
fish.userData.mouseWorld.copy(mouseWorld);
}
}
window.addEventListener('mousemove', onMouseMove, false);
// Animation loop
function animate() {
requestAnimationFrame(animate);
for (const fish of fishArray) {
const data = fish.userData;
const velocity = data.velocity;
const position = fish.position;
// Compute distance to mouse
const dx = position.x - mouseWorld.x;
const dy = position.y - mouseWorld.y;
const dz = position.z - mouseWorld.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Flee behavior
if (distance < data.fleeRadius) {
const fleeForce = (data.fleeRadius - distance) / data.fleeRadius;
const dir = new THREE.Vector3(dx, dy, dz).normalize();
velocity.add(dir.multiplyScalar(fleeForce * 0.15));
}
// Random wander
velocity.x += (Math.random() - 0.5) * 0.004;
velocity.y += (Math.random() - 0.5) * 0.002;
velocity.z += (Math.random() - 0.5) * 0.004;
// Limit speed
const speed = velocity.length();
if (speed > data.maxSpeed) {
velocity.multiplyScalar(data.maxSpeed / speed);
}
// Update position
position.add(velocity);
// Boundaries
if (position.x > 15) position.x = -15;
if (position.x < -15) position.x = 15;
if (position.z > 15) position.z = -15;
if (position.z < -15) position.z = 15;
if (position.y > 2) position.y = 2;
if (position.y < -2.5) position.y = -2.5;
// Rotate fish to face movement direction
if (velocity.length() > 0.001) {
const forward = velocity.clone().normalize();
const defaultForward = new THREE.Vector3(1, 0, 0);
const targetQuaternion = new THREE.Quaternion();
targetQuaternion.setFromUnitVectors(defaultForward, forward);
fish.quaternion.slerp(targetQuaternion, 0.15);
}
}
renderer.render(scene, camera);
}
animate();
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>Reasoning trace (7,751 tokens)
We need to create a 3D aquarium with Three.js where fish swim away when the mouse moves near them. Use the provided import map. Must be self-contained HTML file with inline CSS and JS. No external files except the CDN URLs.
We need to implement a scene with water, fish (3D models) that react to mouse proximity. Since we cannot load external models (no external files), we need to create fish using Three.js primitives (like spheres, cones, etc.) or maybe use a custom mesh. Could use a simple fish shape using a cone or a combination of spheres and cylinders. Or we could use a sprite with a fish texture? But we can't load textures. So we need to create fish using geometry.
We can create a fish using a group: a body (ellipsoid or sphere), a tail (cone or cylinder), fins. Color can be random.
We need to implement mouse interaction: when mouse moves, fish within a certain radius swim away. We can implement a simple repulsion force.
We need to load Three.js via import map. We'll use ES modules.
We need to make it look good: add water effect, maybe a background gradient, some light, maybe a sea floor.
We need to ensure it's responsive to viewport size 1280x800 and down to 375px.
We need to start in self-playing demo mode? The instruction says "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". But this is not a game, it's an aquarium. However the requirement says "Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction." So we need to have the aquarium already rendered with fish swimming. The fish can swim automatically. The mouse interaction is additional. So we can just have the fish swimming randomly, and when mouse moves, they flee.
We don't need a start message because it's not a game? The instruction about "Click or press a key to start" is for games. This is an interactive demo, not a game. But we can include a start message if we want, but it's not required. The requirement is that the demo must be visible on first paint. So we should just render the scene immediately.
We need to ensure no console errors.
Let's plan:
- Use import map to load three.module.js and maybe OrbitControls? But we don't need orbit controls; we can just use mouse movement to affect fish. We can also add a simple camera that looks at the scene.
- Scene, camera, renderer.
- Add water: maybe a plane with a custom shader or just a large sphere? Could use a simple water surface with a color. Or use a box with a blue color. We can add a gradient background.
- Create fish: use a function that creates a fish mesh. Use a group of objects: body (sphere), tail (cone), fins (planes or cylinders). Color them with random colors.
- Fish should swim: each fish has a position, velocity, and a target direction. They swim randomly, with some speed. They also need to avoid the mouse.
- Mouse interaction: track mouse position in normalized device coordinates, convert to world coordinates? Or just use a 3D position of the mouse? Since we have a 3D scene, we can define a plane at the water surface (y = some value) and use a raycaster to get the intersection point of the mouse with that plane. Then fish within a certain distance swim away.
- We'll update fish positions each frame.
- Add some lights: ambient light, directional light.
- Add a sea floor: maybe a plane with a different color.
- Make it look like an aquarium: maybe add some seaweed? Could add some simple cylinders with green color that sway.
- Keep it simple but effective.
Implementation details:
- Use window.innerWidth and innerHeight for camera aspect and renderer size.
- Use OrthographicCamera? Or PerspectiveCamera. Perspective is fine.
- Use a plane for water surface at y=0, with a light blue color, maybe with some transparency? But we can just use a solid color.
- Fish: create a function createFish() that returns a group. The fish will have a body (SphereGeometry with radius 0.2), a tail (ConeGeometry or CylinderGeometry), maybe fins. We'll use MeshStandardMaterial with color.
- Fish behavior: each fish has a state: swimming randomly or fleeing. We can compute a steering behavior: seek a random target, and avoid mouse.
- For simplicity, we can just give each fish a random direction and speed, and when mouse is near, we add a repulsion force.
- We'll store fish in an array.
- Use a raycaster to get mouse world position on a plane (y = 0). Update mouse position on mousemove.
- In the update loop, for each fish, compute distance to mouse, if within a threshold, apply force away.
- Also update fish position based on velocity, and rotate fish to face direction of velocity.
- Keep fish within bounds: maybe a large box.
- Add some particles? Not necessary.
- Add a fog or background gradient.
Let's write the code.
We need to use import map. We'll include the script type="module" that imports THREE from the map.
We'll also need to import OrbitControls? Not needed.
We'll create the scene.
Let's outline the HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>3D Aquarium</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<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>
<script type="module">
import * as THREE from 'three';
// main code
</script>
</body>
</html>
Now write the main code.
We'll create a scene, camera, renderer.
Add lights.
Create water surface: a plane with a light blue color, maybe with some transparency? But we can just use a MeshBasicMaterial with color 0x4fc3f7 and opacity 0.5? But we want it to look like water. Could use a custom shader, but for simplicity, we can use a MeshStandardMaterial with a blue color and low roughness, and add a specular highlight? Or just a simple color.
Better: create a large box for the water? Or a plane with a gradient? We can use a cube camera for reflections? Too complex.
We can create a simple water effect using a plane with a vertex shader that animates waves. But that might be overkill. We can just have a static blue plane.
But the prompt says "3D aquarium", so we need some depth. We can add a background color gradient (from top to bottom) using a large box or just set the scene background to a gradient. We can use a canvas gradient as scene background? Or use a cube texture? We can create a large sphere with a gradient texture? Simpler: use a fog with a color.
We can set scene.background to a color, and add a fog. Or use a vertical gradient by having a large box with a gradient material. But we can't load textures, so we need to create a gradient using a canvas? We can create a canvas with a gradient and use it as a texture. That's allowed because it's inline.
But maybe simpler: just set scene.background = 0x003366 (dark blue) and add some fog. Or use a clear color.
We'll add a sea floor: a plane at y = -2 with a brownish color.
Add some seaweed: cylinders with green color that sway.
But focus on fish.
Let's write the code step by step.
First, set up the scene, camera, renderer.
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x003366);
scene.fog = new THREE.FogExp2(0x003366, 0.02);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
Add lights:
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15);
scene.add(directionalLight);
Maybe add a point light for underwater effect? Or just use these.
Create water surface: a large plane at y=0, with a light blue color, and some shininess.
const waterGeometry = new THREE.PlaneGeometry(200, 200);
const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x1e90ff, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.8 });
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
scene.add(water);
But we need to see through the water? If it's opaque, we won't see the fish below. So we should make it semi-transparent. But fish are swimming below the surface? We can have fish swimming at various depths, some below, some above? Aquarium: fish swim in water, so they should be below the surface. We can set the water surface at y=2, and fish swim between y=-2 and y=1.5. The camera looks from above? Or from side? For an aquarium, we usually view from the front. So we can set the camera at an angle looking slightly down. The water surface can be a plane at y=2, and we can see the fish below it. But if the water is semi-transparent, we can see them.
Better: use a box-shaped aquarium. We can create a large box with transparent sides and a water surface on top. But that's complex.
Simpler: just have a blue background and fish swimming in a 3D space. The "water" can be implied by the blue color and some particles. We can add some floating particles.
We can create a water surface that is a plane with a subtle wave animation using a shader. But we can't use external shaders, we can write inline shader material.
But to keep it simple, we can just use a semi-transparent blue plane at y=0, and fish swim below it. The camera is positioned above looking down at an angle, so we see the fish through the water.
Let's set camera position (0, 8, 12) looking at (0, -1, 0). That gives a view of the aquarium from above-front.
We'll add a floor (sea bottom) at y = -3, with a darker color.
Add some seaweed: a few cylinders with green color, maybe with a sinusoidal deformation? We can just use a cylinder geometry and scale it, and animate its rotation or vertices? We can use a simple mesh and make it sway by rotating it slightly.
But let's focus on fish.
Create fish geometry:
We'll create a function that returns a group representing a fish.
- Body: an ellipsoid? We can use a SphereGeometry with radius 0.3, but stretch it along x-axis by scaling. Or use a custom shape? We can use a CylinderGeometry for the body? Actually, a fish body is typically elongated. We can use a SphereGeometry and scale it: scale.x = 1.5, scale.y = 0.8, scale.z = 0.8. Then we can use a MeshStandardMaterial with a color.
- Tail: a ConeGeometry pointing backwards. We can use a CylinderGeometry tapered? ConeGeometry is good. We'll position it at the back.
- Fins: we can add two small planes or cylinders for pectoral fins, and a dorsal fin.
We'll make the fish face the direction of movement. So we need to orient the fish along its velocity vector.
We'll create a fish as a group, with the body at the origin, and the tail at negative x? We'll define the fish's forward direction as the positive x-axis. Then we can rotate the group to match the velocity direction.
So in createFish, we create a group, add body at (0,0,0), tail at (-0.4, 0, 0) pointing backwards? Actually, if forward is +x, the tail should be at the back, i.e., negative x. So we place tail at (-bodyLength/2 - tailLength/2, 0, 0). We'll use a ConeGeometry with radiusTop=0, radiusBottom=0.2, height=0.4. We'll rotate it to point along the x-axis? ConeGeometry by default points along y-axis. So we need to rotate it.
Better: we can use a CylinderGeometry with radiusTop=0, radiusBottom=0.2, height=0.4, and then rotate it.
We'll also add a dorsal fin on top (positive y), and pectoral fins on sides.
We'll give each fish a random color.
Then we'll add the fish to the scene and store its data (velocity, etc.) in an array.
Fish behavior:
Each fish has:
- position (Vector3)
- velocity (Vector3)
- acceleration (Vector3)
- maxSpeed
- maxForce
- perception radius for mouse
- flee radius
We'll update each frame:
- Compute a steering force: random wander + avoidance of mouse.
- Apply force to velocity.
- Limit velocity.
- Update position.
- Rotate fish to face velocity direction.
We can use a simple wander behavior: have a target point that moves randomly, and seek that target.
Alternatively, just give each fish a random direction and let it swim straight, and when it hits a boundary, turn around.
We'll define a bounding box: x from -10 to 10, y from -3 to 2, z from -10 to 10. Fish should stay inside.
We'll also add a mouse avoidance: if the mouse is within a certain distance (e.g., 3 units), apply a force away from the mouse.
We need to get the mouse world position. We'll use a raycaster from the camera through the mouse, and intersect with a plane at y=0 (or the water surface). But the mouse is 2D, so we need to define a target point in 3D. We can just use the intersection with the plane y=0. That gives a point on the water surface. Fish will flee from that point.
We'll set up event listeners for mousemove to update mouse coordinates.
We'll also handle touch? Not required.
Now, let's write the code.
We'll need to import THREE.
We'll create the fish geometry once and clone it for each fish? But each fish may have different color, so we can create a new mesh each time. Or we can create a prototype and clone it, then change material color. But cloning with different materials might be tricky. We'll just create a new group each time.
We'll create an array of fish objects: each object has mesh (group), velocity, etc.
We'll create, say, 20 fish.
Now, write the code.
We need to ensure it's responsive. We'll handle window resize.
We'll also add a simple post-processing? Not needed.
Let's start coding.
First, the import map and module.
We'll write the code in a single script.
We need to make sure we use `THREE` correctly.
Let's draft:
```javascript
import * as THREE from 'three';
// Scene, camera, renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x003366);
scene.fog = new THREE.FogExp2(0x003366, 0.02);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 12);
camera.lookAt(0, -1, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15);
scene.add(directionalLight);
// Add a point light for underwater effect
const pointLight = new THREE.PointLight(0x80d8ff, 0.6, 100);
pointLight.position.set(0, 5, 0);
scene.add(pointLight);
// Water surface (semi-transparent plane)
const waterGeometry = new THREE.PlaneGeometry(200, 200);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x1e90ff,
transparent: true,
opacity: 0.3,
roughness: 0.1,
metalness: 0.8
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
scene.add(water);
// Sea floor
const floorGeometry = new THREE.PlaneGeometry(200, 200);
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x3a2f1f, roughness: 1 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -3;
scene.add(floor);
// Seaweed (some simple cylinders)
const seaweedGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 8);
const seaweedMaterial = new THREE.MeshStandardMaterial({ color: 0x2e7d32 });
for (let i = 0; i < 10; i++) {
const seaweed = new THREE.Mesh(seaweedGeometry, seaweedMaterial);
seaweed.position.set(
(Math.random() - 0.5) * 20,
-2,
(Math.random() - 0.5) * 20
);
seaweed.scale.y = 0.5 + Math.random() * 0.5;
scene.add(seaweed);
}
// Fish factory
function createFish() {
const fishGroup = new THREE.Group();
// Body (ellipsoid)
const bodyGeometry = new THREE.SphereGeometry(0.3, 16, 16);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(Math.random(), 0.7, 0.6),
roughness: 0.4,
metalness: 0.3
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.scale.set(1.5, 0.8, 0.8); // elongate
fishGroup.add(body);
// Tail
const tailGeometry = new THREE.ConeGeometry(0.2, 0.4, 16);
const tailMaterial = new THREE.MeshStandardMaterial({ color: bodyMaterial.color });
const tail = new THREE.Mesh(tailGeometry, tailMaterial);
tail.position.x = -0.5; // behind the body
tail.rotation.z = Math.PI / 2; // point along x-axis
fishGroup.add(tail);
// Dorsal fin
const dorsalGeometry = new THREE.PlaneGeometry(0.2, 0.3);
const dorsalMaterial = new THREE.MeshStandardMaterial({ color: bodyMaterial.color, side: THREE.DoubleSide });
const dorsal = new THREE.Mesh(dorsalGeometry, dorsalMaterial);
dorsal.position.y = 0.3;
dorsal.position.x = 0.1;
dorsal.rotation.z = Math.PI / 2;
fishGroup.add(dorsal);
// Pectoral fins (simple planes)
const pectoralGeometry = new THREE.PlaneGeometry(0.2, 0.1);
const pectoralMaterial = new THREE.MeshStandardMaterial({ color: bodyMaterial.color, side: THREE.DoubleSide });
const pectoralLeft = new THREE.Mesh(pectoralGeometry, pectoralMaterial);
pectoralLeft.position.set(0.2, 0, 0.2);
pectoralLeft.rotation.y = Math.PI / 4;
fishGroup.add(pectoralLeft);
const pectoralRight = new THREE.Mesh(pectoralGeometry, pectoralMaterial);
pectoralRight.position.set(0.2, 0, -0.2);
pectoralRight.rotation.y = -Math.PI / 4;
fishGroup.add(pectoralRight);
// Set initial position and velocity
fishGroup.position.set(
(Math.random() - 0.5) * 15,
Math.random() * 2 - 1, // between -1 and 1
(Math.random() - 0.5) * 15
);
fishGroup.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.05
),
maxSpeed: 0.08,
fleeRadius: 2.0,
mouse: new THREE.Vector3(0, 0, 0) // will be updated
};
return fishGroup;
}
// Create fish array
const fishArray = [];
for (let i = 0; i < 25; i++) {
const fish = createFish();
scene.add(fish);
fishArray.push(fish);
}
// Mouse interaction
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
const mouseWorld = new THREE.Vector3();
const waterPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // plane at y=0
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(waterPlane, mouseWorld);
}
window.addEventListener('mousemove', onMouseMove, false);
// Also handle touch? Not required.
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update fish
for (const fish of fishArray) {
const data = fish.userData;
const velocity = data.velocity;
const position = fish.position;
// Compute distance to mouse
const dx = position.x - mouseWorld.x;
const dy = position.y - mouseWorld.y;
const dz = position.z - mouseWorld.z;
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
// Flee if within radius
if (distance < data.fleeRadius) {
const fleeForce = (data.fleeRadius - distance) / data.fleeRadius;
const dir = new THREE.Vector3(dx, dy, dz).normalize();
velocity.add(dir.multiplyScalar(fleeForce * 0.2));
}
// Add a little random noise
velocity.x += (Math.random() - 0.5) * 0.005;
velocity.y += (Math.random() - 0.5) * 0.002;
velocity.z += (Math.random() - 0.5) * 0.005;
// Limit velocity
const speed = velocity.length();
if (speed > data.maxSpeed) {
velocity.multiplyScalar(data.maxSpeed / speed);
}
// Update position
position.add(velocity);
// Boundaries
if (position.x > 15) position.x = -15;
if (position.x < -15) position.x = 15;
if (position.z > 15) position.z = -15;
if (position.z < -15) position.z = 15;
if (position.y > 2) position.y = 2;
if (position.y < -2.5) position.y = -2.5;
// Rotate fish to face direction
if (velocity.length() > 0.001) {
const targetQuaternion = new THREE.Quaternion();
const up = new THREE.Vector3(0, 1, 0);
const forward = velocity.clone().normalize();
// Make the fish's forward direction be +x
const defaultForward = new THREE.Vector3(1, 0, 0);
// We need to rotate from defaultForward to forward
// Use setFromUnitVectors
targetQuaternion.setFromUnitVectors(defaultForward, forward);
fish.quaternion.slerp(targetQuaternion, 0.2);
}
}
renderer.render(scene, camera);
}
animate();
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
```
This is a basic version. But we need to ensure the fish look like fish. The tail is a cone pointing backwards, but we rotated it by PI/2 around Z, which makes it point along the X axis? Let's check: ConeGeometry default orientation is along Y axis (pointing up). We want the tail to point along the negative X axis (since the body is at origin and tail is behind). So we need to rotate the cone so its axis is along X. We can rotate by PI/2 around Z, which maps Y to X? Actually, rotating around Z by PI/2: the Y axis becomes the X axis? Let's think: rotation around Z by +90 degrees: the point (0,1,0) goes to (-1,0,0)? Actually, standard rotation: x' = x*cos - y*sin, y' = x*sin + y*cos. For theta=PI/2: cos=0, sin=1. So (0,1,0) becomes (0*0 - 1*1, 0*1 + 1*0, 0) = (-1, 0, 0). So the cone's axis (which was Y) becomes negative X. That's what we want: the tail points backwards (negative X). So tail.position.x = -0.5, and we rotate it by PI/2 around Z, so the cone points along negative X. Good.
But the cone's base is at the top? ConeGeometry: radiusTop=0, radiusBottom=0.2, height=0.4. So the point is at the top (y=0.4) and the base at y=0? Actually, ConeGeometry is centered? Let's check: ConeGeometry(radiusTop, radiusBottom, height, radialSegments, openEnded). The cone is built from the bottom (radiusBottom) to the top (radiusTop). The height is along the Y axis. The geometry is centered? Actually, Three.js ConeGeometry is centered along the Y axis? I think it's centered: the geometry extends from -height/2 to height/2. So the point (radiusTop=0) is at y=height/2? Wait, if radiusTop=0, the top is a point. The geometry is centered, so the point is at y=height/2, and the base is at y=-height/2. So when we rotate it by PI/2 around Z, the point goes to x=-height/2? Actually, we need to be careful.
We want the tail to be behind the body: the body is at origin, the tail should extend from the back of the body (negative X) and point backwards. So we can position the tail so that its base is near the body and its tip is further back. If we use a cone with radiusTop=0, radiusBottom=0.2, height=0.4, and we want the base to be at the body and the tip to be at the back. So we need the base to be at the front (closer to body) and the tip at the back. So we can set the cone's orientation such that the base is at the origin side and the tip is away. We can position the cone at x = -0.4 (so it's behind the body) and rotate it so the tip points further negative X. If the cone's local axis is Y, and we rotate by PI/2 around Z, the Y axis becomes negative X. So the cone's height direction becomes negative X. The cone's base (which was at y=-height/2) becomes at x = +height/2? Let's compute: a point at local (0, -0.2, 0) (base) after rotation by PI/2 around Z becomes (0.2, 0, 0). So the base moves to positive X. That would put the base in front of the body, not behind. We want the base to be near the body (at x ~ -0.3) and the tip at x ~ -0.7. So we need the cone to point along positive X? Actually, if the body is at origin and extends from x=0 to x=0.5 (since body scale.x=1.5, radius=0.3, so body extends from -0.45 to 0.45? Wait, body is a sphere scaled by (1.5, 0.8, 0.8). The sphere geometry has radius 0.3, so after scaling, the half-extent in x is 0.3*1.5 = 0.45. So the body occupies x from -0.45 to 0.45. The tail should be attached at the back, i.e., at x = -0.45, and extend further back (negative X). So we want the tail's base at x ≈ -0.45, and the tip at x ≈ -0.85. So the tail should point in the negative X direction. So we need the cone's axis to be along negative X, with the base at the body (larger x) and the tip at smaller x. So we can set the tail's position at x = -0.45 + (something). Let's define the tail as a cone with height 0.4. We want the base at x = -0.45, and the tip at x = -0.85. So the tail's center should be at x = -0.65. We can set tail.position.x = -0.65. Then we need to rotate the cone so its axis is along negative X. As we saw, rotating by PI/2 around Z maps the local Y axis to negative X. So the cone's height direction (Y) becomes negative X. But the cone's base is at local y = -height/2 = -0.2, and the tip at local y = +0.2. After rotation, the base (y=-0.2) goes to x = +0.2? Wait, let's recalc: rotation by PI/2 around Z: (x,y,z) -> (-y, x, z). So a point at (0, -0.2, 0) goes to (0.2, 0, 0). So the base moves to positive X. That would put the base at x = -0.65 + 0.2 = -0.45 (good!) and the tip (0, 0.2, 0) goes to (-0.2, 0, 0), so at x = -0.65 - 0.2 = -0.85. So that works! So we need to set tail.position.x = -0.65, and rotation.z = Math.PI/2. But wait, we also need to consider that the cone's base is at local y = -0.2, which after rotation becomes x = +0.2. So the base is at x = tail.position.x + 0.2 = -0.45. That's exactly where we want it. So the code above with tail.position.x = -0.5 is close, but we might want to adjust. Let's set tail.position.x = -0.55, then base at -0.35, tip at -0.75. That's fine.
But we also need to make sure the tail's base is visible and not inside the body. We can adjust.
Also, the dorsal fin: we used a PlaneGeometry. We need to orient it properly. We set rotation.z = PI/2, which makes the plane stand up along the X axis? Actually, PlaneGeometry by default is in the XY plane. Rotating by PI/2 around Z makes it in the XZ plane? Actually, rotation around Z by PI/2: the plane's normal was (0,0,1), after rotation it becomes (0,1,0)? Let's not overthink; we can just use a simple cylinder for dorsal fin.
But the fish might look okay.
We also need to ensure the fish rotate correctly. We set the fish's forward direction as +X. So we need to rotate the fish so that its +X axis aligns with the velocity vector. We used `targetQuaternion.setFromUnitVectors(defaultForward, forward)`. That should work. But we need to make sure the fish's up direction is reasonable. We used `up = new THREE.Vector3(0, 1, 0)`. That should be fine.
We also need to update the fish's rotation smoothly. We used slerp.
Now, we need to ensure the mouse interaction works. We set up a raycaster and intersect with a plane at y=0. But the water surface is at y=0? We set water at y=0. So the mouse world position is on the water surface. Fish will flee from that point. That's good.
But we need to make sure the mouseWorld is updated correctly. We set it in onMouseMove. But if the mouse hasn't moved, mouseWorld is (0,0,0). That might cause fish to flee from the origin initially. We can initialize mouseWorld to a far away point, or only update when mouse moves. We can set a flag.
We can initialize mouseWorld to a point far away, like (0, 100, 0), so fish don't flee initially. Or we can set a boolean mouseMoved. Let's do that.
Also, we need to handle the case where the raycaster doesn't intersect the plane (if the camera looks away). But since the plane is large and always in front, it should intersect.
Now, let's refine the fish behavior. The current code adds a random noise and a flee force. But the fish might swim too fast or look unnatural. We can improve by adding a wander behavior: each fish has a target point that it seeks, and the target moves randomly. But for simplicity, the random noise might be enough.
We can also add a constraint that fish don't go too far from the center.
Now, let's consider the visual quality. We have a blue background, a semi-transparent water plane, a floor, some seaweed, and fish. We might want to add some bubbles or particles. But not necessary.
We should also add a subtle animation to the water surface? We can displace the water vertices using a shader, but that might be heavy. We can just rotate the water plane slightly? Or we can use a ShaderMaterial with a simple wave. But let's keep it simple.
We need to ensure the page looks good on small screens. We can adjust the camera position and field of view for mobile? We can just use the same camera; it will adapt because we set aspect ratio. But the fish might be too small or too large. We can adjust the camera distance based on screen size? Not necessary.
Now, let's write the final code. We'll include everything in one HTML file.
We need to make sure we use the import map correctly. The import map must be in a script tag with type="importmap". Then we have another script with type="module" that imports from 'three'.
We also need to handle the case where the module is not loaded? It should be fine.
Let's write the complete HTML.
We'll also add some CSS to ensure the canvas fills the viewport and there's no scrollbars.
We'll add a title? Not needed.
Let's produce the final answer.