• 首页
  • 博客
  • 关于
  • 归档
  • 分类

舞乐漪的小站

一个折腾服务器、Live2D 看板娘和 Typecho 的个人站点
前端折腾

给博客加看板娘:从选型到交互,我把 Live2D 的坑走了一遍

2026-10-17 浏览量 0 暂无评论

为什么要有它

想在博客右下角放一个会眨眼、会跟着鼠标转头的角色。听起来是小事:找个库、装个模型、刷新页面,实际从下午折腾到晚上。

选型:三代格式的版本地狱

一张立绘拆成几十个图层,绑上参数让它们变形,看起来就像活的——它不是视频,也不是 3D,是用代码驱动的纸片人。

麻烦在于格式分了三代,互不兼容:

代际模型文件配置
Cubism 2.mocmodel.json
Cubism 3/4.moc3*.model3.json
Cubism 5.moc3*.model3.json

后两代后缀完全一样,版本号写在文件头第 5 个字节里。我对着文件名猜了半天,最后靠十六进制才读出那是 0x05,即 v5。

运行时更难选。官方 Cubism Core(live2dcubismcore.min.js)只管解析和计算,对版本极其敏感:Core 4 读不了 v5,Core 5 才能向下兼容。live2d.min.js 是 Cubism 2 的老运行时,只能读 .moc,网上教程大多用它——所以教程里的模型搬到新库上统统报错。

封装库这边:live2d-widget 只支持 Cubism 2,pixi-live2d-display 支持 2 和 4 但不自带 Core,还有一个声称自动识别版本的。我选了最后那个,也是出事那个。

模型本身也有坑:27MB 的直播级包,用 PIL 压到 1024 后只有 2.2MB。

库把 Core 打包进了自己身体里

这是整件事最有价值的一条。那个库装上去就报错:

TypeError: g[y[((n + 36) >> 2)]] is not a function
  at c_csmReviveMocInPlace

c_csmReviveMocInPlace 是 Core 内部解析 .moc3 的函数。这种"某个东西不是函数"的错,是 WASM 解码错位的典型表现——Core 的版本低于模型的版本。

第一反应很自然:先加载官方 Core 5,再加载库。顺序、完成事件、延时初始化全查过,报错一模一样。

答案在库的源码里。把 660KB 的压缩产物拆开搜,看到一行:

window.Live2DCubismCore = i;

i 是它自己打包进去的旧版 Core。库一加载,就无条件把我装好的 Core 5 覆盖掉。执行顺序变成:加载 Core 5 → 库把自带的旧 Core 拍到全局 → 解析 v5 的 moc3 → 失败。

加载顺序救不了它。 这不是 bug,是设计:为了开箱即用,它把依赖整个内联进了产物。代价是天花板被焊死——它声称支持新版本,体内的 Core 却停在旧版本,而你从外面既看不见、也换不掉。所谓"升级这个库",本质是等作者重新打包一次,业务代码里做什么都无解。

后来换成三个组件:官方 Core 5 解析 .moc3、pixi.js 渲染、pixi-live2d-display 挂模型。它们都不往全局写 Core,我加载的那份就是生效的那份;代价是画布初始化、缩放适配、鼠标跟随全要自己写,但六十行是我自己的。

静默失败:switchModel 找不到自己的容器

能显示了,下一个需求很自然:点一下换个人。点下去,角色淡出、淡入,动画流畅——还是同一个人。

控制台没有报错,加日志才确认 switchModel() 确实被调用了,参数也对,就是没效果。

往前倒一步。之前为了避开拖动和组件自带动画打架,我把渲染画布从组件创建的容器里搬进自己的容器,再把原容器隐藏。当时这做法是对的,动画确实不打架了。但我没想到 switchModel() 内部要操作它自己创建的那个容器:画布被我搬走,容器被我藏了,它找不到目标,于是什么都不做,安静地失败。

修法不是迁就它,而是绕开:两个实例各挂一个模型、各有自己的容器,"切换"从调用库的接口降级成隐藏一个容器、显示另一个。动画、位置、显隐全归我,组件全程不知道发生过"切换"。第二个实例是首次切换时才现场创建的,为了省内存——这又埋了个时序坑。

交互层:拖动、换人、隐藏、气泡

四个交互:按住拖动、单击换人、双击隐藏(右下角留个小圆点叫回来)、周期性冒气泡。气泡看着最水,细节最多。

一是位置。气泡挂在容器里会被裁掉、压不到头顶,所以它是独立挂载的固定层;每次拖动都得跟着重算,否则就跟角色分家。

二是单击和双击打架:双击隐藏会先触发两次单击,"点两下"变成换两次人再隐藏。用延迟判定——单击先挂 250ms 定时器,双击立刻清掉它。

三是文案按时间分段:清晨催喝水,白天提醒别久坐,傍晚提醒开灯,深夜催睡觉——深夜那几条带催促意味,比卖萌有用。

三个必须踩一遍的坑

坑一:批量隐藏兄弟节点,把整页藏了。 组件右侧自带一条竖条,我写了段"隐藏除 canvas 外所有子元素"。上线后整页正文消失,只剩顶部横幅——因为容器可能是 body,画布直接挂在 body 上时,这句等于隐藏整个页面。现在会先确认容器身份,是 body 就自己包一层。

坑二:getBoundingClientRect() 读到进场动画的中间值。 气泡贴着角色头顶,位置靠它算。冷启动时模型正做淡入加上移 16px 的进场动画,这一刻量到的是中间态,气泡偏高十来像素,动画一结束就错位。解法是别在进场过程中量:等动画结束(或固定 260ms)后算一次,拖动时只在收尾时测。

坑三:rAF 在无头环境不触发。 淡入本来是用 requestAnimationFrame 写的。真实浏览器没问题,用无头浏览器截图验证时模型却消失了:rAF 依赖真实帧调度,虚拟时间下帧不产生,回调从不触发,opacity 停在 0——元素在、DOM 对、就是看不见。改成 setTimeout(fn, 30) 就好,动画本来就不该依赖一个可能不触发的调度器。

首次切换要现场创建那个实例,创建是异步的,画布要等几百毫秒,只能轮询——轮询里别用 rAF 递归,老实 setTimeout(check, 120)。

手机端:触摸事件我从来没绑过

桌面端调完上手机:拖不动、点不动、双击更是不可能。原因很蠢——我只绑了 mousedown / mousemove / mouseup,它们在手机上要么不来,要么来得晚。补 touchstart / touchmove / touchend 还要 preventDefault(),否则页面跟着滚;容器上挂 touch-action: none 更干净。

dblclick 更彻底:移动端浏览器基本不派发它,因为双击在手机上被定义成缩放。要么换成"长按 500ms 隐藏",要么在 touchend 里数两次触碰的间隔(300ms 内算双击)。我选后者,和桌面的 250ms 判定共用一套状态机,只换事件源。

一句话教训

真正卡住我的从来不是"代码写错了",而是几个看起来天经地义、其实从没验证过的前提:变量声明了就有值、rAF 一定在下一帧执行、容器一定是我想的那个元素、鼠标事件在手机上也会来。

封装得越好的库,你违背前提的时候它越安静。 它不抗议,只沉默。

相关阅读

  • 别再「看起来对」:用真浏览器验证前端
  • 一屏能塞多少信息:三列卡片、侧栏重做与移动端
踩坑复盘故障排查Live2D前端
一只 QQ 机器人的掉线编年史:监控瞎了、假在线、风控与两次登录死法
一台 2 核 2G 的服务器,能同时跑机器人和博客吗

评论/ 取消评论

  • 文章目录
  • 站点概览
    舞乐漪

    舞乐漪

    个人站长

    Funny创造世界

    河北省廊坊市广阳区 2026-10-17
    14 文章 6 分类
    10 标签

    分类与标签

    踩坑复盘运维建站故障排查前端工具TypechoLive2DAI安全加固

    分类

    默认分类2运维实录1建站笔记5前端折腾2工具箱3杂谈1

    标签

    踩坑复盘4运维4建站4故障排查3前端3工具3Typecho3Live2D1AI1安全加固1

    浏览量 : 0

    冀ICP备2026047696号-1

    © 2026 舞乐漪 · Powered by Typecho · Theme Sagiri

    This is just a placeholder img.