mirror of
https://github.com/ayangweb/BongoCat.git
synced 2026-03-12 17:51:48 +08:00
refactor: abstract live2d class and refactor hook logic (#10)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
80
src/utils/live2d.ts
Normal 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
|
||||
Reference in New Issue
Block a user