style: use screen component for scenes

This commit is contained in:
Neko Ayaka
2024-12-11 17:02:53 +08:00
parent 01dbaeb931
commit 5262c79702
6 changed files with 96 additions and 64 deletions
+2
View File
@@ -288,6 +288,7 @@ declare global {
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWhisper: typeof import('./composables/whisper')['useWhisper']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
@@ -596,6 +597,7 @@ declare module 'vue' {
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWhisper: UnwrapRef<typeof import('./composables/whisper')['useWhisper']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { EmotionAngryMotionName, EmotionAwkwardMotionName, EmotionHappyMotionName, EmotionQuestionMotionName, EmotionSadMotionName, EmotionSurpriseMotionName, EmotionThinkMotionName } from '~/constants/emotions'
withDefaults(defineProps<{
model: string
mouthOpenSize?: number
}>(), {
mouthOpenSize: 0,
})
const modelRef = ref<{
setMotion: (motionName: string) => Promise<void>
}>()
defineExpose({
setMotion: async (motionName: string): Promise<void> => {
await modelRef.value?.setMotion(motionName)
},
})
</script>
<template>
<Screen v-slot="{ canvasHeight, canvasWidth }" relative>
<div z="10" top="2" absolute w-full flex="~ col" gap-2>
<div flex="~ row" w-full flex-wrap gap-2>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionSurpriseMotionName)"
>
🤯 Surprised
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionSadMotionName)"
>
😫 Sad
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionAngryMotionName)"
>
😠 Angry
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionHappyMotionName)"
>
😄 Happy
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionAwkwardMotionName)"
>
😳 Awkward
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionQuestionMotionName)"
>
🤔 Question
</button>
<button
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setMotion(EmotionThinkMotionName)"
>
🤨 Think
</button>
</div>
</div>
<Live2DViewer ref="modelRef" :canvas-width="canvasWidth" :canvas-height="canvasHeight" :model="model" :mouth-open-size="mouthOpenSize" />
</Screen>
</template>
+14 -49
View File
@@ -3,7 +3,7 @@ import type { InternalModel } from 'pixi-live2d-display/cubism4'
import { Application } from '@pixi/app'
import { extensions } from '@pixi/extensions'
import { Ticker, TickerPlugin } from '@pixi/ticker'
import { breakpointsTailwind, useBreakpoints, useElementBounding, useWindowSize } from '@vueuse/core'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { Live2DModel, MotionPreloadStrategy, MotionPriority } from 'pixi-live2d-display/cubism4'
import { onMounted, onUnmounted, ref, watch } from 'vue'
@@ -23,41 +23,8 @@ const mouthOpenSize = computed(() => {
})
const breakpoints = useBreakpoints(breakpointsTailwind)
const { width, height } = useWindowSize()
const containerElementBounding = useElementBounding(containerRef, { immediate: true, windowResize: true, reset: true })
const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
const isTablet = computed(() => breakpoints.between('md', 'lg').value)
const isDesktop = computed(() => breakpoints.greaterOrEqual('lg').value)
const canvasWidth = computed(() => {
if (isDesktop.value)
return containerElementBounding.width.value
else if (isMobile.value)
return (width.value - 16) // padding
else if (isTablet.value)
return (width.value - 16) // padding
else
return containerElementBounding.width.value
})
const canvasHeight = ref(0)
watch([width, height, containerRef], () => {
const bounding = containerRef.value?.getBoundingClientRect()
if (isDesktop.value) {
canvasHeight.value = bounding?.height || 0
}
else if (isMobile.value) {
canvasHeight.value = bounding?.height || 0
}
else if (isTablet.value) {
canvasHeight.value = bounding?.height || 0
}
else {
canvasHeight.value = 600
}
})
const { height, width } = useElementBounding(containerRef, { immediate: true, windowResize: true, reset: true })
function getCoreModel() {
return model.value!.internalModel.coreModel as any
@@ -72,8 +39,8 @@ function setScale(model: Ref<Live2DModel<InternalModel> | undefined>) {
offsetFactor = 2.5
}
const heightScale = canvasHeight.value * 0.95 / model.value.height * offsetFactor
const widthScale = canvasWidth.value * 0.95 / model.value.width * offsetFactor
const heightScale = height.value * 0.95 / model.value.height * offsetFactor
const widthScale = width.value * 0.95 / model.value.width * offsetFactor
const scale = Math.min(heightScale, widthScale)
model.value.scale.set(scale, scale)
@@ -85,8 +52,8 @@ async function initLive2DPixiStage(parent: HTMLDivElement) {
extensions.add(TickerPlugin)
pixiApp.value = new Application({
width: canvasWidth.value,
height: canvasHeight.value,
width: width.value,
height: height.value,
backgroundAlpha: 0,
})
@@ -97,8 +64,8 @@ async function initLive2DPixiStage(parent: HTMLDivElement) {
model.value = await Live2DModel.from(props.model, { motionPreload: MotionPreloadStrategy.ALL })
pixiApp.value.stage.addChild(model.value as any)
model.value.x = canvasWidth.value / 2
model.value.y = canvasHeight.value
model.value.x = width.value / 2
model.value.y = height.value
model.value.rotation = Math.PI
model.value.skew.x = Math.PI
model.value.anchor.set(0.5, 0.5)
@@ -117,18 +84,18 @@ async function setMotion(motionName: string) {
await model.value!.motion(motionName, undefined, MotionPriority.FORCE)
}
watch([width, height, canvasWidth, canvasHeight], () => {
watch([width, height, width, height], () => {
if (pixiApp.value)
pixiApp.value.renderer.resize(canvasWidth.value, canvasHeight.value)
pixiApp.value.renderer.resize(width.value, height.value)
if (pixiAppCanvas.value) {
pixiAppCanvas.value.width = canvasWidth.value
pixiAppCanvas.value.height = canvasHeight.value
pixiAppCanvas.value.width = width.value
pixiAppCanvas.value.height = height.value
}
if (model.value) {
model.value.x = canvasWidth.value / 2
model.value.y = canvasHeight.value
model.value.x = width.value / 2
model.value.y = height.value
}
})
@@ -136,8 +103,6 @@ onMounted(async () => {
if (!containerRef.value)
return
containerElementBounding.update()
await initLive2DPixiStage(containerRef.value)
})
+2 -2
View File
@@ -22,7 +22,7 @@ import { useSettings } from '../stores/settings'
import { encodeWAVToBase64 } from '../utils/binary'
import { asyncIteratorFromReadableStream } from '../utils/iterator'
import BasicTextarea from './BasicTextarea.vue'
import Live2DViewer from './Live2DViewer.vue'
import Live2DScene from './Live2DScene.vue'
import Settings from './Settings.vue'
import ThreeDScene from './ThreeDScene.vue'
@@ -304,7 +304,7 @@ onUnmounted(() => {
<Settings />
</div>
<div flex="~ row 1" max-h="[calc(100vh-220px)] <sm:[calc(100vh-320px)]" relative h-full w-full items-end gap-2>
<Live2DViewer
<Live2DScene
v-if="stageView === '2d'"
ref="live2DViewerRef"
:mouth-open-size="mouthOpenSize"
@@ -112,33 +112,33 @@ defineExpose({
</div>
</Collapsable>
</div>
<div flex="~ row" w-full gap-2>
<div flex="~ row" w-full flex-wrap gap-2>
<button
rounded-lg bg="zinc-200 dark:zinc-800/50" px-2 py-1
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setExpression('neutral')"
>
🙂 Neutral
</button>
<button
rounded-lg bg="zinc-200 dark:zinc-800/50" px-2 py-1
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setExpression('surprised')"
>
🤯 Surprised
</button>
<button
rounded-lg bg="zinc-200 dark:zinc-800/50" px-2 py-1
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setExpression('sad')"
>
😫 Sad
</button>
<button
rounded-lg bg="zinc-200 dark:zinc-800/50" px-2 py-1
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setExpression('angry')"
>
😠 Angry
</button>
<button
rounded-lg bg="zinc-200 dark:zinc-800/50" px-2 py-1
rounded-lg bg="zinc-100 dark:zinc-800/50" px-2 py-1
@click="modelRef?.setExpression('happy')"
>
😄 Happy
-7
View File
@@ -18,12 +18,5 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/audio': RouteRecordInfo<'/audio', '/audio', Record<never, never>, Record<never, never>>,
'/queue': RouteRecordInfo<'/queue', '/queue', Record<never, never>, Record<never, never>>,
'/test/filter-message': RouteRecordInfo<'/test/filter-message', '/test/filter-message', Record<never, never>, Record<never, never>>,
'/test/queues/delays': RouteRecordInfo<'/test/queues/delays', '/test/queues/delays', Record<never, never>, Record<never, never>>,
'/test/queues/emotions': RouteRecordInfo<'/test/queues/emotions', '/test/queues/emotions', Record<never, never>, Record<never, never>>,
'/test/queues/messages': RouteRecordInfo<'/test/queues/messages', '/test/queues/messages', Record<never, never>, Record<never, never>>,
}
}