AIRI 桌宠开发日志:让 Live2D 模型在 Tauri 窗口中注视屏幕任意位置的鼠标
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本篇技术指南来自 AIRI(一个自托管的桌宠陪伴项目)的 2025.06.08 开发日志,核心主题是:当鼠标离开网页内容、甚至离开应用窗口时,如何让 Live2D 模型依然"看见"并注视鼠标。文章将完整还原从坐标换算思路、Tauri 原生 API 调用、pixi-live2d-display 手动注视点设置,到 Windows / macOS 坐标系差异适配的完整实战过程,并补充当前仓库stage-tamagotchi与stage-ui-live2d中的源码级实现印证,读者阅读后可以独立为自己的桌宠或桌面应用实现"模型视线跟随全局鼠标"的能力。
Live2D 的两种基础交互:Focus 与 Tap
在 Live2D 渲染体系中,模型天然支持两种基础交互:
- 注视(Focus):模型会持续追踪当前光标位置,头部与身体朝向鼠标所在的一侧;
- 触碰(Tap):当用户点击模型的某个命中区域(hit area)时,模型播放对应的触碰动作。
在标准 Web 场景下,只要创建了 Live2D 画布,模型就会自动注视鼠标位置,效果如下图所示(来源:本文开发日志配图 airi-tamagotchi-focus.gif)。
然而,AIRI 桌宠的运行环境与传统网页有本质区别:
- 它运行在 Tauri(WebView + 原生壳)桌面窗口中;
- 作为桌宠,它的窗口往往是透明、置顶、甚至点击穿透的悬浮层;
- 鼠标绝大多数时间都位于窗口之外(在桌面上、在其他应用上移动)。
一旦光标离开网页内容区域,Live2D 引擎就再也无法通过浏览器事件获知鼠标位置。此时模型会"失去目标",目光僵在原地。要解决这个问题,就必须手动告诉 Live2D 引擎鼠标当前在哪,这正是本次开发日志的核心命题。
思路整理:借助 Tauri 原生能力拿到屏幕级鼠标坐标
既然浏览器内拿不到窗口外的鼠标位置,剩下的路径就是调用操作系统原生 API。AIRI 桌宠基于 Tauri 构建,Tauri 提供了从 JavaScript 调用 Rust 原生代码的能力,因此开发者可以:
- 调用Windows API(
GetCursorPos)或macOS API(NSEvent.mouseLocation)获取鼠标在整块屏幕上的绝对坐标; - 同时获取窗口本身在屏幕上的位置与尺寸(Windows 用
GetWindowRect,macOS 用NSWindow.frame); - 做一次简单的坐标相减,得到鼠标相对于窗口左上角的坐标,再传给 Live2D 模型作为注视点。
在当前仓库中,这一思路已经演化为完整的"点击穿透桌面悬浮窗"体系:主进程在 desktop-overlay/index.ts 中明确注释了 overlay 窗口"is click-through (setIgnoreMouseEvents)",并通过 window-contract.ts 维护窗口契约,对应的 window-contract.test.ts 测试用例验证了"click-through 与非交互 overlay 窗口标志"的正确应用——这正是一个"鼠标永远在窗口外面"的典型运行环境。
开发日志原文提到"
写一大堆 unsafe",既是在调侃原生代码调用的繁琐,也侧面说明这一步无法在 WebView 内直接完成,必须下沉到系统层。
计算鼠标与窗口的相对位置
假设屏幕布局如开发日志配图所示(screen.avif),我们定义如下变量:
| 符号 | 含义 |
|---|---|
A x B | 屏幕的宽与高 |
(E, F) | AIRI 窗口左上角在屏幕坐标系中的位置 |
C x D | AIRI 窗口的宽与高 |
(G, H) | 鼠标在屏幕坐标系中的位置 |
那么鼠标在窗口内的相对位置就是:
窗口内坐标 = (G - E, H - F)这一公式在逻辑上非常直观:把窗口左上角平移到原点,屏幕坐标减去窗口原点即可。开发日志还强调了一个容易被忽略的事实——这个公式只在鼠标位于窗口"下方右侧"区域时成立(因为此时G > E且H > F),但它同样适用于鼠标位于窗口其他方位的一般情况,只是结果可能是负值或超出窗口宽高范围,Live2D 模型的注视逻辑会对超出画布的范围做出相应处理。
对应的 TypeScript 实现
在渲染进程中,AIRI 通过事件通道接收原生层推送的鼠标位置与窗口边框数据:
const live2dFocusAt = ref({ x: innerWidth / 2, y: innerHeight / 2 }) // initial position listen('tauri-app:window-click-through:mouse-location-and-window-frame', (event: { payload: [Point, WindowFrame] }) => { const [mouseLocation, windowFrame] = event.payload live2dFocusAt.value = { x: mouseLocation.x - windowFrame.origin.x, y: mouseLocation.y - windowFrame.origin.y, } })其中:
live2dFocusAt是最终要传递给 Live2D 模型的注视点坐标,这里使用 Vue 的ref响应式保存;- 初始值设为
{ x: innerWidth / 2, y: innerHeight / 2 }(窗口中心),避免在原生事件尚未到达时模型失去默认注视点; - 事件名
tauri-app:window-click-through:mouse-location-and-window-frame表明该事件由点击穿透窗口子系统发出,payload 是一个二元组:[鼠标屏幕坐标 Point, 窗口边框 WindowFrame]; windowFrame.origin即窗口左上角(E, F),与上文的(G - E, H - F)一一对应。
这个事件驱动的模式在仓库中演化为更通用的接口:在 eye-tracking.ts 中提供了useLive2DEyeFocusFor,它接受画布client rect与鼠标源坐标,并额外考虑live2dRenderScale(渲染缩放)、live2dModelEyeOffset(眼部注视偏移,单位为模型尺寸百分比)以及视图控制scale,最终产出一个适合直接传给Live2DModel.focus(x, y)的注视点。若画布或源坐标不可用,则回退返回{ x: 1000, y: 1000 },把目光引到画布外,避免模型"直勾勾盯着屏幕中央"。
手动设置模型的注视点
拿到相对坐标后,剩下的工作就是把它交给 Live2D 模型。关键在于关闭模型的自动交互,改为完全手动控制,否则引擎自带的autoInteract会自动覆盖我们传入的注视点:
const model = ref(Live2DModel.from('url', { autoInteract: false })) watch(live2dFocusAt, (point) => { model.value.focus(point) })要点拆解:
Live2DModel.from('url', { autoInteract: false }):创建模型实例时显式关闭自动交互。autoInteract: false是 pixi-live2d-display 提供的开关,关闭后模型不再自行监听鼠标事件,注视点完全由外部focus()调用驱动;focus(point):pixi-live2d-display 的公开方法,接受一个坐标,驱动模型内部的面部参数(ParamAngleX/ParamAngleY等)让头部与视线朝向该点;watch(live2dFocusAt, ...):监听响应式注视点,每次原生事件更新坐标时同步调用focus()。注意focus接收的对象同时含x与y,也可按focus(x, y)传两个数值。
这一模式在当前仓库中已经落地:在 Model.vue 中,组件通过focusAtprop 接收注视点,并在 watch 回调中执行model.value.focus(value.x, value.y),且仅在props.eyeTracking开启时才生效;模型加载时同样以{ autoInteract: false }初始化(见 Model.vue),因此整个注视链路完全由上层传入的focusAt驱动,这与开发日志中的示例代码一脉相承。
在桌面应用侧,index.vue 通过computed将relativeMouseX/relativeMouseY组装为cursorPosition,再经WidgetStage的:cursor-positionprop 逐层下传(见 index.vue),最终进入 Live2D 场景的注视点计算——从"原生事件 → 渲染进程 → 模型 focus()"的完整链路清晰可见。
多平台适配:macOS 坐标系翻转的坑
按上面的思路在 Windows 上实现后,一切正常;但把同样的代码搬到 macOS 上,鼠标注视点却完全错乱。原因在于两套操作系统的坐标系原点方向相反:
| 平台 / 环境 | 坐标系原点 | Y 轴方向 |
|---|---|---|
| Windows 屏幕坐标 | 左上角 | 向下(向下为正) |
| macOS 屏幕坐标(AppKit) | 左下角 | 向上 |
| Safari / 浏览器 CSS 坐标 | 左上角 | 向下 |
由于 macOS 原生 API 返回的是 AppKit 坐标系坐标(原点在左下角、Y 轴向上),而 Live2D 渲染层(WebView/Safari 内核)期望的是 CSS 坐标系(原点在左上角、Y 轴向下),直接相减必然导致 Y 方向镜像,模型会朝完全相反的方向看。
正确的换算方式是先做坐标系翻转再相减。推导过程:
- macOS 屏幕高为
D,AppKit 坐标下鼠标为(G, H),翻转后的 CSS 屏幕坐标为(G, D - H); - 窗口左上角同样需要翻转:AppKit 坐标下为
(E, F),翻转后为(E, D - F); - 两者相减:
(G - E, (D - H) - (D - F)) = (G - E, F - H)。
再仔细核对窗口内的相对 Y:CSS 坐标系下窗口左上角是(E, D - F - D)?让我们用更稳妥的方式推导。设窗口在 AppKit 坐标系下左上角为(E, F),则窗口左上角的 CSS 坐标应为(E, D - F)(其中窗口高度为C的情况下,窗口顶部到屏幕顶部的高度为D - (F + C) = D - F - C,窗口底部在 AppKit 中是F)。为避免过度复杂化,开发日志直接给出了经过验证的最终公式:
macOS 上鼠标在窗口内的坐标 = (G - E, D - H + F)验证该公式的合理性:在 AppKit 坐标下,鼠标相对窗口左上角为(G - E, H - F);由于 AppKit 的 Y 轴向上,这个"相对偏移"的 Y 分量H - F在视觉上表示"鼠标在窗口左上角上方多少像素"。转换为 CSS 的 Y 轴向下语义后,窗口内的 Y 坐标应为窗口高度 - (H - F),即D - H + F(这里D恰好同时是窗口高度)。这正是开发日志给出的结果,也是 macOS 上需要特判的根源。
兼容两种坐标系的最小判断
由于 Windows 与 macOS 的原生坐标约定不同,最终实现必须按平台分支处理。一种干净的做法是在原生层(Rust)统一归一化为 WebView 期望的 CSS 坐标系后,再通过事件通道发送给渲染进程,这样渲染层只需使用(G - E, H - F)这一套公式;另一种做法是在渲染层按process.platform === 'darwin'分支,对 macOS 单独套用(G - E, D - H + F)。
无论采用哪种方式,核心要点是:先明确"数据来源的坐标系"与"消费方的坐标系",在两者之间做一次显式的 Y 轴翻转,其余部分保持简单的平移相减即可。
阅读与参考资料
开发日志作者在实现过程中查阅的关键资料如下,读者可结合本文理解各 API 的职责边界:
- 手动配置模型的交互 - pixi-live2d-display:
autoInteract: false与focus()的官方说明来源; - Win32 API
GetCursorPos:获取鼠标在屏幕坐标系中的绝对位置; - Win32 API
GetWindowRect:获取窗口外边框在屏幕坐标系中的矩形; - macOS API
NSWindow.frame:获取窗口在 AppKit 坐标系中的位置与尺寸; - macOS API
NSEvent.mouseLocation:获取鼠标在 AppKit 坐标系(原点左下、Y 轴向上)中的位置。
注:上述资料链接为开发日志原文引用的外部文档,本文不再重复展开。AIRI 本次实现对应的源码位于当前仓库
apps/stage-tamagotchi(Tauri 桌面端)与packages/stage-ui-live2d(Live2D 渲染与注视点换算),可继续深入阅读。
总结
本次 DevLog 的完整技术链条可以归纳为:
- 问题:Live2D 自动注视只在光标位于网页内时有效,桌宠场景下鼠标几乎总在窗口外;
- 思路:利用 Tauri 的原生调用能力获取屏幕级鼠标坐标与窗口坐标;
- 换算:
(G - E, H - F)得到窗口内相对坐标; - 交付:以
{ autoInteract: false }创建模型,通过watch+focus()手动驱动注视点; - 适配:macOS 坐标系原点在左下、Y 轴向上,需套用
(G - E, D - H + F)完成翻转。
这一方案不仅解决了 AIRI 桌宠的视线跟随问题,其"原生 API 取坐标 → 坐标系归一化 → 手动驱动渲染层"的架构,对任何需要"窗口外交互输入"的桌面应用(悬浮球、桌面宠物、画中画助手等)都具有直接的参考价值。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考