d1-fishpond--moonshotai-kimi-k3
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 | Kimi K3 (Moonshot) moonshotai/kimi-k3 |
|---|---|
| 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:16:25.065Z (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, maximum-scale=1, user-scalable=no">
<title>Koi Pond</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #071e24; }
canvas { position: fixed; inset: 0; display: block; touch-action: none; }
#hint {
position: fixed; left: 50%; bottom: 7%; transform: translateX(-50%);
color: rgba(214, 236, 230, 0.6);
font: italic 15px/1.4 Georgia, 'Times New Roman', serif;
letter-spacing: 0.28em; white-space: nowrap;
pointer-events: none; opacity: 1; transition: opacity 3s ease;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
}
#hint.gone { opacity: 0; }
</style>
</head>
<body>
<canvas id="pond"></canvas>
<div id="hint">drift your cursor across the water</div>
<script>
'use strict';
const TAU = Math.PI * 2;
const canvas = document.getElementById('pond');
const ctx = canvas.getContext('2d');
const hint = document.getElementById('hint');
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const lerp = (a, b, t) => a + (b - a) * t;
function angDiff(a, b) {
let d = (b - a) % TAU;
if (d > Math.PI) d -= TAU;
if (d < -Math.PI) d += TAU;
return d;
}
// ---------- state ----------
let W = 0, H = 0;
const pointer = { x: window.innerWidth / 2, y: window.innerHeight / 2, lastMove: -100 };
let excitement = 0;
let rippleAcc = 0, lastPx = pointer.x, lastPy = pointer.y;
let ambientT = 2.5;
const ripples = [];
const caustics = [];
const motes = [];
let pads = [];
const fishes = [];
// ---------- layout ----------
function layoutPads() {
const u = Math.min(W, H);
pads = [
{ fx: 0.14, fy: 0.32, r: clamp(u * 0.075, 26, 80), rot: rand(0, TAU), seed: rand(0, TAU), lotus: true },
{ fx: 0.86, fy: 0.68, r: clamp(u * 0.090, 30, 90), rot: rand(0, TAU), seed: rand(0, TAU), lotus: false },
{ fx: 0.68, fy: 0.14, r: clamp(u * 0.055, 22, 60), rot: rand(0, TAU), seed: rand(0, TAU), lotus: false }
];
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
layoutPads();
for (const f of fishes) {
f.x = clamp(f.x, 40, W - 40);
f.y = clamp(f.y, 40, H - 40);
}
}
window.addEventListener('resize', resize);
// ---------- koi varieties ----------
function makeVariety() {
const white = '#f0e8d7', red = '#cd4f2b', black = '#22282d';
const gold = '#d9ab4e', orange = '#cf6f2e';
const P = [];
const rp = (x, y, r, c) => P.push({ x, y, r, c });
const pick = Math.random();
let base, fin;
if (pick < 0.24) { // kohaku: white, red patches
base = white;
const n = 2 + (Math.random() * 2 | 0);
for (let i = 0; i < n; i++) rp(rand(-0.30, 0.26), rand(-0.20, 0.20), rand(0.09, 0.19), red);
} else if (pick < 0.34) { // tancho: single crown spot
base = white;
rp(0.33, 0, 0.085, red);
} else if (pick < 0.50) { // ogon: solid gold
base = gold;
} else if (pick < 0.62) { // shiro utsuri: white on black
base = white;
const n = 2 + (Math.random() * 2 | 0);
for (let i = 0; i < n; i++) rp(rand(-0.32, 0.26), rand(-0.20, 0.20), rand(0.09, 0.18), black);
} else if (pick < 0.76) { // calico
base = white;
rp(rand(-0.30, 0.20), rand(-0.20, 0.20), rand(0.10, 0.16), black);
rp(rand(-0.10, 0.28), rand(-0.20, 0.20), rand(0.08, 0.15), red);
rp(rand(-0.34, -0.05), rand(-0.20, 0.20), rand(0.07, 0.13), Math.random() < 0.5 ? black : red);
} else if (pick < 0.88) { // hi: muted orange
base = orange;
rp(rand(-0.20, 0.22), rand(-0.15, 0.15), rand(0.10, 0.16), '#b35521');
} else { // kuro: shadow fish
base = '#262c31';
}
if (base === gold) fin = 'rgba(240,215,150,0.50)';
else if (base === orange) fin = 'rgba(235,160,90,0.50)';
else if (base === '#262c31') fin = 'rgba(190,200,205,0.28)';
else fin = 'rgba(255,250,240,0.45)';
return { base, patches: P, fin };
}
function spawnFish() {
const len = rand(44, 74);
fishes.push(Object.assign({
x: rand(W * 0.15, W * 0.85), y: rand(H * 0.15, H * 0.85),
vx: rand(-20, 20), vy: rand(-20, 20), heading: rand(0, TAU),
len, wid: len * rand(0.26, 0.32),
cruise: rand(22, 38), maxSpeed: rand(85, 135), maxForce: rand(110, 170),
orbit: rand(20, 105), sociability: rand(0.5, 1),
swirlDir: Math.random() < 0.5 ? -1 : 1,
seed: rand(0, 100), phase: rand(0, TAU), bendNow: 0,
rippleT: rand(1, 4)
}, makeVariety()));
}
// ---------- ripples ----------
function addRipple(x, y, s) {
ripples.push({ x, y, r: 3, max: 50 + s * 90, speed: 26 + s * 40, a: 0.28 * s + 0.08, w: 1 + s * 1.2 });
if (ripples.length > 60) ripples.shift();
}
// ---------- update ----------
function updateFish(f, dt, t, follow) {
// wander: smooth pseudo flow field
const wa = Math.sin(f.x * 0.0035 + t * 0.25 + f.seed)
+ Math.cos(f.y * 0.0028 - t * 0.18 + f.seed * 1.7);
let dvx = Math.cos(wa * 1.6) * f.cruise;
let dvy = Math.sin(wa * 1.6) * f.cruise;
// seek the cursor, blended by personality and pointer freshness
const interest = follow * f.sociability;
if (interest > 0.01) {
const dx = pointer.x - f.x, dy = pointer.y - f.y;
const d = Math.hypot(dx, dy) || 1;
const ux = dx / d, uy = dy / d;
const txp = pointer.x - ux * f.orbit, typ = pointer.y - uy * f.orbit;
const tdx = txp - f.x, tdy = typ - f.y;
const td = Math.hypot(tdx, tdy) || 1;
const sp = f.maxSpeed * clamp(td / 130, 0.12, 1) * (1 + excitement * 0.7);
let svx = tdx / td * sp, svy = tdy / td * sp;
if (d < f.orbit * 1.4) { // swirl around the pointer
const sw = 0.6 * f.maxSpeed * (1 - d / (f.orbit * 1.4));
svx += -uy * sw * f.swirlDir;
svy += ux * sw * f.swirlDir;
}
dvx = lerp(dvx, svx, interest);
dvy = lerp(dvy, svy, interest);
}
// steering toward desired velocity, force-capped
let ax = dvx - f.vx, ay = dvy - f.vy;
const am = Math.hypot(ax, ay);
if (am > f.maxForce) { ax = ax / am * f.maxForce; ay = ay / am * f.maxForce; }
// soft walls
const m = 80;
if (f.x < m) ax += (m - f.x) / m * 260;
if (f.x > W - m) ax -= (f.x - (W - m)) / m * 260;
if (f.y < m) ay += (m - f.y) / m * 260;
if (f.y > H - m) ay -= (f.y - (H - m)) / m * 260;
// gentle separation
for (const o of fishes) {
if (o === f) continue;
const dx = f.x - o.x, dy = f.y - o.y;
const rr = (f.len + o.len) * 0.45;
const d2 = dx * dx + dy * dy;
if (d2 < rr * rr && d2 > 0.01) {
const d = Math.sqrt(d2);
ax += dx / d * (rr - d) * 4;
ay += dy / d * (rr - d) * 4;
}
}
f.vx += ax * dt; f.vy += ay * dt;
const sp2 = Math.hypot(f.vx, f.vy);
const maxS = f.maxSpeed * (1 + excitement * 0.7);
if (sp2 > maxS) { f.vx = f.vx / sp2 * maxS; f.vy = f.vy / sp2 * maxS; }
f.x = clamp(f.x + f.vx * dt, -30, W + 30);
f.y = clamp(f.y + f.vy * dt, -30, H + 30);
if (sp2 > 4) {
const th = Math.atan2(f.vy, f.vx);
f.heading += angDiff(f.heading, th) * Math.min(1, dt * 5);
}
// tail beat and body bend scale with speed
f.phase += dt * (2.2 + sp2 * 0.14);
f.bendNow = Math.sin(f.phase) * clamp(sp2 / 70, 0.25, 1.3) * f.len * 0.06;
// hard-working tails occasionally dimple the surface
f.rippleT -= dt;
if (f.rippleT <= 0) {
if (sp2 > 75) addRipple(f.x - Math.cos(f.heading) * f.len * 0.5,
f.y - Math.sin(f.heading) * f.len * 0.5, 0.18);
f.rippleT = rand(0.8, 2.4);
}
}
function update(dt, t) {
excitement = Math.max(0, excitement - dt * 0.45);
const follow = clamp(1 - (t - pointer.lastMove) / 4.5, 0, 1);
const followE = follow * follow * (3 - 2 * follow);
for (const f of fishes) updateFish(f, dt, t, followE);
for (let i = ripples.length - 1; i >= 0; i--) {
const r = ripples[i];
r.r += r.speed * dt;
if (r.r >= r.max) ripples.splice(i, 1);
}
ambientT -= dt;
if (ambientT <= 0) {
addRipple(rand(W * 0.1, W * 0.9), rand(H * 0.1, H * 0.9), rand(0.15, 0.4));
ambientT = rand(2.5, 6);
}
for (const mt of motes) {
mt.x += mt.vx * dt / W; mt.y += mt.vy * dt / H;
if (mt.y < -0.02) { mt.y = 1.02; mt.x = Math.random(); }
if (mt.x < -0.02) mt.x = 1.02; else if (mt.x > 1.02) mt.x = -0.02;
}
}
// ---------- drawing ----------
function drawShadow(f) {
ctx.save();
ctx.translate(f.x + 7, f.y + 11);
ctx.rotate(f.heading);
ctx.beginPath();
ctx.ellipse(0, 0, f.len * 0.46, f.wid * 0.55, 0, 0, TAU);
ctx.fillStyle = 'rgba(2,12,15,0.20)';
ctx.fill();
ctx.restore();
}
function pect(x, y, ang, len, wid, color) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(ang);
ctx.beginPath();
ctx.ellipse(len * 0.42, 0, len * 0.5, wid * 0.5, 0, 0, TAU);
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
}
function drawFish(f) {
const L = f.len, Wd = f.wid, bend = f.bendNow;
const by = x => bend * Math.sin((0.5 - x / L) * Math.PI * 0.85);
const speed = Math.hypot(f.vx, f.vy);
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.heading);
// flowing tail fin
const tb = -L * 0.44, tl = L * 0.36;
const sway = Math.sin(f.phase - 1.1) * L * 0.10 * clamp(speed / 60, 0.3, 1.2);
ctx.beginPath();
ctx.moveTo(tb, -Wd * 0.16 + by(tb));
ctx.quadraticCurveTo(tb - tl * 0.45, -Wd * 0.52 + sway * 0.4, tb - tl, -Wd * 0.40 + sway);
ctx.quadraticCurveTo(tb - tl * 0.52, sway * 0.5, tb - tl, Wd * 0.40 + sway);
ctx.quadraticCurveTo(tb - tl * 0.45, Wd * 0.52 + sway * 0.4, tb, Wd * 0.16 + by(tb));
ctx.closePath();
ctx.fillStyle = f.fin;
ctx.fill();
// body
ctx.beginPath();
ctx.moveTo(L * 0.5, by(L * 0.5));
ctx.bezierCurveTo(L * 0.40, -Wd * 0.46 + by(L * 0.40), L * 0.20, -Wd * 0.54 + by(L * 0.20), -L * 0.02, -Wd * 0.44 + by(-L * 0.02));
ctx.bezierCurveTo(-L * 0.20, -Wd * 0.36 + by(-L * 0.20), -L * 0.34, -Wd * 0.20 + by(-L * 0.34), -L * 0.46, -Wd * 0.10 + by(-L * 0.46));
ctx.quadraticCurveTo(-L * 0.50, by(-L * 0.5), -L * 0.46, Wd * 0.10 + by(-L * 0.46));
ctx.bezierCurveTo(-L * 0.34, Wd * 0.20 + by(-L * 0.34), -L * 0.20, Wd * 0.36 + by(-L * 0.20), -L * 0.02, Wd * 0.44 + by(-L * 0.02));
ctx.bezierCurveTo(L * 0.20, Wd * 0.54 + by(L * 0.20), L * 0.40, Wd * 0.46 + by(L * 0.40), L * 0.5, by(L * 0.5));
ctx.closePath();
ctx.fillStyle = f.base;
ctx.fill();
// pattern + sheen, clipped to the body
ctx.save();
ctx.clip();
for (const p of f.patches) {
ctx.beginPath();
ctx.ellipse(p.x * L, p.y * Wd * 1.6 + by(p.x * L), p.r * L, p.r * L * 0.62, 0, 0, TAU);
ctx.fillStyle = p.c;
ctx.globalAlpha = 0.92;
ctx.fill();
ctx.globalAlpha = 1;
}
const sg = ctx.createLinearGradient(0, -Wd * 0.6, 0, Wd * 0.6);
sg.addColorStop(0, 'rgba(255,255,255,0.14)');
sg.addColorStop(0.45, 'rgba(255,255,255,0)');
sg.addColorStop(1, 'rgba(0,20,25,0.14)');
ctx.fillStyle = sg;
ctx.fillRect(-L * 0.6, -Wd * 0.7, L * 1.2, Wd * 1.4);
ctx.restore();
ctx.strokeStyle = 'rgba(8,24,28,0.22)';
ctx.lineWidth = 1;
ctx.stroke();
// faint dorsal line
ctx.beginPath();
ctx.moveTo(L * 0.30, by(L * 0.30));
ctx.quadraticCurveTo(-L * 0.05, by(-L * 0.05), -L * 0.34, by(-L * 0.34));
ctx.strokeStyle = 'rgba(8,24,28,0.12)';
ctx.lineWidth = 1.2;
ctx.stroke();
// pectoral fins
const flap = Math.sin(f.phase * 0.6 + 1.3) * 0.28;
pect(L * 0.14, -Wd * 0.40, -0.85 + flap, L * 0.20, Wd * 0.30, f.fin);
pect(L * 0.14, Wd * 0.40, 0.85 - flap, L * 0.20, Wd * 0.30, f.fin);
// eyes
ctx.fillStyle = 'rgba(15,18,20,0.85)';
const er = Math.max(1.1, L * 0.026);
ctx.beginPath(); ctx.arc(L * 0.36, -Wd * 0.20, er, 0, TAU); ctx.fill();
ctx.beginPath(); ctx.arc(L * 0.36, Wd * 0.20, er, 0, TAU); ctx.fill();
ctx.restore();
}
function drawShaft(x, ang, w) {
ctx.save();
ctx.translate(x, -H * 0.05);
ctx.rotate(ang);
const lg = ctx.createLinearGradient(-w, 0, w, 0);
lg.addColorStop(0, 'rgba(150,215,200,0)');
lg.addColorStop(0.5, 'rgba(150,215,200,0.05)');
lg.addColorStop(1, 'rgba(150,215,200,0)');
ctx.fillStyle = lg;
ctx.fillRect(-w, 0, w * 2, H * 1.3);
ctx.restore();
}
function drawPad(p, t) {
const x = p.fx * W, y = p.fy * H;
const rot = p.rot + Math.sin(t * 0.12 + p.seed) * 0.05;
ctx.beginPath();
ctx.ellipse(x + 5, y + 7, p.r, p.r * 0.92, 0, 0, TAU);
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.fill();
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
const notch = 0.5;
const g = ctx.createRadialGradient(0, 0, p.r * 0.1, 0, 0, p.r);
g.addColorStop(0, '#3a7a4a');
g.addColorStop(0.8, '#2a5c38');
g.addColorStop(1, '#1e4a2c');
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, p.r, notch, TAU - notch);
ctx.closePath();
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = 'rgba(10,30,18,0.5)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.strokeStyle = 'rgba(180,230,190,0.18)';
ctx.lineWidth = 1;
for (let a = notch + 0.3; a < TAU - notch; a += 0.5) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(a) * p.r * 0.92, Math.sin(a) * p.r * 0.92);
ctx.stroke();
}
ctx.restore();
if (p.lotus) drawLotus(x, y, p.r * 0.45, t);
}
function drawLotus(x, y, s, t) {
for (let layer = 1; layer >= 0; layer--) {
const n = layer ? 6 : 8;
const pr = s * (layer ? 0.55 : 1);
for (let i = 0; i < n; i++) {
const a = i / n * TAU + (layer ? 0.4 : 0) + Math.sin(t * 0.2) * 0.02;
ctx.save();
ctx.translate(x, y);
ctx.rotate(a);
const g = ctx.createLinearGradient(0, 0, pr, 0);
g.addColorStop(0, '#f7d9e0');
g.addColorStop(1, '#e58aa5');
ctx.beginPath();
ctx.ellipse(pr * 0.55, 0, pr * 0.55, pr * 0.24, 0, 0, TAU);
ctx.fillStyle = g;
ctx.fill();
ctx.restore();
}
}
ctx.beginPath();
ctx.arc(x, y, s * 0.18, 0, TAU);
ctx.fillStyle = '#f2c14e';
ctx.fill();
}
function draw(t) {
// water
const g = ctx.createRadialGradient(W * 0.5, H * 0.4, Math.min(W, H) * 0.08, W * 0.5, H * 0.52, Math.max(W, H) * 0.8);
g.addColorStop(0, '#1c4f57');
g.addColorStop(0.5, '#123a43');
g.addColorStop(1, '#071e24');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// drifting caustic light
ctx.globalCompositeOperation = 'lighter';
for (const c of caustics) {
const x = c.x * W + Math.sin(t * c.sx + c.px) * c.ax;
const y = c.y * H + Math.cos(t * c.sy + c.py) * c.ay;
const rg = ctx.createRadialGradient(x, y, 0, x, y, c.r);
rg.addColorStop(0, 'rgba(120,200,185,0.05)');
rg.addColorStop(1, 'rgba(120,200,185,0)');
ctx.fillStyle = rg;
ctx.beginPath();
ctx.arc(x, y, c.r, 0, TAU);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
// light shafts
drawShaft(W * 0.28, 0.32 + Math.sin(t * 0.05) * 0.02, Math.min(W, H) * 0.16);
drawShaft(W * 0.62, 0.30 + Math.cos(t * 0.04) * 0.02, Math.min(W, H) * 0.10);
// motes
for (const mt of motes) {
const tw = 0.6 + 0.4 * Math.sin(t * mt.tw + mt.p);
ctx.beginPath();
ctx.arc(mt.x * W, mt.y * H, mt.r, 0, TAU);
ctx.fillStyle = 'rgba(200,225,215,' + (mt.a * tw).toFixed(3) + ')';
ctx.fill();
}
// fish: shadows, then bodies, small (distant) first
const sorted = fishes.slice().sort((a, b) => a.len - b.len);
for (const f of sorted) drawShadow(f);
for (const f of sorted) drawFish(f);
// surface ripples
for (const r of ripples) {
const k = 1 - r.r / r.max;
ctx.beginPath();
ctx.ellipse(r.x, r.y, r.r, r.r * 0.93, 0, 0, TAU);
ctx.strokeStyle = 'rgba(190,228,220,' + (r.a * k).toFixed(3) + ')';
ctx.lineWidth = r.w;
ctx.stroke();
}
// lily pads float above the fish
for (const p of pads) drawPad(p, t);
// vignette
const vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.34, W / 2, H / 2, Math.max(W, H) * 0.78);
vg.addColorStop(0, 'rgba(3,14,18,0)');
vg.addColorStop(1, 'rgba(3,14,18,0.45)');
ctx.fillStyle = vg;
ctx.fillRect(0, 0, W, H);
}
// ---------- input ----------
window.addEventListener('pointermove', e => {
pointer.x = e.clientX; pointer.y = e.clientY;
pointer.lastMove = performance.now() / 1000;
rippleAcc += Math.hypot(e.clientX - lastPx, e.clientY - lastPy);
lastPx = e.clientX; lastPy = e.clientY;
if (rippleAcc > 130) { addRipple(pointer.x, pointer.y, 0.35); rippleAcc = 0; }
hint.classList.add('gone');
});
window.addEventListener('pointerdown', e => {
pointer.x = e.clientX; pointer.y = e.clientY;
pointer.lastMove = performance.now() / 1000;
excitement = 1;
addRipple(e.clientX, e.clientY, 1);
addRipple(e.clientX, e.clientY, 0.6);
hint.classList.add('gone');
});
setTimeout(() => hint.classList.add('gone'), 12000);
// ---------- init ----------
for (let i = 0; i < 6; i++) caustics.push({
x: Math.random(), y: Math.random(), r: rand(140, 320),
sx: rand(0.05, 0.14), sy: rand(0.04, 0.12),
px: rand(0, TAU), py: rand(0, TAU),
ax: rand(30, 90), ay: rand(20, 70)
});
for (let i = 0; i < 46; i++) motes.push({
x: Math.random(), y: Math.random(), r: rand(0.5, 1.8),
vx: rand(-4, 4), vy: rand(-6, -1),
a: rand(0.05, 0.2), tw: rand(0.5, 2), p: rand(0, TAU)
});
resize();
const FISH_N = clamp(Math.round(W * H / 120000), 5, 9);
for (let i = 0; i < FISH_N; i++) spawnFish();
let last = performance.now();
requestAnimationFrame(function frame(now) {
const t = now / 1000;
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
update(dt, t);
draw(t);
requestAnimationFrame(frame);
});
</script>
</body>
</html>
Explanation
I built the pond as a single canvas scene rendered procedurally — no images, no libraries. The calm feel comes from three layers of decisions. First, motion: each fish is a small steering system with a capped force and an "arrive" behavior (it slows as it nears its goal), so movement is always eased rather than snappy. When the cursor is idle, fish wander along a smooth pseudo flow field (layered sines of position and time), which gives meandering, non-repetitive paths. When the cursor moves, a freshness factor eases in over a few seconds, and each fish blends between wandering and seeking according to its own personality — every koi has a sociability score, a preferred standoff distance, and a swirl direction, so they gather around the pointer loosely and circle it instead of stacking on top of it. A tap or click adds a brief "excitement" that lets them dart. Second, the artwork: each koi is drawn from bezier outlines with a traveling sine bend through the body, a flowing tail fin whose beat rate scales with speed, flapping pectoral fins, and clipped color patches generated per fish in classic varieties (kohaku, tancho, ogon, shiro utsuri, calico, hi, and a shadowy black koi for contrast). Third, atmosphere: a deep teal radial gradient, drifting caustic light blobs, two soft swaying light shafts, floating motes, expanding ripple rings (spawned by fast cursor movement, clicks, ambient disturbances, and hard-working tails), lily pads with a lotus that fish pass beneath, and a vignette to settle the edges. Everything runs on a delta-time loop with devicePixelRatio capped at 2 for crispness without waste, and it resizes cleanly.
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.