refactor: abstract live2d class and refactor hook logic (#10)

This commit is contained in:
KC
2025-03-28 22:35:29 +01:00
committed by GitHub
parent a96bc0fe32
commit 40d38cacff
4 changed files with 242 additions and 120 deletions

View File

@@ -1,67 +1,116 @@
import { Live2DModel } from 'pixi-live2d-display'
import { Application } from 'pixi.js'
import { onMounted, ref } from 'vue'
import type { ModelType } from '../constants'
interface Motion {
Name: string
File: string
Sound?: string
FadeInTime: number
FadeOutTime: number
}
import { LogicalSize } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { computed, ref } from 'vue'
interface Expression {
Name: string
File: string
}
import { MODEL_BACKGROUND } from '../constants'
import live2d from '../utils/live2d'
import { getCursorMonitor } from '../utils/monitor'
export function useModel() {
const mode = ref('standard')
const model = ref<Live2DModel>()
const motions = ref<Record<string, Motion[]>>({})
const expressions = ref<Expression[]>([])
const mode = ref<ModelType>(localStorage.getItem('mode') as ModelType ?? 'STANDARD')
onMounted(() => {
loadModel()
})
const background = computed(() => MODEL_BACKGROUND[mode.value])
const loadModel = async () => {
destroyModel()
async function handleSetMode(value: ModelType) {
mode.value = value
localStorage.setItem('mode', value)
await handleLoadModel()
}
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
const app = new Application({
view,
resizeTo: window,
backgroundAlpha: 0,
async function handleLoadModel() {
await live2d.load(mode.value).catch((error) => {
console.error('模型加载失败:', error)
})
const loadedModel = await Live2DModel.from(`/models/${mode.value}/cat.model3.json`)
app.stage.addChild(loadedModel)
const { definitions, expressionManager }
= loadedModel.internalModel.motionManager
model.value = loadedModel
motions.value = definitions as Record<string, Motion[]>
expressions.value = expressionManager?.definitions as Expression[]
}
const destroyModel = () => {
model.value?.destroy()
async function handleDestroy() {
await live2d.destroy().catch((error) => {
console.error('模型销毁失败:', error)
})
}
const setParameterValue = (id: string, value: number | boolean) => {
return model.value?.internalModel.coreModel.setParameterValueById(id, Number(value))
async function handleResized() {
if (!live2d.currentModel) return
try {
const { innerWidth } = window
await getCurrentWebviewWindow().setSize(
new LogicalSize({
width: innerWidth,
height: innerWidth * (354 / 612),
}),
)
live2d.currentModel?.scale.set(innerWidth / 612)
} catch (error) {
console.error('窗口调整大小出错:', error)
}
}
async function handleKeyDown(value: string[]) {
try {
const hasArrowKey = value.some(key => key.endsWith('Arrow'))
const hasNonArrowKey = value.some(key => !key.endsWith('Arrow'))
live2d.setParameterValue('CatParamRightHandDown', hasArrowKey)
live2d.setParameterValue('CatParamLeftHandDown', hasNonArrowKey)
} catch (error) {
console.error('按键捕获出错:', error)
}
}
async function handleMouseMove() {
if (!live2d.currentModel) return
try {
const monitor = await getCursorMonitor()
if (!monitor) return
const { size, cursorX, cursorY } = monitor
const { width, height } = size
const xRatio = cursorX / width
const yRatio = cursorY / height
const x = (xRatio * 60) - 30
const y = (yRatio * 60) - 30
live2d.setParameterValue('ParamMouseX', -x)
live2d.setParameterValue('ParamMouseY', -y)
live2d.setParameterValue('ParamAngleX', x)
live2d.setParameterValue('ParamAngleY', -y)
} catch (error) {
console.error('鼠标移动捕获出错:', error)
}
}
async function handleMouseClick(value: string[]) {
try {
const hasLeftClick = value.includes('Left')
const hasRightClick = value.includes('Right')
live2d.setParameterValue('ParamMouseLeftDown', hasLeftClick)
live2d.setParameterValue('ParamMouseRightDown', hasRightClick)
} catch (error) {
console.error('鼠标点击捕获出错:', error)
}
}
return {
model,
motions,
expressions,
loadModel,
destroyModel,
setParameterValue,
mode,
background,
motions: live2d.currentMotions,
expressions: live2d.currentExpressions,
handleSetMode,
handleLoadModel,
handleDestroy,
handleResized,
handleKeyDown,
handleMouseMove,
handleMouseClick,
}
}

View File

@@ -1,4 +1,16 @@
export const LISTEN_KEY = {
SHOW_WINDOW: 'show-window',
HIDE_WINDOW: 'hide-window',
}
} as const
export const MODEL_PATH = {
STANDARD: '/models/standard/cat.model3.json',
KEYBOARD: '/models/keyboard/cat.model3.json',
} as const
export const MODEL_BACKGROUND = {
STANDARD: '/images/backgrounds/standard.png',
KEYBOARD: '/images/backgrounds/keyboard.png',
} as const
export type ModelType = keyof typeof MODEL_PATH

View File

@@ -1,83 +1,54 @@
<script setup lang="ts">
import { LogicalSize } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Live2DModel } from 'pixi-live2d-display'
import { Ticker } from 'pixi.js'
import { onMounted, ref, watch } from 'vue'
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useDevice } from '../composables/useDevice'
import { useModel } from '../composables/useModel'
import { getCursorMonitor } from '../utils/monitor'
Live2DModel.registerTicker(Ticker)
const mode = ref('standard')
const { pressedKeys, pressedMouses, mousePosition } = useDevice()
const { model, setParameterValue } = useModel()
const { handleLoadModel, handleDestroy, handleMouseClick, handleResized, handleMouseMove, handleKeyDown, handleSetMode, mode, background } = useModel()
onMounted(() => {
window.addEventListener('resize', handleResized)
})
watch(model, (value) => {
if (!value) return
const isOverLap = ref(false)
let resizeTimer: NodeJS.Timeout | null = null
async function handleSwitch() {
const newMode = mode.value === 'STANDARD' ? 'KEYBOARD' : 'STANDARD'
await handleSetMode(newMode)
handleResized()
})
watch(pressedKeys, (value) => {
const hasArrowKey = value.some(key => key.endsWith('Arrow'))
const hasNonArrowKey = value.some(key => !key.endsWith('Arrow'))
setParameterValue('CatParamRightHandDown', hasArrowKey)
setParameterValue('CatParamLeftHandDown', hasNonArrowKey)
})
watch(pressedMouses, (value) => {
const isLeftDown = value.includes('Left')
const isRightDown = value.includes('Right')
setParameterValue('ParamMouseLeftDown', isLeftDown)
setParameterValue('ParamMouseRightDown', isRightDown)
})
watch(mousePosition, async () => {
if (!model.value) return
const monitor = await getCursorMonitor()
if (!monitor) return
const { size, cursorX, cursorY } = monitor
const { width, height } = size
const xRatio = cursorX / width
const yRatio = cursorY / height
const x = (xRatio * 60) - 30
const y = (yRatio * 60) - 30
setParameterValue('ParamMouseX', -x)
setParameterValue('ParamMouseY', -y)
setParameterValue('ParamAngleX', x)
setParameterValue('ParamAngleY', -y)
})
async function handleResized() {
if (!model.value) return
const { innerWidth } = window
await getCurrentWebviewWindow().setSize(
new LogicalSize({
width: innerWidth,
height: innerWidth * (354 / 612),
}),
)
model.value.scale.set(innerWidth / 612)
}
function handleResize() {
isOverLap.value = true
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(async () => {
await handleResized()
isOverLap.value = false
}, 100)
}
onMounted(async () => {
await nextTick()
await handleLoadModel()
handleResized()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
handleDestroy()
window.removeEventListener('resize', handleResize)
if (resizeTimer)
clearTimeout(resizeTimer)
})
watch(pressedKeys, handleKeyDown)
watch(mousePosition, handleMouseMove)
watch(pressedMouses, handleMouseClick)
function handleMouseDown() {
const appWindow = getCurrentWebviewWindow()
@@ -90,7 +61,17 @@ function handleMouseDown() {
class="relative children:(absolute h-screen w-screen)"
@mousedown="handleMouseDown"
>
<img :src="`/images/backgrounds/${mode}.png`">
<div v-if="isOverLap" class="absolute inset-0 z-99 flex items-center justify-center bg-black">
<span class="text-center text-5xl text-white">
重绘中...
</span>
</div>
<button class="absolute left-0 top-0 z-9 rounded-full bg-sky text-center text-white h-8! w-20! hover:shadow-lg" @click.stop="handleSwitch">
切换模式
</button>
<img :src="background">
<canvas id="live2dCanvas" />

80
src/utils/live2d.ts Normal file
View File

@@ -0,0 +1,80 @@
import type { ModelType } from '../constants'
import { Live2DModel } from 'pixi-live2d-display'
import { Application, Ticker } from 'pixi.js'
import { MODEL_PATH } from '../constants'
Live2DModel.registerTicker(Ticker)
interface Motion {
Name: string
File: string
Sound?: string
FadeInTime: number
FadeOutTime: number
}
interface Expression {
Name: string
File: string
}
class ModelManager {
private app: Application | null = null
public currentModel: Live2DModel | null = null
public currentMotions = new Map<string, Motion[]>()
public currentExpressions = new Map<string, Expression>()
constructor() { }
private mount() {
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
this.app = new Application({
view,
resizeTo: window,
backgroundAlpha: 0,
antialias: true,
})
}
public async load(type: ModelType) {
const modelPath = MODEL_PATH[type]
if (!this.app) {
this.mount()
}
const model = await Live2DModel.from(modelPath)
if (this.app?.stage.children.length) {
this.app.stage.removeChildren()
}
this.app?.stage.addChild(model)
const { definitions, expressionManager } = model.internalModel.motionManager
this.currentModel = model
this.currentMotions = new Map(
Object.entries(definitions),
) as Map<string, Motion[]>
this.currentExpressions = new Map(
Object.entries(expressionManager?.definitions || {}),
) as Map<string, Expression>
}
public async destroy() {
await this.currentModel?.destroy()
}
public setParameterValue(id: string, value: number | boolean) {
return this.currentModel?.internalModel.coreModel.setParameterValueById(id, Number(value))
}
}
const live2d = new ModelManager()
export default live2d