Skip to content

Commit b097857

Browse files
nekomeowwwskirkru
andcommitted
feat(stage-tamagotchi): bring back fade on hover
Co-authored-by: Ilya Bogdanov <34226834+skirkru@users.noreply.github.com>
1 parent f6fbbc5 commit b097857

File tree

4 files changed

+131
-41
lines changed

4 files changed

+131
-41
lines changed

apps/stage-tamagotchi/src/renderer/pages/index.vue

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
<script setup lang="ts">
22
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
3+
import { useCanvasPixelIsTransparentAtPoint } from '@proj-airi/stage-ui/composables/canvas-alpha'
34
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
45
import { storeToRefs } from 'pinia'
5-
import { computed, ref, watch } from 'vue'
6+
import { computed, ref, toRef, watch } from 'vue'
67
78
import ControlsIsland from '../components/Widgets/ControlsIsland/index.vue'
89
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
910
10-
import { useWindowStore } from '../stores/window'
11+
import { useElectronRelativeMouse, useWindowStore } from '../stores/window'
1112
import { useWindowControlStore } from '../stores/window-controls'
1213
import { WindowControlMode } from '../types/window-controls'
1314
14-
export interface Point {
15-
x: number
16-
y: number
17-
}
18-
19-
const windowControlStore = useWindowControlStore()
20-
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
21-
22-
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
23-
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
2415
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
25-
16+
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
17+
const stageCanvas = toRef(() => widgetStageRef.value?.canvasElement())
2618
const isClickThrough = ref(false)
27-
const isFirstTime = ref(true)
19+
const isPassingThrough = ref(false)
2820
const isLoading = ref(true)
2921
const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
3022
31-
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
23+
const windowControlStore = useWindowControlStore()
24+
const { x: relativeMouseX, y: relativeMouseY } = useElectronRelativeMouse()
25+
const isTransparent = useCanvasPixelIsTransparentAtPoint(stageCanvas, relativeMouseX, relativeMouseY)
26+
27+
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
28+
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
3229
3330
const modeIndicatorClass = computed(() => {
3431
switch (windowControlStore.controlMode) {
@@ -42,33 +39,53 @@ const modeIndicatorClass = computed(() => {
4239
return ''
4340
}
4441
})
42+
43+
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
44+
watch(isTransparent, (transparent) => {
45+
isClickThrough.value = transparent
46+
isPassingThrough.value = transparent
47+
windowControlStore.isIgnoringMouseEvent = !transparent
48+
})
4549
</script>
4650

4751
<template>
4852
<div
49-
:class="[modeIndicatorClass, {
50-
'op-0': windowControlStore.isIgnoringMouseEvent && !isClickThrough && !isFirstTime,
51-
}]"
53+
:class="[modeIndicatorClass]"
5254
max-h="[100vh]"
5355
max-w="[100vw]"
5456
flex="~ col"
5557
relative z-2 h-full overflow-hidden rounded-xl
5658
transition="opacity duration-500 ease-in-out"
5759
>
58-
<div v-show="!isLoading" relative h-full w-full items-end gap-2 class="view">
59-
<ResourceStatusIsland ref="resourceStatusIslandRef" />
60-
<WidgetStage
61-
ref="widgetStageRef"
62-
v-model:state="componentStateStage"
63-
h-full w-full
64-
flex-1
65-
:focus-at="{ x: live2dLookAtX, y: live2dLookAtY }"
66-
:scale="scale"
67-
:x-offset="positionInPercentageString.x"
68-
:y-offset="positionInPercentageString.y"
69-
mb="<md:18"
70-
/>
71-
<ControlsIsland />
60+
<div
61+
v-show="!isLoading"
62+
:class="[
63+
'relative h-full w-full items-end gap-2',
64+
'transition-opacity duration-250 ease-in-out',
65+
]"
66+
>
67+
<div
68+
:class="[
69+
windowControlStore.isIgnoringMouseEvent && !isClickThrough ? 'op-0' : 'op-100',
70+
'absolute',
71+
'top-0 left-0 w-full h-full',
72+
'transition-opacity duration-250 ease-in-out',
73+
]"
74+
>
75+
<ResourceStatusIsland ref="resourceStatusIslandRef" />
76+
<WidgetStage
77+
ref="widgetStageRef"
78+
v-model:state="componentStateStage"
79+
h-full w-full
80+
flex-1
81+
:focus-at="{ x: live2dLookAtX, y: live2dLookAtY }"
82+
:scale="scale"
83+
:x-offset="positionInPercentageString.x"
84+
:y-offset="positionInPercentageString.y"
85+
mb="<md:18"
86+
/>
87+
<ControlsIsland />
88+
</div>
7289
</div>
7390
<div v-show="isLoading" h-full w-full>
7491
<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(() => {
138155
</template>
139156

140157
<style scoped>
141-
.view {
142-
transition: opacity 0.5s ease-in-out;
143-
144-
.show-on-hover {
145-
opacity: 1;
146-
}
147-
}
148-
149158
@keyframes wall-move {
150159
0% {
151160
transform: translateX(calc(var(--wall-width) * -2));

apps/stage-tamagotchi/src/renderer/stores/window-controls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { startClickThrough, stopClickThrough } from '../utils/windows'
1111
export const useWindowControlStore = defineStore('windowControl', () => {
1212
const controlMode = ref<WindowControlMode>(WindowControlMode.NONE)
1313
const isControlActive = ref(false)
14-
const isIgnoringMouseEvent = ref(true)
14+
const isIgnoringMouseEvent = ref(false)
1515

1616
function toggleMode(mode: WindowControlMode) {
1717
controlMode.value = mode
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { MaybeRefOrGetter } from '@vueuse/core'
2+
import type { Ref } from 'vue'
3+
4+
import { toRef, unrefElement, useElementBounding } from '@vueuse/core'
5+
import { computed } from 'vue'
6+
7+
export function useCanvasPixelAtPoint(
8+
canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>,
9+
pointX: MaybeRefOrGetter<number>,
10+
pointY: MaybeRefOrGetter<number>,
11+
): {
12+
inCanvas: Ref<boolean>
13+
pixel: Ref<Uint8Array | number[]>
14+
} {
15+
const canvasRef = toRef(canvas)
16+
17+
const { left, top, width, height } = useElementBounding(canvasRef)
18+
const xRef = toRef(pointX)
19+
const yRef = toRef(pointY)
20+
21+
const inCanvas = computed(() => {
22+
if (canvasRef.value == null) {
23+
return false
24+
}
25+
26+
const xIn = xRef.value - left.value
27+
const yIn = yRef.value - top.value
28+
return xIn >= 0 && yIn >= 0 && xIn < width.value && yIn < height.value
29+
})
30+
31+
const pixel = computed(() => {
32+
const el = unrefElement(canvasRef)
33+
if (!el || !inCanvas.value)
34+
return new Uint8Array([0, 0, 0, 0])
35+
36+
const gl = (el.getContext('webgl2') || el.getContext('webgl')) as WebGL2RenderingContext | WebGLRenderingContext | null
37+
if (!gl)
38+
return new Uint8Array([0, 0, 0, 0])
39+
40+
const xIn = xRef.value - left.value
41+
const yIn = yRef.value - top.value
42+
43+
const scaleX = gl.drawingBufferWidth / width.value
44+
const scaleY = gl.drawingBufferHeight / height.value
45+
const pixelX = Math.floor(xIn * scaleX)
46+
// Flip Y; subtract 1 to avoid top-edge off-by-one
47+
const pixelY = Math.floor(gl.drawingBufferHeight - 1 - yIn * scaleY)
48+
49+
const data = new Uint8Array(4)
50+
try {
51+
gl.readPixels(pixelX, pixelY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data)
52+
}
53+
catch {
54+
return new Uint8Array([0, 0, 0, 0])
55+
}
56+
return data
57+
})
58+
59+
return {
60+
inCanvas,
61+
pixel,
62+
}
63+
}
64+
65+
export function useCanvasPixelIsTransparent(
66+
pixel: Ref<Uint8Array | number[]>,
67+
threshold = 10,
68+
): Ref<boolean> {
69+
return computed(() => pixel.value[3] < threshold)
70+
}
71+
72+
export function useCanvasPixelIsTransparentAtPoint(
73+
canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>,
74+
pointX: MaybeRefOrGetter<number>,
75+
pointY: MaybeRefOrGetter<number>,
76+
threshold = 10,
77+
): Ref<boolean> {
78+
const { pixel } = useCanvasPixelAtPoint(canvas, pointX, pointY)
79+
return useCanvasPixelIsTransparent(pixel, threshold)
80+
}

packages/stage-ui/src/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './audio'
2+
export * from './canvas-alpha'
23
export * from './llmmarkerParser'
34
export * from './markdown'
45
export * from './micvad'

0 commit comments

Comments
 (0)