feat(stage-tamagotchi-electron): always on top & loading
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-tamagotchi-electron",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with Vue and TypeScript",
|
||||
"author": "example.com",
|
||||
@@ -10,6 +11,8 @@
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"app:dev": "pnpm run dev",
|
||||
"app:build": "pnpm run build",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "pnpm run typecheck && electron-vite build",
|
||||
|
||||
@@ -40,6 +40,7 @@ function createWindow(): void {
|
||||
hasShadow: false,
|
||||
})
|
||||
|
||||
mainWindow.setAlwaysOnTop(true)
|
||||
mainWindow.setWindowButtonVisibility(false)
|
||||
mainWindow.on('ready-to-show', () => mainWindow!.show())
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// import type { AiriTamagotchiEvents } from './composables/tauri'
|
||||
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
// import { useMcpStore } from '@proj-airi/stage-ui/stores/mcp'
|
||||
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
// import { Window } from '@tauri-apps/api/window'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
// import { useAppRuntime } from './composables/runtime'
|
||||
// import { useTauriEvent } from './composables/tauri'
|
||||
import { useWindowMode } from './stores/window-controls'
|
||||
|
||||
useWindowMode()
|
||||
@@ -22,10 +16,6 @@ const settingsStore = useSettings()
|
||||
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(settingsStore)
|
||||
const onboardingStore = useOnboardingStore()
|
||||
|
||||
// const mcpStore = useMcpStore()
|
||||
// const { listen } = useTauriEvent<AiriTamagotchiEvents>()
|
||||
// const { platform } = useAppRuntime()
|
||||
|
||||
watch(language, () => {
|
||||
i18n.locale.value = language.value
|
||||
})
|
||||
@@ -45,26 +35,6 @@ watch(themeColorsHue, () => {
|
||||
watch(themeColorsHueDynamic, () => {
|
||||
document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value)
|
||||
}, { immediate: true })
|
||||
|
||||
// onMounted(() => {
|
||||
// listen('mcp_plugin_destroyed', () => {
|
||||
// mcpStore.connected = false
|
||||
// })
|
||||
// })
|
||||
|
||||
// const isMac = computed(() => platform.value === 'macos')
|
||||
|
||||
// if (isMac.value) {
|
||||
// watch(allowVisibleOnAllWorkspaces, async (value) => {
|
||||
// const window = await Window.getByLabel('main')
|
||||
|
||||
// if (!window) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// window.setVisibleOnAllWorkspaces(value)
|
||||
// })
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// import type { AiriTamagotchiEvents, Point } from '../composables/tauri'
|
||||
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useMcpStore } from '@proj-airi/stage-ui/stores/mcp'
|
||||
// import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
// import { connectServer } from '@proj-airi/tauri-plugin-mcp'
|
||||
// import { watchThrottled } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
|
||||
|
||||
// import { commands as passThroughCommands } from '../bindings/tauri-plugins/window-pass-through-on-hover'
|
||||
// import { commands } from '../bindings/tauri-plugins/window-router-link'
|
||||
// import { useTauriCore, useTauriEvent, useTauriWindow } from '../composables/tauri'
|
||||
// import { useTauriGlobalShortcuts } from '../composables/tauri-global-shortcuts'
|
||||
// import { useRdevMouse } from '../composables/use-rdev-mouse'
|
||||
// import { useResourcesStore } from '../stores/resources'
|
||||
import { useWindowStore } from '../stores/window'
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
@@ -27,138 +15,20 @@ export interface Point {
|
||||
y: number
|
||||
}
|
||||
|
||||
// const onboardingStore = useOnboardingStore()
|
||||
// const { shouldShowSetup } = storeToRefs(onboardingStore)
|
||||
|
||||
// useTauriGlobalShortcuts()
|
||||
const windowControlStore = useWindowControlStore()
|
||||
// const resourcesStore = useResourcesStore()
|
||||
const mcpStore = useMcpStore()
|
||||
// const { getPosition } = useTauriWindow()
|
||||
// const { mouseX, mouseY } = useRdevMouse()
|
||||
|
||||
// const { listen } = useTauriEvent<AiriTamagotchiEvents>()
|
||||
// const { invoke } = useTauriCore()
|
||||
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
|
||||
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
|
||||
const { centerPos, live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
|
||||
const live2dFocusAt = ref<Point>(centerPos.value)
|
||||
const widgetStageRef = ref<{ canvasElement: () => HTMLCanvasElement }>()
|
||||
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
|
||||
// const buttonsContainerRef = ref<HTMLDivElement>()
|
||||
// const windowX = ref(0)
|
||||
// const windowY = ref(0)
|
||||
|
||||
const isClickThrough = ref(false)
|
||||
// const isPassingThrough = ref(false)
|
||||
// const isOverUI = ref(false)
|
||||
const isFirstTime = ref(true)
|
||||
const isLoading = ref(true)
|
||||
const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
|
||||
|
||||
// watch(shouldShowSetup, () => {
|
||||
// if (shouldShowSetup.value) {
|
||||
// commands.go('/onboarding', 'onboarding')
|
||||
// }
|
||||
// }, { immediate: true })
|
||||
|
||||
// watchThrottled([mouseX, mouseY], async ([x, y]) => {
|
||||
// const canvas = widgetStageRef.value?.canvasElement()
|
||||
// if (!canvas) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// isFirstTime.value = false
|
||||
|
||||
// if (windowControlStore.controlMode === WindowControlMode.RESIZE || windowControlStore.controlMode === WindowControlMode.MOVE) {
|
||||
// if (isPassingThrough.value) {
|
||||
// passThroughCommands.stopPassThrough()
|
||||
// isPassingThrough.value = false
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// const relativeX = x - windowX.value
|
||||
// const relativeY = y - windowY.value
|
||||
|
||||
// const islandEl = resourceStatusIslandRef.value?.$el as HTMLElement
|
||||
// const buttonsEl = buttonsContainerRef.value
|
||||
|
||||
// let isOverUIElements = false
|
||||
// if (!windowControlStore.isIgnoringMouseEvent) {
|
||||
// if (islandEl) {
|
||||
// const rect = islandEl.getBoundingClientRect()
|
||||
// if (relativeX >= rect.left && relativeX <= rect.right && relativeY >= rect.top && relativeY <= rect.bottom) {
|
||||
// isOverUIElements = true
|
||||
// }
|
||||
// }
|
||||
// if (!isOverUIElements && buttonsEl) {
|
||||
// const rect = buttonsEl.getBoundingClientRect()
|
||||
// if (relativeX >= rect.left && relativeX <= rect.right && relativeY >= rect.top && relativeY <= rect.bottom) {
|
||||
// isOverUIElements = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// isOverUI.value = isOverUIElements
|
||||
|
||||
// if (isOverUI.value) {
|
||||
// isClickThrough.value = false
|
||||
// if (isPassingThrough.value) {
|
||||
// passThroughCommands.stopPassThrough()
|
||||
// isPassingThrough.value = false
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// let isTransparent = false
|
||||
// if (
|
||||
// relativeX >= 0
|
||||
// && relativeX < canvas.clientWidth
|
||||
// && relativeY >= 0
|
||||
// && relativeY < canvas.clientHeight
|
||||
// ) {
|
||||
// const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
|
||||
// if (gl) {
|
||||
// const pixelX = relativeX * (gl.drawingBufferWidth / canvas.clientWidth)
|
||||
// const pixelY
|
||||
// = gl.drawingBufferHeight
|
||||
// - relativeY * (gl.drawingBufferHeight / canvas.clientHeight)
|
||||
// const data = new Uint8Array(4)
|
||||
// gl.readPixels(
|
||||
// Math.floor(pixelX),
|
||||
// Math.floor(pixelY),
|
||||
// 1,
|
||||
// 1,
|
||||
// gl.RGBA,
|
||||
// gl.UNSIGNED_BYTE,
|
||||
// data,
|
||||
// )
|
||||
// isTransparent = data[3] < 100 // Use a small threshold for anti-aliasing
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// isTransparent = true
|
||||
// }
|
||||
|
||||
// isClickThrough.value = isTransparent
|
||||
|
||||
// if (windowControlStore.isIgnoringMouseEvent) {
|
||||
// if (!isPassingThrough.value) {
|
||||
// passThroughCommands.startPassThrough()
|
||||
// isPassingThrough.value = true
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (isTransparent && !isPassingThrough.value) {
|
||||
// passThroughCommands.startPassThrough()
|
||||
// isPassingThrough.value = true
|
||||
// }
|
||||
// else if (!isTransparent && isPassingThrough.value) {
|
||||
// passThroughCommands.stopPassThrough()
|
||||
// isPassingThrough.value = false
|
||||
// }
|
||||
// }, { throttle: 33 })
|
||||
|
||||
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
|
||||
watch([live2dLookAtX, live2dLookAtY], ([x, y]) => live2dFocusAt.value = { x, y }, { immediate: true })
|
||||
|
||||
const modeIndicatorClass = computed(() => {
|
||||
@@ -173,85 +43,6 @@ const modeIndicatorClass = computed(() => {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const unListenFuncs: (() => void)[] = []
|
||||
|
||||
// function openSettings() {
|
||||
// invoke('open_settings_window')
|
||||
// }
|
||||
|
||||
// function openChat() {
|
||||
// invoke('open_chat_window')
|
||||
// }
|
||||
|
||||
// async function setupVADModelLoadingProgressListener() {
|
||||
// // VAD
|
||||
// unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-ipc-audio-vad-ort:load-model-silero-vad-progress', (event) => {
|
||||
// const [_, filename, progress, totalSize, currentSize] = event.payload
|
||||
// resourcesStore.updateResourceProgress('hearing', 'vad', { filename, progress, totalSize, currentSize })
|
||||
// }))
|
||||
// }
|
||||
|
||||
// async function setupVADModel() {
|
||||
// await setupVADModelLoadingProgressListener()
|
||||
// invoke('plugin:ipc-audio-vad-ort|load_ort_model_silero_vad')
|
||||
// }
|
||||
|
||||
// async function setupWhisperModelLoadingProgressListener() {
|
||||
// // Whisper
|
||||
// unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-ipc-audio-transcription-ort:load-model-whisper-progress', (event) => {
|
||||
// const [_, filename, progress, totalSize, currentSize] = event.payload
|
||||
// resourcesStore.updateResourceProgress('hearing', 'whisper', { filename, progress, totalSize, currentSize })
|
||||
// }))
|
||||
// }
|
||||
|
||||
// async function setupWhisperModel() {
|
||||
// await setupWhisperModelLoadingProgressListener()
|
||||
// invoke('plugin:ipc-audio-transcription-ort|load_ort_model_whisper', { modelType: 'medium' })
|
||||
// }
|
||||
|
||||
onMounted(async () => {
|
||||
// const pos = await getPosition()
|
||||
// if (pos) {
|
||||
// windowX.value = pos.x
|
||||
// windowY.value = pos.y
|
||||
// }
|
||||
// unListenFuncs.push(await listen('tauri://move', (event) => {
|
||||
// windowX.value = event.payload.x
|
||||
// windowY.value = event.payload.y
|
||||
// }))
|
||||
|
||||
// await setupVADModel()
|
||||
// await setupWhisperModel()
|
||||
|
||||
if (connected.value)
|
||||
return
|
||||
if (!serverCmd.value || !serverArgs.value)
|
||||
return
|
||||
try {
|
||||
// await connectServer(serverCmd.value, serverArgs.value.split(' '))
|
||||
connected.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unListenFuncs.forEach(fn => fn?.())
|
||||
unListenFuncs.length = 0
|
||||
})
|
||||
|
||||
if (import.meta.hot) { // For better DX
|
||||
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||
unListenFuncs.forEach(fn => fn?.())
|
||||
unListenFuncs.length = 0
|
||||
})
|
||||
import.meta.hot.on('vite:afterUpdate', async () => {
|
||||
// await setupVADModelLoadingProgressListener()
|
||||
// await setupWhisperModelLoadingProgressListener()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -265,44 +56,32 @@ if (import.meta.hot) { // For better DX
|
||||
relative z-2 h-full overflow-hidden rounded-xl
|
||||
transition="opacity duration-500 ease-in-out"
|
||||
>
|
||||
<div relative h-full w-full items-end gap-2 class="view">
|
||||
<div v-show="!isLoading" relative h-full w-full items-end gap-2 class="view">
|
||||
<WidgetStage
|
||||
ref="widgetStageRef"
|
||||
h-full w-full flex-1
|
||||
:focus-at="live2dFocusAt" :scale="scale"
|
||||
v-model:state="componentStateStage"
|
||||
h-full w-full
|
||||
flex-1
|
||||
:focus-at="live2dFocusAt"
|
||||
:scale="scale"
|
||||
:x-offset="positionInPercentageString.x"
|
||||
:y-offset="positionInPercentageString.y" mb="<md:18"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
mb="<md:18"
|
||||
/>
|
||||
<ResourceStatusIsland ref="resourceStatusIslandRef" />
|
||||
<!-- <div
|
||||
ref="buttonsContainerRef"
|
||||
absolute bottom-4 left-4 flex gap-1 op-0 transition="opacity duration-500"
|
||||
:class="{
|
||||
'pointer-events-none': isClickThrough && !isOverUI,
|
||||
'show-on-hover': !windowControlStore.isIgnoringMouseEvent && (!isClickThrough || isOverUI),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
border="solid 2 primary-100 "
|
||||
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
|
||||
bg="primary-50 dark:primary-50" max-h="[10lh]" min-h="[1lh]"
|
||||
flex cursor-pointer items-center justify-center rounded-l-xl p-4 transition-colors
|
||||
@click="openChat"
|
||||
>
|
||||
<div i-solar:chat-line-bold-duotone />
|
||||
</div>
|
||||
<div
|
||||
border="solid 2 primary-100 "
|
||||
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
|
||||
bg="primary-50 dark:primary-50" max-h="[10lh]" min-h="[1lh]"
|
||||
flex cursor-pointer items-center justify-center rounded-r-xl p-4 transition-colors
|
||||
@click="openSettings"
|
||||
>
|
||||
<div i-solar:settings-bold-duotone />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div v-if="windowControlStore.controlMode === WindowControlMode.DEBUG" class="debug-controls" />
|
||||
<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">
|
||||
<div
|
||||
class="absolute h-24 w-full flex items-center justify-center overflow-hidden rounded-xl"
|
||||
bg="white/80 dark:neutral-950/80" backdrop-blur="md"
|
||||
>
|
||||
<div class="drag-region absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite select-none items-center justify-center text-1.5rem text-primary-400 font-normal">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-250"
|
||||
@@ -314,18 +93,17 @@ if (import.meta.hot) { // For better DX
|
||||
>
|
||||
<div
|
||||
v-if="windowControlStore.controlMode === WindowControlMode.MOVE"
|
||||
data-tauri-drag-region
|
||||
class="absolute left-0 top-0 z-999 h-full w-full flex cursor-grab items-center justify-center overflow-hidden"
|
||||
class="drag-region absolute left-0 top-0 z-99 h-full w-full flex cursor-grab items-center justify-center overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="absolute h-32 w-full flex items-center justify-center overflow-hidden rounded-xl"
|
||||
bg="white/80 dark:neutral-950/80" backdrop-blur="md"
|
||||
>
|
||||
<div class="wall absolute top-0 h-8" />
|
||||
<div data-tauri-drag-region class="absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite select-none items-center justify-center text-1.5rem text-primary-400 font-normal">
|
||||
<div class="drag-region absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite select-none items-center justify-center text-1.5rem text-primary-400 font-normal">
|
||||
DRAG HERE TO MOVE
|
||||
</div>
|
||||
<div data-tauri-drag-region class="wall absolute bottom-0 h-8" />
|
||||
<div class="wall drag-region absolute bottom-0 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -347,6 +125,10 @@ if (import.meta.hot) { // For better DX
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drag-region {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
.view {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"dev:ui": "pnpm -rF @proj-airi/stage-ui run story:dev",
|
||||
"dev:web": "pnpm -rF @proj-airi/stage-web run dev",
|
||||
"dev:tamagotchi": "pnpm -rF @proj-airi/stage-tamagotchi run app:dev",
|
||||
"dev:tamagotchi-electron": "pnpm -rF @proj-airi/stage-tamagotchi-electron run app:dev",
|
||||
"dev:apps": "pnpm -rF=\"./apps/*\" run --parallel dev",
|
||||
"dev:packages": "pnpm -rF=\"./packages/*\" --parallel run dev",
|
||||
"build": "turbo run build -F=\"./packages/*\" -F=\"./apps/*\"",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import Screen from '../Misc/Screen.vue'
|
||||
import Live2DCanvas from './Live2D/Canvas.vue'
|
||||
@@ -24,8 +24,19 @@ withDefaults(defineProps<{
|
||||
scale: 1,
|
||||
})
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
|
||||
const componentStateCanvas = defineModel<'pending' | 'loading' | 'mounted'>('canvasState', { default: 'pending' })
|
||||
const componentStateModel = defineModel<'pending' | 'loading' | 'mounted'>('modelState', { default: 'pending' })
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
|
||||
watch([componentStateModel, componentStateCanvas], () => {
|
||||
componentState.value = (componentStateModel.value === 'mounted' && componentStateCanvas.value === 'mounted')
|
||||
? 'mounted'
|
||||
: 'loading'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
canvasElement: () => {
|
||||
return live2dCanvasRef.value?.canvasElement()
|
||||
@@ -38,12 +49,14 @@ defineExpose({
|
||||
<Live2DCanvas
|
||||
ref="live2dCanvasRef"
|
||||
v-slot="{ app }"
|
||||
v-model:state="componentStateCanvas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:resolution="2"
|
||||
max-h="100dvh"
|
||||
>
|
||||
<Live2DModel
|
||||
v-model:state="componentStateModel"
|
||||
:model-src="modelSrc"
|
||||
:app="app"
|
||||
:mouth-open-size="mouthOpenSize"
|
||||
|
||||
@@ -13,13 +13,16 @@ const props = withDefaults(defineProps<{
|
||||
resolution: 2,
|
||||
})
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const pixiAppReady = ref(false)
|
||||
const isPixiCanvasReady = ref(false)
|
||||
const pixiApp = ref<Application>()
|
||||
const pixiAppCanvas = ref<HTMLCanvasElement>()
|
||||
|
||||
async function initLive2DPixiStage(parent: HTMLDivElement) {
|
||||
pixiAppReady.value = false
|
||||
componentState.value = 'loading'
|
||||
isPixiCanvasReady.value = false
|
||||
|
||||
// https://guansss.github.io/pixi-live2d-display/#package-importing
|
||||
Live2DModel.registerTicker(Ticker)
|
||||
@@ -43,7 +46,9 @@ async function initLive2DPixiStage(parent: HTMLDivElement) {
|
||||
pixiAppCanvas.value.style.display = 'block'
|
||||
|
||||
parent.appendChild(pixiApp.value.view)
|
||||
pixiAppReady.value = true
|
||||
|
||||
isPixiCanvasReady.value = true
|
||||
componentState.value = 'mounted'
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
@@ -90,6 +95,6 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" h-full w-full>
|
||||
<slot v-if="pixiAppReady" :app="pixiApp" />
|
||||
<slot v-if="isPixiCanvasReady" :app="pixiApp" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,6 +46,8 @@ const emits = defineEmits<{
|
||||
(e: 'modelLoaded'): void
|
||||
}>()
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
|
||||
function parsePropsOffset() {
|
||||
let xOffset = Number.parseFloat(String(props.xOffset)) || 0
|
||||
let yOffset = Number.parseFloat(String(props.yOffset)) || 0
|
||||
@@ -129,9 +131,11 @@ async function loadModel() {
|
||||
await until(modelLoading).not.toBeTruthy()
|
||||
|
||||
modelLoading.value = true
|
||||
componentState.value = 'loading'
|
||||
|
||||
if (!pixiApp.value) {
|
||||
modelLoading.value = false
|
||||
componentState.value = 'mounted'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,18 +147,19 @@ async function loadModel() {
|
||||
if (!modelSrcRef.value) {
|
||||
console.warn('No Live2D model source provided.')
|
||||
modelLoading.value = false
|
||||
componentState.value = 'mounted'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const modelInstance = new Live2DModel<PixiLive2DInternalModel>()
|
||||
const live2DModel = new Live2DModel<PixiLive2DInternalModel>()
|
||||
if (modelSrcRef.value.startsWith('blob:')) {
|
||||
const res = await fetch(modelSrcRef.value)
|
||||
const blob = await res.blob()
|
||||
await Live2DFactory.setupLive2DModel(modelInstance, [new File([blob], 'model.zip')], { autoInteract: false })
|
||||
await Live2DFactory.setupLive2DModel(live2DModel, [new File([blob], 'model.zip')], { autoInteract: false })
|
||||
}
|
||||
else {
|
||||
await Live2DFactory.setupLive2DModel(modelInstance, modelSrcRef.value, { autoInteract: false })
|
||||
await Live2DFactory.setupLive2DModel(live2DModel, modelSrcRef.value, { autoInteract: false })
|
||||
}
|
||||
|
||||
availableMotions.value.forEach((motion) => {
|
||||
@@ -166,33 +171,37 @@ async function loadModel() {
|
||||
}
|
||||
})
|
||||
|
||||
model.value = modelInstance
|
||||
// --- Scene
|
||||
|
||||
model.value = live2DModel
|
||||
pixiApp.value.stage.addChild(model.value)
|
||||
initialModelWidth.value = model.value.width
|
||||
initialModelHeight.value = model.value.height
|
||||
model.value.anchor.set(0.5, 0.5)
|
||||
setScaleAndPosition()
|
||||
|
||||
// --- Interaction
|
||||
|
||||
model.value.on('hit', (hitAreas) => {
|
||||
if (model.value && hitAreas.includes('body'))
|
||||
model.value.motion('tap_body')
|
||||
})
|
||||
|
||||
// --- Motion
|
||||
|
||||
const internalModel = model.value.internalModel
|
||||
const coreModel = internalModel.coreModel
|
||||
const motionManager = internalModel.motionManager
|
||||
coreModel.setParameterValueById('ParamMouthOpenY', mouthOpenSize.value)
|
||||
|
||||
availableMotions.value = Object.entries(motionManager.definitions).flatMap(([motionName, definition]) => {
|
||||
if (!definition)
|
||||
return []
|
||||
|
||||
return definition.map((motion: any, index: number) => ({
|
||||
availableMotions.value = Object
|
||||
.entries(motionManager.definitions)
|
||||
.flatMap(([motionName, definition]) => (definition?.map((motion: any, index: number) => ({
|
||||
motionName,
|
||||
motionIndex: index,
|
||||
fileName: motion.File,
|
||||
}))
|
||||
}).filter(Boolean)
|
||||
})) || []))
|
||||
.filter(Boolean)
|
||||
|
||||
// Remove eye ball movements from idle motion group to prevent conflicts
|
||||
// This is too hacky
|
||||
@@ -214,6 +223,7 @@ async function loadModel() {
|
||||
lastUpdateTime.value = now
|
||||
|
||||
hookedUpdate?.call(this, model, now)
|
||||
|
||||
// Possibility 1: Only update eye focus when the model is idle
|
||||
// Possibility 2: For models having no mo`ti`on groups, currentGroup will be undefined while groups can be { idle: ... }
|
||||
if (!motionManager.state.currentGroup || motionManager.state.currentGroup === motionManager.groups.idle) {
|
||||
@@ -221,26 +231,26 @@ async function loadModel() {
|
||||
|
||||
// If the model has eye blink parameters
|
||||
if (internalModel.eyeBlink != null) {
|
||||
// For the part of the auto eye blink implementation in pixi-live2d-display
|
||||
//
|
||||
// this.emit("beforeMotionUpdate");
|
||||
// const motionUpdated = this.motionManager.update(this.coreModel, now);
|
||||
// this.emit("afterMotionUpdate");
|
||||
// model.saveParameters();
|
||||
// this.motionManager.expressionManager?.update(model, now);
|
||||
// if (!motionUpdated) {
|
||||
// this.eyeBlink?.updateParameters(model, dt);
|
||||
// }
|
||||
//
|
||||
// https://github.com/guansss/pixi-live2d-display/blob/31317b37d5e22955a44d5b11f37f421e94a11269/src/cubism4/Cubism4InternalModel.ts#L202-L214
|
||||
//
|
||||
// If the this.motionManager.update returns true, as motion updated flag on,
|
||||
// the eye blink parameters will not be updated, in another hand, the auto eye blink is disabled
|
||||
//
|
||||
// Since we are hooking the motionManager.update method currently,
|
||||
// and previously a always `true` was returned, eye blink parameters were never updated.
|
||||
//
|
||||
// Thous we are here to manually update the eye blink parameters within this hooked method
|
||||
// For the part of the auto eye blink implementation in pixi-live2d-display
|
||||
//
|
||||
// this.emit("beforeMotionUpdate");
|
||||
// const motionUpdated = this.motionManager.update(this.coreModel, now);
|
||||
// this.emit("afterMotionUpdate");
|
||||
// model.saveParameters();
|
||||
// this.motionManager.expressionManager?.update(model, now);
|
||||
// if (!motionUpdated) {
|
||||
// this.eyeBlink?.updateParameters(model, dt);
|
||||
// }
|
||||
//
|
||||
// https://github.com/guansss/pixi-live2d-display/blob/31317b37d5e22955a44d5b11f37f421e94a11269/src/cubism4/Cubism4InternalModel.ts#L202-L214
|
||||
//
|
||||
// If the this.motionManager.update returns true, as motion updated flag on,
|
||||
// the eye blink parameters will not be updated, in another hand, the auto eye blink is disabled
|
||||
//
|
||||
// Since we are hooking the motionManager.update method currently,
|
||||
// and previously a always `true` was returned, eye blink parameters were never updated.
|
||||
//
|
||||
// Thous we are here to manually update the eye blink parameters within this hooked method
|
||||
internalModel.eyeBlink.updateParameters(model, (now - lastUpdateTime.value) / 1000)
|
||||
}
|
||||
|
||||
@@ -259,6 +269,7 @@ async function loadModel() {
|
||||
}
|
||||
finally {
|
||||
modelLoading.value = false
|
||||
componentState.value = 'mounted'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ withDefaults(defineProps<{
|
||||
scale?: number
|
||||
}>(), { paused: false, scale: 1 })
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
|
||||
const db = ref<DuckDBWasmDrizzleDatabase>()
|
||||
// const transformersProvider = createTransformers({ embedWorkerURL })
|
||||
|
||||
@@ -286,7 +288,8 @@ defineExpose({
|
||||
<Live2DScene
|
||||
v-if="stageModelRenderer === 'live2d' && showStage"
|
||||
ref="live2dSceneRef"
|
||||
min-w="50% <lg:full" min-h="100 sm:100" h-full w-full flex-1
|
||||
v-model:state="componentState" min-w="50% <lg:full" min-h="100 sm:100" h-full w-full
|
||||
flex-1
|
||||
:model-src="stageModelSelectedUrl"
|
||||
:focus-at="focusAt"
|
||||
:mouth-open-size="mouthOpenSize"
|
||||
|
||||
Reference in New Issue
Block a user