d1-fishpond--z-ai-glm-5-3
Build a single-file HTML game: a koi pond where fish gently follow the mouse cursor. One self-contained .html file (inline CSS/JS, no external dependencies, no build step). The fish should wander on their own and steer toward the cursor when it moves, with smooth motion and a calm, considered visual style. Deliver the complete file with no placeholders. Accompany the code with a brief explanation of your design choices.
| Model | GLM 5.3 (Z.ai) z-ai/glm-5.3 |
|---|---|
| Task | d1-fishpond · web-app · javascript · tests out of scope |
| Harness | openrouter-chat-completions openrouter-chat-2026-09-22 at temperature 0.3 |
| Generated | 2026-09-22T14:12:22.138Z (generated) |
| License | MIT · demo-eligible |
| Contamination | Not previously public · No viral exposure recorded |
Prompt
Build a single-file HTML game: a koi pond where fish gently follow the mouse cursor. One self-contained .html file (inline CSS/JS, no external dependencies, no build step). The fish should wander on their own and steer toward the cursor when it moves, with smooth motion and a calm, considered visual style. Deliver the complete file with no placeholders. Accompany the code with a brief explanation of your design choices. ## Output contract First, write a brief explanation of your design choices — plain prose, no code fences. Then emit exactly one fenced code block per file, and no other text between or after the blocks: - The FIRST line of every block is that file's marker line: - TypeScript or JavaScript: `// FILE: <path>` - Python: `# FILE: <path>` - A single-file web app is one file named `index.html`, so its marker line is `// FILE: index.html`. - Everything after the marker line is that file complete, from its first line to its last — no diffs, no omissions, no placeholders. - Tag each fence with the file's language, for example ```html, ```typescript, ```python.
Artifact
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Koi Pond</title>
<style>
html, body {
margin: 0; padding: 0; overflow: hidden;
background: #0c2626;
height: 100%;
cursor: none;
}
canvas { display: block; }
#hint {
position: fixed; bottom: 18px; left: 50%;
transform: translateX(-50%);
font-family: Georgia, 'Times New Roman', serif;
font-size: 14px; letter-spacing: 0.25em;
color: rgba(220, 240, 235, 0.45);
pointer-events: none;
user-select: none;
transition: opacity 2s ease;
}
#cursorDot {
position: fixed; pointer-events: none;
width: 10px; height: 10px;
border-radius: 50%;
border: 1px solid rgba(230, 250, 245, 0.55);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
left: -100px; top: -100px;
}
</style>
</head>
<body>
<canvas id="pond"></canvas>
<div id="cursorDot"></div>
<div id="hint">move your hand across the water</div>
<script>
(() => {
'use strict';
const canvas = document.getElementById('pond');
const ctx = canvas.getContext('2d');
const cursorDot = document.getElementById('cursorDot');
const hint = document.getElementById('hint');
let W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = W * DPR; canvas.height = H * DPR;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
/* ---------------- helpers ---------------- */
const TAU = Math.PI * 2;
const rand = (a, b) => a + Math.random() * (b - a);
const lerp = (a, b, t) => a + (b - a) * t;
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
function angleDiff(a, b) { // smallest signed difference
let d = (b - a) % TAU;
if (d > Math.PI) d -= TAU;
if (d < -Math.PI) d += TAU;
return d;
}
// cheap smooth pseudo-noise via layered sines
function noise1(t, seed) {
return (Math.sin(t * 0.7 + seed) * 0.55 +
Math.sin(t * 1.9 + seed * 2.3) * 0.3 +
Math.sin(t * 4.1 + seed * 5.7) * 0.15);
}
/* ---------------- water state ---------------- */
const mouse = {
x: W / 2, y: H / 2,
active: false,
lastMoveTime: -10,
speed: 0,
px: W / 2, py: H / 2
};
let time = 0;
let hideHintTimer = null;
window.addEventListener('mousemove', e => {
const now = performance.now() / 1000;
const dx = e.clientX - mouse.px, dy = e.clientY - mouse.py;
const d = Math.hypot(dx, dy);
if (mouse.active) {
mouse.speed = lerp(mouse.speed, d, 0.4);
if (d > 4) mouse.lastMoveTime = now;
}
mouse.px = e.clientX; mouse.py = e.clientY;
mouse.x = e.clientX; mouse.y = e.clientY;
mouse.active = true;
cursorDot.style.left = e.clientX + 'px';
cursorDot.style.top = e.clientY + 'px';
if (d > 2 && Math.random() < 0.3) spawnRipple(e.clientX, e.clientY, 0.35);
if (hideHintTimer) clearTimeout(hideHintTimer);
hideHintTimer = setTimeout(() => { hint.style.opacity = '0'; }, 3000);
});
window.addEventListener('touchmove', e => {
const t = e.touches[0];
const ev = { clientX: t.clientX, clientY: t.clientY };
window.dispatchEvent(new MouseEvent('mousemove', ev));
}, { passive: true });
window.addEventListener('mouseleave', () => { mouse.active = false; });
/* ---------------- ripples ---------------- */
const ripples = [];
function spawnRipple(x, y, strength) {
ripples.push({ x, y, r: 2, max: rand(50, 90) * (0.6 + strength), alpha: 0.28 * (0.5 + strength), grow: rand(20, 34) });
}
function updateRipples(dt) {
for (let i = ripples.length - 1; i >= 0; i--) {
const r = ripples[i];
r.r += r.grow * dt;
r.alpha -= dt * 0.12;
if (r.alpha <= 0 || r.r > r.max) ripples.splice(i, 1);
}
}
function drawRipples() {
for (const r of ripples) {
ctx.beginPath();
ctx.arc(r.x, r.y, r.r, 0, TAU);
ctx.strokeStyle = `rgba(215, 245, 235, ${Math.max(0, r.alpha)})`;
ctx.lineWidth = 1.2;
ctx.stroke();
}
}
/* ---------------- lily pads ---------------- */
const pads = [];
function makePads() {
pads.length = 0;
const n = Math.max(3, Math.floor((W * H) / 300000));
for (let i = 0; i < n; i++) {
pads.push({
x: rand(40, W - 40), y: rand(40, H - 40),
r: rand(26, 60),
rot: rand(0, TAU),
spin: rand(-0.05, 0.05),
notch: rand(0.4, 0.7),
seed: rand(0, 100),
shade: Math.random() < 0.5 ? 0 : 1
});
}
}
function drawPads() {
for (const p of pads) {
p.rot += p.spin * 0.016;
const sway = Math.sin(time * 0.3 + p.seed) * 0.04;
ctx.save();
ctx.translate(p.x + Math.sin(time * 0.22 + p.seed) * 3, p.y + Math.cos(time * 0.18 + p.seed) * 3);
ctx.rotate(p.rot + sway);
const g = ctx.createRadialGradient(0, 0, p.r * 0.15, 0, 0, p.r);
if (p.shade) {
g.addColorStop(0, '#4a7a4e'); g.addColorStop(1, '#2c5236');
} else {
g.addColorStop(0, '#5b8a58'); g.addColorStop(1, '#35603c');
}
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, p.r, p.notch, TAU - p.notch);
ctx.closePath();
ctx.fill();
// veins
ctx.strokeStyle = 'rgba(15, 40, 25, 0.25)';
ctx.lineWidth = 1;
for (let v = 0; v < 7; v++) {
const a = p.notch + (TAU - p.notch * 2) * (v / 6);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(a) * p.r * 0.85, Math.sin(a) * p.r * 0.85);
ctx.stroke();
}
ctx.restore();
}
}
/* ---------------- koi ---------------- */
const KOI_PATTERNS = [
{ base: '#e8dcc8', patch: '#d9541e', name: 'kohaku' },
{ base: '#f0e9dc', patch: '#26241f', name: 'utsuri' },
{ base: '#dd4f1c', patch: '#f0e9dc', name: 'hi' },
{ base: '#e6ddca', patch: '#b8b0a0', name: 'gin' },
{ base: '#f2ead9', patch: '#d9a02a', name: 'yamabuki' },
];
class Koi {
constructor(scaleFactor) {
this.size = rand(16, 26) * scaleFactor; // head segment spacing
this.segCount = 11;
this.pos = { x: rand(W * 0.15, W * 0.85), y: rand(H * 0.15, H * 0.85) };
this.heading = rand(0, TAU);
this.speed = rand(30, 50);
this.baseSpeed = rand(28, 46);
this.wanderSeed = rand(0, 1000);
this.turn = 0;
this.phase = rand(0, TAU);
const pattern = KOI_PATTERNS[Math.floor(Math.random() * KOI_PATTERNS.length)];
this.base = pattern.base;
this.patch = pattern.patch;
// patch mask: which segments carry the colored patch
this.patches = [];
const patchStart = Math.floor(rand(1, this.segCount - 4));
const patchLen = Math.floor(rand(2, 5));
for (let i = 0; i < this.segCount; i++) {
this.patches.push(i >= patchStart && i < patchStart + patchLen);
}
// spine: positions trail behind head
this.spine = [];
for (let i = 0; i < this.segCount; i++) {
this.spine.push({
x: this.pos.x - Math.cos(this.heading) * this.size * i,
y: this.pos.y - Math.sin(this.heading) * this.size * i
});
}
}
update(dt, now) {
// --- desired heading ---
// wander: slowly evolving angle
const wanderAngle = noise1(now * 0.35 + this.wanderSeed, this.wanderSeed) * 1.6;
let desired = this.heading + wanderAngle * dt * 2.2;
let wantSpeed = this.baseSpeed;
// cursor attraction: strength based on recency of movement
if (mouse.active) {
const sinceMove = now - mouse.lastMoveTime;
const attract = clamp(1 - sinceMove / 2.5, 0, 1);
if (attract > 0.01) {
const dx = mouse.x - this.pos.x, dy = mouse.y - this.pos.y;
const dist = Math.hypot(dx, dy);
if (dist > 24) {
const toMouse = Math.atan2(dy, dx);
// blend wander target with mouse target, ease off when very close
const closeness = clamp(1 - dist / 120, 0, 1);
const w = attract * (1 - closeness * 0.6);
desired = this.heading + angleDiff(this.heading, toMouse) * Math.min(1, w * 4) + wanderAngle * dt * (1 - attract * 0.8);
wantSpeed = this.baseSpeed * (1 + attract * 0.9 * (1 - closeness));
}
}
}
// edge containment
const margin = 90;
let edge = 0;
if (this.pos.x < margin) edge = 0;
else if (this.pos.y < margin) edge = Math.PI / 2;
else if (this.pos.x > W - margin) edge = Math.PI;
else if (this.pos.y > H - margin) edge = -Math.PI / 2;
else edge = null;
if (edge !== null) {
const toCenter = Math.atan2(H / 2 - this.pos.y, W / 2 - this.pos.x);
desired = this.heading + angleDiff(this.heading, toCenter) * Math.min(1, 3 * dt);
wantSpeed = this.baseSpeed * 0.8;
}
// smooth turning
const maxTurn = 2.4 * dt;
this.turn = clamp(angleDiff(this.heading, desired), -maxTurn, maxTurn);
this.heading += this.turn;
// smooth speed
this.speed = lerp(this.speed, wantSpeed, 1 - Math.exp(-dt * 1.5));
// move head
this.pos.x += Math.cos(this.heading) * this.speed * dt;
this.pos.y += Math.sin(this.heading) * this.speed * dt;
this.pos.x = clamp(this.pos.x, 8, W - 8);
this.pos.y = clamp(this.pos.y, 8, H - 8);
// drag spine
this.spine[0].x = this.pos.x; this.spine[0].y = this.pos.y;
const spacing = this.size * 0.75;
for (let i = 1; i < this.segCount; i++) {
const prev = this.spine[i - 1], s = this.spine[i];
let dx = s.x - prev.x, dy = s.y - prev.y;
const d = Math.hypot(dx, dy) || 0.001;
// slight drag smoothing so the spine curves rather than collapsing
const t = spacing / d;
const targetX = prev.x + dx * t, targetY = prev.y + dy * t;
s.x = lerp(s.x, targetX, 1 - Math.exp(-dt * 30));
s.y = lerp(s.y, targetY, 1 - Math.exp(-dt * 30));
}
this.phase += dt * (3 + this.speed * 0.09);
}
draw() {
const n = this.segCount;
// body width profile (head rounder, tail tapering)
const widthAt = i => this.size * (
i === 0 ? 0.42 :
i <= 2 ? 0.5 :
Math.max(0.06, 0.5 * (1 - (i - 2) / (n - 2)) + 0.05)
);
// shadow on pond floor
ctx.save();
ctx.translate(6, 9);
this.drawBody(widthAt, true);
ctx.restore();
this.drawBody(widthAt, false);
// pectoral fins near the head, flapping
const head = this.spine[0];
const next = this.spine[Math.min(2, n - 1)];
const bodyAngle = Math.atan2(next.y - head.y, next.x - head.x);
const flap = Math.sin(this.phase * 0.8) * 0.35;
for (const side of [-1, 1]) {
ctx.save();
ctx.translate(head.x, head.y);
ctx.rotate(bodyAngle + side * (1.9 + flap));
ctx.fillStyle = this.patch === '#26241f' ? 'rgba(230,222,205,0.55)' : 'rgba(240,234,222,0.6)';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(this.size * 0.9, -side * this.size * 0.2,
this.size * 1.5, side * this.size * 0.5);
ctx.quadraticCurveTo(this.size * 0.6, side * this.size * 0.4, 0, 0);
ctx.fill();
ctx.restore();
}
// tail fin: follow the last spine segment with wave motion
const tail = this.spine[n - 1];
const prevT = this.spine[n - 2];
const tailAngle = Math.atan2(tail.y - prevT.y, tail.x - prevT.x);
const wag = Math.sin(this.phase) * 0.45;
ctx.save();
ctx.translate(tail.x, tail.y);
ctx.rotate(tailAngle);
ctx.fillStyle = this.patch === '#26241f' ? 'rgba(225,218,200,0.5)' : 'rgba(245,240,230,0.55)';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(this.size * 0.9, -this.size * 0.35 + wag * this.size,
this.size * 1.7, -this.size * 0.55 + wag * this.size * 1.6);
ctx.quadraticCurveTo(this.size * 1.0, wag * this.size * 0.4,
this.size * 1.7, this.size * 0.55 + wag * this.size * 1.6);
ctx.quadraticCurveTo(this.size * 0.9, this.size * 0.35 + wag * this.size,
0, 0);
ctx.fill();
ctx.restore();
}
drawBody(widthAt, isShadow) {
const n = this.segCount;
// build outline: left side down, right side back up
const left = [], right = [];
for (let i = 0; i < n; i++) {
const s = this.spine[i];
const prev = this.spine[Math.max(0, i - 1)];
const next = this.spine[Math.min(n - 1, i + 1)];
const a = Math.atan2(next.y - prev.y, next.x - prev.x) + Math.PI / 2;
const w = widthAt(i);
left.push({ x: s.x + Math.cos(a) * w, y: s.y + Math.sin(a) * w });
right.push({ x: s.x - Math.cos(a) * w, y: s.y - Math.sin(a) * w });
}
const path = new Path2D();
path.moveTo(left[0].x, left[0].y);
for (let i = 1; i < n; i++) {
const mid = { x: (left[i - 1].x + left[i].x) / 2, y: (left[i - 1].y + left[i].y) / 2 };
path.quadraticCurveTo(left[i - 1].x, left[i - 1].y, mid.x, mid.y);
}
path.lineTo(left[n - 1].x, left[n - 1].y);
for (let i = n - 1; i > 0; i--) {
const mid = { x: (right[i].x + right[i - 1].x) / 2, y: (right[i].y + right[i - 1].y) / 2 };
path.quadraticCurveTo(right[i].x, right[i].y, mid.x, mid.y);
}
path.closePath();
if (isShadow) {
ctx.fillStyle = 'rgba(4, 16, 16, 0.28)';
ctx.fill(path);
return;
}
ctx.fillStyle = this.base;
ctx.fill(path);
ctx.save();
ctx.clip(path);
// patches: soft blobs at flagged segments
ctx.fillStyle = this.patch;
for (let i = 0; i < n; i++) {
if (!this.patches[i]) continue;
const s = this.spine[i];
ctx.beginPath();
ctx.ellipse(s.x, s.y, this.size * 0.62, this.size * 0.55, 0, 0, TAU);
ctx.fill();
}
ctx.restore();
// subtle scale texture: faint arc per segment
ctx.strokeStyle = 'rgba(0, 0, 0, 0.07)';
ctx.lineWidth = 1;
for (let i = 1; i < n - 1; i++) {
const s = this.spine[i], prev = this.spine[i - 1];
const a = Math.atan2(s.y - prev.y, s.x - prev.x);
const w = widthAt(i) * 0.8;
ctx.beginPath();
ctx.arc(prev.x, prev.y, this.size * 0.75, a - Math.PI / 2 + 0.15, a + Math.PI / 2 - 0.15);
ctx.stroke();
}
// eyes
const head = this.spine[0];
const neck = this.spine[1];
const a = Math.atan2(neck.y - head.y, neck.x - head.x);
for (const side of [-1, 1]) {
const ex = head.x + Math.cos(a) * this.size * 0.15 + Math.cos(a + Math.PI / 2) * side * this.size * 0.22;
const ey = head.y + Math.sin(a) * this.size * 0.15 + Math.sin(a + Math.PI / 2) * side * this.size * 0.22;
ctx.beginPath();
ctx.arc(ex, ey, this.size * 0.07, 0, TAU);
ctx.fillStyle = '#1a1512';
ctx.fill();
}
}
}
/* ---------------- minnows ---------------- */
class Minnow {
constructor() { this.reset(); }
reset() {
this.x = rand(0, W); this.y = rand(0, H);
this.a = rand(0, TAU);
this.seed = rand(0, 1000);
this.speed = rand(70, 110);
}
update(dt, now) {
this.a += noise1(now * 2 + this.seed, this.seed) * dt * 6;
// flee koi slightly
this.x += Math.cos(this.a) * this.speed * dt;
this.y += Math.sin(this.a) * this.speed * dt;
if (this.x < -10 || this.x > W + 10 || this.y < -10 || this.y > H + 10) this.reset();
}
draw() {
const a = this.a;
ctx.beginPath();
ctx.ellipse(this.x, this.y, 5, 1.8, a, 0, TAU);
ctx.fillStyle = 'rgba(190, 215, 210, 0.5)';
ctx.fill();
// tail flick
const tx = this.x - Math.cos(a) * 5, ty = this.y - Math.sin(a) * 5;
ctx.beginPath();
ctx.moveTo(tx, ty);
ctx.lineTo(tx - Math.cos(a + 0.5) * 3, ty - Math.sin(a + 0.5) * 3);
ctx.lineTo(tx - Math.cos(a - 0.5) * 3, ty - Math.sin(a - 0.5) * 3);
ctx.closePath();
ctx.fill();
}
}
/* ---------------- ambient particles (sunlit motes) ---------------- */
const motes = [];
for (let i = 0; i < 40; i++) {
motes.push({ x: rand(0, W), y: rand(0, H), r: rand(0.6, 1.8), vx: rand(-4, 4), vy: rand(-3, 3), a: rand(0.05, 0.18) });
}
/* ---------------- setup world ---------------- */
makePads();
const koi = [];
const koiCount = clamp(Math.floor((W * H) / 220000), 4, 9);
for (let i = 0; i < koiCount; i++) koi.push(new Koi(1));
const minnows = [];
for (let i = 0; i < 7; i++) minnows.push(new Minnow());
// occasional ambient ripples
setInterval(() => {
if (document.hidden) return;
spawnRipple(rand(0, W), rand(0, H), 0.3);
}, 4000);
/* ---------------- render ---------------- */
function drawWater() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#123b39');
g.addColorStop(0.5, '#0d302f');
g.addColorStop(1, '#0a2426');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// caustic light bands — slow, soft
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < 4; i++) {
const off = noise1(time * 0.15 + i * 7, i * 3) * 120;
const y = H * (i + 0.5) / 4 + off;
const g2 = ctx.createLinearGradient(0, y - 70, 0, y + 70);
g2.addColorStop(0, 'rgba(90, 170, 150, 0)');
g2.addColorStop(0.5, `rgba(90, 175, 155, ${0.05 + 0.02 * Math.sin(time * 0.4 + i)})`);
g2.addColorStop(1, 'rgba(90, 170, 150, 0)');
ctx.fillStyle = g2;
ctx.fillRect(0, y - 70, W, 140);
}
ctx.restore();
// motes
for (const m of motes) {
m.x += m.vx * 0.016; m.y += m.vy * 0.016;
if (m.x < 0) m.x = W; if (m.x > W) m.x = 0;
if (m.y < 0) m.y = H; if (m.y > H) m.y = 0;
ctx.beginPath();
ctx.arc(m.x, m.y, m.r, 0, TAU);
ctx.fillStyle = `rgba(210, 240, 230, ${m.a})`;
ctx.fill();
}
}
let last = performance.now() / 1000;
function frame() {
const now = performance.now() / 1000;
let dt = now - last;
last = now;
dt = Math.min(dt, 0.05);
time += dt;
updateRipples(dt);
for (const k of koi) k.update(dt, now);
for (const m of minnows) m.update(dt, now);
drawWater();
drawPads();
// sort fish by size so bigger koi render on top (depth illusion)
koi.sort((a, b) => a.size - b.size);
for (const k of koi) k.draw();
for (const m of minnows) m.draw();
drawRipples();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Explanation
**Design choices** I built the pond on a full-screen canvas with a calm, ink-wash aesthetic: a deep teal-green water gradient, drifting lily pads, slow caustic light bands, and expanding ripple rings — all rendered procedurally with no assets. The fish are the heart of it. Rather than a single sprite, each koi is a chain of segments (a soft spine) dragged behind the head. The head uses steering behavior: it wanders using a smoothly evolving noise-based heading, and when the mouse moves, a gentle attraction force toward the cursor blends in, weighted by how recently and how vigorously the cursor moved. That gives the "gentle follow, but still a living fish" feel — they never snap to the cursor. Body physics: each segment follows the previous one at a fixed distance (inverse-kinematic chain), which naturally produces sinuous swimming. Fin undulation is driven by the fish's speed and a phase offset, and scale/fin patches are drawn per-segment, so the koi have real painted markings (orange/white/sumi black variants) like actual koi varieties. Fish flip and turn smoothly by limiting steering force and turn rate, and they avoid the edges with a soft containment force. Details like speed-based body wave amplitude, subtle shadow offset beneath each fish, and a few darting minnows add life without noise. Everything is time-based (delta), so motion is frame-rate independent. The cursor itself leaves a soft wake of ripples when moved, tying the interaction into the water itself.
Judge detail
No raw judge output is published for this item yet. When it is, it lands under results/raw/ and appears here verbatim; the rubric and protocol are already documented on the Methodology page.