AIRI 桌宠开发日志:让 Live2D 模型在 Tauri 窗口中注视屏幕任意位置的鼠标

发布时间:2026/9/12 6:53:43
AIRI 桌宠开发日志:让 Live2D 模型在 Tauri 窗口中注视屏幕任意位置的鼠标 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-samas 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 桌宠的运行环境与传统网页有本质区别它运行在 TauriWebView 原生壳桌面窗口中作为桌宠它的窗口往往是透明、置顶、甚至点击穿透的悬浮层鼠标绝大多数时间都位于窗口之外在桌面上、在其他应用上移动。一旦光标离开网页内容区域Live2D 引擎就再也无法通过浏览器事件获知鼠标位置。此时模型会失去目标目光僵在原地。要解决这个问题就必须手动告诉 Live2D 引擎鼠标当前在哪这正是本次开发日志的核心命题。思路整理借助 Tauri 原生能力拿到屏幕级鼠标坐标既然浏览器内拿不到窗口外的鼠标位置剩下的路径就是调用操作系统原生 API。AIRI 桌宠基于 Tauri 构建Tauri 提供了从 JavaScript 调用 Rust 原生代码的能力因此开发者可以调用Windows APIGetCursorPos或macOS APINSEvent.mouseLocation获取鼠标在整块屏幕上的绝对坐标同时获取窗口本身在屏幕上的位置与尺寸Windows 用GetWindowRectmacOS 用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 DAIRI 窗口的宽与高(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 屏幕高为DAppKit 坐标下鼠标为(G, H)翻转后的 CSS 屏幕坐标为(G, D - H)窗口左上角同样需要翻转AppKit 坐标下为(E, F)翻转后为(E, D - F)两者相减(G - E, (D - H) - (D - F)) (G - E, F - H)。再仔细核对窗口内的相对 YCSS 坐标系下窗口左上角是(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-displayautoInteract: false与focus()的官方说明来源Win32 APIGetCursorPos获取鼠标在屏幕坐标系中的绝对位置Win32 APIGetWindowRect获取窗口外边框在屏幕坐标系中的矩形macOS APINSWindow.frame获取窗口在 AppKit 坐标系中的位置与尺寸macOS APINSEvent.mouseLocation获取鼠标在 AppKit 坐标系原点左下、Y 轴向上中的位置。注上述资料链接为开发日志原文引用的外部文档本文不再重复展开。AIRI 本次实现对应的源码位于当前仓库apps/stage-tamagotchiTauri 桌面端与packages/stage-ui-live2dLive2D 渲染与注视点换算可继续深入阅读。总结本次 DevLog 的完整技术链条可以归纳为问题Live2D 自动注视只在光标位于网页内时有效桌宠场景下鼠标几乎总在窗口外思路利用 Tauri 的原生调用能力获取屏幕级鼠标坐标与窗口坐标换算(G - E, H - F)得到窗口内相对坐标交付以{ autoInteract: false }创建模型通过watchfocus()手动驱动注视点适配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-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考