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.

ModelKimi K3 (Moonshot) moonshotai/kimi-k3
Taskd1-fishpond · web-app · javascript · tests out of scope
Harnessopenrouter-chat-completions openrouter-chat-2026-09-22 at temperature 0.3
Generated2026-09-22T14:16:25.065Z (generated)
LicenseMIT · demo-eligible
ContaminationNot 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

Form: single-file

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.