Hugo博客配置Live2D看板娘——免费皮套 + 表情装扮切换
Hugo博客配置Live2D看板娘——免费皮套 + 表情装扮切换
博客右下角的小人是 Live2D 看板娘,这篇记录一下怎么配置的,皮套用的是 B 站 UP 分享的免费模型,用 oh-my-live2d 这个库加载,顺带加了表情和装扮的切换按钮。
也给大家分享一下我的个人博客啦:Link

一、获取免费 Live2D 皮套
皮套来自这个视频:BV1S8411H7zf
视频里 UP 主分享了免费的模型文件,下载解压之后大概长这样:
model/ ariu.model3.json ← 模型入口文件 ariu.moc3 ← 模型数据 ariu.physics3.json ← 物理效果 ariu.cdi3.json ← 显示信息 ariu.2048/ ← 贴图文件夹 aixin.exp3.json ← 表情文件(一个文件一个表情) qqy.exp3.json heilian.exp3.json ...
把整个文件夹放到博客的 static/live2d/model/ 目录下就行。
注意:表情文件名不要用中文,不然跨平台可能会有路径问题,建议重命名成英文,然后在 ariu.model3.json 的 Expressions 里也对应改一下:
"Expressions": [
{ "Name": "aixin", "File": "aixin.exp3.json" },
{ "Name": "qqy", "File": "qqy.exp3.json" },
{ "Name": "heilian", "File": "heilian.exp3.json" },
{ "Name": "mz", "File": "mz.exp3.json" },
{ "Name": "waitao", "File": "waitao.exp3.json" },
{ "Name": "quinzi", "File": "quinzi.exp3.json" },
{ "Name": "jkbao", "File": "jkbao.exp3.json" },
{ "Name": "shoubing", "File": "shoubing.exp3.json" },
{ "Name": "maweir_hide", "File": "maweir_hide.exp3.json" },
{ "Name": "maweil_hide", "File": "maweil_hide.exp3.json" }
]
二、关掉主题自带的 Live2D
Reimu 主题自带两种 Live2D 方案,先都关掉,避免和我们自己的冲突。打开 config/_default/params.yml:
live2d: enable: false live2d_widgets: enable: false
三、用 oh-my-live2d 加载模型
新建 layouts/partials/live2d.html,写入以下完整代码:
<script src="https://unpkg.com/oh-my-live2d@latest"></script>
<script>
(function () {
if (window.__blogLive2dInitialized) return;
window.__blogLive2dInitialized = true;
var CLICK_MESSAGES = [
"你为什么要戳我?",
"今天天气真好啊!",
"快来和我一起玩耍吧,嘻嘻!",
"哎呀不要再碰我了!",
"你喜欢我的新衣服吗?",
"我好像听到了什么声音哦!",
"你在看什么呢?",
"我感觉有点冷,能不能给我一个拥抱?",
"你喜欢吃什么呢?",
"我好像看到了一只可爱的小动物!",
"你觉得我今天的发型怎么样?",
"我好像闻到了什么好吃的味道!",
"你喜欢我的新配件吗?",
"我好像看到了一只小鸟在飞!",
"你觉得我今天的表情怎么样?"
];
var IDLE_MESSAGES = [
"哎呀好无聊啊!",
"接下来应该干什么呢?",
"我好像听到了什么声音哦!",
"你在看什么呢?",
"我感觉有点冷,能不能给我一个拥抱?",
"你喜欢我吗?",
"我好像看到了一只可爱的小动物!",
"啦啦啦啦~"
];
// 表情分两组:脸部表情 和 装扮
var FACES = [
{ id: 'aixin', label: '爱心眼' },
{ id: 'qqy', label: '圈圈眼' },
{ id: 'heilian', label: '黑化' },
{ id: 'mz', label: '戴帽子' }
];
var OUTFITS = [
{ id: 'waitao', label: '脱外套' },
{ id: 'quinzi', label: '裙子' },
{ id: 'jkbao', label: 'JK包' },
{ id: 'shoubing', label: '手柄' },
{ id: 'maweir_hide', label: '马尾R隐藏' },
{ id: 'maweil_hide', label: '马尾L隐藏' }
];
var faceIdx = -1;
var outfitIdx = -1;
function nextFace(oml2d) {
faceIdx = (faceIdx + 1) % FACES.length;
var face = FACES[faceIdx];
oml2d.models.model.expression(face.id);
oml2d.tipsMessage('表情:' + face.label, 2500, 8);
}
function nextOutfit(oml2d) {
outfitIdx = (outfitIdx + 1) % OUTFITS.length;
var outfit = OUTFITS[outfitIdx];
oml2d.models.model.expression(outfit.id);
oml2d.tipsMessage('装扮:' + outfit.label, 2500, 8);
}
function startLive2D() {
if (typeof OML2D === "undefined") return;
try {
var oml2d = OML2D.loadOml2d({
dockedPosition: "right",
models: [
{
path: "/live2d/model/ariu/ariu.model3.json",
position: [0, 60],
scale: 0.1,
stageStyle: { width: 280, height: 400 }
}
],
menus: {
items: function (defaultItems) {
// 去掉默认的"切换衣服"按钮,加上我们自己的两个
var filtered = defaultItems.filter(function (item) {
return item.id !== 'SwitchModelClothes';
});
return filtered.concat([
{
id: 'SwitchFace',
title: '切换表情',
onClick: function (oml2d) { nextFace(oml2d); }
},
{
id: 'SwitchOutfit',
title: '切换装扮',
onClick: function (oml2d) { nextOutfit(oml2d); }
}
]);
}
},
tips: {
idleTips: {
wordTheDay: false,
duration: 4000,
interval: 15000,
message: IDLE_MESSAGES
}
}
});
// 用 MutationObserver 等菜单 DOM 就绪后注入图标,避免 setTimeout 固定延迟在首次加载时失效
var iconObserver = new MutationObserver(function () {
var faceIcon = document.querySelector('#SwitchFace .oml2d-icon');
var outfitIcon = document.querySelector('#SwitchOutfit .oml2d-icon');
if (!faceIcon || !outfitIcon) return;
faceIcon.setAttribute('viewBox', '0 0 24 24');
faceIcon.innerHTML =
'<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" fill="none"/>' +
'<circle cx="9" cy="10" r="1.5" fill="currentColor"/>' +
'<circle cx="15" cy="10" r="1.5" fill="currentColor"/>' +
'<path d="M9 14.5 Q12 17 15 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>';
outfitIcon.setAttribute('viewBox', '0 0 24 24');
outfitIcon.innerHTML =
'<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.57a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.57a2 2 0 0 0-1.34-2.13z"' +
' stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
iconObserver.disconnect();
});
iconObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(function () { iconObserver.disconnect(); }, 30000);
// 点击模型触发随机消息
document.addEventListener("click", function (event) {
var stage = document.getElementById("oml2d-stage");
if (!stage || !stage.contains(event.target)) return;
oml2d.tipsMessage(
CLICK_MESSAGES[Math.floor(Math.random() * CLICK_MESSAGES.length)],
2600,
10
);
});
} catch (error) {
console.error("Failed to load Live2D model", error);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startLive2D, { once: true });
} else {
startLive2D();
}
})();
</script>
几个参数说明一下:
-
path:模型入口文件路径,对应static/下的位置 -
scale:缩放倍数,越小模型越小 -
position:[x, y]偏移,正值往右/下移 -
stageStyle:画布宽高,根据模型大小调整 -
dockedPosition:"right"或"left",靠哪边
四、修一个 pjax 的报错
配完之后发现控制台一直报这个:
pjax_main.js: Cannot read properties of null (reading 'style') at __sidebarTopScrollHandler
原因是主题的 assets/js/pjax_main.ts 里有个滚动监听,用了 TypeScript 非空断言 !,但 pjax 跳转到没有侧边栏的页面时,.sidebar-top 元素不存在就直接报错了。
找到这段代码,加一行 null 检查就好:
// 修改前
__sidebarTopScrollHandler = () => {
const sidebarTop = _$(".sidebar-top")!;
if (document.documentElement.scrollTop < 10) {
sidebarTop.style.opacity = "0";
} else {
sidebarTop.style.opacity = "1";
}
};
// 修改后
__sidebarTopScrollHandler = () => {
const sidebarTop = _$(".sidebar-top");
if (!sidebarTop) return;
if (document.documentElement.scrollTop < 10) {
sidebarTop.style.opacity = "0";
} else {
sidebarTop.style.opacity = "1";
}
};
五、修一个图标首次加载不显示的问题
这个修改已经在上面三给的代码中更新了,主要是记录一下。
最初图标注入用的是 setTimeout(fn, 1000),但首次打开页面时模型还在网络加载,1 秒内菜单 DOM 根本不存在,注入失败,图标就没了。刷新后资源已经缓存,加载够快才正常显示。
换成 MutationObserver 来监听 DOM 变化,菜单元素一出现就立刻注入,不依赖固定延迟:
var iconObserver = new MutationObserver(function () {
var faceIcon = document.querySelector('#SwitchFace .oml2d-icon');
var outfitIcon = document.querySelector('#SwitchOutfit .oml2d-icon');
if (!faceIcon || !outfitIcon) return;
// 注入 SVG ...
iconObserver.disconnect();
});
iconObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(function () { iconObserver.disconnect(); }, 30000); // 兜底 30s 断开
找到两个图标就注入并断开,setTimeout 兜底是防止模型加载彻底失败时 observer 一直挂着。
就这些啦
整体步骤就是:找模型 → 放到 static/ → 关掉主题自带的 Live2D → 写 live2d.html 配置 → 加表情切换按钮。
唯一折腾了一会儿的地方是图标,oh-my-live2d 菜单的图标 SVG 类名是 .oml2d-icon(注意是 oml2d 不是 onl2d,差一个字母,一开始拼错了找了半天)~~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)