Lavender's Blog

浩瀚众星,皆降为尘

在字里行间,遇见灵感

从零给 Hexo + Shoka 博客加一只桌面宠物:会呼吸、会挥手、能拖动、能记住位置和大小,电脑默认显示、手机默认隐藏。本文提供一个可以直接复制运行的最小版本,完全不需要安装依赖;文末再说明如何升级成本站的 106 动作完整版。

#前言

很多博客的“宠物”看起来很复杂,实际上可以拆成三个很朴素的部分:

  1. 一张会动的图:透明背景的 WebM 视频,或者一张透明 WebP 静图;
  2. 一个固定在屏幕上的盒子:用 position: fixed 放在右下角,用 CSS transform 移动;
  3. 一点本地状态:用 localStorage 记住显示/隐藏、大小和位置,不需要数据库,也不需要服务器。

本文会带你做一个最小可用版:

  • 右下角工具栏出现宠物开关;
  • 电脑默认显示,手机/平板默认隐藏,点开关可手动打开;
  • 待机循环动画,点击播放一次挥手,拖拽时切换拖动动画;
  • 拖到任意位置,松手后保存;刷新后仍在原处;
  • 菜单里可以调大小、回到右下角、隐藏宠物;
  • 支持日间/夜间主题色、减弱动效、键盘操作和 PJAX 站内切页;
  • 不调用 AI、不联网、不需要后端。

本文的示例素材来自开源项目 PC2005-cloud/dsh-pet。代码可以自由修改,但角色和动画素材禁止商用,大肥鱼形象为 CC BY-NC-SA 4.0。如果要商用,请先换成你有商用授权的角色素材。文末有完整说明。

#开始前准备

#1. 确认环境

在博客根目录打开终端(Windows 用 PowerShell,macOS 用 Terminal),执行:

node -v
npx hexo version

能看到 Node.js 和 Hexo 版本即可。本文假设你已经有一个能正常运行的 Hexo + Shoka 博客,并且知道:

  • 博客根目录:包含 source/ 、 themes/ 、 _config.yml 的那一层;
  • 主题目录: themes/shoka/ ;
  • 会用文件管理器复制文件;
  • 会用编辑器(VS Code、Sublime、记事本均可)打开文本文件。

#2. 备份

先备份这几个位置,改坏了可以恢复:

# macOS / Linux
cp -r source/assets /tmp/blog-assets-backup
cp themes/shoka/layout/_partials/layout.njk /tmp/layout.njk.backup

Windows PowerShell:

Copy-Item -Recurse source\assets "$env:TEMP\blog-assets-backup"
Copy-Item themes\shoka\layout\_partials\layout.njk "$env:TEMP\layout.njk.backup"

#3. 下载示例包

本文配套一个 ZIP,里面已经放好:

  • 3 个透明 WebM 动画:待机、拖动、挥手;
  • 1 张透明 WebP 海报;
  • 完整的 blog-pet-lite.js 和 blog-pet-lite.css ;
  • README 与素材许可说明。

下载宠物 starter ZIP

下载 SHA-256 校验文件

下载后校验(可选,但建议做):

Get-FileHash .\blog-pet-starter-v1.zip -Algorithm SHA256
sha256sum blog-pet-starter-v1.zip

对比文件里的值;不一致就不要使用。

如果你不想用示例素材,也可以自己准备动画;第 6 步会讲素材要求。先把示例跑通,再替换,排错会简单很多。

#文件结构

解压 ZIP 后是:

blog-pet-starter/
├─ assets/blog-pet/
│  ├─ idle.webm
│  ├─ idle.webp
│  ├─ drag.webm
│  └─ wave.webm
├─ js/blog-pet-lite.js
├─ css/blog-pet-lite.css
├─ README.md
└─ LICENSE.txt

复制到博客后,会多出这些文件:

source/assets/blog-pet/          ← 新建,放 4 个素材
themes/shoka/source/js/blog-pet-lite.js
themes/shoka/source/css/blog-pet-lite.css
themes/shoka/layout/_partials/layout.njk   ← 只改 2 行

#实现原理

先理解这一张图,后面就不会晕:

透明视频/海报
      ↓
<div class="pet-lite">         固定在屏幕左上角,用 transform 移到右下角
      ↓
pointerdown / pointermove     拖动时改 x、y
      ↓
localStorage                  保存显示状态、大小、坐标
  • 视频: <video muted playsinline loop> 自动播放待机动画;点击时换 wave.webm ,播完切回待机;拖动时换 drag.webm 。
  • 透明:WebM 使用 VP9 + alpha 通道;Safari 对带 alpha 的 VP9 支持不稳定,所以准备一张 idle.webp 作为静图后备。
  • 拖动:监听 pointerdown / pointermove / pointerup ,把鼠标或手指的位移加到宠物坐标上,同时把坐标限制在视口内。
  • 记忆:显示状态按设备(电脑/手机)分别保存,大小和位置共用;刷新后从 localStorage 恢复。
  • 不重复:脚本用 window.ShokaPetLite 做单例保护;Shoka 的 PJAX 只换正文,不重新执行脚本,宠物不会被创建两次。

#第 1 步:放素材

把解压出来的 assets/blog-pet 整个文件夹复制到:

source/assets/blog-pet/

复制完成后检查:

source/assets/blog-pet/idle.webm
source/assets/blog-pet/idle.webp
source/assets/blog-pet/drag.webm
source/assets/blog-pet/wave.webm

不要放进 themes/shoka/source/assets/ ,也不要放进 public/ 。 public/ 是生成结果,下次 hexo clean 会被删掉。正确位置是博客根目录下的 source/assets/ 。

#第 2 步:放 JS 和 CSS

把两个文件分别复制到:

themes/shoka/source/js/blog-pet-lite.js
themes/shoka/source/css/blog-pet-lite.css

这两个文件不会自动加载,下一步要在模板里引入。

#第 3 步:在模板里引入

打开:

themes/shoka/layout/_partials/layout.njk

#3.1 引入 CSS

找到 </head> ,在它上面加一行:

  <link rel="stylesheet" href="{{ url_for('/css/blog-pet-lite.css') }}?v=20261027">

改完大概是这样:

  <link rel="stylesheet" href="{{ url_for('/css/blog-pet-lite.css') }}?v=20261027">
</head>

#3.2 引入 JS

找到文件末尾的 </body> ,在它上面加一行:

  <script src="{{ url_for('/js/blog-pet-lite.js') }}?v=20261027" defer></script>
</body>

defer 表示等 HTML 解析完再执行,这样脚本一定能找到 #tool 和 body 。

url_for() 会处理博客的根路径(例如站点部署在 /blog/ 时也能用)。不要写成 src="js/..." 这种相对路径,否则在文章页会找错目录。

#第 4 步:启动并验收

在博客根目录执行:

hexo clean
hexo server

打开终端提示的地址(通常是 http://localhost:4000 ),按顺序检查:

  1. 右下角工具栏出现一个宠物图标;
  2. 电脑上宠物默认出现在右下角,待机动画在动;
  3. 点击宠物,播放一次挥手,然后回到待机;
  4. 按住拖动,宠物跟随鼠标,松手后停在新位置;
  5. 点宠物右上角的 ··· ,菜单打开;
  6. 拖动“大小”滑块,宠物变大变小;
  7. 点“回到右下角”,回到右下角;
  8. 点“隐藏宠物”,消失;再点工具栏图标,重新出现;
  9. 刷新页面,大小和位置仍然保留;
  10. 按 F12 打开控制台,确认没有红色报错。

如果 1~10 都通过,最小宠物已经完成。

第一次点击可能需要等 1 秒加载 wave.webm ,这是正常的:为了不让手机用户一进站就下载全部动画,脚本只在需要时加载拖动/挥手视频。

#第 5 步:手机和平板

用 Chrome 开发者工具的设备模拟,或直接用手机访问:

  • 手机/平板默认不显示宠物,这是 defaultVisible: 'desktop' 的设计;
  • 点右下角工具栏的宠物图标即可显示;
  • 手机上的选择会保存在当前浏览器,不会影响电脑;
  • 拖动是触摸操作,长按约 0.55 秒也可以打开菜单。

为什么默认隐藏?宠物动画有几百 KB,而且会占屏幕;手机流量和注意力都更宝贵。需要的人手动打开即可。

#第 6 步:换成自己的宠物

打开 js/blog-pet-lite.js ,文件顶部就是配置:

var PET = {
  name: '大肥鱼',
  defaultVisible: 'desktop',
  size: 300,
  minSize: 180,
  maxSize: 520,
  assetBase: (window.CONFIG && window.CONFIG.root ? window.CONFIG.root : '/') + 'assets/blog-pet/',
  assets: {
    idle: { video: 'idle.webm', poster: 'idle.webp' },
    drag: { video: 'drag.webm', poster: 'idle.webp' },
    wave: { video: 'wave.webm', poster: 'idle.webp' }
  }
};

#6.1 改名字和默认行为

  • name :宠物名字,会用于无障碍标签;
  • defaultVisible :
    • 'desktop' :电脑显示、手机隐藏(推荐);
    • 'always' :所有设备默认显示;
    • false :所有设备默认隐藏。
  • size :初始宽度(像素);高度按 16:9 自动计算;
  • minSize / maxSize :滑块范围。

#6.2 换动画

保持文件名不变,直接替换这 4 个文件最省事:

idle.webm   待机循环
drag.webm   拖动时循环
wave.webm   点击时播放一次
idle.webp   静图后备(Safari/视频失败时显示)

素材要求:

  • 画布比例建议 16:9;
  • 背景必须透明;
  • 视频使用 WebM(VP9 + alpha),不要用带黑底的普通 MP4;
  • 文件尽量控制在 200–500 KB/个,太大手机加载慢;
  • idle.webp 用待机第一帧,保留透明通道。

如果你有带透明通道的 MOV/PNG 序列,可以用 ffmpeg 转:

ffmpeg -i input.mov \
  -c:v libvpx-vp9 \
  -pix_fmt yuva420p \
  -b:v 0 -crf 32 \
  -an idle.webm

参数解释:

  • libvpx-vp9 :VP9 编码;
  • yuva420p :保留 alpha 透明通道;
  • -crf 32 :质量/体积平衡,数字越小越清晰也越大;
  • -an :不要音轨,宠物视频不需要声音。

如果源文件没有透明通道(例如普通 MP4),需要先抠掉背景,否则会出现黑框或白框。

#6.3 改颜色和图标

颜色在 css/blog-pet-lite.css 中,优先使用 Shoka 已有变量:

.pet-lite__menu-button {
  background: var(--grey-1-a7, rgba(255, 255, 255, .85));
  color: var(--primary-color, #8b5cf6);
}

var(--primary-color) 会跟随主题色变化;后面的 #8b5cf6 只是变量不存在时的后备颜色。想换宠物图标,改 blog-pet-lite.js 里 toggle.innerHTML 的 SVG 即可。

#第 7 步:可选,改成多动作版

最小版只有 idle / drag / wave 三个动作。要增加动作,可以按这个顺序扩展:

  1. 在 PET.assets 中增加一项,例如:

    sleep: { video: 'sleep.webm', poster: 'idle.webp' }
  2. 在 HTML 模板字符串里增加一个 <video data-pet-video="sleep"> ;

  3. 在菜单里增加一个按钮,点击时调用 playAction('sleep') ;

  4. 在 playAction 的结束逻辑里处理“播放一次后回待机”。

当动作变多(例如几十个),不建议继续把每个动作都硬编码进 HTML,而应该:

  • 用 manifest.json 描述动作、分组、视频、海报;
  • 只保留 3 个 <video> 缓冲区,切换时复用;
  • 设置随机动作间隔、点击回应、拖拽/甩出等状态机;
  • 按需加载动作配置,避免首屏变大。

本站的完整大肥鱼就是这样做的:106 个动作、分类搜索菜单、AI 聊天、甩出去弹跳、减弱动效和移动端适配。它在 starter 的基础上扩展了:

  • blog-pet.js :完整动画/菜单/聊天/物理状态机;
  • _app/pet.js :懒加载配置、按设备记忆显示;
  • blog-pet-meta.cjs :固定动作 ID,避免依赖中文标签;
  • script.js :生成 assets/blog-pet/config.json ;
  • blog-ai.js :可选的“大肥鱼”聊天;
  • blog-pet.styl :完整菜单、气泡和聊天样式。

完整版不适合作为第一篇教程一次贴完;先把本文最小版跑通,再按需阅读本站完整宠物的源码与实现说明,会轻松很多。

#第 8 步:部署

本地检查无误后,执行:

hexo clean
hexo generate

确认 public/assets/blog-pet/ 下已经有 4 个素材,然后把 public/ 发布到你的服务器或 CDN。

如果使用 CDN,建议:

  • 给 blog-pet-lite.js 和 blog-pet-lite.css 的 ?v= 换成新的日期;
  • 素材文件也带版本号或刷新 CDN 缓存;
  • 先传静态资源,再刷新页面,避免 HTML 已更新但视频还是旧缓存。

宠物完全运行在浏览器里,不需要 Node 服务、数据库或后端接口。

#常见问题

现象可能原因解决
右下角没有宠物图标JS/CSS 没加载、路径写错、模板没保存F12 → Network 看 blog-pet-lite.js 是否 404;确认 layout.njk 两行都在
图标有,但宠物不出现手机默认隐藏;或上次点了隐藏点工具栏宠物图标;清除 shoka-pet-lite-v1 后刷新
宠物是黑框/白框视频没有 alpha 透明通道用 -pix_fmt yuva420p 重新导出 WebM;Safari 用 idle.webp
点击不播放挥手减弱动效、Safari 静图模式、视频 404关掉“减弱动效”看;F12 查 wave.webm
拖动没反应按钮被别的元素盖住; touch-action 被删检查 .pet-lite__body 的 pointer-events 和 touch-action:none
站内切页后出现两只脚本被重复引入确认模板只引入一次;脚本已有 window.ShokaPetLite 单例保护
刷新后位置/大小丢失浏览器无痕模式、禁用了 localStorage换普通窗口;检查控制台是否报存储错误
菜单跑到屏幕外宠物在屏幕顶部,菜单向上溢出CSS 已有 is-menu-below 自动向下;可调整 250 阈值
手机加载很慢视频太大压缩 WebM、降低分辨率、把 CRF 调到 34–36

#还是不行,怎么排查?

按顺序做:

  1. F12 → Console:先看第一条红色错误,不要看最后一条;

  2. F12 → Network:筛选 blog-pet ,确认 4 个素材和 JS/CSS 都是 200,不是 404;

  3. 在 Console 输入:

    localStorage.removeItem('shoka-pet-lite-v1');
    location.reload();
  4. 如果还是空白,把 blog-pet-lite.js 里的 if (window.ShokaPetLite) return; 临时注释掉,确认不是重复加载;

  5. 确认没有把文件放进 public/ ,而是放进 source/ 和 themes/shoka/source/ 。

#检查清单

#素材与许可

  • 示例素材来自 PC2005-cloud/dsh-pet:https://github.com/PC2005-cloud/dsh-pet
  • 上游代码 MIT;动画/角色素材禁止商用;
  • 大肥鱼形象:CC BY-NC-SA 4.0(署名 · 非商业 · 相同方式共享);
  • 使用或再发布时保留作者与来源链接;
  • 商业用途请替换成你有商用授权的宠物素材,不要直接使用本包角色。

#参考资料

  • PC2005-cloud/dsh-pet
  • Shoka 主题文档
  • 本站完整宠物实现: themes/shoka/source/js/blog-pet.js 、 themes/shoka/source/js/_app/pet.js 、 themes/shoka/source/css/blog-pet.styl
更新于 阅读次数 … 次

请我喝[茶]~( ̄▽ ̄)~*

Lavender 微信支付

微信支付

Lavender 支付宝

支付宝