Interactive 3D aquariumOne-line
Claude Haiku 5.5
by AnthropicReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 25.2 s
- Cost
- $0.0037
- Output tokens
- 7,184
- Spent thinking
- 0 tokens
- Speed
- 293.7 tok/s
- Console errors
- 0
- First token after
- 723 ms
- First visible text after
- 723 ms
- Prompt tokens
- 648
- Code size
- 13.6 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"none"}- Max output
- 32,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 420 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;
width: 100%;
height: 100%;
overflow: hidden;
background: #03131f;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
#canvas-wrap {
position: fixed;
inset: 0;
}
canvas { display: block; }
#hud {
position: fixed;
left: 16px;
bottom: 14px;
color: rgba(200, 240, 255, 0.85);
font-size: 13px;
letter-spacing: 0.02em;
pointer-events: none;
text-shadow: 0 1px 4px rgba(0,0,0,0.6);
max-width: calc(100vw - 32px);
}
#title {
position: fixed;
top: 16px;
left: 0;
right: 0;
text-align: center;
color: #d9f6ff;
font-size: 20px;
font-weight: 600;
pointer-events: none;
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
#hint {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #e8fbff;
background: rgba(0, 30, 50, 0.55);
padding: 12px 20px;
border-radius: 10px;
font-size: 16px;
pointer-events: none;
border: 1px solid rgba(160, 230, 255, 0.25);
transition: opacity 0.6s ease;
white-space: nowrap;
}
@media (max-width: 480px) {
#title { font-size: 16px; top: 10px; }
#hud { font-size: 11px; left: 10px; bottom: 8px; }
#hint { font-size: 13px; padding: 10px 14px; }
}
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<div id="title">3D Aquarium</div>
<div id="hint">Move your mouse near the fish and watch them scatter</div>
<div id="hud">Move the mouse · Fish swim away from the cursor</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';
// ---------- Renderer / scene / camera ----------
const wrap = document.getElementById('canvas-wrap');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
wrap.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x03131f);
scene.fog = new THREE.FogExp2(0x03313f, 0.035);
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 1.5, 22);
camera.lookAt(0, 0, 0);
// ---------- Lights ----------
scene.add(new THREE.HemisphereLight(0x9fe8ff, 0x0a2a3a, 0.9));
const sun = new THREE.DirectionalLight(0xbff0ff, 1.1);
sun.position.set(6, 14, 8);
scene.add(sun);
const caustic = new THREE.PointLight(0x6fd8ff, 25, 60, 1.5);
caustic.position.set(0, 10, 0);
scene.add(caustic);
// ---------- Tank: sand floor, rocks, bubbles, light rays ----------
const floorGeo = new THREE.PlaneGeometry(80, 40, 40, 20);
{
// gentle undulation on the sand
const pos = floorGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), y = pos.getY(i);
const h = Math.sin(x * 0.4) * 0.3 + Math.cos(y * 0.5) * 0.25 + Math.random() * 0.08;
pos.setZ(i, h);
}
floorGeo.computeVertexNormals();
}
const sand = new THREE.Mesh(
floorGeo,
new THREE.MeshStandardMaterial({ color: 0xc9b58a, roughness: 1 })
);
sand.rotation.x = -Math.PI / 2;
sand.position.y = -7;
scene.add(sand);
// Rocks
const rockMat = new THREE.MeshStandardMaterial({ color: 0x4a5a63, roughness: 0.95, flatShading: true });
for (let i = 0; i < 14; i++) {
const r = 0.8 + Math.random() * 1.6;
const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(r, 0), rockMat);
const a = Math.random() * Math.PI * 2;
const d = 6 + Math.random() * 14;
rock.position.set(Math.cos(a) * d, -7 + r * 0.3, Math.sin(a) * d * 0.4 - 3);
rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
rock.scale.y = 0.6 + Math.random() * 0.4;
scene.add(rock);
}
// Seaweed
const weeds = [];
const weedMat = new THREE.MeshStandardMaterial({ color: 0x2f9e6a, roughness: 0.6, side: THREE.DoubleSide });
for (let i = 0; i < 22; i++) {
const h = 2.5 + Math.random() * 4;
const geo = new THREE.PlaneGeometry(0.35, h, 1, 8);
geo.translate(0, h / 2, 0);
const weed = new THREE.Mesh(geo, weedMat);
const a = Math.random() * Math.PI * 2;
const d = 3 + Math.random() * 16;
weed.position.set(Math.cos(a) * d, -7, Math.sin(a) * d * 0.35 - 4);
weed.rotation.y = Math.random() * Math.PI;
weed.userData.phase = Math.random() * 10;
weed.userData.base = geo.attributes.position.array.slice();
scene.add(weed);
weeds.push(weed);
}
// Bubbles
const bubbles = [];
const bubbleGeo = new THREE.SphereGeometry(0.12, 10, 8);
const bubbleMat = new THREE.MeshStandardMaterial({
color: 0xdff8ff, transparent: true, opacity: 0.45, roughness: 0.1, metalness: 0.1
});
for (let i = 0; i < 40; i++) {
const b = new THREE.Mesh(bubbleGeo, bubbleMat);
resetBubble(b, true);
scene.add(b);
bubbles.push(b);
}
function resetBubble(b, initial = false) {
b.position.set(
(Math.random() - 0.5) * 30,
initial ? -7 + Math.random() * 14 : -7.5,
(Math.random() - 0.5) * 10 - 2
);
b.userData.speed = 0.6 + Math.random() * 0.8;
b.userData.wobble = Math.random() * 10;
b.scale.setScalar(0.5 + Math.random() * 1.2);
}
// Light rays from the surface
const rays = [];
const rayMat = new THREE.MeshBasicMaterial({
color: 0xaef0ff, transparent: true, opacity: 0.06, depthWrite: false, side: THREE.DoubleSide
});
for (let i = 0; i < 8; i++) {
const ray = new THREE.Mesh(new THREE.PlaneGeometry(2.5, 26), rayMat);
ray.position.set(-14 + i * 4 + (Math.random() - 0.5) * 2, 2, -8 - Math.random() * 4);
ray.rotation.z = -0.25 + Math.random() * 0.1;
ray.userData.phase = Math.random() * 10;
scene.add(ray);
rays.push(ray);
}
// ---------- Fish ----------
function makeFish(color, accent) {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color, roughness: 0.4, metalness: 0.1 });
const finMat = new THREE.MeshStandardMaterial({ color: accent, roughness: 0.5, transparent: true, opacity: 0.85, side: THREE.DoubleSide });
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x111111 });
const body = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 14), bodyMat);
body.scale.set(1.5, 0.75, 0.55);
group.add(body);
const tail = new THREE.Mesh(new THREE.ConeGeometry(0.7, 1.1, 4), finMat);
tail.rotation.z = -Math.PI / 2;
tail.position.x = -1.7;
tail.scale.set(1, 1, 0.25);
group.add(tail);
group.userData.tail = tail;
const dorsal = new THREE.Mesh(new THREE.ConeGeometry(0.4, 0.8, 3), finMat);
dorsal.position.set(0, 0.8, 0);
dorsal.scale.z = 0.2;
group.add(dorsal);
const fin = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.7, 3), finMat);
fin.rotation.z = Math.PI / 2.5;
fin.position.set(0.1, -0.3, 0.4);
fin.scale.z = 0.2;
group.add(fin);
for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.14, 8, 6), eyeMat);
eye.position.set(0.9, 0.2, side * 0.42);
group.add(eye);
}
return group;
}
const palettes = [
[0xff7a2f, 0xffd08a], [0x3aa8ff, 0xb8e6ff], [0xffd23f, 0xfff3b0],
[0xff4d6d, 0xffb3c1], [0x9b5de5, 0xd9c2ff], [0x2ec4b6, 0xa8f0e8],
[0xf15bb5, 0xffc2e5], [0xffffff, 0xffd08a]
];
const fishes = [];
const FISH_COUNT = 26;
for (let i = 0; i < FISH_COUNT; i++) {
const [c, a] = palettes[i % palettes.length];
const f = makeFish(c, a);
const s = 0.55 + Math.random() * 0.55;
f.scale.setScalar(s);
f.position.set(
(Math.random() - 0.5) * 30,
(Math.random() - 0.5) * 12,
(Math.random() - 0.5) * 8
);
f.userData = {
vel: new THREE.Vector3((Math.random() - 0.5) * 1.5, (Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.6),
home: new THREE.Vector3((Math.random() - 0.5) * 22, (Math.random() - 0.5) * 9, (Math.random() - 0.5) * 6),
phase: Math.random() * Math.PI * 2,
size: s,
maxSpeed: 2.2 + Math.random() * 1.2,
fear: 0,
};
scene.add(f);
fishes.push(f);
}
// ---------- Mouse -> 3D point projected onto a plane at z=0 ----------
const mouseNDC = new THREE.Vector2(9999, 9999);
const mouseWorld = new THREE.Vector3(9999, 9999, 0);
const raycaster = new THREE.Raycaster();
const zPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
let mouseActive = false;
let mouseIdleTimer = 0;
function onPointer(x, y) {
mouseNDC.set((x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1);
mouseActive = true;
mouseIdleTimer = 0;
}
window.addEventListener('pointermove', e => onPointer(e.clientX, e.clientY));
window.addEventListener('pointerdown', e => onPointer(e.clientX, e.clientY));
window.addEventListener('pointerleave', () => { mouseActive = false; });
window.addEventListener('touchmove', e => {
if (e.touches[0]) onPointer(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: true });
function updateMouseWorld() {
if (!mouseActive) return;
raycaster.setFromCamera(mouseNDC, camera);
const hit = new THREE.Vector3();
if (raycaster.ray.intersectPlane(zPlane, hit)) {
mouseWorld.copy(hit);
}
}
// Ambient "demo" cursor used when the mouse is idle, so the effect is visible on first paint
const demoCursor = new THREE.Vector3();
function updateDemoCursor(t) {
demoCursor.set(Math.sin(t * 0.35) * 14, Math.sin(t * 0.5) * 4, 0);
}
// ---------- Resize ----------
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
// narrow screens: pull the camera back so the tank fits
camera.position.z = window.innerWidth < 600 ? 30 : 22;
});
if (window.innerWidth < 600) camera.position.z = 30;
// ---------- Simulation ----------
const tmp = new THREE.Vector3();
const flee = new THREE.Vector3();
const FEAR_RADIUS = 6;
const BOUNDS = { x: 15, y: 5.5, z: 3.5 };
const clock = new THREE.Clock();
const hint = document.getElementById('hint');
let hintHidden = false;
function animate() {
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
// Hide the hint once the user interacts
if (!hintHidden && mouseActive) {
hint.style.opacity = '0';
hintHidden = true;
}
mouseIdleTimer += dt;
if (mouseIdleTimer > 4) mouseActive = false;
updateMouseWorld();
const cursor = mouseActive ? mouseWorld : demoCursor;
if (!mouseActive) updateDemoCursor(t);
for (const f of fishes) {
const d = f.userData;
// Fear: distance from cursor (in the fish's own plane) drives a flee vector
tmp.copy(f.position).sub(cursor);
tmp.z *= 0.5; // cursor is effectively a 2D probe, reduce depth influence
const dist = tmp.length();
const fearTarget = THREE.MathUtils.clamp(1 - dist / FEAR_RADIUS, 0, 1);
d.fear += (fearTarget - d.fear) * Math.min(1, dt * 6);
if (d.fear > 0.01) {
flee.copy(tmp).normalize().multiplyScalar(d.fear * 14);
d.vel.addScaledVector(flee, dt);
}
// Gentle wander toward home point
tmp.copy(d.home).sub(f.position);
d.vel.addScaledVector(tmp, dt * 0.05);
// Wobbling swimming noise
d.vel.x += Math.sin(t * 0.7 + d.phase) * dt * 0.4;
d.vel.y += Math.cos(t * 0.9 + d.phase * 1.3) * dt * 0.25;
d.vel.z += Math.sin(t * 0.5 + d.phase * 0.7) * dt * 0.2;
// Speed limit (faster when scared)
const maxS = d.maxSpeed * (1 + d.fear * 1.5);
const sp = d.vel.length();
if (sp > maxS) d.vel.multiplyScalar(maxS / sp);
if (sp < 0.6) d.vel.multiplyScalar(1.02);
f.position.addScaledVector(d.vel, dt);
// Soft bounds: turn around smoothly near walls
if (Math.abs(f.position.x) > BOUNDS.x) d.vel.x -= Math.sign(f.position.x) * dt * 8;
if (f.position.y > BOUNDS.y || f.position.y < -BOUNDS.y) d.vel.y -= Math.sign(f.position.y) * dt * 6;
if (Math.abs(f.position.z) > BOUNDS.z) d.vel.z -= Math.sign(f.position.z) * dt * 4;
// Orient along velocity
if (d.vel.lengthSq() > 0.0001) {
const heading = Math.atan2(d.vel.z, d.vel.x);
const pitch = Math.atan2(d.vel.y, Math.hypot(d.vel.x, d.vel.z));
// body faces +x; rotate around y for heading, z for pitch
f.rotation.set(0, -heading, 0);
f.rotation.z = THREE.MathUtils.lerp(f.rotation.z, pitch * 0.6, 0.15);
}
// Wiggle tail
const wiggle = Math.sin(t * (8 + d.vel.length() * 2) + d.phase) * 0.35;
d.tail && (d.tail.rotation.y = wiggle);
f.children[1].rotation.y = wiggle; // tail mesh
f.children[1].rotation.z = -Math.PI / 2;
}
// Bubbles
for (const b of bubbles) {
b.position.y += b.userData.speed * dt;
b.position.x += Math.sin(t * 2 + b.userData.wobble) * dt * 0.3;
if (b.position.y > 9) resetBubble(b);
}
// Seaweed sway
for (const w of weeds) {
const arr = w.geometry.attributes.position.array;
const base = w.userData.base;
const h = w.geometry.parameters.height;
for (let i = 0; i < arr.length; i += 3) {
const yNorm = base[i + 1] / h;
arr[i] = base[i] + Math.sin(t * 1.2 + w.userData.phase + yNorm * 2) * yNorm * 0.6;
}
w.geometry.attributes.position.needsUpdate = true;
}
// Light rays shimmer
for (const r of rays) {
r.material.opacity = 0.04 + Math.sin(t * 0.6 + r.userData.phase) * 0.025;
}
// Caustic light drifting
caustic.position.x = Math.sin(t * 0.3) * 6;
caustic.intensity = 22 + Math.sin(t * 1.3) * 5;
// Subtle camera drift for life
camera.position.x = Math.sin(t * 0.1) * 1.2;
camera.position.y = 1.5 + Math.sin(t * 0.13) * 0.4;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>