From 48b10c43deabc410b95a914289955e0d3dbfb5c9 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Fri, 21 Aug 2026 16:24:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BB=91=E9=BC=A0=E7=A7=BB=E5=88=B0=E5=AF=B5?= =?UTF-8?q?=E7=89=A9=E8=BA=AB=E4=B8=8A=E6=99=82=E8=B7=B3=E8=BA=8D=E4=B8=89?= =?UTF-8?q?=E6=AC=A1=EF=BC=8Chover=20=E5=88=A4=E5=AE=9A=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E7=9F=A9=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要: 把 hover 跳躍加回來,並把觸發判定從逐像素 alpha 改成人物方框。 根本原因: 1. hover 跳躍在「逐項對照 Codex」那次被換成 proximity 版面(理由是 Codex 的 respondToHover 全 app 沒有開啟),上一次移除浮出選單鈕時把整套版面拿掉, 跳躍也跟著沒了,變成 hover 完全沒有反應。 2. 先前的 hover 判定用 ctx.getImageData 逐像素測 alpha。sprite 每一格的輪廓不同, 游標停著不動時,人物身上同一點會隨著動畫在不透明與透明之間變化, 造成 setHovered 在 true/false 間高速抖動(實測 2 ms 內來回多次), 連帶 pet:ignore-mouse 也被瘋狂切換。 影響: 滑鼠移到寵物身上沒有任何反應;即使觸發也會因抖動而重複進出 hover 狀態。 修法: - 還原 setHovered:進入時 play("jumping", STATE_REPEATS),對應 Codex 的 `respondToHover && hovered ? "jumping" : state` 走 f() 播 3 次後沉澱; 一次性動作或拖曳進行中不打斷,離開視窗時清除。 - 新增 overPetArea:hover 用 canvas 的矩形判定,與 Codex 的 pet-area-hover-changed 同樣是用 rect 算的。點穿判定維持逐像素 alpha,透明處仍然點得到底下的視窗。 - README 的日常使用、行為對照表、與 Codex 的對照三處同步更新, 對照表補上 jumping 與 hover 判定兩列。 驗證: - 在 renderer 暫時加診斷 log 實測,確認進入人物區時 `setHovered true overlay=null` 之後 overlay 變成 jumping,觸發路徑正確; 同一份 log 也是抖動問題的證據來源。改成矩形判定後移除診斷。 - 以 ELECTRON_RUN_AS_NODE 讀打包後 app.asar:版本 2.0.0、hover 跳躍與矩形判定都在、 點穿仍用 alpha、診斷已移除、無選單鈕殘留、右鍵選單仍在,九項全過。 Co-Authored-By: Claude Fable 5 --- README.md | 7 +++++-- renderer/renderer.js | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a33cc8..b1669a7 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ npm run autostart:install # (可選)開機自動啟動 | 左鍵點一下 | 揮手(若有非固定的氣泡,先關閉氣泡) | | 左鍵拖曳 | 搬家;往左播 running-left、往右播 running-right;放開後位置寫入設定檔 | | 右鍵 / 系統匣左鍵 | 開啟選單 | +| 滑鼠移到她身上 | 跳躍 3 次後回到原本狀態(Codex 的 hover 行為) | | 滑鼠靠近 | 盯著游標看(選單開啟「跟著游標看」後才有) | **選單功能** @@ -124,6 +125,7 @@ npm run autostart:install # (可選)開機自動啟動 | `SessionEnd` | | 移除該 session | 最後一個結束時 👋 掰掰 | | (無事件) | 游標移動(追視開啟時) | 16 方向 `look`,覆蓋所有狀態 | | | (無事件) | 拖曳 | 單次位移 ≥ 4 px 才決定 `running-left` / `running-right` | | +| (無事件) | 游標進入人物方框 | `jumping` × 3 → 回原狀態 | | 規則: @@ -256,8 +258,9 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO | `waiting` | 授權、`requestUserInput`、計畫待確認 | 相同;`idle_prompt` 不算 | | `review` | turn 完成且未讀,直到使用者回到該 thread | 直到該 session 下一個動作 | | `waving` | 只在 first-awake(每隻寵物一次,8 秒) | 相同(記在設定檔 `greetedPetIds`) | -| `jumping` | 只在 `respondToHover`,而整個 app 沒人開啟它 | 只在選單「動作測試」 | -| hover | 40 px 內進入、56 px 外退出;人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 未採用——控制鈕的功能就是選單,右鍵已經有了 | +| `jumping` | 定義在 `respondToHover ? "jumping" : state`,但整個 app 沒人開啟那個 prop | **選擇打開**:游標進入人物方框就播 3 次 | +| hover 版面 | 40 px 內進入、56 px 外退出;人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 未採用——控制鈕的功能就是選單,右鍵已經有了 | +| hover 判定 | 用矩形(`pet-area-hover-changed` 由 rect 算) | 相同(逐像素會因每格輪廓不同而抖動) | | 追視 | 看 quick chat 文字游標/computer-use 游標,死區 1 px,覆蓋所有狀態 | 沒有對應來源;改為**選配**看滑鼠,預設關 | | 拖曳方向 | 單次取樣水平位移 ≥ 4 px | 相同 | | 拖曳放開 | 彈簧回彈(spring 220 / damping 26) | 無(直接停在放開處) | diff --git a/renderer/renderer.js b/renderer/renderer.js index 3e5cac8..07bc45e 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -124,6 +124,7 @@ function startAnim(anim, now) { let bubbleTimer = null; let bubbleSticky = false; let ignoringMouse = true; +let hovered = false; // 游標是否壓在人物的不透明像素上 let pressed = null; let lastReported = ""; @@ -552,6 +553,23 @@ function overSprite(x, y) { } } +// hover 判定用人物方框,不用逐像素 alpha:sprite 每一格的輪廓不同,逐像素會讓游標停著不動時 +// 也在 true/false 之間高速抖動。Codex 的 pet-area-hover 同樣是用矩形算的。 +function overPetArea(x, y) { + const r = canvas.getBoundingClientRect(); + return x >= r.left && x < r.right && y >= r.top && y < r.bottom; +} + +// Codex 的寵物元件在 hover 時會把狀態換成 jumping(codex-pet-assets 的 +// `respondToHover && hovered ? "jumping" : state`),走 f() 的規則播 3 次後沉澱回慢速 idle。 +// 那個 prop 在 Codex 桌面寵物那層沒有開啟,這裡選擇打開它。 +function setHovered(on) { + if (on === hovered) return; + hovered = on; + // 只在「進入」時觸發一次;一次性動作或拖曳進行中不打斷 + if (on && !overlay && !drag) play("jumping", STATE_REPEATS); +} + function setIgnore(ignore) { if (ignore === ignoringMouse) return; ignoringMouse = ignore; @@ -569,11 +587,14 @@ window.addEventListener("mousemove", (e) => { } return; } + setHovered(overPetArea(e.clientX, e.clientY)); + // 點穿判定仍看實際像素,透明處要能點到底下的視窗 setIgnore(!(overSprite(e.clientX, e.clientY) || overBubble(e.clientX, e.clientY))); }); document.addEventListener("mouseout", (e) => { if (e.relatedTarget || pressed) return; + setHovered(false); setIgnore(true); });