从零给 Hexo + Shoka 博客加一只桌面宠物:会呼吸、会挥手、能拖动、能记住位置和大小,电脑默认显示、手机默认隐藏。本文提供一个可以直接复制运行的最小版本,完全不需要安装依赖;文末再说明如何升级成本站的 106 动作完整版。
#前言
很多博客的“宠物”看起来很复杂,实际上可以拆成三个很朴素的部分:
- 一张会动的图:透明背景的 WebM 视频,或者一张透明 WebP 静图;
- 一个固定在屏幕上的盒子:用
position: fixed放在右下角,用 CSStransform移动; - 一点本地状态:用
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 与素材许可说明。
下载后校验(可选,但建议做):
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 ),按顺序检查:
- 右下角工具栏出现一个宠物图标;
- 电脑上宠物默认出现在右下角,待机动画在动;
- 点击宠物,播放一次挥手,然后回到待机;
- 按住拖动,宠物跟随鼠标,松手后停在新位置;
- 点宠物右上角的
···,菜单打开; - 拖动“大小”滑块,宠物变大变小;
- 点“回到右下角”,回到右下角;
- 点“隐藏宠物”,消失;再点工具栏图标,重新出现;
- 刷新页面,大小和位置仍然保留;
- 按 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 三个动作。要增加动作,可以按这个顺序扩展:
在
PET.assets中增加一项,例如:sleep: { video: 'sleep.webm', poster: 'idle.webp' }
在 HTML 模板字符串里增加一个
<video data-pet-video="sleep">;在菜单里增加一个按钮,点击时调用
playAction('sleep');在
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 |
#还是不行,怎么排查?
按顺序做:
F12 → Console:先看第一条红色错误,不要看最后一条;
F12 → Network:筛选
blog-pet,确认 4 个素材和 JS/CSS 都是 200,不是 404;在 Console 输入:
localStorage.removeItem('shoka-pet-lite-v1');
location.reload();
如果还是空白,把
blog-pet-lite.js里的if (window.ShokaPetLite) return;临时注释掉,确认不是重复加载;确认没有把文件放进
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