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.jsonExpressions 里也对应改一下:

"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,差一个字母,一开始拼错了找了半天)~~


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐