2026-10-12-cockatiel-actions-design.md 7.0 KB

玄凤鹦鹉精细化动作 设计文档

  • 日期:2026-10-12
  • 组件:cfc-desktop-pet(Tauri v2 桌面宠物)
  • 平台范围:先 Windows(代码跨平台可编译,macOS 坐标/多屏细化留后续)

1. 需求(原文)

实现宠物的精细化动作,比如对于玄凤鹦鹉,要能够走路,也能够跳着走。鼠标放在鹦鹉上时变成手指的样式,放在胸部 5 秒钟,鹦鹉会迈到手指上,然后手指鼠标移到哪儿,鹦鹉就跟到哪儿。鼠标抖动一下鹦鹉会飞到屏幕的任意地方。

拆解为 4 个行为:

  1. 自主走 / 跳着走:空闲时宠物会在屏幕上走动,或跳着走。
  2. 悬停变手指:鼠标移到鹦鹉身上时光标变为手指(pointer)。
  3. 胸部按住 5 秒 → 站到手指上并跟随:悬停鹦鹉胸部 5 秒,鹦鹉迈到"手指"(光标)上,之后跟随鼠标移动。
  4. 抖动甩飞:跟随(站手)状态下抖动鼠标,鹦鹉飞到屏幕任意位置后落地。

2. 决策(已与用户确认)

  • 动作素材:程序化动作。当前 src/DefaultPets/{xibao,fubao}/ 只有 pet-config.json,无任何图片,宠物实际渲染 emoji 兜底形象。因此不依赖精灵帧,改用「整体位移 / 弹跳 / 挤压拉伸 / 旋转 / 扑腾」等程序化姿态 + 移动窗口实现动作。
  • 范围:先 Windows。

3. 核心架构

宠物所在窗口仅 200×200 且透明置顶,装不下跨屏走动/飞行——所以"走/跳/飞/跟随"必须移动窗口本身。窗口位置只能由 Rust 侧修改,故分层:

层 职责
Rust(移动者) 轮询全局光标 cursor_position();按显示器 current_monitor().work_area() 夹取边界;移动窗口 set_position();抖动检测;驱动 locomotion 状态机;通过 pet://gait 事件把姿态种类下发给前端
JS(渲染者) 只渲染局部姿态(走路摆动、跳跃挤压、飞行扑腾、站手平衡),以及窗口内的悬停/胸部悬停判定与气泡

选用理由:避免 JS 每帧 invoke 跨进程调用造成卡顿(方案 B 被否),不引入物理/动画库(方案 C 被否,YAGNI)。

关键 Tauri API(已在本机 crate 源码确认存在)

  • WebviewWindow::cursor_position() -> Result<PhysicalPosition<f64>>(全局光标,无需全局钩子 → 免 macOS 辅助功能权限)
  • WebviewWindow::set_position(Position) / outer_position() / outer_size()
  • WebviewWindow::current_monitor() -> Result<Option<Monitor>>
  • Monitor::work_area() -> &PhysicalRect<i32,u32>(position + size)

分层动画合成

#pet(情绪动画,由 pet-engine.js 管理 .anim-*)内新增 #pet-body 包装层承载姿态动画(.pose-*)。两层 transform 独立叠加,互不覆盖。

#pet  ← 情绪:.anim-wings_idle / .anim-clap …(pet-engine)
  └── #pet-body  ← 姿态:.pose-walk / .pose-hop / .pose-fly / .pose-perch(pet-pose)
        └── #pet-img
  └── #chest(脆弱热区:胸部 hover 判定)
  └── #bubble

4. 组件设计

4.1 Rust locomotion.rs(新)

  • 循环间隔 60ms。
  • 模式机:Idle(静立)→ 随机触发 AutoWalk/Hop;Perch(跟随)由命令进入;抖动触发 Fly。
  • 移动窗口:维护当前窗口物理坐标,按步进趋近目标;Perch 时直接 窗口 = 光标 - 锚点(锚点 = 宠物双脚在窗口内的坐标 ≈ (100, 200))。
  • 边界:一律 clamp 到 current_monitor().work_area(),保证整窗可见。
  • 抖动检测:保存最近 N 个光标采样点(位置+时间),在 ~600ms 窗口内统计方向反转次数与总位移;满足阈值 → Fly。
  • Fly:约 600ms 内插值移动到工作区内随机目标点,落地回 Idle。
  • 事件:pet://gait {kind, direction, phase}(kind ∈ idle/walk/hop/fly/perch)。
  • 事件:pet://perch {active}(进入/退出跟随,供前端同步"是否站手")。

4.2 Rust state.rs(纯函数 + 单测)

新增可单测的纯逻辑(不依赖 tauri 运行时):

  • detect_shake(samples, now) -> bool:方向反转 + 位移阈值。
  • clamp_window_pos(pos, work_area, win_size) -> (i32,i32):把窗口左上角夹进工作区。
  • pick_random_target(work_area, win_size, seed) -> (i32,i32):工作区内的随机落点(用简单 LCG,便于测试确定性)。

4.3 Rust app_state.rs / commands.rs / main.rs

  • AppState 增 follow_cursor: AtomicBool(命令与循环共享)。
  • 命令 start_perch(胸部悬停满 5s 时前端调用)→ 置 follow_cursor=true,emit pet://gait {kind:"perch"} + pet://perch {active:true}。
  • 命令 stop_perch → 置 false,emit pet://perch {active:false}。
  • main.rs:注册新命令;setup 内 locomotion::spawn(...) 启动循环(与 poller::spawn 并列)。

4.4 JS

  • index.html:新增 #pet-body 包装、#chest 热区;#pet { cursor: pointer }(手指);引入 pet-pose.js;#chest 定位在鹦鹉胸部(上方中区)。
  • animations.css:新增 @keyframes walk_bob / hop / fly_flutter / perch_lean 与 .pose-* 类。
  • pet-pose.js(新):监听 pet://gait 切换 #pet-body 的姿态类;监听 pet://perch 设置 window.__petPerched。
  • bridge.js:cfcHost 增 startPerch() / stopPerch() → invoke('start_perch'|'stop_perch')。
  • interactions.js:
    • #chest mouseenter 起 5s 计时 → cfcHost.startPerch();mouseleave 前取消;成功后面板气泡「飞到你手上~」。
    • __petPerched 为真时跳过 drag_start(避免跟随与原生拖拽打架)。

5. 交互映射

操作 结果
空闲 偶发 walk / hop(窗口横移;hop 带弹跳)
鼠标移到鹦鹉上 光标变手指(pointer)
悬停胸部 5 秒 迈到"手指"上 → 跟随鼠标(窗口跟随光标,双脚落在光标处)
跟随中抖动鼠标 fly → 飞到当前显示器工作区随机点 → 落地回 idle

6. 边界与冲突

  • 多屏/边界:一律以 current_monitor() 工作区为准 clamp。
  • 「胸部悬停」用纯 hover(不按键),与「按住拖拽」互不冲突;站手期间禁用原生拖拽。
  • 窗口保持置顶;跟随由 Rust 轮询驱动,不依赖窗口事件,避免光标快速移动导致丢失。
  • set_ignore_cursor_events(鼠标穿透)与移动窗口互不影响。

7. 验收

  • Rust:cargo test 覆盖 detect_shake / clamp_window_pos / pick_random_target,全绿。
  • Rust:cargo check --all-targets 通过。
  • JS:全部 .js node --check 通过。
  • 画面:由用户运行新构建 exe 肉眼确认(本机 WebView2 CDP 不可用,无法自动截图)。

8. 非目标(本轮)

  • 真实多帧精灵动画(待美术素材到位后,按 pet_config.animations.frames 接入)。
  • macOS 多屏/Retina 坐标细化。
  • 宠物沿任意曲线路径行走(本轮为水平步进 + 弹跳)。