feat(stage-tamagotchi): bring back fade on hover

Co-authored-by: Ilya Bogdanov <34226834+skirkru@users.noreply.github.com>
This commit is contained in:
Neko Ayaka
2025-10-23 15:36:52 +08:00
co-authored by Ilya Bogdanov
parent f6fbbc5f75
commit b097857f93
4 changed files with 131 additions and 41 deletions
@@ -1,34 +1,31 @@
<script setup lang="ts">
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 { storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { computed, ref, toRef, watch } from 'vue'
import ControlsIsland from '../components/Widgets/ControlsIsland/index.vue'
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
import { useWindowStore } from '../stores/window'
import { useElectronRelativeMouse, useWindowStore } from '../stores/window'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
export interface Point {
x: number
y: number
}
const windowControlStore = useWindowControlStore()
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
const stageCanvas = toRef(() => widgetStageRef.value?.canvasElement())
const isClickThrough = ref(false)
const isFirstTime = ref(true)
const isPassingThrough = ref(false)
const isLoading = ref(true)
const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
const windowControlStore = useWindowControlStore()
const { x: relativeMouseX, y: relativeMouseY } = useElectronRelativeMouse()
const isTransparent = useCanvasPixelIsTransparentAtPoint(stageCanvas, relativeMouseX, relativeMouseY)
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
const modeIndicatorClass = computed(() => {
switch (windowControlStore.controlMode) {
@@ -42,33 +39,53 @@ const modeIndicatorClass = computed(() => {
return ''
}
})
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
watch(isTransparent, (transparent) => {
isClickThrough.value = transparent
isPassingThrough.value = transparent
windowControlStore.isIgnoringMouseEvent = !transparent
})
</script>
<template>
<div
:class="[modeIndicatorClass, {
'op-0': windowControlStore.isIgnoringMouseEvent && !isClickThrough && !isFirstTime,
}]"
:class="[modeIndicatorClass]"
max-h="[100vh]"
max-w="[100vw]"
flex="~ col"
relative z-2 h-full overflow-hidden rounded-xl
transition="opacity duration-500 ease-in-out"
>
<div v-show="!isLoading" relative h-full w-full items-end gap-2 class="view">
<ResourceStatusIsland ref="resourceStatusIslandRef" />
<WidgetStage
ref="widgetStageRef"
v-model:state="componentStateStage"
h-full w-full
flex-1
:focus-at="{ x: live2dLookAtX, y: live2dLookAtY }"
:scale="scale"
:x-offset="positionInPercentageString.x"
:y-offset="positionInPercentageString.y"
mb="<md:18"
/>
<ControlsIsland />
<div
v-show="!isLoading"
:class="[
'relative h-full w-full items-end gap-2',
'transition-opacity duration-250 ease-in-out',
]"
>
<div
:class="[
windowControlStore.isIgnoringMouseEvent && !isClickThrough ? 'op-0' : 'op-100',
'absolute',
'top-0 left-0 w-full h-full',
'transition-opacity duration-250 ease-in-out',
]"
>
<ResourceStatusIsland ref="resourceStatusIslandRef" />
<WidgetStage
ref="widgetStageRef"
v-model:state="componentStateStage"
h-full w-full
flex-1
:focus-at="{ x: live2dLookAtX, y: live2dLookAtY }"
:scale="scale"
:x-offset="positionInPercentageString.x"
:y-offset="positionInPercentageString.y"
mb="<md:18"
/>
<ControlsIsland />
</div>
</div>
<div v-show="isLoading" h-full w-full>
<div class="absolute left-0 top-0 z-99 h-full w-full flex cursor-grab items-center justify-center overflow-hidden">
@@ -138,14 +155,6 @@ const modeIndicatorClass = computed(() => {
</template>
<style scoped>
.view {
transition: opacity 0.5s ease-in-out;
.show-on-hover {
opacity: 1;
}
}
@keyframes wall-move {
0% {
transform: translateX(calc(var(--wall-width) * -2));
@@ -11,7 +11,7 @@ import { startClickThrough, stopClickThrough } from '../utils/windows'
export const useWindowControlStore = defineStore('windowControl', () => {
const controlMode = ref<WindowControlMode>(WindowControlMode.NONE)
const isControlActive = ref(false)
const isIgnoringMouseEvent = ref(true)
const isIgnoringMouseEvent = ref(false)
function toggleMode(mode: WindowControlMode) {
controlMode.value = mode
@@ -0,0 +1,80 @@
import type { MaybeRefOrGetter } from '@vueuse/core'
import type { Ref } from 'vue'
import { toRef, unrefElement, useElementBounding } from '@vueuse/core'
import { computed } from 'vue'
export function useCanvasPixelAtPoint(
canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>,
pointX: MaybeRefOrGetter<number>,
pointY: MaybeRefOrGetter<number>,
): {
inCanvas: Ref<boolean>
pixel: Ref<Uint8Array | number[]>
} {
const canvasRef = toRef(canvas)
const { left, top, width, height } = useElementBounding(canvasRef)
const xRef = toRef(pointX)
const yRef = toRef(pointY)
const inCanvas = computed(() => {
if (canvasRef.value == null) {
return false
}
const xIn = xRef.value - left.value
const yIn = yRef.value - top.value
return xIn >= 0 && yIn >= 0 && xIn < width.value && yIn < height.value
})
const pixel = computed(() => {
const el = unrefElement(canvasRef)
if (!el || !inCanvas.value)
return new Uint8Array([0, 0, 0, 0])
const gl = (el.getContext('webgl2') || el.getContext('webgl')) as WebGL2RenderingContext | WebGLRenderingContext | null
if (!gl)
return new Uint8Array([0, 0, 0, 0])
const xIn = xRef.value - left.value
const yIn = yRef.value - top.value
const scaleX = gl.drawingBufferWidth / width.value
const scaleY = gl.drawingBufferHeight / height.value
const pixelX = Math.floor(xIn * scaleX)
// Flip Y; subtract 1 to avoid top-edge off-by-one
const pixelY = Math.floor(gl.drawingBufferHeight - 1 - yIn * scaleY)
const data = new Uint8Array(4)
try {
gl.readPixels(pixelX, pixelY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data)
}
catch {
return new Uint8Array([0, 0, 0, 0])
}
return data
})
return {
inCanvas,
pixel,
}
}
export function useCanvasPixelIsTransparent(
pixel: Ref<Uint8Array | number[]>,
threshold = 10,
): Ref<boolean> {
return computed(() => pixel.value[3] < threshold)
}
export function useCanvasPixelIsTransparentAtPoint(
canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>,
pointX: MaybeRefOrGetter<number>,
pointY: MaybeRefOrGetter<number>,
threshold = 10,
): Ref<boolean> {
const { pixel } = useCanvasPixelAtPoint(canvas, pointX, pointY)
return useCanvasPixelIsTransparent(pixel, threshold)
}
@@ -1,4 +1,5 @@
export * from './audio'
export * from './canvas-alpha'
export * from './llmmarkerParser'
export * from './markdown'
export * from './micvad'