refactor: split Live2D module into two components

This commit is contained in:
Neko Ayaka
2025-02-19 13:28:34 +08:00
parent 9b3025d7e8
commit eaec85489a
7 changed files with 121 additions and 83 deletions
+1
View File
@@ -31,6 +31,7 @@
"@pixi/core": "6",
"@pixi/display": "6",
"@pixi/extensions": "6",
"@pixi/interaction": "6",
"@pixi/loaders": "6",
"@pixi/math": "6",
"@pixi/runner": "6",
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { Application } from '@pixi/app'
import { extensions } from '@pixi/extensions'
import { Ticker, TickerPlugin } from '@pixi/ticker'
import { Live2DModel } from 'pixi-live2d-display/cubism4'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
width: number
height: number
}>()
const containerRef = ref<HTMLDivElement>()
const pixiApp = ref<Application>()
const pixiAppCanvas = ref<HTMLCanvasElement>()
async function initLive2DPixiStage(parent: HTMLDivElement) {
// https://guansss.github.io/pixi-live2d-display/#package-importing
Live2DModel.registerTicker(Ticker)
extensions.add(TickerPlugin)
pixiApp.value = new Application({
width: props.width,
height: props.height,
backgroundAlpha: 0,
})
pixiAppCanvas.value = pixiApp.value.view
pixiAppCanvas.value.style.objectFit = 'contain'
parent.appendChild(pixiApp.value.view)
}
function handleResize() {
if (pixiApp.value)
pixiApp.value.renderer.resize(props.width, props.height)
if (pixiApp.value?.view) {
pixiApp.value.view.width = props.width
pixiApp.value.view.height = props.height
}
}
watch([() => props.width, () => props.height], () => handleResize())
onMounted(async () => containerRef.value && await initLive2DPixiStage(containerRef.value))
onUnmounted(() => pixiApp.value?.destroy())
</script>
<template>
<div ref="containerRef" h-full w-full>
<slot :app="pixiApp" />
</div>
</template>
@@ -1,39 +1,38 @@
<script setup lang="ts">
import type { Application } from '@pixi/app'
import type { InternalModel } from 'pixi-live2d-display/cubism4'
import type { Ref } from 'vue'
import { Application } from '@pixi/app'
import { extensions } from '@pixi/extensions'
import { InteractionManager } from '@pixi/interaction'
import { Ticker, TickerPlugin } from '@pixi/ticker'
import { breakpointsTailwind, useBreakpoints, useDark, useDebounceFn, useElementBounding } from '@vueuse/core'
import { breakpointsTailwind, useBreakpoints, useDark, useDebounceFn } from '@vueuse/core'
import { DropShadowFilter } from 'pixi-filters'
import { Live2DModel, MotionPreloadStrategy, MotionPriority } from 'pixi-live2d-display/cubism4'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
import { useLive2DIdleEyeFocus } from '../../composables/live2d'
const props = withDefaults(defineProps<{
app?: Application
model: string
mouthOpenSize?: number
width: number
height: number
motion?: string
}>(), {
mouthOpenSize: 0,
})
const containerRef = ref<HTMLDivElement>()
const pixiApp = ref<Application>()
const pixiAppCanvas = ref<HTMLCanvasElement>()
const pixiApp = toRef(() => props.app)
const model = ref<Live2DModel>()
const initialModelWidth = ref<number>(0)
const initialModelHeight = ref<number>(0)
const mouthOpenSize = computed(() => {
return Math.max(0, Math.min(100, props.mouthOpenSize))
})
const mouthOpenSize = computed(() => Math.max(0, Math.min(100, props.mouthOpenSize)))
const dark = useDark()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
const { height, width } = useElementBounding(containerRef, { immediate: true, windowResize: true, reset: true })
const idleEyeFocus = useLive2DIdleEyeFocus()
function getCoreModel() {
@@ -49,35 +48,29 @@ function setScale(model: Ref<Live2DModel<InternalModel> | undefined>) {
offsetFactor = 2.2
}
const heightScale = height.value * 0.95 / initialModelHeight.value * offsetFactor
const widthScale = width.value * 0.95 / initialModelWidth.value * offsetFactor
const heightScale = props.height * 0.95 / initialModelHeight.value * offsetFactor
const widthScale = props.width * 0.95 / initialModelWidth.value * offsetFactor
const scale = Math.min(heightScale, widthScale)
model.value.scale.set(scale, scale)
}
async function initLive2DPixiStage(parent: HTMLDivElement) {
async function initLive2DPixiStage() {
if (!pixiApp.value)
return
// https://guansss.github.io/pixi-live2d-display/#package-importing
Live2DModel.registerTicker(Ticker)
extensions.add(TickerPlugin)
pixiApp.value = new Application({
width: width.value,
height: height.value,
backgroundAlpha: 0,
})
pixiAppCanvas.value = pixiApp.value.view
pixiAppCanvas.value.style.objectFit = 'contain'
parent.appendChild(pixiApp.value.view)
extensions.add(InteractionManager)
model.value = await Live2DModel.from(props.model, { motionPreload: MotionPreloadStrategy.ALL })
pixiApp.value.stage.addChild(model.value as any)
initialModelWidth.value = model.value.width
initialModelHeight.value = model.value.height
model.value.x = width.value / 2
model.value.y = height.value
model.value.x = props.width / 2
model.value.y = props.height
model.value.rotation = Math.PI
model.value.skew.x = Math.PI
model.value.anchor.set(0.5, 0.5)
@@ -123,17 +116,9 @@ async function setMotion(motionName: string) {
}
const handleResize = useDebounceFn(() => {
if (pixiApp.value)
pixiApp.value.renderer.resize(width.value, height.value)
if (pixiAppCanvas.value) {
pixiAppCanvas.value.width = width.value
pixiAppCanvas.value.height = height.value
}
if (model.value) {
model.value.x = width.value / 2
model.value.y = height.value
model.value.x = props.width / 2
model.value.y = props.height
setScale(model)
}
}, 100)
@@ -150,38 +135,17 @@ function updateDropShadowFilter() {
}
}
watch([width, height], () => {
handleResize()
})
watch(dark, updateDropShadowFilter, {
immediate: true,
})
watch([() => props.width, () => props.height], () => handleResize())
watch(dark, updateDropShadowFilter, { immediate: true })
watch(model, updateDropShadowFilter)
watch(mouthOpenSize, value => getCoreModel().setParameterValueById('ParamMouthOpenY', value))
watch(pixiApp, initLive2DPixiStage)
watch(() => props.motion, () => props.motion && setMotion(props.motion))
onMounted(updateDropShadowFilter)
onMounted(async () => {
if (!containerRef.value)
return
await initLive2DPixiStage(containerRef.value)
})
onUnmounted(() => {
pixiApp.value?.destroy()
})
watch(mouthOpenSize, (value) => {
getCoreModel().setParameterValueById('ParamMouthOpenY', value)
})
defineExpose({
setMotion,
})
onUnmounted(() => model.value && pixiApp.value?.stage.removeChild(model.value))
</script>
<template>
<div ref="containerRef" h-full w-full />
<slot />
</template>
@@ -10,8 +10,10 @@ import {
EmotionSurpriseMotionName,
EmotionThinkMotionName,
} from '../../constants/emotions'
import Live2DViewer from '../Live2D/Viewer.vue'
import Live2DCanvas from '../Live2D/Canvas.vue'
import Live2DModel from '../Live2D/Model.vue'
import Screen from '../Screen.vue'
import TransitionVertical from '../TransitionVertical.vue'
withDefaults(defineProps<{
model: string
@@ -20,21 +22,15 @@ withDefaults(defineProps<{
mouthOpenSize: 0,
})
const modelRef = ref<{
setMotion: (motionName: string) => Promise<void>
}>()
const motion = defineModel<string>('motion')
const show = ref(false)
defineExpose({
setMotion: async (motionName: string): Promise<void> => {
await modelRef.value?.setMotion(motionName)
},
})
</script>
<template>
<Screen v-slot="{ width, height }" relative>
<Live2DViewer ref="modelRef" :canvas-width="width" :canvas-height="height" :model="model" :mouth-open-size="mouthOpenSize" />
<Live2DCanvas v-slot="{ app }" :width="width" :height="height">
<Live2DModel :app="app" :model="model" :mouth-open-size="mouthOpenSize" :width="width" :height="height" :motion="motion" />
</Live2DCanvas>
<div absolute bottom="3" right="3">
<div flex="~ row" cursor-pointer>
<label
@@ -65,43 +61,43 @@ defineExpose({
<div flex="~ row" flex-wrap gap-2>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionSurpriseMotionName)"
@click="motion = EmotionSurpriseMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.surprised') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionSadMotionName)"
@click="motion = EmotionSadMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.sad') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionAngryMotionName)"
@click="motion = EmotionAngryMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.angry') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionHappyMotionName)"
@click="motion = EmotionHappyMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.happy') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionAwkwardMotionName)"
@click="motion = EmotionAwkwardMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.awkward') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionQuestionMotionName)"
@click="motion = EmotionQuestionMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.question') }}
</button>
<button
rounded-lg bg="zinc-100/70 dark:zinc-800/50" px-2 py-1 backdrop-blur-sm
@click="modelRef?.setMotion(EmotionThinkMotionName)"
@click="motion = EmotionThinkMotionName"
>
{{ $t('stage.viewers.debug-menu.emotions-btn.think') }}
</button>
+2 -1
View File
@@ -1,6 +1,7 @@
export { default as BasicTextarea } from './BasicTextarea.vue'
export { default as Live2DViewer } from './Live2D/Viewer.vue'
export { default as Live2DCanvas } from './Live2D/Canvas.vue'
export { default as Live2DModel } from './Live2D/Model.vue'
export { default as SceneLive2D } from './Scenes/Live2D.vue'
export { default as SceneVRM } from './Scenes/VRM.vue'
+1
View File
@@ -26,6 +26,7 @@
"@pixi/core": "6",
"@pixi/display": "6",
"@pixi/extensions": "6",
"@pixi/interaction": "6",
"@pixi/loaders": "6",
"@pixi/math": "6",
"@pixi/runner": "6",
+23
View File
@@ -350,6 +350,9 @@ importers:
'@pixi/extensions':
specifier: '6'
version: 6.5.10
'@pixi/interaction':
specifier: '6'
version: 6.5.10(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/loaders':
specifier: '6'
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
@@ -629,6 +632,9 @@ importers:
'@pixi/extensions':
specifier: '6'
version: 6.5.10
'@pixi/interaction':
specifier: '6'
version: 6.5.10(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/loaders':
specifier: '6'
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
@@ -3784,6 +3790,15 @@ packages:
'@pixi/core': ^6.0.0
'@pixi/math': ^6.0.0
'@pixi/interaction@6.5.10':
resolution: {integrity: sha512-v809pJmXA2B9dV/vdrDMUqJT+fBB/ARZli2YRmI2dPbEbkaYr8FNmxCAJnwT8o+ymTx044Ie820hn9tVrtMtfA==}
peerDependencies:
'@pixi/core': 6.5.10
'@pixi/display': 6.5.10
'@pixi/math': 6.5.10
'@pixi/ticker': 6.5.10
'@pixi/utils': 6.5.10
'@pixi/loaders@6.5.10':
resolution: {integrity: sha512-AuK7mXBmyVsDFL9DDFPB8sqP8fwQ2NOktvu98bQuJl0/p/UeK/0OAQnF3wcf3FeBv5YGXfNHL21c2DCisjKfTg==}
peerDependencies:
@@ -14026,6 +14041,14 @@ snapshots:
'@pixi/core': 6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/math': 6.5.10
'@pixi/interaction@6.5.10(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))':
dependencies:
'@pixi/core': 6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/display': 6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/math': 6.5.10
'@pixi/ticker': 6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@pixi/utils': 6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@pixi/loaders@6.5.10(@pixi/constants@6.5.10)(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))':
dependencies:
'@pixi/constants': 6.5.10