像素电子宠物

Canvas 像素风格电子宠物:喂食 / 玩耍 / 睡觉三状态,饱食度、快乐值与精力实时衰减。

饱食度
快乐值
精力

宠物正在打盹…

使用体验

  1. 三条状态条随时间缓慢衰减:饱食度 / 快乐值 / 精力;宠物表情随状态变化。
  2. “喂食”加饱食度、“玩耍”加快乐但耗精力、“睡觉”快速回精力并暂停衰减。
  3. Canvas 使用像素化渲染(image-rendering: pixelated),8 位机质感拉满。

代码要点

js
const pet = { full: 80, happy: 60, energy: 70, sleeping: false };

function tick(dt) {          // 每帧衰减
  if (!pet.sleeping) {
    pet.full  = clamp(pet.full  - dt * 0.5);   // 每秒 -0.5
    pet.happy = clamp(pet.happy - dt * 0.3);
    pet.energy = clamp(pet.energy - dt * 0.4);
  } else {
    pet.energy = clamp(pet.energy + dt * 2.2); // 睡觉快速回精力
  }
  updateBars();
  drawPet(faceByState());    // 表情由状态推导
}

// 像素画:按 8x8 精灵图逐格填充,实现像素风
function drawPixel(ctx, sprite, px, py, cell) {
  for (let y = 0; y < sprite.length; y++)
    for (let x = 0; x < sprite[y].length; x++)
      if (sprite[y][x]) ctx.fillRect(px + x * cell, py + y * cell, cell, cell);
}

完整可运行工程

可直接运行纯前端 · 零依赖状态每秒衰减

把演示区的「像素电子宠物」抽成独立工程:状态机(喂食 / 玩耍 / 睡觉)+ 按秒衰减 + 像素风渲染,状态机是纯函数,可在命令行自测。
依赖清单:零第三方依赖(Canvas 2D + localStorage),无需 npm install。

pixel-pet/ ├─ index.html 画布 + 交互按钮 + 状态条 ├─ app.js 状态机 / 衰减 / 像素渲染 ├─ styles.css 样式(跟随系统深浅色) └─ README.md 运行方式与扩展点

index.html

画布 + 三个交互按钮 + 状态条

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>像素电子宠物 · 独立工程版</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="wrap">
    <h1>像素电子宠物</h1>
    <p class="sub">喂食 / 玩耍 / 睡觉三状态,饱食度、快乐值与精力实时衰减。</p>
    <section class="panel">
      <canvas id="pet" height="220"></canvas>
      <div class="row" style="margin-top:14px">
        <button id="feed" type="button" class="btn">喂食</button>
        <button id="play" type="button" class="btn ghost">玩耍</button>
        <button id="sleep" type="button" class="btn ghost">睡觉 / 唤醒</button>
      </div>
      <div class="bars" id="bars"></div>
      <p class="msg" id="say">它正看着你。</p>
    </section>
  </main>
  <script src="app.js"></script>
</body>
</html>

app.js

状态机 / 衰减 / 像素渲染(action 可测)

javascript
/* app.js —— 像素电子宠物:状态机 + 衰减 + 渲染(状态机为纯逻辑,node 可测) */
var DECAY = { full: 0.55, happy: 0.4, energy: 0.3 };

function createPet() { return { full: 80, happy: 60, energy: 70, sleeping: false }; }

/* clamp100:数值收敛到 [0,100] */
function clamp100(v) { return Math.max(0, Math.min(100, v)); }

/* action:执行一次交互,返回 { pet, msg }(纯函数,便于自测) */
function action(pet, kind) {
  var p = { full: pet.full, happy: pet.happy, energy: pet.energy, sleeping: pet.sleeping };
  var msg = "";
  if (kind === "feed") {
    if (p.full >= 98) { msg = "它吃饱了,扭头不理你。"; }
    else { p.full = clamp100(p.full + 18); p.happy = clamp100(p.happy + 3); msg = "吃得很香。"; }
  } else if (kind === "play") {
    if (p.sleeping) { msg = "它睡着了,别吵。"; }
    else if (p.energy < 12) { msg = "太累了,玩不动。"; }
    else { p.happy = clamp100(p.happy + 16); p.energy = clamp100(p.energy - 14); p.full = clamp100(p.full - 6); msg = "玩得很开心。"; }
  } else if (kind === "sleep") {
    p.sleeping = !p.sleeping;
    msg = p.sleeping ? "它蜷起来睡了。" : "它醒了,伸了个懒腰。";
  }
  return { pet: p, msg: msg };
}

/* tickDecay:按秒衰减 */
function tickDecay(pet, dt) {
  var rate = pet.sleeping ? 0.35 : 1;
  if (pet.sleeping) pet.energy = clamp100(pet.energy + 0.9 * dt);
  else { pet.full = clamp100(pet.full - DECAY.full * dt); pet.happy = clamp100(pet.happy - DECAY.happy * dt); pet.energy = clamp100(pet.energy - DECAY.energy * dt); }
  if (pet.full <= 0 || pet.happy <= 0) pet.happy = clamp100(pet.happy - 1.5 * dt * rate);
  return pet;
}

if (typeof module !== "undefined" && module.exports) {
  module.exports = { createPet: createPet, action: action, tickDecay: tickDecay, clamp100: clamp100 };
}

if (typeof document !== "undefined") {
  document.addEventListener("DOMContentLoaded", function () {
    var canvas = document.getElementById("pet"), ctx = canvas.getContext("2d");
    var barsEl = document.getElementById("bars"), sayEl = document.getElementById("say");
    var pet = createPet(), last = 0, blink = 0;

    ["full", "happy", "energy"].forEach(function (k) {
      var row = document.createElement("div");
      row.className = "bar";
      row.innerHTML = "<span>" + (k === "full" ? "饱食" : k === "happy" ? "快乐" : "精力") + "</span><span><i id='bar-" + k + "'></i></span><span id='val-" + k + "'>0</span>";
      barsEl.appendChild(row);
    });

    function resize() {
      var dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = canvas.clientWidth * dpr;
      canvas.height = 220 * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.imageSmoothingEnabled = false;
    }

    function draw() {
      var W = canvas.clientWidth, H = 220;
      ctx.clearRect(0, 0, W, H);
      ctx.fillStyle = pet.sleeping ? "#0d1420" : "#121a14";
      ctx.fillRect(0, 0, W, H);
      var cx = W / 2, cy = H / 2 + (pet.sleeping ? 12 : 0);
      ctx.fillStyle = pet.happy > 40 ? "#8fd6a8" : "#c9a06a";
      ctx.fillRect(cx - 34, cy - 30, 68, 52);
      ctx.fillRect(cx - 46, cy - 14, 12, 20);
      ctx.fillRect(cx + 34, cy - 14, 12, 20);
      ctx.fillRect(cx - 20, cy + 22, 14, 8);
      ctx.fillRect(cx + 6, cy + 22, 14, 8);
      ctx.fillStyle = "#0d1117";
      if (pet.sleeping || blink % 100 < 12) {
        ctx.fillRect(cx - 18, cy - 10, 12, 3);
        ctx.fillRect(cx + 6, cy - 10, 12, 3);
      } else {
        ctx.fillRect(cx - 18, cy - 14, 12, 12);
        ctx.fillRect(cx + 6, cy - 14, 12, 12);
      }
      if (!pet.sleeping && pet.happy > 55) {
        ctx.fillStyle = "#f7768e";
        ctx.fillRect(cx - 26, cy + 4, 10, 2);
        ctx.fillRect(cx + 16, cy + 4, 10, 2);
      }
      if (pet.sleeping) {
        ctx.fillStyle = "rgba(160,190,255,.8)";
        ctx.font = "13px system-ui";
        ctx.fillText("Z", cx + 42, cy - 24);
        ctx.fillText("z", cx + 54, cy - 38);
      }
    }

    function paint() {
      var map = { full: "bar-full", happy: "bar-happy", energy: "bar-energy" };
      Object.keys(map).forEach(function (k) {
        document.getElementById(map[k]).style.width = pet[k] + "%";
        document.getElementById("val-" + k).textContent = Math.round(pet[k]);
      });
      draw();
    }

    function loop(now) {
      var dt = last ? Math.min((now - last) / 1000, 1) : 0;
      last = now;
      tickDecay(pet, dt);
      blink++;
      paint();
      requestAnimationFrame(loop);
    }

    ["feed", "play", "sleep"].forEach(function (kind) {
      document.getElementById(kind).addEventListener("click", function () {
        var r = action(pet, kind);
        pet = r.pet;
        sayEl.textContent = r.msg;
        paint();
      });
    });
    window.addEventListener("resize", function () { resize(); draw(); });
    resize();
    requestAnimationFrame(loop);
  });
}

styles.css

样式:跟随系统深浅色

css
:root {
  --bg: #0e1014; --panel: #171a20; --ink: #e8ecf1; --muted: #9aa4b2; --line: #262b33; --brand: #6fb3a8;
}
@media (prefers-color-scheme: light) {
  :root { --bg: #f6f7f8; --panel: #ffffff; --ink: #1a1d21; --muted: #5b636e; --line: #e3e6ea; }
}
* { box-sizing: border-box; }
body {
  margin: 0; padding: 24px 16px 48px; background: var(--bg); color: var(--ink);
  font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
}
.wrap { max-width: 760px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 6px; }
h2 { font-size: 1.05rem; margin: 22px 0 10px; }
.sub { color: var(--muted); margin: 0 0 18px; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.btn {
  padding: 8px 16px; border-radius: 8px; border: 1px solid var(--brand);
  background: var(--brand); color: #0b0f12; cursor: pointer; font-size: .92rem;
}
.btn.ghost { background: transparent; color: var(--brand); }
.msg { color: var(--muted); margin: 12px 0 0; font-size: .92rem; }
.bars { display: grid; gap: 6px; margin-top: 10px; }
.bar { display: grid; grid-template-columns: 64px 1fr 44px; gap: 8px; align-items: center; font-size: .88rem; }
.bar i { display: block; height: 8px; border-radius: 6px; background: var(--brand); }
.bar span:first-child { color: var(--muted); }
input, select {
  padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg); color: var(--ink); font: inherit;
}
textarea { width: 100%; min-height: 90px; padding: 10px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); color: var(--ink); font: inherit; }
canvas { width: 100%; display: block; border-radius: 10px; }
.chat { max-height: 260px; overflow: auto; display: grid; gap: 10px; }
.chat .me, .chat .pet { padding: 9px 12px; border-radius: 12px; max-width: 82%; font-size: .93rem; }
.chat .me { background: rgba(111,179,168,.16); justify-self: end; }
.chat .pet { background: rgba(255,255,255,.06); justify-self: start; }

README.md

运行方式与扩展点

markdown
# 像素电子宠物(独立可运行工程)

作品站 fun 区「像素电子宠物」的独立工程版:纯前端、零依赖。

## 文件
- `index.html`:画布 + 三个交互按钮 + 状态条
- `app.js`:状态机(喂食 / 玩耍 / 睡觉)、按秒衰减、像素渲染
- `styles.css`:样式

## 运行
1. 双击 `index.html`,点按钮观察状态变化与衰减
2. 逻辑自测:`node -e "const m=require('./app.js');const r=m.action(m.createPet(),'play');console.log(r.pet.happy>60, r.msg)"` → 期望 `true 玩得很开心。`

## 扩展点
- 存档:把 `pet` 序列化到 localStorage,页面加载时恢复
- 加情绪动画:按 `happy` 与 `energy` 阈值切换多套精灵帧

运行方式

  1. 新建一个目录,把上面全部文件按文件名分别保存(用「一键复制整份工程」可一次拿全)。
  2. 前端部分:直接双击 index.html 即可运行;若浏览器限制本地文件读取,执行 python -m http.server 8000 后访问 http://localhost:8000/。
  3. 逻辑自测:各文件 README.md 中给出了对应的 Node / Python 自测命令(均为真实可跑的最小用例)。

标注:可直接运行、无需构建;要改造只需动 app.js 顶部常量与 styles.css 里的变量。

复制结果为 # ===== 文件名 ===== 分隔的多文件文本,按文件名分别保存即可;也可逐块用右上角“复制代码”单独取用。

来源参考

实现涉及的官方文档(可直接访问):