From c4a3c215295360dba6decf04aed2e9482ba7ada4 Mon Sep 17 00:00:00 2001 From: LemonNeko Date: Thu, 6 Mar 2025 11:29:23 +0800 Subject: [PATCH] refactor(stage-ui): motion mapping editor (#45) * refactor(stage-ui): move motion to pinia store * wip: patch model file to change motion mapping * feat(stage-ui): save and export remapped motions --- .../src/components/Widgets/Live2DSettings.vue | 158 +++++++++++++++++- .../stage-ui/src/components/Live2D/Model.vue | 71 +++++--- .../stage-ui/src/components/Scenes/Live2D.vue | 30 ++-- .../stage-ui/src/components/Widgets/Stage.vue | 14 +- packages/stage-ui/src/constants/emotions.ts | 19 ++- packages/stage-ui/src/stores/settings.ts | 18 +- 6 files changed, 245 insertions(+), 65 deletions(-) diff --git a/apps/stage-web/src/components/Widgets/Live2DSettings.vue b/apps/stage-web/src/components/Widgets/Live2DSettings.vue index b01726079..c3d7c9ca1 100644 --- a/apps/stage-web/src/components/Widgets/Live2DSettings.vue +++ b/apps/stage-web/src/components/Widgets/Live2DSettings.vue @@ -1,8 +1,11 @@ diff --git a/packages/stage-ui/src/components/Live2D/Model.vue b/packages/stage-ui/src/components/Live2D/Model.vue index 39479d63b..12d084a0d 100644 --- a/packages/stage-ui/src/components/Live2D/Model.vue +++ b/packages/stage-ui/src/components/Live2D/Model.vue @@ -21,11 +21,9 @@ const props = withDefaults(defineProps<{ mouthOpenSize?: number width: number height: number - motion?: string paused: boolean }>(), { mouthOpenSize: 0, - motion: '', }) const pixiApp = toRef(() => props.app) @@ -60,10 +58,17 @@ function setScale(model: Ref | undefined>) { model.value.scale.set(scale, scale) } -const { live2dModel, loadingLive2dModel } = storeToRefs(useSettings()) +const { + live2dModelFile, + loadingLive2dModel, + live2dCurrentMotion, + availableLive2dMotions, + live2dLoadSource, + live2dModelUrl, +} = storeToRefs(useSettings()) +const currentMotion = ref<{ group: string, index: number }>({ group: 'Idle', index: 0 }) -// FIXME: it cannot blink if loading other model -async function loadModel(source: string | Blob) { +async function loadModel() { if (!pixiApp.value) return @@ -73,15 +78,13 @@ async function loadModel(source: string | Blob) { model.value = undefined } - loadingLive2dModel.value = true - const modelInstance = new Live2DModel() - if (source instanceof Blob) { - await Live2DFactory.setupLive2DModel(modelInstance, [source]) + if (live2dLoadSource.value === 'file') { + await Live2DFactory.setupLive2DModel(modelInstance, [live2dModelFile.value]) } - else { - await Live2DFactory.setupLive2DModel(modelInstance, source) + else if (live2dLoadSource.value === 'url') { + await Live2DFactory.setupLive2DModel(modelInstance, live2dModelUrl.value) } model.value = modelInstance @@ -106,8 +109,20 @@ async function loadModel(source: string | Blob) { const motionManager = internalModel.motionManager coreModel.setParameterValueById('ParamMouthOpenY', mouthOpenSize.value) + availableLive2dMotions.value = Object.entries(motionManager.definitions).flatMap(([motionName, definition]) => { + if (!definition) + return [] + + return definition.map((motion: any, index: number) => ({ + motionName, + motionIndex: index, + fileName: motion.File, + })) + }).filter(Boolean) + // Remove eye ball movements from idle motion group to prevent conflicts // This is too hacky + // FIXME: it cannot blink if loading a model only have idle motion if (motionManager.groups.idle) { motionManager.motionGroups[motionManager.groups.idle]?.forEach((motion) => { motion._motionData.curves.forEach((curve: any) => { @@ -130,8 +145,14 @@ async function loadModel(source: string | Blob) { return true } + motionManager.on('motionStart', (group, index) => { + currentMotion.value = { group, index } + }) + // save to indexdb - await localforage.setItem('live2dModel', source) + if (live2dModelFile.value) { + await localforage.setItem('live2dModel', live2dModelFile.value) + } loadingLive2dModel.value = false } @@ -146,17 +167,25 @@ async function initLive2DPixiStage() { extensions.add(InteractionManager) // load indexdb model first - const live2dModelBlob = await localforage.getItem('live2dModel') - if (live2dModelBlob) { - await loadModel(live2dModelBlob) + const live2dModelFromIndexedDB = await localforage.getItem('live2dModel') + if (live2dModelFromIndexedDB) { + live2dModelFile.value = live2dModelFromIndexedDB + live2dLoadSource.value = 'file' + loadingLive2dModel.value = true return } - await loadModel(live2dModel.value) + if (live2dModelUrl.value) { + live2dLoadSource.value = 'url' + loadingLive2dModel.value = true + return + } + + loadingLive2dModel.value = false } -async function setMotion(motionName: string) { - await model.value!.motion(motionName, undefined, MotionPriority.FORCE) +async function setMotion(motionName: string, index: number) { + await model.value!.motion(motionName, index, MotionPriority.FORCE) } const handleResize = useDebounceFn(() => { @@ -184,16 +213,16 @@ 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)) +watch(live2dCurrentMotion, value => setMotion(value.group, value.index)) watch(paused, (value) => { value ? pixiApp.value?.stop() : pixiApp.value?.start() }) -watchDebounced(live2dModel, (value) => { +watchDebounced(loadingLive2dModel, (value) => { if (!value) return - loadModel(value) + loadModel() }, { debounce: 1000 }) onMounted(updateDropShadowFilter) diff --git a/packages/stage-ui/src/components/Scenes/Live2D.vue b/packages/stage-ui/src/components/Scenes/Live2D.vue index fd54c51d1..f6bdcfb5d 100644 --- a/packages/stage-ui/src/components/Scenes/Live2D.vue +++ b/packages/stage-ui/src/components/Scenes/Live2D.vue @@ -1,19 +1,11 @@