d1-fishpond--google-gemini-3-8-flash

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.

ModelGemini 3.8 Flash (Google) google/gemini-3.8-flash
Taskd1-fishpond · web-app · javascript · tests out of scope
Harnessopenrouter-chat-completions openrouter-chat-2026-09-22 at temperature 0.3
Generated2026-09-22T14:11:46.189Z (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.0, user-scalable=no">
  <title>Serene Koi Pond</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    body, html {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #06161c;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: block;
      cursor: grab;
    }
    canvas:active {
      cursor: grabbing;
    }
    .hud {
      position: fixed;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%);
      color: rgba(220, 240, 245, 0.75);
      font-size: 13px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      pointer-events: none;
      background: rgba(8, 28, 34, 0.55);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      padding: 9px 20px;
      border-radius: 999px;
      border: 1px solid rgba(255, 255, 255, 0.08);
      transition: opacity 1.5s ease;
      white-space: nowrap;
    }
  </style>
</head>
<body>
  <canvas id="pond"></canvas>
  <div class="hud" id="hud">Move cursor to guide &bull; Click to feed</div>

  <script>
    const canvas = document.getElementById('pond');
    const ctx = canvas.getContext('2d');
    const hud = document.getElementById('hud');

    let width = 0;
    let height = 0;
    let dpr = 1;

    // Interaction State
    const mouse = {
      x: -1000,
      y: -1000,
      prevX: -1000,
      prevY: -1000,
      speed: 0,
      active: false,
      lastMoved: 0
    };

    function resize() {
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      width = window.innerWidth;
      height = window.innerHeight;
      canvas.width = Math.floor(width * dpr);
      canvas.height = Math.floor(height * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      if (pads.length === 0) initLilyPads();
    }
    window.addEventListener('resize', resize);

    // Fade hint after initial interaction
    function dismissHud() {
      if (hud && hud.style.opacity !== '0') {
        setTimeout(() => {
          hud.style.opacity = '0';
          setTimeout(() => hud.remove(), 1600);
        }, 3000);
      }
    }

    // Ripple simulation
    class Ripple {
      constructor(x, y, maxRadius = 45, strength = 1.0) {
        this.x = x;
        this.y = y;
        this.radius = 2;
        this.maxRadius = maxRadius;
        this.strength = strength;
        this.alpha = strength;
        this.speed = 0.75 + Math.random() * 0.5;
      }
      update() {
        this.radius += this.speed;
        this.alpha = (1 - (this.radius / this.maxRadius)) * this.strength;
        return this.radius < this.maxRadius;
      }
      draw(c) {
        if (this.alpha <= 0) return;
        c.save();
        c.beginPath();
        c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        c.strokeStyle = `rgba(180, 230, 245, ${this.alpha * 0.28})`;
        c.lineWidth = 1.4;
        c.stroke();

        // Inner harmonic crest
        if (this.radius > 8) {
          c.beginPath();
          c.arc(this.x, this.y, this.radius * 0.65, 0, Math.PI * 2);
          c.strokeStyle = `rgba(210, 245, 255, ${this.alpha * 0.14})`;
          c.lineWidth = 1;
          c.stroke();
        }
        c.restore();
      }
    }

    // Food Pellets
    class Food {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.vx = (Math.random() - 0.5) * 0.6;
        this.vy = (Math.random() - 0.5) * 0.6;
        this.radius = 3.5 + Math.random() * 1.5;
        this.eaten = false;
        this.sink = 0;
        this.age = 0;
        this.bobble = Math.random() * Math.PI * 2;
      }
      update() {
        this.age++;
        this.bobble += 0.04;
        this.x += this.vx;
        this.y += this.vy;
        this.vx *= 0.96;
        this.vy *= 0.96;
        return this.age < 1800 && !this.eaten;
      }
      draw(c) {
        const floatOffset = Math.sin(this.bobble) * 1.2;
        // Shadow
        c.beginPath();
        c.arc(this.x + 4, this.y + 6, this.radius * 0.85, 0, Math.PI * 2);
        c.fillStyle = 'rgba(2, 10, 14, 0.4)';
        c.fill();
        // Pellet
        c.beginPath();
        c.arc(this.x, this.y + floatOffset, this.radius, 0, Math.PI * 2);
        c.fillStyle = '#b3783a';
        c.fill();
        c.strokeStyle = '#dfa55c';
        c.lineWidth = 0.8;
        c.stroke();
      }
    }

    // Lily Pad
    class LilyPad {
      constructor(x, y, radius, rotation) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.rotation = rotation;
        this.notchAngle = 0.38;
        this.driftAngle = Math.random() * Math.PI * 2;
        this.hasFlower = Math.random() < 0.35;
        this.flowerPetals = 8 + Math.floor(Math.random() * 4);
      }
      draw(c) {
        const swayX = Math.sin(this.driftAngle) * 3;
        const swayY = Math.cos(this.driftAngle * 0.8) * 3;
        const px = this.x + swayX;
        const py = this.y + swayY;

        c.save();
        c.translate(px, py);
        c.rotate(this.rotation);

        // Pad Shadow
        c.beginPath();
        c.arc(10, 14, this.radius, this.notchAngle, Math.PI * 2 - this.notchAngle);
        c.lineTo(10, 14);
        c.closePath();
        c.fillStyle = 'rgba(2, 8, 12, 0.45)';
        c.fill();

        // Pad Leaf
        c.beginPath();
        c.arc(0, 0, this.radius, this.notchAngle, Math.PI * 2 - this.notchAngle);
        c.lineTo(0, 0);
        c.closePath();
        const grad = c.createRadialGradient(-this.radius * 0.2, -this.radius * 0.2, 3, 0, 0, this.radius);
        grad.addColorStop(0, '#2f6c44');
        grad.addColorStop(0.75, '#1e4f30');
        grad.addColorStop(1, '#133621');
        c.fillStyle = grad;
        c.fill();

        // Leaf veins
        c.strokeStyle = 'rgba(255, 255, 255, 0.08)';
        c.lineWidth = 1;
        for (let i = 0; i < 7; i++) {
          const a = this.notchAngle + 0.3 + (i * ((Math.PI * 2 - this.notchAngle * 2 - 0.6) / 6));
          c.beginPath();
          c.moveTo(0, 0);
          c.quadraticCurveTo(Math.cos(a) * this.radius * 0.5, Math.sin(a) * this.radius * 0.5, Math.cos(a) * (this.radius - 2), Math.sin(a) * (this.radius - 2));
          c.stroke();
        }

        // Pad Rim highlight
        c.beginPath();
        c.arc(0, 0, this.radius, this.notchAngle, Math.PI * 2 - this.notchAngle);
        c.strokeStyle = 'rgba(160, 220, 160, 0.25)';
        c.lineWidth = 1.2;
        c.stroke();

        // Optional Lotus Blossom
        if (this.hasFlower) {
          c.save();
          c.translate(-this.radius * 0.25, -this.radius * 0.2);
          for (let p = 0; p < this.flowerPetals; p++) {
            const pa = (p / this.flowerPetals) * Math.PI * 2;
            c.save();
            c.rotate(pa);
            c.beginPath();
            c.ellipse(12, 0, 12, 4.5, 0, 0, Math.PI * 2);
            c.fillStyle = 'rgba(255, 240, 245, 0.9)';
            c.fill();
            c.restore();
          }
          for (let p = 0; p < this.flowerPetals; p++) {
            const pa = (p / this.flowerPetals) * Math.PI * 2 + 0.25;
            c.save();
            c.rotate(pa);
            c.beginPath();
            c.ellipse(8, 0, 8, 3.5, 0, 0, Math.PI * 2);
            c.fillStyle = '#ffb8cc';
            c.fill();
            c.restore();
          }
          // Center
          c.beginPath();
          c.arc(0, 0, 4.5, 0, Math.PI * 2);
          c.fillStyle = '#f9d342';
          c.fill();
          c.restore();
        }

        c.restore();
      }
    }

    // Koi Variety Palettes & Markings
    const KOI_TYPES = [
      {
        name: 'Kohaku',
        base: '#f6f3eb',
        markings: ['#d9381e', '#e64a19'],
        spotCount: 3,
        accent: '#be2608'
      },
      {
        name: 'Taisho Sanke',
        base: '#fdfbf7',
        markings: ['#d8391d', '#1e1f24', '#e64f2a'],
        spotCount: 5,
        accent: '#111215'
      },
      {
        name: 'Yamabuki Ogon',
        base: '#ebb22f',
        markings: ['#ffd45e', '#cf9518'],
        spotCount: 2,
        accent: '#ffea88'
      },
      {
        name: 'Shiro Utsuri',
        base: '#1b1d22',
        markings: ['#f0eee6', '#ded9cb'],
        spotCount: 4,
        accent: '#ffffff'
      },
      {
        name: 'Asagi',
        base: '#879ca8',
        markings: ['#d34828', '#c03a1a'],
        spotCount: 2,
        accent: '#de5935'
      }
    ];

    // Single Koi Fish Representation
    class Koi {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.type = KOI_TYPES[Math.floor(Math.random() * KOI_TYPES.length)];
        this.scale = 0.78 + Math.random() * 0.45;
        this.numJoints = 12;
        this.jointDistance = 7.5 * this.scale;

        // Kinematic Spine
        this.joints = [];
        this.angles = [];
        const initialAngle = Math.random() * Math.PI * 2;
        for (let i = 0; i < this.numJoints; i++) {
          this.joints.push({
            x: this.x - Math.cos(initialAngle) * i * this.jointDistance,
            y: this.y - Math.sin(initialAngle) * i * this.jointDistance
          });
          this.angles.push(initialAngle);
        }

        // Motion physics
        this.heading = initialAngle;
        this.speed = 1.2 + Math.random() * 0.6;
        this.targetSpeed = this.speed;
        this.maxSpeed = 3.6;
        this.minSpeed = 0.9;
        this.turnSpeed = 0.045 + Math.random() * 0.015;
        this.swimCycle = Math.random() * Math.PI * 2;
        this.wanderAngle = initialAngle;

        // Radii for body taper from head to tail
        this.radii = [
          13.5 * this.scale, // Head
          15.5 * this.scale,
          16.5 * this.scale, // Broadest body
          15.8 * this.scale,
          14.2 * this.scale,
          12.2 * this.scale,
          10.0 * this.scale,
          7.8 * this.scale,
          5.6 * this.scale,
          4.0 * this.scale,
          2.8 * this.scale,
          2.0 * this.scale  // Peduncle
        ];

        // Generate procedural spots anchored to body segments
        this.spots = [];
        for (let s = 0; s < this.type.spotCount; s++) {
          this.spots.push({
            jointIndex: 1 + Math.floor(Math.random() * (this.numJoints - 4)),
            offsetDist: (Math.random() - 0.5) * 9 * this.scale,
            radiusX: (6 + Math.random() * 9) * this.scale,
            radiusY: (5 + Math.random() * 7) * this.scale,
            color: this.type.markings[s % this.type.markings.length]
          });
        }
      }

      update(koiList, foodList, ripples) {
        // Find nearest food
        let nearestFood = null;
        let minDist = 320;
        for (const food of foodList) {
          if (food.eaten) continue;
          const d = Math.hypot(food.x - this.joints[0].x, food.y - this.joints[0].y);
          if (d < minDist) {
            minDist = d;
            nearestFood = food;
          }
        }

        let desiredAngle = this.heading;

        if (nearestFood) {
          // Food seeking
          desiredAngle = Math.atan2(nearestFood.y - this.joints[0].y, nearestFood.x - this.joints[0].x);
          this.targetSpeed = this.maxSpeed * 0.95;

          // Eat food if close
          if (minDist < 12 * this.scale) {
            nearestFood.eaten = true;
            ripples.push(new Ripple(nearestFood.x, nearestFood.y, 35, 0.9));
            this.targetSpeed = this.minSpeed * 0.8;
          }
        } else if (mouse.active) {
          const mouseDist = Math.hypot(mouse.x - this.joints[0].x, mouse.y - this.joints[0].y);

          if (mouseDist < 420) {
            const angleToMouse = Math.atan2(mouse.y - this.joints[0].y, mouse.x - this.joints[0].x);
            // Circle or glide past cursor if very close to feel natural and unhurried
            if (mouseDist < 65) {
              desiredAngle = angleToMouse + Math.PI * 0.55;
              this.targetSpeed = this.minSpeed * 1.1;
            } else {
              desiredAngle = angleToMouse;
              this.targetSpeed = Math.min(this.maxSpeed, 1.4 + (mouseDist / 220));
            }
          } else {
            this.naturalWander();
          }
        } else {
          this.naturalWander();
        }

        // Soft separation from other koi
        let sepX = 0;
        let sepY = 0;
        for (const other of koiList) {
          if (other === this) continue;
          const dx = this.joints[0].x - other.joints[0].x;
          const dy = this.joints[0].y - other.joints[0].y;
          const d = Math.hypot(dx, dy);
          const comfortDist = 45 * this.scale;
          if (d > 0 && d < comfortDist) {
            sepX += (dx / d) * (comfortDist - d) * 0.03;
            sepY += (dy / d) * (comfortDist - d) * 0.03;
          }
        }
        if (sepX !== 0 || sepY !== 0) {
          desiredAngle = Math.atan2(Math.sin(desiredAngle) + sepY, Math.cos(desiredAngle) + sepX);
        }

        // Screen boundary soft steering
        const margin = 90;
        if (this.joints[0].x < margin) desiredAngle = Math.atan2(Math.sin(desiredAngle), Math.cos(desiredAngle) + 0.35);
        if (this.joints[0].x > width - margin) desiredAngle = Math.atan2(Math.sin(desiredAngle), Math.cos(desiredAngle) - 0.35);
        if (this.joints[0].y < margin) desiredAngle = Math.atan2(Math.sin(desiredAngle) + 0.35, Math.cos(desiredAngle));
        if (this.joints[0].y > height - margin) desiredAngle = Math.atan2(Math.sin(desiredAngle) - 0.35, Math.cos(desiredAngle));

        // Smooth heading turn
        let diff = desiredAngle - this.heading;
        while (diff < -Math.PI) diff += Math.PI * 2;
        while (diff > Math.PI) diff -= Math.PI * 2;
        this.heading += diff * this.turnSpeed;

        // Smooth speed transition
        this.speed += (this.targetSpeed - this.speed) * 0.04;

        // Swim undulation frequency tied to speed
        this.swimCycle += this.speed * 0.085;

        // Advance head
        this.joints[0].x += Math.cos(this.heading) * this.speed;
        this.joints[0].y += Math.sin(this.heading) * this.speed;
        this.angles[0] = this.heading;

        // Cascade IK down the spine
        for (let i = 1; i < this.numJoints; i++) {
          const prev = this.joints[i - 1];
          const curr = this.joints[i];

          // Spine bending wave
          const wave = Math.sin(this.swimCycle - i * 0.42) * (i * 0.55 * this.scale);
          const angle = Math.atan2(curr.y - prev.y, curr.x - prev.x);

          curr.x = prev.x + Math.cos(angle) * this.jointDistance + Math.cos(angle + Math.PI / 2) * wave * 0.12;
          curr.y = prev.y + Math.sin(angle) * this.jointDistance + Math.sin(angle + Math.PI / 2) * wave * 0.12;
          this.angles[i] = Math.atan2(prev.y - curr.y, prev.x - curr.x);
        }

        // Occasional wake ripple at tail when swimming fast
        if (this.speed > 2.0 && Math.random() < 0.05) {
          const tail = this.joints[this.numJoints - 1];
          ripples.push(new Ripple(tail.x, tail.y, 25 * this.scale, 0.4));
        }
      }

      naturalWander() {
        this.wanderAngle += (Math.random() - 0.5) * 0.14;
        this.targetSpeed = this.minSpeed + Math.sin(this.swimCycle * 0.3) * 0.35;
        return this.wanderAngle;
      }

      // Compute left and right body perimeter points
      getPerimeter() {
        const left = [];
        const right = [];
        for (let i = 0; i < this.numJoints; i++) {
          const j = this.joints[i];
          const a = this.angles[i];
          const r = this.radii[i];
          const normA = a + Math.PI / 2;
          left.push({
            x: j.x + Math.cos(normA) * r,
            y: j.y + Math.sin(normA) * r
          });
          right.push({
            x: j.x - Math.cos(normA) * r,
            y: j.y - Math.sin(normA) * r
          });
        }
        return { left, right };
      }

      drawShadow(c) {
        const shadowOffset = 18 * this.scale;
        c.save();
        c.translate(shadowOffset * 0.7, shadowOffset);

        const { left, right } = this.getPerimeter();
        c.beginPath();
        c.moveTo(this.joints[0].x, this.joints[0].y);

        for (let i = 0; i < left.length; i++) {
          c.lineTo(left[i].x, left[i].y);
        }
        for (let i = right.length - 1; i >= 0; i--) {
          c.lineTo(right[i].x, right[i].y);
        }
        c.closePath();
        c.fillStyle = 'rgba(2, 9, 14, 0.38)';
        c.fill();
        c.restore();
      }

      draw(c) {
        const { left, right } = this.getPerimeter();

        // Pectoral Fins (Joint 2)
        this.drawPectoralFins(c);

        // Ventral Pelvic Fins (Joint 6)
        this.drawPelvicFins(c);

        // Caudal (Tail) Fin (End Joint)
        this.drawCaudalFin(c);

        // Fish Body Silhouette
        c.save();
        c.beginPath();
        // Snout rounded tip
        const snoutX = this.joints[0].x + Math.cos(this.heading) * (8 * this.scale);
        const snoutY = this.joints[0].y + Math.sin(this.heading) * (8 * this.scale);
        c.moveTo(snoutX, snoutY);

        // Left body curve
        for (let i = 0; i < left.length; i++) {
          c.lineTo(left[i].x, left[i].y);
        }
        // Tail apex
        c.lineTo(this.joints[this.numJoints - 1].x, this.joints[this.numJoints - 1].y);
        // Right body curve
        for (let i = right.length - 1; i >= 0; i--) {
          c.lineTo(right[i].x, right[i].y);
        }
        c.closePath();

        // Base Coat
        c.fillStyle = this.type.base;
        c.fill();
        c.clip(); // Constrain patterns neatly within fish body

        // Koi Markings (Hi & Sumi patches)
        for (const spot of this.spots) {
          const j = this.joints[spot.jointIndex];
          const a = this.angles[spot.jointIndex];
          const normA = a + Math.PI / 2;
          const sx = j.x + Math.cos(normA) * spot.offsetDist;
          const sy = j.y + Math.sin(normA) * spot.offsetDist;

          c.beginPath();
          c.ellipse(sx, sy, spot.radiusX, spot.radiusY, a, 0, Math.PI * 2);
          c.fillStyle = spot.color;
          c.fill();
        }

        // Soft dorsal spine highlight for cylindrical 3D look
        c.beginPath();
        c.moveTo(this.joints[0].x, this.joints[0].y);
        for (let i = 1; i < this.numJoints - 2; i++) {
          c.lineTo(this.joints[i].x, this.joints[i].y);
        }
        c.strokeStyle = 'rgba(255, 255, 255, 0.28)';
        c.lineWidth = 4 * this.scale;
        c.lineCap = 'round';
        c.stroke();

        c.restore();

        // Dorsal fin ridge
        c.beginPath();
        c.moveTo(this.joints[3].x, this.joints[3].y);
        c.quadraticCurveTo(this.joints[5].x, this.joints[5].y, this.joints[8].x, this.joints[8].y);
        c.strokeStyle = 'rgba(255, 255, 255, 0.4)';
        c.lineWidth = 1.4 * this.scale;
        c.stroke();

        // Eyes
        this.drawEyes(c);
      }

      drawEyes(c) {
        const eyeAngle = this.angles[0];
        const eyeOffsetDist = 8.8 * this.scale;
        const forwardOffset = 4.2 * this.scale;
        for (const side of [-1, 1]) {
          const norm = eyeAngle + (Math.PI / 2) * side;
          const ex = this.joints[0].x + Math.cos(norm) * eyeOffsetDist + Math.cos(eyeAngle) * forwardOffset;
          const ey = this.joints[0].y + Math.sin(norm) * eyeOffsetDist + Math.sin(eyeAngle) * forwardOffset;

          // Eye socket
          c.beginPath();
          c.arc(ex, ey, 2.2 * this.scale, 0, Math.PI * 2);
          c.fillStyle = '#101214';
          c.fill();

          // Highlight
          c.beginPath();
          c.arc(ex - 0.5, ey - 0.5, 0.8 * this.scale, 0, Math.PI * 2);
          c.fillStyle = '#ffffff';
          c.fill();
        }
      }

      drawPectoralFins(c) {
        const j = this.joints[2];
        const a = this.angles[2];
        const finLength = 26 * this.scale;
        const finSway = Math.sin(this.swimCycle * 1.1) * 0.2;

        for (const side of [-1, 1]) {
          const baseAngle = a + (Math.PI / 2.2) * side;
          const fx = j.x + Math.cos(baseAngle) * (12 * this.scale);
          const fy = j.y + Math.sin(baseAngle) * (12 * this.scale);
          const finAngle = baseAngle + (0.55 * side) + (finSway * side);

          c.save();
          c.translate(fx, fy);
          c.rotate(finAngle);

          // Flowing fan fin
          c.beginPath();
          c.moveTo(0, 0);
          c.bezierCurveTo(finLength * 0.4, -side * 5, finLength * 0.8, -side * 9, finLength, 0);
          c.bezierCurveTo(finLength * 0.7, side * 11, finLength * 0.3, side * 7, 0, 0);
          c.fillStyle = 'rgba(255, 255, 255, 0.6)';
          c.fill();

          // Fin rays
          c.strokeStyle = 'rgba(255, 255, 255, 0.4)';
          c.lineWidth = 0.8;
          for (let r = 0; r < 4; r++) {
            c.beginPath();
            c.moveTo(0, 0);
            c.lineTo(finLength * (0.6 + r * 0.12), (r - 1.5) * 3 * this.scale);
            c.stroke();
          }

          c.restore();
        }
      }

      drawPelvicFins(c) {
        const j = this.joints[6];
        const a = this.angles[6];
        const finLength = 14 * this.scale;

        for (const side of [-1, 1]) {
          const baseAngle = a + (Math.PI / 2.1) * side;
          const fx = j.x + Math.cos(baseAngle) * (7 * this.scale);
          const fy = j.y + Math.sin(baseAngle) * (7 * this.scale);
          const finAngle = a + Math.PI * 0.85 * side;

          c.save();
          c.translate(fx, fy);
          c.rotate(finAngle);
          c.beginPath();
          c.moveTo(0, 0);
          c.bezierCurveTo(finLength * 0.6, -2, finLength, 0, finLength * 0.85, 5);
          c.bezierCurveTo(finLength * 0.4, 4, finLength * 0.2, 2, 0, 0);
          c.fillStyle = 'rgba(255, 255, 255, 0.45)';
          c.fill();
          c.restore();
        }
      }

      drawCaudalFin(c) {
        const tail = this.joints[this.numJoints - 1];
        const a = this.angles[this.numJoints - 1];
        const tailLength = 34 * this.scale;
        const wave = Math.sin(this.swimCycle - this.numJoints * 0.42);

        c.save();
        c.translate(tail.x, tail.y);
        c.rotate(a + Math.PI + wave * 0.18);

        // Delicate dual-lobe tail fin
        c.beginPath();
        c.moveTo(0, 0);
        c.bezierCurveTo(tailLength * 0.4, -18 * this.scale, tailLength * 0.85, -24 * this.scale, tailLength, -12 * this.scale);
        c.bezierCurveTo(tailLength * 0.7, -4 * this.scale, tailLength * 0.7, 4 * this.scale, tailLength, 12 * this.scale);
        c.bezierCurveTo(tailLength * 0.85, 24 * this.scale, tailLength * 0.4, 18 * this.scale, 0, 0);

        const tailGrad = c.createLinearGradient(0, 0, tailLength, 0);
        tailGrad.addColorStop(0, 'rgba(255, 255, 255, 0.75)');
        tailGrad.addColorStop(0.5, 'rgba(255, 240, 235, 0.55)');
        tailGrad.addColorStop(1, 'rgba(255, 255, 255, 0.12)');
        c.fillStyle = tailGrad;
        c.fill();

        // Delicate fin rays
        c.strokeStyle = 'rgba(255, 255, 255, 0.45)';
        c.lineWidth = 0.75;
        for (let ray = -5; ray <= 5; ray++) {
          c.beginPath();
          c.moveTo(0, 0);
          c.quadraticCurveTo(tailLength * 0.5, ray * 2.5 * this.scale, tailLength * 0.9, ray * 3.4 * this.scale);
          c.stroke();
        }

        c.restore();
      }
    }

    // World Elements
    let koiPond = [];
    let foodList = [];
    let ripples = [];
    let pads = [];

    function initLilyPads() {
      pads = [];
      const numPads = Math.max(5, Math.floor((width * height) / 160000));
      for (let i = 0; i < numPads; i++) {
        const padX = 70 + Math.random() * (width - 140);
        const padY = 70 + Math.random() * (height - 140);
        const padR = 40 + Math.random() * 32;
        pads.push(new LilyPad(padX, padY, padR, Math.random() * Math.PI * 2));
      }
    }

    function initKoi() {
      koiPond = [];
      const count = Math.max(7, Math.floor((width * height) / 95000));
      for (let i = 0; i < count; i++) {
        const x = width * 0.2 + Math.random() * (width * 0.6);
        const y = height * 0.2 + Math.random() * (height * 0.6);
        koiPond.push(new Koi(x, y));
      }
    }

    // Ambient floating dust / water specks
    const motes = [];
    for (let i = 0; i < 40; i++) {
      motes.push({
        x: Math.random() * window.innerWidth,
        y: Math.random() * window.innerHeight,
        vx: (Math.random() - 0.5) * 0.3,
        vy: -0.15 - Math.random() * 0.2,
        r: 0.8 + Math.random() * 1.4,
        alpha: 0.2 + Math.random() * 0.4
      });
    }

    // Interaction Listeners
    function onPointerMove(px, py) {
      dismissHud();
      const dx = px - mouse.prevX;
      const dy = py - mouse.prevY;
      mouse.speed = Math.hypot(dx, dy);
      mouse.prevX = mouse.x;
      mouse.prevY = mouse.y;
      mouse.x = px;
      mouse.y = py;
      mouse.active = true;
      mouse.lastMoved = performance.now();

      // Spawn soft ripples on energetic cursor motion
      if (mouse.speed > 8 && Math.random() < 0.35) {
        ripples.push(new Ripple(px, py, 32, 0.4));
      }
    }

    window.addEventListener('mousemove', (e) => {
      onPointerMove(e.clientX, e.clientY);
    });

    window.addEventListener('touchmove', (e) => {
      if (e.touches.length > 0) {
        onPointerMove(e.touches[0].clientX, e.touches[0].clientY);
      }
    }, { passive: true });

    function addFood(x, y) {
      dismissHud();
      // Drop a tight pinch of 2-3 food pellets
      const pelletCount = 1 + Math.floor(Math.random() * 3);
      for (let i = 0; i < pelletCount; i++) {
        const ox = x + (Math.random() - 0.5) * 16;
        const oy = y + (Math.random() - 0.5) * 16;
        foodList.push(new Food(ox, oy));
      }
      ripples.push(new Ripple(x, y, 40, 0.7));
    }

    window.addEventListener('click', (e) => {
      addFood(e.clientX, e.clientY);
    });

    window.addEventListener('touchstart', (e) => {
      if (e.touches.length > 0) {
        const t = e.touches[0];
        onPointerMove(t.clientX, t.clientY);
        addFood(t.clientX, t.clientY);
      }
    }, { passive: true });

    document.addEventListener('mouseleave', () => {
      mouse.active = false;
    });

    // Main Render Loop
    let lastTime = performance.now();

    function render(time) {
      requestAnimationFrame(render);
      const dt = Math.min((time - lastTime) / 1000, 0.1);
      lastTime = time;

      // Inactivity timeout: cursor influence gently ceases after 3.5s of no motion
      if (mouse.active && time - mouse.lastMoved > 3500) {
        mouse.active = false;
      }

      // Pond Water Bed Background
      const waterGrad = ctx.createRadialGradient(width * 0.5, height * 0.45, 50, width * 0.5, height * 0.5, Math.max(width, height) * 0.75);
      waterGrad.addColorStop(0, '#0a2a35');
      waterGrad.addColorStop(0.55, '#061c24');
      waterGrad.addColorStop(1, '#020d12');
      ctx.fillStyle = waterGrad;
      ctx.fillRect(0, 0, width, height);

      // Deep Water Caustic Sunlight Sheen
      ctx.save();
      const causticT = time * 0.0006;
      ctx.fillStyle = 'rgba(64, 165, 185, 0.035)';
      for (let c = 0; c < 3; c++) {
        ctx.beginPath();
        const cx = (Math.sin(causticT + c * 2.1) * 0.3 + 0.5) * width;
        const cy = (Math.cos(causticT * 0.8 + c * 1.7) * 0.3 + 0.5) * height;
        ctx.arc(cx, cy, width * 0.38, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();

      // Update & Draw Food
      for (let i = foodList.length - 1; i >= 0; i--) {
        const food = foodList[i];
        if (!food.update()) {
          foodList.splice(i, 1);
        } else {
          food.draw(ctx);
        }
      }

      // Update & Draw Ripples
      for (let i = ripples.length - 1; i >= 0; i--) {
        const ripple = ripples[i];
        if (!ripple.update()) {
          ripples.splice(i, 1);
        } else {
          ripple.draw(ctx);
        }
      }

      // 1. Draw All Koi Shadows on Pond Bed
      for (const koi of koiPond) {
        koi.update(koiPond, foodList, ripples);
        koi.drawShadow(ctx);
      }

      // 2. Draw Koi Bodies
      for (const koi of koiPond) {
        koi.draw(ctx);
      }

      // 3. Draw Floating Lily Pads Above Water
      for (const pad of pads) {
        pad.draw(ctx);
      }

      // 4. Subtle Floating Sun Particles / Water Motes
      ctx.save();
      for (const mote of motes) {
        mote.x += mote.vx;
        mote.y += mote.vy;
        if (mote.y < -10) {
          mote.y = height + 10;
          mote.x = Math.random() * width;
        }
        if (mote.x < -10) mote.x = width + 10;
        if (mote.x > width + 10) mote.x = -10;

        ctx.beginPath();
        ctx.arc(mote.x, mote.y, mote.r, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(180, 235, 245, ${mote.alpha})`;
        ctx.fill();
      }
      ctx.restore();

      // Soft vignette on pond edges
      const vignette = ctx.createRadialGradient(width * 0.5, height * 0.5, Math.min(width, height) * 0.45, width * 0.5, height * 0.5, Math.max(width, height) * 0.85);
      vignette.addColorStop(0, 'rgba(0, 0, 0, 0)');
      vignette.addColorStop(1, 'rgba(1, 6, 8, 0.65)');
      ctx.fillStyle = vignette;
      ctx.fillRect(0, 0, width, height);
    }

    // Startup
    resize();
    initKoi();
    requestAnimationFrame(render);
  </script>
</body>
</html>

Explanation

The design of this koi pond focuses on organic movement, visual depth, and a serene, meditative atmosphere. ### Motion and Kinematics Rather than treating each fish as a rigid sprite, every koi is simulated with a multi-jointed inverse-kinematics spine. As the head steers, movement propagates down the spine with progressive phase delay and lateral undulation, producing the characteristic serpentine wave of swimming fish. Steering relies on gentle autonomous wander vectors influenced by smooth rotational constraints. When the cursor moves or food is scattered, an inquisitive steering force gently draws the fish toward the point of interest without snapping their momentum. ### Depth and Environment To create the illusion of swimming suspended in clear water, each koi casts a soft, offset directional shadow onto the pond bed below. Semi-transparent pectoral, dorsal, and flowing caudal fins are drawn with delicate fin rays and alpha blending, moving with their own oscillation cycles. Water ripples propagate outwards from swimming strokes, feeding strikes, and cursor ripples. Floating lily pads and subtle ambient light caustics complete the natural composition. ### Color and Variety The pond hosts distinct varieties of traditional Nishikigoi (including Kohaku, Taisho Sanke, and Yamabuki Ogon). Individual markings and scale hues are procedurally mapped to each fish's anatomical contours, ensuring every koi is visually unique.

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.