feat(stage-tamagotchi): bring back pass through on hover, with intelligence hover detection
This commit is contained in:
@@ -24,4 +24,5 @@ export function createWindowService(params: { context: ReturnType<typeof createC
|
||||
defineInvokeHandler(params.context, startLoopGetBounds, () => start())
|
||||
|
||||
defineInvokeHandler(params.context, electron.window.getBounds, () => params.window.getBounds())
|
||||
defineInvokeHandler(params.context, electron.window.setIgnoreMouseEvents, invokeOpts => params.window.setIgnoreMouseEvents(invokeOpts[0], invokeOpts[1]))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export { useElectronEventaContext, useElectronEventaInvoke } from './use-electron-eventa-context'
|
||||
export { useElectronMouse, useElectronMouseEventTarget } from './use-electron-mouse'
|
||||
export type { UseMouseInElementReturn } from './use-electron-mouse-in-element'
|
||||
export { useElectronMouseInElement } from './use-electron-mouse-in-element'
|
||||
export { useElectronRelativeMouse } from './use-electron-relative-mouse'
|
||||
export { useElectronWindowBounds } from './use-electron-window-bounds'
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { InvokeEventa } from '@unbird/eventa'
|
||||
|
||||
import { defineInvoke } from '@unbird/eventa'
|
||||
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useElectronEventaContext() {
|
||||
const context = ref(createContext(window.electron.ipcRenderer).context)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export function useElectronEventaInvoke<Res, Req = undefined, ResErr = Error, ReqErr = Error>(invoke: InvokeEventa<Res, Req, ResErr, ReqErr>, context?: ReturnType<typeof createContext>['context']) {
|
||||
return defineInvoke(context ?? useElectronEventaContext().value, invoke)
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import type { MaybeElementRef, UseMouseOptions } from '@vueuse/core'
|
||||
|
||||
import { defaultWindow, tryOnMounted, unrefElement, useEventListener, useMutationObserver, useResizeObserver } from '@vueuse/core'
|
||||
import { shallowRef, watch } from 'vue'
|
||||
|
||||
import { useElectronRelativeMouse } from '../use-electron-relative-mouse'
|
||||
|
||||
export interface MouseInElementOptions extends UseMouseOptions {
|
||||
/**
|
||||
* Whether to handle mouse events when the cursor is outside the target element.
|
||||
* When enabled, mouse position will continue to be tracked even when outside the element bounds.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
handleOutside?: boolean
|
||||
|
||||
/**
|
||||
* Listen to window resize event
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
windowScroll?: boolean
|
||||
|
||||
/**
|
||||
* Listen to window scroll event
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
windowResize?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactive mouse position related to an element.
|
||||
*
|
||||
* @see https://vueuse.org/useMouseInElement
|
||||
* @param target
|
||||
* @param options
|
||||
*/
|
||||
export function useElectronMouseInElement(
|
||||
target?: MaybeElementRef,
|
||||
options: MouseInElementOptions = {},
|
||||
) {
|
||||
const {
|
||||
windowResize = true,
|
||||
windowScroll = true,
|
||||
handleOutside = true,
|
||||
window = defaultWindow,
|
||||
} = options
|
||||
const type = options.type || 'page'
|
||||
|
||||
const { x, y, sourceType } = useElectronRelativeMouse(options)
|
||||
|
||||
const targetRef = shallowRef(target ?? window?.document.body)
|
||||
const elementX = shallowRef(0)
|
||||
const elementY = shallowRef(0)
|
||||
const elementPositionX = shallowRef(0)
|
||||
const elementPositionY = shallowRef(0)
|
||||
const elementHeight = shallowRef(0)
|
||||
const elementWidth = shallowRef(0)
|
||||
const isOutside = shallowRef(true)
|
||||
|
||||
function update() {
|
||||
if (!window)
|
||||
return
|
||||
|
||||
const el = unrefElement(targetRef)
|
||||
if (!el || !(el instanceof Element))
|
||||
return
|
||||
|
||||
const {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
} = el.getBoundingClientRect()
|
||||
|
||||
elementPositionX.value = left + (type === 'page' ? window.pageXOffset : 0)
|
||||
elementPositionY.value = top + (type === 'page' ? window.pageYOffset : 0)
|
||||
elementHeight.value = height
|
||||
elementWidth.value = width
|
||||
|
||||
const elX = x.value - elementPositionX.value
|
||||
const elY = y.value - elementPositionY.value
|
||||
isOutside.value = width === 0 || height === 0
|
||||
|| elX < 0 || elY < 0
|
||||
|| elX > width || elY > height
|
||||
|
||||
if (handleOutside || !isOutside.value) {
|
||||
elementX.value = elX
|
||||
elementY.value = elY
|
||||
}
|
||||
}
|
||||
|
||||
const stopFnList: Array<() => void> = []
|
||||
function stop() {
|
||||
stopFnList.forEach(fn => fn())
|
||||
stopFnList.length = 0
|
||||
}
|
||||
|
||||
tryOnMounted(() => {
|
||||
update()
|
||||
})
|
||||
|
||||
if (window) {
|
||||
const {
|
||||
stop: stopResizeObserver,
|
||||
} = useResizeObserver(targetRef, update)
|
||||
const {
|
||||
stop: stopMutationObserver,
|
||||
} = useMutationObserver(targetRef, update, {
|
||||
attributeFilter: ['style', 'class'],
|
||||
})
|
||||
|
||||
const stopWatch = watch(
|
||||
[targetRef, x, y],
|
||||
update,
|
||||
)
|
||||
|
||||
stopFnList.push(
|
||||
stopResizeObserver,
|
||||
stopMutationObserver,
|
||||
stopWatch,
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
document,
|
||||
'mouseleave',
|
||||
() => isOutside.value = true,
|
||||
{ passive: true },
|
||||
)
|
||||
|
||||
if (windowScroll) {
|
||||
stopFnList.push(
|
||||
useEventListener('scroll', update, { capture: true, passive: true }),
|
||||
)
|
||||
}
|
||||
if (windowResize) {
|
||||
stopFnList.push(
|
||||
useEventListener('resize', update, { passive: true }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
sourceType,
|
||||
elementX,
|
||||
elementY,
|
||||
elementPositionX,
|
||||
elementPositionY,
|
||||
elementHeight,
|
||||
elementWidth,
|
||||
isOutside,
|
||||
stop,
|
||||
}
|
||||
}
|
||||
|
||||
export type UseMouseInElementReturn = ReturnType<typeof useElectronMouseInElement>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import type { UseMouseOptions } from '@vueuse/core'
|
||||
|
||||
import { defineInvoke } from '@unbird/eventa'
|
||||
import { useMouse } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { cursorScreenPoint, startLoopGetCursorScreenPoint } from '../../../../shared/electron/screen'
|
||||
import { useElectronEventaContext } from '../use-electron-eventa-context'
|
||||
|
||||
export function useElectronMouseEventTarget() {
|
||||
const context = useElectronEventaContext()
|
||||
const eventTarget = ref(new EventTarget())
|
||||
|
||||
context.value.on(cursorScreenPoint, (event) => {
|
||||
const e = new MouseEvent('mousemove', { screenX: event.body?.x, screenY: event.body?.y })
|
||||
eventTarget.value.dispatchEvent(e)
|
||||
})
|
||||
|
||||
defineInvoke(context.value!, startLoopGetCursorScreenPoint)()
|
||||
return eventTarget
|
||||
}
|
||||
|
||||
export function useElectronMouse(options?: UseMouseOptions) {
|
||||
const eventTarget = useElectronMouseEventTarget()
|
||||
return useMouse({ ...options, target: eventTarget, type: 'screen' })
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import type { UseMouseOptions } from '@vueuse/core'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useElectronMouse } from '../use-electron-mouse'
|
||||
import { useElectronWindowBounds } from '../use-electron-window-bounds'
|
||||
|
||||
export function useElectronRelativeMouse(options?: UseMouseOptions) {
|
||||
const mouse = useElectronMouse(options)
|
||||
const { x: windowX, y: windowY } = useElectronWindowBounds()
|
||||
|
||||
// Transform screen coordinates to window-relative coordinates
|
||||
const x = computed(() => mouse.x.value - windowX.value)
|
||||
const y = computed(() => mouse.y.value - windowY.value)
|
||||
|
||||
return {
|
||||
...mouse,
|
||||
x,
|
||||
y,
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { defineInvoke } from '@unbird/eventa'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { bounds, startLoopGetBounds } from '../../../../shared/electron/window'
|
||||
import { useElectronEventaContext } from '../use-electron-eventa-context'
|
||||
|
||||
export function useElectronWindowBounds() {
|
||||
const context = useElectronEventaContext()
|
||||
const windowBoundsX = ref(0)
|
||||
const windowBoundsY = ref(0)
|
||||
const windowBoundsWidth = ref(0)
|
||||
const windowBoundsHeight = ref(0)
|
||||
|
||||
context.value.on(bounds, (event) => {
|
||||
if (!event || !event.body)
|
||||
return
|
||||
|
||||
windowBoundsX.value = event.body.x
|
||||
windowBoundsY.value = event.body.y
|
||||
windowBoundsWidth.value = event.body.width
|
||||
windowBoundsHeight.value = event.body.height
|
||||
})
|
||||
|
||||
defineInvoke(context.value!, startLoopGetBounds)()
|
||||
return {
|
||||
x: windowBoundsX,
|
||||
y: windowBoundsY,
|
||||
width: windowBoundsWidth,
|
||||
height: windowBoundsHeight,
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,21 @@
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useCanvasPixelIsTransparentAtPoint } from '@proj-airi/stage-ui/composables/canvas-alpha'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { debouncedRef, watchPausable } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
|
||||
import ControlsIsland from '../components/Widgets/ControlsIsland/index.vue'
|
||||
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
|
||||
|
||||
import { useElectronRelativeMouse, useWindowStore } from '../stores/window'
|
||||
import { electron } from '../../shared/electron'
|
||||
import { useElectronEventaInvoke, useElectronMouseInElement, useElectronRelativeMouse } from '../composables/electron-vueuse'
|
||||
import { useWindowStore } from '../stores/window'
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
|
||||
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
|
||||
const controlsIslandRef = ref<InstanceType<typeof ControlsIsland>>()
|
||||
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
|
||||
const stageCanvas = toRef(() => widgetStageRef.value?.canvasElement())
|
||||
const isClickThrough = ref(false)
|
||||
@@ -23,6 +27,9 @@ const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
|
||||
const windowControlStore = useWindowControlStore()
|
||||
const { x: relativeMouseX, y: relativeMouseY } = useElectronRelativeMouse()
|
||||
const isTransparent = useCanvasPixelIsTransparentAtPoint(stageCanvas, relativeMouseX, relativeMouseY)
|
||||
const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMouseEvents)
|
||||
const { isOutside } = useElectronMouseInElement(controlsIslandRef)
|
||||
const isOutsideFor250Ms = debouncedRef(isOutside, 250)
|
||||
|
||||
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
|
||||
@@ -41,10 +48,34 @@ const modeIndicatorClass = computed(() => {
|
||||
})
|
||||
|
||||
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
|
||||
watch(isTransparent, (transparent) => {
|
||||
const { pause, resume } = watchPausable(isTransparent, (transparent) => {
|
||||
isClickThrough.value = transparent
|
||||
isPassingThrough.value = transparent
|
||||
isPassingThrough.value = !transparent
|
||||
windowControlStore.isIgnoringMouseEvent = !transparent
|
||||
|
||||
if (windowControlStore.isIgnoringMouseEvent) {
|
||||
setIgnoreMouseEvents([true, { forward: true }])
|
||||
}
|
||||
else {
|
||||
setIgnoreMouseEvents([false, { forward: true }])
|
||||
}
|
||||
})
|
||||
|
||||
watch(isOutsideFor250Ms, () => {
|
||||
if (!isOutsideFor250Ms.value) {
|
||||
isClickThrough.value = false
|
||||
isPassingThrough.value = false
|
||||
windowControlStore.isIgnoringMouseEvent = false
|
||||
setIgnoreMouseEvents([false, { forward: true }])
|
||||
pause()
|
||||
}
|
||||
else {
|
||||
isClickThrough.value = true
|
||||
isPassingThrough.value = true
|
||||
windowControlStore.isIgnoringMouseEvent = true
|
||||
setIgnoreMouseEvents([true, { forward: true }])
|
||||
resume()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -84,7 +115,7 @@ watch(isTransparent, (transparent) => {
|
||||
:y-offset="positionInPercentageString.y"
|
||||
mb="<md:18"
|
||||
/>
|
||||
<ControlsIsland />
|
||||
<ControlsIsland ref="controlsIslandRef" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isLoading" h-full w-full>
|
||||
|
||||
@@ -1,34 +1,15 @@
|
||||
import { defineInvoke } from '@unbird/eventa'
|
||||
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
|
||||
import { useAsyncState, useIntervalFn, useMouse, useWindowSize } from '@vueuse/core'
|
||||
import { useAsyncState, useIntervalFn, useWindowSize } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { cursorScreenPoint, startLoopGetCursorScreenPoint } from '../../shared/electron/screen'
|
||||
import { bounds, startLoopGetBounds } from '../../shared/electron/window'
|
||||
import { electron } from '../../shared/eventa'
|
||||
import { useElectronRelativeMouse } from '../composables/electron-vueuse'
|
||||
import { useElectronEventaContext } from '../composables/electron-vueuse/use-electron-eventa-context'
|
||||
import { useWindowControlStore } from './window-controls'
|
||||
|
||||
export function useElectronMouseEventTarget() {
|
||||
const context = ref(createContext(window.electron.ipcRenderer).context)
|
||||
const eventTarget = ref(new EventTarget())
|
||||
|
||||
context.value.on(cursorScreenPoint, (event) => {
|
||||
const e = new MouseEvent('mousemove', { screenX: event.body?.x, screenY: event.body?.y })
|
||||
eventTarget.value.dispatchEvent(e)
|
||||
})
|
||||
|
||||
defineInvoke(context.value!, startLoopGetCursorScreenPoint)()
|
||||
return eventTarget
|
||||
}
|
||||
|
||||
export function useElectronMouse(options?: { x: number, y: number }) {
|
||||
const eventTarget = useElectronMouseEventTarget()
|
||||
return useMouse({ target: eventTarget, type: 'screen', initialValue: options })
|
||||
}
|
||||
|
||||
export function useElectronAllDisplays() {
|
||||
const context = ref(createContext(window.electron.ipcRenderer).context)
|
||||
const context = useElectronEventaContext()
|
||||
const getAllDisplays = defineInvoke(context.value, electron.screen.getAllDisplays)
|
||||
const { state: allDisplays, execute } = useAsyncState(() => getAllDisplays(), [])
|
||||
|
||||
@@ -39,43 +20,6 @@ export function useElectronAllDisplays() {
|
||||
return allDisplays
|
||||
}
|
||||
|
||||
export function useElectronWindowBounds() {
|
||||
const context = ref(createContext(window.electron.ipcRenderer).context)
|
||||
const windowBoundsX = ref(0)
|
||||
const windowBoundsY = ref(0)
|
||||
const windowBoundsWidth = ref(0)
|
||||
const windowBoundsHeight = ref(0)
|
||||
|
||||
context.value.on(bounds, (event) => {
|
||||
if (!event || !event.body)
|
||||
return
|
||||
|
||||
windowBoundsX.value = event.body.x
|
||||
windowBoundsY.value = event.body.y
|
||||
windowBoundsWidth.value = event.body.width
|
||||
windowBoundsHeight.value = event.body.height
|
||||
})
|
||||
|
||||
defineInvoke(context.value!, startLoopGetBounds)()
|
||||
return {
|
||||
x: windowBoundsX,
|
||||
y: windowBoundsY,
|
||||
width: windowBoundsWidth,
|
||||
height: windowBoundsHeight,
|
||||
}
|
||||
}
|
||||
|
||||
export function useElectronRelativeMouse(initialValue?: { x: number, y: number }) {
|
||||
const { x: mouseX, y: mouseY } = useElectronMouse(initialValue)
|
||||
const { x: windowX, y: windowY } = useElectronWindowBounds()
|
||||
|
||||
// Transform screen coordinates to window-relative coordinates
|
||||
const x = computed(() => mouseX.value - windowX.value)
|
||||
const y = computed(() => mouseY.value - windowY.value)
|
||||
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
export const useWindowStore = defineStore('tamagotchi-window', () => {
|
||||
const windowControlStore = useWindowControlStore()
|
||||
|
||||
@@ -84,7 +28,7 @@ export const useWindowStore = defineStore('tamagotchi-window', () => {
|
||||
|
||||
// Use window-relative mouse coordinates for Live2D focus
|
||||
// Transforms screen coordinates to window-relative coordinates
|
||||
const { x: live2dLookAtX, y: live2dLookAtY } = useElectronRelativeMouse(centerPos.value)
|
||||
const { x: live2dLookAtX, y: live2dLookAtY } = useElectronRelativeMouse({ initialValue: centerPos.value })
|
||||
|
||||
const isCursorInside = ref(false)
|
||||
const shouldHideView = computed(() => isCursorInside.value && !windowControlStore.isControlActive && windowControlStore.isIgnoringMouseEvent)
|
||||
|
||||
@@ -6,7 +6,9 @@ export const bounds = defineEventa<Rectangle>('eventa:event:electron:window:boun
|
||||
export const startLoopGetBounds = defineInvokeEventa('eventa:event:electron:window:start-loop-get-bounds')
|
||||
|
||||
const getBounds = defineInvokeEventa<ReturnType<BrowserWindow['getBounds']>>('eventa:invoke:electron:window:get-bounds')
|
||||
const setIgnoreMouseEvents = defineInvokeEventa<void, [boolean, { forward: boolean }]>('eventa:invoke:electron:window:set-ignore-mouse-events')
|
||||
|
||||
export const window = {
|
||||
getBounds,
|
||||
setIgnoreMouseEvents,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user