Skip to content

虾塘-养鱼养虾项目

使用 canvas 实现 鱼和虾游泳

在线访问 https://docs.ffffee.com/examples/pond/index.html

源码

index.html

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>虾塘</title>
    <style>
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100vw;
        height: 100vh;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>

  <body>
    <canvas id="pond" width="800" height="600"></canvas>
    <script src="pond.js"></script>

    <script>
      function setCanvasSize() {
        const pond = document.querySelector("#pond");
        pond.style.width = window.innerWidth + "px";
        pond.style.height = window.innerHeight + "px";

        pond.width = window.innerWidth;
        pond.height = window.innerHeight;
      }

      setCanvasSize();

      window.addEventListener("resize", setCanvasSize);
    </script>
  </body>
</html>

pond.js

js
var canvas = document.getElementById("pond");
var ctx = canvas.getContext("2d");

var fishTypes = [
  { name: "金鱼", color: "#FFD700" },
  { name: "鲤鱼", color: "#FF4500" },
  { name: "鲨鱼", color: "#708090" },
];

var fishes = [];
var shrimps = [];

for (var i = 0; i < 15; i++) {
  var fishType = fishTypes[Math.floor(Math.random() * fishTypes.length)];
  fishes.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speed: 1 + Math.random() * 2,
    direction: Math.random() * Math.PI * 2,
    hunger: Math.random() * 50,
    mood: "开心",
    type: fishType,
    size: 1,
  });

  shrimps.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speed: 1 + Math.random() * 2,
    direction: Math.random() * Math.PI * 2,
    size: 1,
  });
}

var food = [];

canvas.addEventListener("click", function (e) {
  var rect = canvas.getBoundingClientRect();
  var x = e.clientX - rect.left;
  var y = e.clientY - rect.top;
  food.push({ x: x, y: y, size: 10 });
});

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawPond();

  food.forEach(function (f, index) {
    drawFood(f.x, f.y, f.size);
    if (f.size <= 0) {
      food.splice(index, 1);
    }
  });

  fishes.forEach(function (fish, index) {
    drawFish(fish.x, fish.y, fish.type.color, fish.size);
    if (food.length > 0) {
      var closestFood = food[0];
      var minDist = distance(fish.x, fish.y, closestFood.x, closestFood.y);
      for (var i = 1; i < food.length; i++) {
        var dist = distance(fish.x, fish.y, food[i].x, food[i].y);
        if (dist < minDist) {
          closestFood = food[i];
          minDist = dist;
        }
      }
      fish.direction = Math.atan2(
        closestFood.y - fish.y,
        closestFood.x - fish.x
      );
      if (minDist < 5) {
        closestFood.size -= 1;
        fish.hunger -= 10;
        fish.size += 0.1; // 鱼长大
        if (fish.hunger < 0) fish.hunger = 0;
        fish.mood = "开心"; // 鱼吃到食物,心情变好
      }
    }

    fish.x += Math.cos(fish.direction) * fish.speed;
    fish.y += Math.sin(fish.direction) * fish.speed;
    if (
      fish.x < 0 ||
      fish.x > canvas.width ||
      fish.y < 0 ||
      fish.y > canvas.height
    ) {
      fish.direction += Math.PI;
    }

    drawFish(fish.x, fish.y, fish.type.color);
    drawFishInfo(fish.x, fish.y, fish.type.name, fish.hunger, fish.mood);

    // 如果鱼饥饿,心情变坏
    if (fish.hunger > 80) {
      fish.mood = "饿";
    }

    // 如果鱼长时间未吃到食物,心情变沮丧
    if (fish.hunger > 90) {
      fish.mood = "沮丧";
    }

    // 每帧鱼的饥饿度增加
    fish.hunger += 0.1;
  });

  shrimps.forEach(function (shrimp) {
    drawShrimp(shrimp.x, shrimp.y, shrimp.size);

    shrimp.x += Math.cos(shrimp.direction) * shrimp.speed;
    shrimp.y += Math.sin(shrimp.direction) * shrimp.speed;
    if (
      shrimp.x < 0 ||
      shrimp.x > canvas.width ||
      shrimp.y < 0 ||
      shrimp.y > canvas.height
    ) {
      shrimp.direction += Math.PI;
    }
    drawShrimp(shrimp.x, shrimp.y);
  });

  requestAnimationFrame(update);
}

function drawPond() {
  // 画塘
  ctx.fillStyle = "#00aaff";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}

function drawFish(x, y, color, size) {
  ctx.fillStyle = color;

  // 画鱼的身体
  ctx.beginPath();
  ctx.ellipse(x, y, size * 15, size * 5, 0, 0, Math.PI * 2);
  ctx.fill();

  // 画鱼的尾巴
  ctx.beginPath();
  ctx.moveTo(x + size * 15, y);
  ctx.lineTo(x + size * 25, y + size * 5);
  ctx.lineTo(x + size * 25, y - size * 5);
  ctx.closePath();
  ctx.fill();

  // 画鱼的眼睛
  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.arc(x + size * 2, y - size * 2, size * 2, 0, Math.PI * 2);
  ctx.fill();
}

function drawFishInfo(x, y, name, hunger, mood) {
  ctx.fillStyle = "black";
  ctx.fillText("品种: " + name, x, y - 40);
  ctx.fillText("饥饿度: " + Math.floor(hunger), x, y - 30);
  ctx.fillText("心情: " + mood, x, y - 20);
}

function drawShrimp(x, y, size) {
  ctx.fillStyle = "#FF4500";

  // 画虾的身体
  ctx.beginPath();
  ctx.ellipse(x, y, size * 6, size * 3, 0, 0, Math.PI * 2);
  ctx.fill();

  // 画虾的尾巴
  ctx.beginPath();
  ctx.moveTo(x - size * 6, y);
  ctx.lineTo(x - size * 10, y + size * 4);
  ctx.lineTo(x - size * 10, y - size * 4);
  ctx.closePath();
  ctx.fill();

  // 画虾的触须
  ctx.strokeStyle = "#FF4500";
  ctx.beginPath();
  ctx.moveTo(x + size * 6, y);
  ctx.lineTo(x + size * 12, y);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(x + size * 6, y);
  ctx.lineTo(x + size * 10, y + size * 2);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(x + size * 6, y);
  ctx.lineTo(x + size * 10, y - size * 2);
  ctx.stroke();
}

function drawFood(x, y, size) {
  if (size <= 0) return; // 如果大小为0或负,不画这块食物

  ctx.fillStyle = "#8B4513";
  ctx.beginPath();
  ctx.arc(x, y, size, 0, Math.PI * 2);
  ctx.fill();
}

function distance(x1, y1, x2, y2) {
  return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}

update();