feat(stage-tamagotchi): bring back fade on hover
Co-authored-by: Ilya Bogdanov <34226834+skirkru@users.noreply.github.com>
This commit is contained in:
co-authored by
Ilya Bogdanov
parent
f6fbbc5f75
commit
b097857f93
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user