给博客加一个 Live2D 看板娘,支持少女前线全角色 + 流萤 Firefly 自由切换,切换模型时对话内容也跟着换。全部资源本地化(不依赖 jsdelivr / raw.githubusercontent,国内直连可用),只有桌面端才加载,移动端零开销。
致谢:三个 Live2D 项目源头
本方案由三个开源项目组合而成,先致谢:
- stevenjoezhang/live2d-widget —— 看板娘主体框架。新版(npm
live2d-widgets系)自带 cubism2 + cubism5 双内核:老版.moc模型和 Cubism4/5 的.moc3模型都能加载,这是把两个不同世代的模型塞进同一个看板娘的关键。 - nova1751/live2d-api —— 少女前线全部角色的 Cubism2 模型(95 式、HK416、UMP9、M4、AK12、AN-94、G36C 等 20+ 角色,含多套皮肤/破损形态)。原项目走 jsdelivr CDN,国内不稳,本文把它本地化。
- Iskongkongyo/Firefly-live2d —— 流萤(崩坏:星穹铁道)Cubism4 模型,含唱歌、表情、语音、对话消息框。原项目是 PixiJS 独立加载器,本文只取它的模型文件(
model/)喂给 live2d-widget 的 cubism5 内核。
三个项目均按各自 LICENSE 使用;模型版权归原厂商(少女前线 / 米哈游),本文仅作个人博客装饰用途。
原理:为什么能一个看板娘装两个世代的模型
- live2d-widget 新版加载模型时先 fetch 模型的
index.json,看Version字段或有没有FileReferences自动分流:- Version 2 / 老格式(
model.moc+textures+motions)→ 用 cubism2 内核(live2d.min.js+chunk/index.js) - Version 3 / 有 FileReferences(
.moc3+model3.json)→ 用 cubism5 内核(live2dcubismcore.min.js+chunk/index2.js)
- Version 2 / 老格式(
- 少女前线模型就是老格式 → cubism2 内核;流萤是 Cubism4(
.moc3)→ 把它model3.json复制成index.json放流萤目录,自动走 cubism5 内核。 - 模型列表写在
waifu-tips.json的models数组里,每项paths是该模型全部形态(切衣服用),message是它专属的开场白——switch-model工具循环模型的同时把message也带上,对话随模型切换。
博客目录结构(public/live2d-widget/)
public/live2d-widget/├─ waifu-tips.js # 看板娘逻辑(新版 ESM)├─ waifu-tips.json # 对话内容 + 模型列表(重点配置)├─ waifu.css # 样式(含右下角定位)├─ live2d.min.js # cubism2 内核├─ cubismcore/│ └─ live2dcubismcore.min.js # cubism5 内核(来自 Firefly 项目)├─ chunk/│ ├─ index.js # cubism2 辅助模块(ESM chunk)│ └─ index2.js # cubism5 内核模块(ESM chunk,AppDelegate)└─ model/ ├─ <少女前线角色>/... # 从 live2d-api 全量拷贝(20+ 角色) └─ firefly/ # 从 Firefly-live2d 的 model/ 拷贝 ├─ index.json # = Firefly.model3.json 复制改名(关键!) ├─ FileReferences_Moc_0.moc3 ├─ textures/ motions/ physics/ sounds/ expressions/步骤
1. 下载三个项目
# live2d-widget:只要 dist/ 下的产物# dist/waifu-tips.js dist/waifu-tips.json dist/waifu.css# dist/live2d.min.js dist/chunk/index.js dist/chunk/index2.js# live2d-api:只要 model/ 目录(全量角色)# Firefly-live2d:只要 model/ 目录 + load/live2dcubismcore.min.jsGitHub 直链下载:https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>,或 codeload.github.com/<owner>/<repo>/zip/refs/heads/<branch> 下整包再挑文件。
2. 组装到博客 public/
把上述文件按上面的目录结构放进 public/live2d-widget/。注意两点:
- 流萤的
index.json:把Firefly.model3.json原样复制一份改名为index.json。live2d-widget 固定 fetch<模型路径>/index.json,看到Version: 3+FileReferences才走 cubism5 内核。 - GFL 模型不用改:每个形态目录(如
HK416/HK416_805/normal/)自带的index.json就是老格式,直接可用。
3. 写 waifu-tips.json(模型列表 + 对话迁移)
保留 stevenjoezhang 默认的 message / seasons(点按、拍照、节日对话模板),替换 models 数组为本地模型。每个模型条目:
{ "name": "95type", "paths": [ "/live2d-widget/model/95type/95type_405/normal/index.json", "/live2d-widget/model/95type/95type_405/destroy/index.json", "/live2d-widget/model/95type/95type_3702/normal/index.json", "/live2d-widget/model/95type/95type_3702/destroy/index.json" ], "message": "少女前线 Girls Frontline~ 95type"}paths必须是绝对路径(以/开头),因为页面可能挂在/posts/xxx/这类子路径,相对路径会解析错。paths里多个形态 =switch-texture切衣服的候选。message是该模型的专属开场白——切到它时自动显示(对话随模型迁移)。少女前线的 message 直接用 live2d-api 的model_list.json里同款文案,流萤的自定义。
批量生成:读 live2d-api 的 model_list.json(models 数组每组路径列表 + messages 平行数组),展开成上面的格式,再追加流萤条目即可。
4. 页面注入(Layout.astro)
在站点全局布局末尾加一段内联脚本,桌面端才加载(screen.width >= 768),避免移动端下载 200MB+ 模型:
<script is:inline> if (screen.width >= 768) { (function () { const live2d_path = "/live2d-widget/"; function loadExternalResource(url, type) { return new Promise((resolve, reject) => { let tag; if (type === "css") { tag = document.createElement("link"); tag.rel = "stylesheet"; tag.href = url; } else if (type === "js") { tag = document.createElement("script"); tag.type = "module"; tag.src = url; } if (tag) { tag.onload = () => resolve(url); tag.onerror = () => reject(url); document.head.appendChild(tag); } }); } (async () => { // 避免跨域图片问题(拍屏功能用) const OriginalImage = window.Image; window.Image = function (...args) { const img = new OriginalImage(...args); img.crossOrigin = "anonymous"; return img; }; window.Image.prototype = OriginalImage.prototype; await Promise.all([ loadExternalResource(live2d_path + "waifu.css", "css"), loadExternalResource(live2d_path + "waifu-tips.js", "js"), ]); initWidget({ waifuPath: live2d_path + "waifu-tips.json", cubism2Path: live2d_path + "live2d.min.js", cubism5Path: live2d_path + "cubismcore/live2dcubismcore.min.js", tools: ["hitokoto", "switch-model", "switch-texture", "photo", "info", "quit"], logLevel: "warn", drag: false, }); })(); })(); }</script>与官方 autoload.js 的差异:
live2d_path指向本地/live2d-widget/;cubism2Path/cubism5Path全部本地化(官方默认 cubism5Path 指向 cubism.live2d.com,国内不一定通);去掉了asteroids工具(它动态加载 jsdelivr 上的脚本,国内不稳)。若想保留可以自行把 asteroids.js 也本地化。
5. 验证
# 部署后逐个验证资源可达$d='https://<你的域名>.pages.dev'foreach($u in @("$d/live2d-widget/waifu-tips.json","$d/live2d-widget/waifu-tips.js","$d/live2d-widget/live2d.min.js","$d/live2d-widget/chunk/index2.js","$d/live2d-widget/model/firefly/index.json")){ try { $r=Invoke-WebRequest $u -UseBasicParsing -TimeoutSec 20; "$($r.StatusCode) $u" } catch { "ERR $u" }}# 页面确认注入(Invoke-WebRequest "$d/" -UseBasicParsing).Content.Contains('live2d-widget')浏览器 Ctrl+F5 强刷首页,右下角出现看板娘;点工具栏的「切换模型」图标逐个换角色,观察模型与开场白一起变。
坑(全是实测踩出来的)
chunk/index.js和chunk/index2.js必须都在——cubism2 用 index.js,cubism5 用 index2.js,缺一个对应内核加载即报错。- cubism5 内核文件来自 Firefly 项目的
load/live2dcubismcore.min.js,别用 live2d-widget 官方 autoload 里那个 cubism.live2d.com 地址(国内可达性差)。 - waifu-tips.js 是 ESM module(
tag.type = "module"),它内部import("./chunk/index2.js")是相对路径——所以 chunk 目录必须和 waifu-tips.js 同级,不能改层级。 paths用绝对路径,否则子路径页面 404。- 移动端必须跳过加载——22 模型 125 形态约 235MB,全塞给手机 = 灾难。
screen.width >= 768门控。 - Pages 单文件 25MB 上限:本方案最大文件是流萤纹理 png(5MB),GFL 纹理 1-5MB,全部安全;如果以后塞进超大模型注意这个限制。
- 类型检查:
astro check0 errors 再部署;is:inline脚本不会被 Astro 打包处理,直接原样输出。
效果
- 一个看板娘位置(右下角),
switch-model循环 22 个模型 / 125 个形态 - 少女前线全角色(Cubism2)+ 流萤(Cubism4)自由切换,模型与对话同步迁移
- 全部资源本地托管在博客自己的 Pages 域名下,国内直连,无外部 CDN 依赖
- 桌面端才加载;页面缓存友好(静态文件,CF 边缘缓存)
更新记录(2026-09-17)
角色专属对话(220 句)
早期版本只有一套全局对话模板,切角色只是模型变、台词不变。新版给 waifu-tips.json 的每个模型加了 dialog 字段,每个角色一整套对话:
{ "name": "HK416", "paths": ["/live2d-widget/model/HK416/HK416_805/index.json"], "message": ["HK416 报道!指挥官,今天有什么任务?"], "dialog": { "default": ["哼,本小姐今天心情不错。", "指挥官,别盯着我看啦!"], "tapBody": ["喂喂!谁让你随便碰的!", "再碰的话我可要生气了!"], "hoverBody": ["看什么看,没见过美女吗?"], "welcome": "哼,这不是回来了吗?本小姐等你很久了。" }}default:定时随机闲聊(4 句)tapBody:点击模型(3 句)hoverBody:鼠标悬停(2 句)welcome:欢迎语(1 句)
22 个角色 × 10 句 = 220 句。实现上在 waifu-tips.js 注入了一个 q(key) 读取器:按 localStorage.modelId 取当前模型的 dialog[key],没有则回退全局 message[key]。这样日常随机、点击、悬停都会说该角色的台词。
注意优先级:切换模型的
message显示 4 秒(priority 10),期间会压住点击/悬停对话(9/8)——这是正常的,等切换语播完就轮到角色日常对话。
截图修复(via 浏览器不再卡死)
photo 工具原来用 canvas.toDataURL() 生成超大 data URL 塞进 <a download>——via 等手机浏览器处理超长 data URL 会内存暴涨直接卡死。改为标准 canvas.toBlob() + URL.createObjectURL():
e.toBlob((t) => { if (!t) return; const u = document.createElement("a"); const r = URL.createObjectURL(t); u.style.display = "none"; u.href = r; u.download = "live2d-photo.png"; document.body.appendChild(u); u.click(); document.body.removeChild(u); setTimeout(() => URL.revokeObjectURL(r), 1e3);});Blob 由浏览器原生管理,不产生超长字符串,下载后 1 秒回收 URL。
其他细节
- 模型位置:右下角 → 左下角(
#waifu { left: 0 }),工具菜单移到模型右侧(right: -35px),不再遮挡模型。 - 隐藏/唤出(移动端):
#waifu-toggle需要z-index: 2147483648高于#waifu(2147483647)——否则 waifu 背景透明但 z-index 高会拦截点击,手机上点橙色按钮没反应。移动端 toggle 常露 28px 可点,隐藏模型后露出全部 60px。 - 对话框位置:
#waifu-tips的margin-top从 -30px 改为 -70px,避免遮住角色的枪械模型。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 39 天
部分内容可能已过时