细节调整及优化(26.8.3)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
type Point, type Sel, type CaptureData, type WindowInfo, type HandleDir,
|
||||
type RectAnno, type EllipseAnno, type ArrowAnno, type PenAnno, type TextAnno,
|
||||
type MosaicAnno, type HighlightAnno, type NumberAnno,
|
||||
type ScreenshotBeginPayload,
|
||||
} from './types'
|
||||
|
||||
// ===== 窗口 / 底图 =====
|
||||
@@ -65,6 +66,21 @@ const currentHwnd = ref(0)
|
||||
let pickRaf = 0
|
||||
let lastPickX = -1
|
||||
let lastPickY = -1
|
||||
/** 拾取窗口列表(Z 序顶→底,begin 事件携带,与冻结底图同一时刻),JS 本地命中测试零 IPC */
|
||||
let pickWindows: WindowInfo[] = []
|
||||
/** 最近一次鼠标物理坐标(ESC 等回到 pick 阶段时按当前位置恢复高亮,无需移动鼠标) */
|
||||
const lastMousePhys = { x: 0, y: 0 }
|
||||
|
||||
// 入场淡入(快门定格感):idle=整体透明(窗口 show 前的起点)→ fade=淡入中 → done=常态
|
||||
const enterState = ref<'idle' | 'fade' | 'done'>('done')
|
||||
let enterTimer: number | null = null
|
||||
function resetEnterAnim() {
|
||||
if (enterTimer !== null) {
|
||||
clearTimeout(enterTimer)
|
||||
enterTimer = null
|
||||
}
|
||||
enterState.value = 'idle'
|
||||
}
|
||||
|
||||
// 移动 / 缩放
|
||||
const moving = ref(false)
|
||||
@@ -655,51 +671,22 @@ function canvasPoint(e: MouseEvent): Point {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 窗口识别 =====
|
||||
function scheduleWindowPick(cssX: number, cssY: number) {
|
||||
const physX = Math.round(winOuterX + cssX * dpr)
|
||||
const physY = Math.round(winOuterY + cssY * dpr)
|
||||
if (physX === lastPickX && physY === lastPickY) return
|
||||
lastPickX = physX
|
||||
lastPickY = physY
|
||||
if (pickRaf) return
|
||||
pickRaf = requestAnimationFrame(async () => {
|
||||
pickRaf = 0
|
||||
try {
|
||||
const info = await invoke<WindowInfo | null>('screenshot_window_from_point', {
|
||||
x: physX,
|
||||
y: physY,
|
||||
})
|
||||
if (phase.value !== 'pick') return
|
||||
if (info) {
|
||||
// 高亮框用 DWM 视觉边界,避免 GetWindowRect 包含隐形缩放边框导致大一圈
|
||||
const r = info.visualRect ?? info.rect
|
||||
winHighlight.value = {
|
||||
x: (r.x - winOuterX) / dpr,
|
||||
y: (r.y - winOuterY) / dpr,
|
||||
w: r.width / dpr,
|
||||
h: r.height / dpr,
|
||||
title: info.title,
|
||||
}
|
||||
currentHwnd.value = info.hwnd
|
||||
} else {
|
||||
winHighlight.value = null
|
||||
currentHwnd.value = 0
|
||||
}
|
||||
} catch {
|
||||
// 忽略拾取错误
|
||||
// ===== 窗口识别(本地命中测试,零 IPC) =====
|
||||
/** 在缓存列表中命中测试(与 Rust 拾取同语义:rect 包含点,取 Z 序最顶的第一个命中) */
|
||||
function hitTestWindow(physX: number, physY: number): WindowInfo | null {
|
||||
for (const info of pickWindows) {
|
||||
const r = info.rect
|
||||
if (physX >= r.x && physX < r.x + r.width && physY >= r.y && physY < r.y + r.height) {
|
||||
return info
|
||||
}
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 按物理坐标拾取窗口(覆盖层打开时定位鼠标所在窗口) */
|
||||
async function pickAt(physX: number, physY: number) {
|
||||
try {
|
||||
const info = await invoke<WindowInfo | null>('screenshot_window_from_point', {
|
||||
x: physX,
|
||||
y: physY,
|
||||
})
|
||||
if (phase.value !== 'pick' || !info) return
|
||||
/** 应用窗口命中结果到高亮状态 */
|
||||
function applyWindowInfo(info: WindowInfo | null) {
|
||||
if (info) {
|
||||
// 高亮框用 DWM 视觉边界,避免 GetWindowRect 包含隐形缩放边框导致大一圈
|
||||
const r = info.visualRect ?? info.rect
|
||||
winHighlight.value = {
|
||||
x: (r.x - winOuterX) / dpr,
|
||||
@@ -709,11 +696,34 @@ async function pickAt(physX: number, physY: number) {
|
||||
title: info.title,
|
||||
}
|
||||
currentHwnd.value = info.hwnd
|
||||
} catch {
|
||||
// 忽略拾取错误
|
||||
} else {
|
||||
winHighlight.value = null
|
||||
currentHwnd.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleWindowPick(cssX: number, cssY: number) {
|
||||
const physX = Math.round(winOuterX + cssX * dpr)
|
||||
const physY = Math.round(winOuterY + cssY * dpr)
|
||||
if (physX === lastPickX && physY === lastPickY) return
|
||||
lastPickX = physX
|
||||
lastPickY = physY
|
||||
if (pickRaf) return
|
||||
pickRaf = requestAnimationFrame(() => {
|
||||
pickRaf = 0
|
||||
if (phase.value !== 'pick') return
|
||||
// 用最新位置命中(一帧内多次移动时取最后一次,不丢帧)
|
||||
applyWindowInfo(hitTestWindow(lastPickX, lastPickY))
|
||||
})
|
||||
}
|
||||
|
||||
/** 按物理坐标拾取窗口并立即应用高亮(同步,无 IPC) */
|
||||
function pickWindowAt(physX: number, physY: number) {
|
||||
lastPickX = physX
|
||||
lastPickY = physY
|
||||
applyWindowInfo(hitTestWindow(physX, physY))
|
||||
}
|
||||
|
||||
// ===== 选区流转 =====
|
||||
function resetAnnotations() {
|
||||
annotations.value = []
|
||||
@@ -739,6 +749,13 @@ function enterSelected() {
|
||||
phase.value = 'selected'
|
||||
}
|
||||
|
||||
/** 回到窗口拾取阶段,并按当前鼠标位置立即恢复高亮(ESC/点击选区外时无需移动鼠标) */
|
||||
function backToPick() {
|
||||
dragTracking.value = false
|
||||
phase.value = 'pick'
|
||||
pickWindowAt(lastMousePhys.x, lastMousePhys.y)
|
||||
}
|
||||
|
||||
/** 点击窗口 → 以窗口矩形为选区 */
|
||||
function selectWindow(w: { x: number; y: number; w: number; h: number }) {
|
||||
sel.value = { x: w.x, y: w.y, w: w.w, h: w.h }
|
||||
@@ -752,9 +769,9 @@ function onMouseDown(e: MouseEvent) {
|
||||
dragTracking.value = true
|
||||
dragStart.value = { x: e.clientX, y: e.clientY }
|
||||
} else if (phase.value === 'selected') {
|
||||
// 点击选区外 → 取消选中,回到窗口识别
|
||||
// 点击选区外 → 取消选中,回到窗口识别(按点击位置恢复高亮)
|
||||
resetAnnotations()
|
||||
phase.value = 'pick'
|
||||
backToPick()
|
||||
} else if (phase.value === 'editing') {
|
||||
// 点击选区外 → 提交未完成的文字、取消选中并退出标注,回到选区调整
|
||||
commitText()
|
||||
@@ -816,6 +833,9 @@ function onHandleMouseDown(dir: HandleDir, e: MouseEvent) {
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
// 追踪鼠标物理坐标(ESC 等回到 pick 阶段时按当前位置恢复窗口高亮)
|
||||
lastMousePhys.x = Math.round(winOuterX + e.clientX * dpr)
|
||||
lastMousePhys.y = Math.round(winOuterY + e.clientY * dpr)
|
||||
// 取色器放大镜更新(rAF 节流)
|
||||
scheduleMagnifier(e)
|
||||
|
||||
@@ -975,10 +995,9 @@ function onKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
} else if (phase.value === 'selected') {
|
||||
resetAnnotations()
|
||||
phase.value = 'pick'
|
||||
backToPick()
|
||||
} else if (phase.value === 'drawing') {
|
||||
dragTracking.value = false
|
||||
phase.value = 'pick'
|
||||
backToPick()
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
@@ -1901,15 +1920,17 @@ onMounted(async () => {
|
||||
// 禁用窗口显示/隐藏过渡动画(消除进入/关闭时的缩放动画),失败静默
|
||||
commands.screenshotDisableTransitions(WINDOWS.screenshotOverlay).catch(() => {})
|
||||
// 先注册 begin 监听再通知 store 就绪,避免首轮事件丢失
|
||||
beginUnlisten = await listen(EVENTS.screenshotBegin, () => {
|
||||
void beginCapture()
|
||||
beginUnlisten = await listen<ScreenshotBeginPayload>(EVENTS.screenshotBegin, (e) => {
|
||||
void beginCapture(e.payload)
|
||||
})
|
||||
await emit(EVENTS.screenshotOverlayReady)
|
||||
})
|
||||
|
||||
/** 响应 store 的 'screenshot-begin':先装载底图(隐藏中),解码完成后再一次性显示窗口 */
|
||||
async function beginCapture() {
|
||||
async function beginCapture(payload?: ScreenshotBeginPayload) {
|
||||
try {
|
||||
// 入场动画准备:整体置为透明起点(窗口 show 后从透明淡入,底图/遮罩/高亮同步出现)
|
||||
resetEnterAnim()
|
||||
// 每次截图开始时同步主界面主题(覆盖层是常驻窗口,主界面可能已切换主题)
|
||||
applyTheme()
|
||||
// 重置到"拾取"初始状态
|
||||
@@ -1927,13 +1948,23 @@ async function beginCapture() {
|
||||
pixelCanvas = null
|
||||
pixelCtx = null
|
||||
|
||||
// 缓存窗口拾取列表与捕获时刻光标(pick 阶段鼠标移动零 IPC 命中测试)
|
||||
pickWindows = payload?.windows ?? []
|
||||
lastMousePhys.x = payload?.cursorX ?? 0
|
||||
lastMousePhys.y = payload?.cursorY ?? 0
|
||||
lastPickX = -1
|
||||
lastPickY = -1
|
||||
|
||||
// 先清除上一轮底图,避免"旧图一闪";窗口隐藏期间完成新底图的传输与解码
|
||||
imgSrc.value = ''
|
||||
resetImgReady()
|
||||
loading.value = true
|
||||
|
||||
// 取全屏捕获的 BMP 原始字节(raw IPC → ArrayBuffer),Blob URL 免编码直接显示
|
||||
const buf = await invoke<ArrayBuffer>('screenshot_get_fullscreen_bmp')
|
||||
// 并行:BMP 传输(raw IPC → ArrayBuffer,Blob URL 免编码直接显示)+ 窗口外框位置
|
||||
const [buf, pos] = await Promise.all([
|
||||
invoke<ArrayBuffer>('screenshot_get_fullscreen_bmp'),
|
||||
win.outerPosition(),
|
||||
])
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
||||
const blob = new Blob([buf], { type: 'image/bmp' })
|
||||
objectUrl = URL.createObjectURL(blob)
|
||||
@@ -1946,31 +1977,36 @@ async function beginCapture() {
|
||||
])
|
||||
loading.value = false
|
||||
|
||||
const pos = await win.outerPosition()
|
||||
winOuterX = pos.x
|
||||
winOuterY = pos.y
|
||||
dpr = window.devicePixelRatio || 1
|
||||
// 窗口显示前同步尺寸:store 的 setSize 可能在 webview ready 前就已调用,
|
||||
// onResized 回调可能错过,此处主动刷新确保 winW/winH 正确
|
||||
await refreshWinSize()
|
||||
await win.show()
|
||||
await win.setFocus()
|
||||
// 显示前完成初始窗口命中(捕获时刻光标处):首帧即"窗口高亮",无"全屏遮罩→高亮"闪烁
|
||||
pickWindowAt(lastMousePhys.x, lastMousePhys.y)
|
||||
// 一次 IPC 完成 show + setFocus(比两次 JS 调用少一次往返)
|
||||
await commands.screenshotShowOverlay(WINDOWS.screenshotOverlay)
|
||||
// 窗口已显示(首帧即透明起点):整体从透明淡入,定格画面+遮罩+高亮同步浮现,不突兀
|
||||
enterState.value = 'fade'
|
||||
enterTimer = window.setTimeout(() => {
|
||||
enterState.value = 'done'
|
||||
enterTimer = null
|
||||
}, 220)
|
||||
// 窗口显示后再次刷新尺寸:隐藏窗口的 innerWidth/innerHeight 可能仍是初始 800×600,
|
||||
// show() 后 WebView2 才更新 DOM 尺寸,需重新读取确保工具栏定位正确
|
||||
await refreshWinSize()
|
||||
// 再延迟一帧重读(WebView2 尺寸更新可能滞后一帧)
|
||||
requestAnimationFrame(() => void refreshWinSize())
|
||||
|
||||
// 默认识别鼠标所在窗口(按下快捷键时鼠标仍在原位)
|
||||
try {
|
||||
const [cx, cy] = await invoke<[number, number]>('screenshot_cursor_pos')
|
||||
await pickAt(cx, cy)
|
||||
} catch {
|
||||
// 忽略初始定位失败,移动鼠标后自动识别
|
||||
}
|
||||
} catch (e) {
|
||||
errorMsg.value = (e as Error).message
|
||||
loading.value = false
|
||||
// 错误提示需立即可见:跳过淡入(idle 全透明会看不到错误信息)
|
||||
if (enterTimer !== null) {
|
||||
clearTimeout(enterTimer)
|
||||
enterTimer = null
|
||||
}
|
||||
enterState.value = 'done'
|
||||
// 出错时也显示窗口,展示错误信息(带关闭按钮)
|
||||
await win.show().catch(() => {})
|
||||
}
|
||||
@@ -1986,6 +2022,7 @@ onUnmounted(() => {
|
||||
if (pickRaf) cancelAnimationFrame(pickRaf)
|
||||
if (magRaf) cancelAnimationFrame(magRaf)
|
||||
if (redrawRaf) cancelAnimationFrame(redrawRaf)
|
||||
if (enterTimer !== null) clearTimeout(enterTimer)
|
||||
staticCanvas = null
|
||||
magGridCanvas = null
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
||||
@@ -1997,7 +2034,7 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div
|
||||
class="overlay-root"
|
||||
:class="cursorClass"
|
||||
:class="[cursorClass, enterState !== 'done' ? `overlay-enter-${enterState}` : '']"
|
||||
@mousedown="onMouseDown"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseup="onMouseUp"
|
||||
@@ -2317,6 +2354,14 @@ onUnmounted(() => {
|
||||
-webkit-user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
/* 入场淡入:idle=透明起点(窗口 show 前已就位),fade=180ms ease-out 浮现定格画面+遮罩 */
|
||||
.overlay-enter-idle {
|
||||
opacity: 0;
|
||||
}
|
||||
.overlay-enter-fade {
|
||||
opacity: 1;
|
||||
transition: opacity 180ms ease-out;
|
||||
}
|
||||
.bg-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user