refactor(stage-ui,stage-ui-live2d,stage-ui-three): split out stage-ui-live2d component

This commit is contained in:
Neko Ayaka
2026-01-08 00:31:07 +08:00
parent 58c38e4329
commit 20b6978a9d
38 changed files with 681 additions and 422 deletions
+69
View File
@@ -0,0 +1,69 @@
{
"name": "@proj-airi/stage-ui-live2d",
"type": "module",
"private": true,
"description": "Live2D scene components and stores for Project AIRI",
"author": {
"name": "Moeru AI Project AIRI Team",
"email": "airi@moeru.ai",
"url": "https://github.com/moeru-ai"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/stage-ui-live2d"
},
"exports": {
".": "./src/index.ts",
"./components": "./src/components/index.ts",
"./components/scenes": "./src/components/scenes/index.ts",
"./components/scenes/Live2D.vue": "./src/components/scenes/Live2D.vue",
"./components/scenes/live2d": "./src/components/scenes/live2d/index.ts",
"./composables/live2d": "./src/composables/live2d/index.ts",
"./constants/emotions": "./src/constants/emotions.ts",
"./stores": "./src/stores/index.ts",
"./stores/live2d": "./src/stores/live2d.ts",
"./utils/eye-motions": "./src/utils/eye-motions.ts",
"./utils/live2d-preview": "./src/utils/live2d-preview.ts",
"./utils/live2d-zip-loader": "./src/utils/live2d-zip-loader.ts",
"./utils/live2d-opfs-registration": "./src/utils/live2d-opfs-registration.ts",
"./utils/live2d-uri-encode-filenames": "./src/utils/live2d-uri-encode-filenames.ts",
"./utils/opfs-loader": "./src/utils/opfs-loader.ts"
},
"scripts": {
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@lemonneko/crop-empty-pixels": "0.1.1",
"@pixi/app": "^6.5.10",
"@pixi/constants": "^6.5.10",
"@pixi/core": "^6.5.10",
"@pixi/display": "^6.5.10",
"@pixi/extensions": "^6.5.10",
"@pixi/interaction": "^6.5.10",
"@pixi/loaders": "^6.5.10",
"@pixi/math": "^6.5.10",
"@pixi/runner": "^6.5.10",
"@pixi/settings": "^6.5.10",
"@pixi/sprite": "^6.5.10",
"@pixi/ticker": "^6.5.10",
"@pixi/utils": "^6.5.10",
"@proj-airi/stage-shared": "workspace:^",
"@proj-airi/ui": "workspace:^",
"@vueuse/core": "^14.1.0",
"culori": "^4.0.2",
"es-toolkit": "catalog:",
"jszip": "^3.10.1",
"pinia": "^3.0.4",
"pixi-filters": "4",
"pixi-live2d-display": "^0.4.0",
"three": "^0.182.0",
"vue": "^3.5.25"
},
"devDependencies": {
"@types/culori": "^4.0.1",
"@types/three": "^0.182.0",
"vue-tsc": "^3.1.8"
}
}
@@ -0,0 +1 @@
export * from './scenes'
@@ -19,11 +19,24 @@ withDefaults(defineProps<{
mouthOpenSize?: number
focusAt?: { x: number, y: number }
disableFocusAt?: boolean
scale?: number
themeColorsHue?: number
themeColorsHueDynamic?: boolean
live2dIdleAnimationEnabled?: boolean
live2dAutoBlinkEnabled?: boolean
live2dForceAutoBlinkEnabled?: boolean
live2dShadowEnabled?: boolean
}>(), {
paused: false,
focusAt: () => ({ x: 0, y: 0 }),
mouthOpenSize: 0,
scale: 1,
themeColorsHue: 220.44,
themeColorsHueDynamic: false,
live2dIdleAnimationEnabled: true,
live2dAutoBlinkEnabled: true,
live2dForceAutoBlinkEnabled: false,
live2dShadowEnabled: true,
})
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
@@ -77,6 +90,12 @@ defineExpose({
:y-offset="position.y"
:scale="scale"
:disable-focus-at="disableFocusAt"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-shadow-enabled="live2dShadowEnabled"
/>
</Live2DCanvas>
</Screen>
@@ -0,0 +1,2 @@
export { default as Live2DScene } from './Live2D.vue'
export { Live2DCanvas, Live2DModel } from './live2d'
@@ -24,7 +24,6 @@ import {
} from '../../../composables/live2d'
import { Emotion, EmotionNeutralMotionName } from '../../../constants/emotions'
import { useLive2d } from '../../../stores/live2d'
import { useSettings } from '../../../stores/settings'
const props = withDefaults(defineProps<{
modelSrc?: string
@@ -40,12 +39,24 @@ const props = withDefaults(defineProps<{
xOffset?: number | string
yOffset?: number | string
scale?: number
themeColorsHue?: number
themeColorsHueDynamic?: boolean
live2dIdleAnimationEnabled?: boolean
live2dAutoBlinkEnabled?: boolean
live2dForceAutoBlinkEnabled?: boolean
live2dShadowEnabled?: boolean
}>(), {
mouthOpenSize: 0,
paused: false,
focusAt: () => ({ x: 0, y: 0 }),
disableFocusAt: false,
scale: 1,
themeColorsHue: 220.44,
themeColorsHueDynamic: false,
live2dIdleAnimationEnabled: true,
live2dAutoBlinkEnabled: true,
live2dForceAutoBlinkEnabled: false,
live2dShadowEnabled: true,
})
const emits = defineEmits<{
@@ -136,14 +147,12 @@ const {
modelParameters,
} = storeToRefs(live2dStore)
const {
themeColorsHue,
themeColorsHueDynamic,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
} = storeToRefs(useSettings())
const themeColorsHue = toRef(() => props.themeColorsHue)
const themeColorsHueDynamic = toRef(() => props.themeColorsHueDynamic)
const live2dIdleAnimationEnabled = toRef(() => props.live2dIdleAnimationEnabled)
const live2dAutoBlinkEnabled = toRef(() => props.live2dAutoBlinkEnabled)
const live2dForceAutoBlinkEnabled = toRef(() => props.live2dForceAutoBlinkEnabled)
const live2dShadowEnabled = toRef(() => props.live2dShadowEnabled)
const localCurrentMotion = ref<{ group: string, index: number }>({ group: 'Idle', index: 0 })
const beatSync = createBeatSyncController({
@@ -1,6 +1,6 @@
import type { InternalModel } from 'pixi-live2d-display/cubism4'
import { lerp, randFloat } from 'three/src/math/MathUtils.js'
import { MathUtils } from 'three'
import { randomSaccadeInterval } from '../../utils'
@@ -17,7 +17,7 @@ export function useLive2DIdleEyeFocus() {
// Function to handle idle eye saccades and focus (head) movements
function update(model: InternalModel, now: number) {
if (now >= nextSaccadeAfter || now < lastSaccadeAt) {
focusTarget = [randFloat(-1, 1), randFloat(-1, 0.7)]
focusTarget = [MathUtils.randFloat(-1, 1), MathUtils.randFloat(-1, 0.7)]
lastSaccadeAt = now
nextSaccadeAfter = now + (randomSaccadeInterval() / 1000)
model.focusController.focus(focusTarget![0] * 0.5, focusTarget![1] * 0.5, false)
@@ -26,8 +26,8 @@ export function useLive2DIdleEyeFocus() {
model.focusController.update(now - lastSaccadeAt)
const coreModel = model.coreModel as any
// TODO: After emotion mapper, stage editor, eye related parameters should be take cared to be dynamical instead of hardcoding
coreModel.setParameterValueById('ParamEyeBallX', lerp(coreModel.getParameterValueById('ParamEyeBallX'), focusTarget![0], 0.3))
coreModel.setParameterValueById('ParamEyeBallY', lerp(coreModel.getParameterValueById('ParamEyeBallY'), focusTarget![1], 0.3))
coreModel.setParameterValueById('ParamEyeBallX', MathUtils.lerp(coreModel.getParameterValueById('ParamEyeBallX'), focusTarget![0], 0.3))
coreModel.setParameterValueById('ParamEyeBallY', MathUtils.lerp(coreModel.getParameterValueById('ParamEyeBallY'), focusTarget![1], 0.3))
}
return { update }
@@ -0,0 +1,56 @@
export const EMOTION_HAPPY = '<|EMOTE_HAPPY|>'
export const EMOTION_SAD = '<|EMOTE_SAD|>'
export const EMOTION_ANGRY = '<|EMOTE_ANGRY|>'
export const EMOTION_THINK = '<|EMOTE_THINK|>'
export const EMOTION_SURPRISE = '<|EMOTE_SURPRISED|>'
export const EMOTION_AWKWARD = '<|EMOTE_AWKWARD|>'
export const EMOTION_QUESTION = '<|EMOTE_QUESTION|>'
export const EMOTION_CURIOUS = '<|EMOTE_CURIOUS|>'
export enum Emotion {
Idle = '<|EMOTE_NEUTRAL|>',
Happy = '<|EMOTE_HAPPY|>',
Sad = '<|EMOTE_SAD|>',
Angry = '<|EMOTE_ANGRY|>',
Think = '<|EMOTE_THINK|>',
Surprise = '<|EMOTE_SURPRISED|>',
Awkward = '<|EMOTE_AWKWARD|>',
Question = '<|EMOTE_QUESTION|>',
Curious = '<|EMOTE_CURIOUS|>',
}
export const EMOTION_VALUES = Object.values(Emotion)
export const EmotionHappyMotionName = 'Happy'
export const EmotionSadMotionName = 'Sad'
export const EmotionAngryMotionName = 'Angry'
export const EmotionAwkwardMotionName = 'Awkward'
export const EmotionThinkMotionName = 'Think'
export const EmotionSurpriseMotionName = 'Surprise'
export const EmotionQuestionMotionName = 'Question'
export const EmotionNeutralMotionName = 'Idle'
export const EmotionCuriousMotionName = 'Curious'
export const EMOTION_EmotionMotionName_value = {
[Emotion.Happy]: EmotionHappyMotionName,
[Emotion.Sad]: EmotionSadMotionName,
[Emotion.Angry]: EmotionAngryMotionName,
[Emotion.Think]: EmotionThinkMotionName,
[Emotion.Surprise]: EmotionSurpriseMotionName,
[Emotion.Awkward]: EmotionAwkwardMotionName,
[Emotion.Question]: EmotionQuestionMotionName,
[Emotion.Idle]: EmotionNeutralMotionName,
[Emotion.Curious]: EmotionCuriousMotionName,
}
export const EMOTION_VRMExpressionName_value = {
[Emotion.Happy]: 'happy',
[Emotion.Sad]: 'sad',
[Emotion.Angry]: 'angry',
[Emotion.Think]: undefined,
[Emotion.Surprise]: 'surprised',
[Emotion.Awkward]: undefined,
[Emotion.Question]: undefined,
[Emotion.Idle]: undefined,
[Emotion.Curious]: 'surprised',
} satisfies Record<Emotion, string | undefined>
+11
View File
@@ -0,0 +1,11 @@
export { Live2DCanvas, Live2DModel } from './components/scenes/live2d'
export { default as Live2DScene } from './components/scenes/Live2D.vue'
export * from './composables/live2d'
export * from './constants/emotions'
export * from './stores'
export { randomSaccadeInterval } from './utils/eye-motions'
export * from './utils/live2d-opfs-registration'
export * from './utils/live2d-preview'
export * from './utils/live2d-uri-encode-filenames'
export * from './utils/live2d-zip-loader'
export * from './utils/opfs-loader'
@@ -0,0 +1 @@
export * from './live2d'
@@ -0,0 +1,92 @@
import { refManualReset, useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
type BroadcastChannelEvents
= | BroadcastChannelEventShouldUpdateView
interface BroadcastChannelEventShouldUpdateView {
type: 'should-update-view'
}
export const defaultModelParameters = {
angleX: 0,
angleY: 0,
angleZ: 0,
leftEyeOpen: 1,
rightEyeOpen: 1,
leftEyeSmile: 0,
rightEyeSmile: 0,
leftEyebrowLR: 0,
rightEyebrowLR: 0,
leftEyebrowY: 0,
rightEyebrowY: 0,
leftEyebrowAngle: 0,
rightEyebrowAngle: 0,
leftEyebrowForm: 0,
rightEyebrowForm: 0,
mouthOpen: 0,
mouthForm: 0,
cheek: 0,
bodyAngleX: 0,
bodyAngleY: 0,
bodyAngleZ: 0,
breath: 0,
}
export const useLive2d = defineStore('live2d', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-live2d' })
const shouldUpdateViewHooks = ref<Array<() => void>>([])
const onShouldUpdateView = (hook: () => void) => {
shouldUpdateViewHooks.value.push(hook)
}
function shouldUpdateView() {
post({ type: 'should-update-view' })
shouldUpdateViewHooks.value.forEach(hook => hook())
}
watch(data, (event) => {
if (event.type === 'should-update-view') {
shouldUpdateViewHooks.value.forEach(hook => hook())
}
})
const position = refManualReset<{ x: number, y: number }>(useLocalStorage('settings/live2d/position', { x: 0, y: 0 })) // position is relative to the center of the screen, units are %
const positionInPercentageString = computed(() => ({
x: `${position.value.x}%`,
y: `${position.value.y}%`,
}))
const currentMotion = refManualReset<{ group: string, index?: number }>(() => ({ group: 'Idle', index: 0 }))
const availableMotions = refManualReset<{ motionName: string, motionIndex: number, fileName: string }[]>(() => [])
const motionMap = refManualReset<Record<string, string>>(useLocalStorage('settings/live2d/motion-map', {}))
const scale = refManualReset<number>(useLocalStorage('settings/live2d/scale', 1))
// Live2D model parameters
const modelParameters = refManualReset<Record<string, number>>(useLocalStorage('settings/live2d/parameters', defaultModelParameters))
function resetState() {
position.reset()
currentMotion.reset()
availableMotions.reset()
motionMap.reset()
scale.reset()
modelParameters.reset()
shouldUpdateView()
}
return {
position,
positionInPercentageString,
currentMotion,
availableMotions,
motionMap,
scale,
modelParameters,
onShouldUpdateView,
shouldUpdateView,
resetState,
}
})
@@ -0,0 +1,7 @@
export { randomSaccadeInterval } from './eye-motions'
export * from './live2d-opfs-registration'
export * from './live2d-preview'
export * from './live2d-uri-encode-filenames'
export * from './live2d-zip-loader'
export * from './opfs-loader'
@@ -0,0 +1,90 @@
import cropImg from '@lemonneko/crop-empty-pixels'
import { Application } from '@pixi/app'
import { extensions } from '@pixi/extensions'
import { Ticker, TickerPlugin } from '@pixi/ticker'
import { Live2DFactory, Live2DModel } from 'pixi-live2d-display/cubism4'
/**
* Render a Live2D zip/file to an offscreen canvas and return a padded preview data URL.
*/
export async function loadLive2DModelPreview(file: File) {
Live2DModel.registerTicker(Ticker)
extensions.add(TickerPlugin)
const previewWidth = 1440
const previewHeight = 2560
const previewResolution = 2
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = previewWidth * previewResolution
offscreenCanvas.height = previewHeight * previewResolution
offscreenCanvas.style.position = 'absolute'
offscreenCanvas.style.top = '0'
offscreenCanvas.style.left = '0'
offscreenCanvas.style.objectFit = 'cover'
offscreenCanvas.style.display = 'block'
offscreenCanvas.style.zIndex = '10000000000'
offscreenCanvas.style.opacity = '0'
document.body.appendChild(offscreenCanvas)
const app = new Application({
view: offscreenCanvas,
width: offscreenCanvas.width,
height: offscreenCanvas.height,
// Ensure the drawing buffer persists so toDataURL() can read pixels
preserveDrawingBuffer: true,
backgroundAlpha: 0,
autoDensity: false,
resolution: 1,
autoStart: false,
})
app.stage.scale.set(previewResolution)
app.ticker.stop()
const modelInstance = new Live2DModel()
const objUrl = URL.createObjectURL(file)
const res = await fetch(objUrl)
const blob = await res.blob()
const cleanup = () => {
app.destroy()
if (offscreenCanvas.isConnected)
document.body.removeChild(offscreenCanvas)
URL.revokeObjectURL(objUrl)
}
try {
await Live2DFactory.setupLive2DModel(modelInstance, [new File([blob], file.name)], { autoInteract: false })
app.stage.addChild(modelInstance)
modelInstance.x = 275
modelInstance.y = 450
modelInstance.width = previewWidth
modelInstance.height = previewHeight
modelInstance.scale.set(0.1, 0.1)
modelInstance.anchor.set(0.5, 0.5)
await new Promise(resolve => setTimeout(resolve, 500))
app.renderer.render(app.stage)
const croppedCanvas = cropImg(offscreenCanvas)
// padding to 12:16
const paddingCanvas = document.createElement('canvas')
paddingCanvas.width = croppedCanvas.width > croppedCanvas.height / 16 * 12 ? croppedCanvas.width : croppedCanvas.height / 16 * 12
paddingCanvas.height = paddingCanvas.width / 12 * 16
const paddingCanvasCtx = paddingCanvas.getContext('2d')!
paddingCanvasCtx.drawImage(croppedCanvas, (paddingCanvas.width - croppedCanvas.width) / 2, (paddingCanvas.height - croppedCanvas.height) / 2, croppedCanvas.width, croppedCanvas.height)
const paddingDataUrl = paddingCanvas.toDataURL()
cleanup()
return paddingDataUrl
}
catch (error) {
console.error(error)
cleanup()
}
}
+36
View File
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": [
"DOM",
"ESNext",
"DOM.AsyncIterable"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"types": [
"vite/client"
],
"allowJs": true,
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue"
],
"exclude": [
"dist",
"node_modules"
]
}
+1
View File
@@ -17,6 +17,7 @@
"exports": {
"./assets/vrm": "./src/assets/vrm/index.ts",
"./composables/vrm": "./src/composables/vrm/index.ts",
"./utils/vrm-preview": "./src/utils/vrm-preview.ts",
".": "./src/index.ts"
},
"scripts": {
+1
View File
@@ -1,2 +1,3 @@
export { default as ThreeScene } from './components/ThreeScene.vue'
export { useModelStore } from './stores/model-store'
export * from './utils/vrm-preview'
@@ -0,0 +1,97 @@
import type { VRM } from '@pixiv/three-vrm'
import { AmbientLight, AnimationMixer, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three'
import { animations } from '../assets/vrm'
import { clipFromVRMAnimation, loadVrm, loadVRMAnimation, reAnchorRootPositionTrack } from '../composables/vrm'
/**
* Render a VRM file to an offscreen canvas and return a preview data URL.
*/
export async function loadVrmModelPreview(file: File) {
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = 1440
offscreenCanvas.height = 2560
offscreenCanvas.style.position = 'absolute'
offscreenCanvas.style.top = '0'
offscreenCanvas.style.left = '0'
offscreenCanvas.style.objectFit = 'cover'
offscreenCanvas.style.display = 'block'
offscreenCanvas.style.zIndex = '10000000000'
offscreenCanvas.style.opacity = '0'
document.body.appendChild(offscreenCanvas)
const renderer = new WebGLRenderer({
canvas: offscreenCanvas,
alpha: true,
antialias: true,
preserveDrawingBuffer: true,
})
renderer.setSize(offscreenCanvas.width, offscreenCanvas.height, false)
renderer.setPixelRatio(1)
const scene = new Scene()
const camera = new PerspectiveCamera(40, offscreenCanvas.width / offscreenCanvas.height, 0.01, 1000)
const ambientLight = new AmbientLight(0xFFFFFF, 0.8)
const directionalLight = new DirectionalLight(0xFFFFFF, 0.8)
directionalLight.position.set(1, 1, 1)
scene.add(ambientLight, directionalLight)
const objUrl = URL.createObjectURL(file)
let vrmInstance: VRM | undefined
try {
const vrmData = await loadVrm(objUrl, { scene, lookAt: true })
if (!vrmData)
return
vrmInstance = vrmData._vrm
const { modelCenter, initialCameraOffset } = vrmData
camera.position.copy(modelCenter).add(initialCameraOffset)
camera.lookAt(modelCenter)
camera.updateProjectionMatrix()
try {
const animation = await loadVRMAnimation(animations.idleLoop.toString())
const clip = await clipFromVRMAnimation(vrmData._vrm, animation)
if (clip) {
reAnchorRootPositionTrack(clip, vrmData._vrm)
const mixer = new AnimationMixer(vrmData._vrm.scene)
const action = mixer.clipAction(clip)
action.play()
mixer.update(0.1)
}
}
catch (err) {
console.warn('Failed to load VRM animation for preview:', err)
}
renderer.render(scene, camera)
const dataUrl = offscreenCanvas.toDataURL()
return dataUrl
}
finally {
renderer.dispose()
if (vrmInstance) {
vrmInstance.scene.traverse((child) => {
const node = child as any
if (node.geometry?.dispose)
node.geometry.dispose()
if (node.material) {
const materials = Array.isArray(node.material) ? node.material : [node.material]
for (const mat of materials) {
if (mat?.map?.dispose)
mat.map.dispose()
mat?.dispose?.()
}
}
})
}
URL.revokeObjectURL(objUrl)
if (offscreenCanvas.isConnected)
document.body.removeChild(offscreenCanvas)
}
}
+1 -23
View File
@@ -51,24 +51,7 @@
"@date-fns/utc": "^2.1.1",
"@formkit/auto-animate": "^0.9.0",
"@huggingface/transformers": "^3.8.1",
"@lemonneko/crop-empty-pixels": "0.1.1",
"@moeru/eventa": "^1.0.0-alpha.10",
"@pixi/app": "^6.5.10",
"@pixi/constants": "^6.5.10",
"@pixi/core": "^6.5.10",
"@pixi/display": "^6.5.10",
"@pixi/extensions": "^6.5.10",
"@pixi/interaction": "^6.5.10",
"@pixi/loaders": "^6.5.10",
"@pixi/math": "^6.5.10",
"@pixi/runner": "^6.5.10",
"@pixi/settings": "^6.5.10",
"@pixi/sprite": "^6.5.10",
"@pixi/ticker": "^6.5.10",
"@pixi/utils": "6",
"@pixiv/three-vrm": "^3.4.4",
"@pixiv/three-vrm-animation": "^3.4.4",
"@pixiv/three-vrm-core": "^3.4.4",
"@proj-airi/audio": "workspace:^",
"@proj-airi/ccc": "workspace:^",
"@proj-airi/chromatic": "^1.0.2",
@@ -79,6 +62,7 @@
"@proj-airi/model-driver-lipsync": "workspace:^",
"@proj-airi/server-sdk": "workspace:^",
"@proj-airi/stage-shared": "workspace:^",
"@proj-airi/stage-ui-live2d": "workspace:^",
"@proj-airi/stage-ui-three": "workspace:^",
"@proj-airi/tauri-plugin-mcp": "workspace:^",
"@proj-airi/ui": "workspace:^",
@@ -110,17 +94,13 @@
"gpuu": "^1.0.6",
"hono": "catalog:",
"html2canvas": "^1.4.1",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"mediabunny": "^1.27.2",
"nanoid": "^5.1.6",
"node-vibrant": "^4.0.3",
"ofetch": "^1.5.1",
"pinia": "^3.0.4",
"pixi-filters": "4",
"pixi-live2d-display": "^0.4.0",
"posthog-js": "catalog:",
"postprocessing": "^6.38.2",
"rehype-katex": "^7.0.1",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
@@ -130,7 +110,6 @@
"remark-rehype": "^11.1.2",
"shiki": "^3.20.0",
"splitpanes": "catalog:",
"three": "^0.182.0",
"unified": "^11.0.5",
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.0.0",
@@ -174,7 +153,6 @@
"@types/culori": "^4.0.1",
"@types/hast": "catalog:",
"@types/splitpanes": "catalog:",
"@types/three": "^0.182.0",
"@types/unist": "catalog:",
"@unocss/reset": "^66.5.11",
"@vitejs/plugin-vue": "^6.0.3",
@@ -1,18 +1,17 @@
<script setup lang="ts">
import type { DisplayModel } from '../../../../stores/display-models'
import { Live2DScene, useLive2d } from '@proj-airi/stage-ui-live2d'
import { ThreeScene, useModelStore } from '@proj-airi/stage-ui-three'
import { Button, Callout } from '@proj-airi/ui'
import { useMouse } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
import Live2DScene from '../../../scenes/Live2D.vue'
import Live2D from './live2d.vue'
import VRM from './vrm.vue'
import { DisplayModelFormat } from '../../../../stores/display-models'
import { useLive2d } from '../../../../stores/live2d'
import { useSettings } from '../../../../stores/settings'
import { ModelSelectorDialog } from '../../dialogs/model-selector'
@@ -32,7 +31,18 @@ const selectedModel = ref<DisplayModel | undefined>()
const positionCursor = useMouse()
const settingsStore = useSettings()
const { live2dDisableFocus, stageModelSelectedUrl, stageModelSelected, stageModelRenderer } = storeToRefs(settingsStore)
const {
live2dDisableFocus,
stageModelSelectedUrl,
stageModelSelected,
stageModelRenderer,
themeColorsHue,
themeColorsHueDynamic,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
} = storeToRefs(settingsStore)
watch(selectedModel, async () => {
stageModelSelected.value = selectedModel.value?.id
@@ -86,6 +96,12 @@ watch(selectedModel, async () => {
:model-src="stageModelSelectedUrl"
:model-id="stageModelSelected"
:disable-focus-at="live2dDisableFocus"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-shadow-enabled="live2dShadowEnabled"
/>
</div>
</template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d'
import { Button, Checkbox, FieldRange } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { defaultModelParameters, useLive2d } from '../../../../stores/live2d'
import { useSettings } from '../../../../stores/settings'
import { Section } from '../../../layouts'
import { ColorPalette } from '../../../widgets'
@@ -13,6 +13,7 @@ import { drizzle } from '@proj-airi/drizzle-duckdb-wasm'
import { getImportUrlBundles } from '@proj-airi/drizzle-duckdb-wasm/bundles/import-url-browser'
import { createLive2DLipSync } from '@proj-airi/model-driver-lipsync'
import { wlipsyncProfile } from '@proj-airi/model-driver-lipsync/shared/wlipsync'
import { Live2DScene, useLive2d } from '@proj-airi/stage-ui-live2d'
import { ThreeScene, useModelStore } from '@proj-airi/stage-ui-three'
import { animations } from '@proj-airi/stage-ui-three/assets/vrm'
import { useBroadcastChannel } from '@vueuse/core'
@@ -23,14 +24,11 @@ import { generateSpeech } from '@xsai/generate-speech'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref } from 'vue'
import Live2DScene from './Live2D.vue'
import { useDelayMessageQueue, useEmotionsMessageQueue, usePipelineCharacterSpeechPlaybackQueueStore, usePipelineWorkflowTextSegmentationStore } from '../../composables/queues'
import { llmInferenceEndToken } from '../../constants'
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
import { useChatStore } from '../../stores/chat'
import { useLive2d } from '../../stores/live2d'
import { useSpeechStore } from '../../stores/modules/speech'
import { useProvidersStore } from '../../stores/providers'
import { useSettings } from '../../stores/settings'
@@ -75,7 +73,19 @@ const { connectAudioContext, connectAudioAnalyser, connectLipSyncNode, clearAll,
const { currentAudioSource, playbackQueue } = storeToRefs(characterSpeechPlaybackQueue)
const settingsStore = useSettings()
const { stageModelRenderer, stageViewControlsEnabled, live2dDisableFocus, stageModelSelectedUrl, stageModelSelected } = storeToRefs(settingsStore)
const {
stageModelRenderer,
stageViewControlsEnabled,
live2dDisableFocus,
stageModelSelectedUrl,
stageModelSelected,
themeColorsHue,
themeColorsHueDynamic,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
} = storeToRefs(settingsStore)
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
const { audioContext } = useAudioContext()
connectAudioContext(audioContext)
@@ -378,6 +388,12 @@ onPlaybackStarted(({ text }) => {
:y-offset="yOffset"
:scale="scale"
:disable-focus-at="live2dDisableFocus"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-shadow-enabled="live2dShadowEnabled"
/>
<ThreeScene
v-if="stageModelRenderer === 'vrm' && showStage"
@@ -1,5 +1,5 @@
<script setup lang="ts">
import MarkdownRenderer from '../markdown/MarkdownRenderer.vue'
import MarkdownRenderer from '../markdown/markdown-renderer.vue'
import { useChatStore } from '../../stores/chat'
@@ -1,4 +1,6 @@
export { Live2DCanvas, Live2DModel } from './live2d'
export { default as SceneLive2D } from './Live2D.vue'
export { default as WidgetStage } from './Stage.vue'
export {
Live2DCanvas,
Live2DModel,
Live2DScene as SceneLive2D,
} from '@proj-airi/stage-ui-live2d/components/scenes'
@@ -4,7 +4,7 @@ import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { useChatStore } from '../stores/chat'
import { useDisplayModelsStore } from '../stores/display-models'
import { useLive2d } from '../stores/live2d'
import { useLive2d } from '@proj-airi/stage-ui-live2d'
import { useMcpStore } from '../stores/mcp'
import { useAiriCardStore } from '../stores/modules/airi-card'
import { useConsciousnessStore } from '../stores/modules/consciousness'
+6 -190
View File
@@ -1,22 +1,14 @@
import type { VRM } from '@pixiv/three-vrm'
import cropImg from '@lemonneko/crop-empty-pixels'
import localforage from 'localforage'
import { Application } from '@pixi/app'
import { extensions } from '@pixi/extensions'
import { Ticker, TickerPlugin } from '@pixi/ticker'
import { animations } from '@proj-airi/stage-ui-three/assets/vrm'
import { clipFromVRMAnimation, loadVrm, loadVRMAnimation, reAnchorRootPositionTrack } from '@proj-airi/stage-ui-three/composables/vrm'
import { loadLive2DModelPreview as generateLive2DPreview } from '@proj-airi/stage-ui-live2d/utils/live2d-preview'
import { loadVrmModelPreview as generateVrmPreview } from '@proj-airi/stage-ui-three/utils/vrm-preview'
import { until } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { Live2DFactory, Live2DModel } from 'pixi-live2d-display/cubism4'
import { AmbientLight, AnimationMixer, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three'
import { ref } from 'vue'
import '../utils/live2d-zip-loader'
import '../utils/live2d-opfs-registration'
import '@proj-airi/stage-ui-live2d/utils/live2d-zip-loader'
import '@proj-airi/stage-ui-live2d/utils/live2d-opfs-registration'
export enum DisplayModelFormat {
Live2dZip = 'live2d-zip',
@@ -103,186 +95,10 @@ export const useDisplayModelsStore = defineStore('display-models', () => {
return displayModelsPresets.find(model => model.id === id)
}
async function loadLive2DModelPreview(file: File) {
Live2DModel.registerTicker(Ticker)
extensions.add(TickerPlugin)
const previewWidth = 1440
const previewHeight = 2560
const previewResolution = 2
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = previewWidth * previewResolution
offscreenCanvas.height = previewHeight * previewResolution
offscreenCanvas.style.position = 'absolute'
offscreenCanvas.style.top = '0'
offscreenCanvas.style.left = '0'
offscreenCanvas.style.objectFit = 'cover'
offscreenCanvas.style.display = 'block'
offscreenCanvas.style.zIndex = '10000000000'
offscreenCanvas.style.opacity = '0'
document.body.appendChild(offscreenCanvas)
const app = new Application({
view: offscreenCanvas,
width: offscreenCanvas.width,
height: offscreenCanvas.height,
// Ensure the drawing buffer persists so toDataURL() can read pixels
preserveDrawingBuffer: true,
backgroundAlpha: 0,
autoDensity: false,
resolution: 1,
autoStart: false,
})
app.stage.scale.set(previewResolution)
app.ticker.stop()
const modelInstance = new Live2DModel()
const objUrl = URL.createObjectURL(file)
const res = await fetch(objUrl)
const blob = await res.blob()
const cleanup = () => {
app.destroy()
if (offscreenCanvas.isConnected)
document.body.removeChild(offscreenCanvas)
URL.revokeObjectURL(objUrl)
}
try {
await Live2DFactory.setupLive2DModel(modelInstance, [new File([blob], file.name)], { autoInteract: false })
app.stage.addChild(modelInstance)
// transforms
modelInstance.x = 275
modelInstance.y = 450
modelInstance.width = previewWidth
modelInstance.height = previewHeight
modelInstance.scale.set(0.1, 0.1)
modelInstance.anchor.set(0.5, 0.5)
await new Promise(resolve => setTimeout(resolve, 500))
// Force a render to ensure the latest frame is in the drawing buffer
app.renderer.render(app.stage)
const croppedCanvas = cropImg(offscreenCanvas)
// padding to 12:16
const paddingCanvas = document.createElement('canvas')
paddingCanvas.width = croppedCanvas.width > croppedCanvas.height / 16 * 12 ? croppedCanvas.width : croppedCanvas.height / 16 * 12
paddingCanvas.height = paddingCanvas.width / 12 * 16
const paddingCanvasCtx = paddingCanvas.getContext('2d')!
paddingCanvasCtx.drawImage(croppedCanvas, (paddingCanvas.width - croppedCanvas.width) / 2, (paddingCanvas.height - croppedCanvas.height) / 2, croppedCanvas.width, croppedCanvas.height)
const paddingDataUrl = paddingCanvas.toDataURL()
cleanup()
// return dataUrl
return paddingDataUrl
}
catch (error) {
console.error(error)
cleanup()
}
}
const loadLive2DModelPreview = (file: File) => generateLive2DPreview(file)
async function loadVrmModelPreview(file: File) {
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = 1440
offscreenCanvas.height = 2560
offscreenCanvas.style.position = 'absolute'
offscreenCanvas.style.top = '0'
offscreenCanvas.style.left = '0'
offscreenCanvas.style.objectFit = 'cover'
offscreenCanvas.style.display = 'block'
offscreenCanvas.style.zIndex = '10000000000'
offscreenCanvas.style.opacity = '0'
document.body.appendChild(offscreenCanvas)
const renderer = new WebGLRenderer({
canvas: offscreenCanvas,
alpha: true,
antialias: true,
preserveDrawingBuffer: true,
})
renderer.setSize(offscreenCanvas.width, offscreenCanvas.height, false)
renderer.setPixelRatio(1)
const scene = new Scene()
const camera = new PerspectiveCamera(40, offscreenCanvas.width / offscreenCanvas.height, 0.01, 1000)
const ambientLight = new AmbientLight(0xFFFFFF, 0.8)
const directionalLight = new DirectionalLight(0xFFFFFF, 0.8)
directionalLight.position.set(1, 1, 1)
scene.add(ambientLight, directionalLight)
const objUrl = URL.createObjectURL(file)
let vrmInstance: VRM | undefined
try {
const vrmData = await loadVrm(objUrl, { scene, lookAt: true })
if (!vrmData) {
return
}
vrmInstance = vrmData._vrm
const { modelCenter, initialCameraOffset } = vrmData
camera.position.copy(modelCenter).add(initialCameraOffset)
camera.lookAt(modelCenter)
camera.updateProjectionMatrix()
try {
const animation = await loadVRMAnimation(animations.idleLoop.toString())
const clip = await clipFromVRMAnimation(vrmData._vrm, animation)
if (clip) {
reAnchorRootPositionTrack(clip, vrmData._vrm)
const mixer = new AnimationMixer(vrmData._vrm.scene)
mixer.clipAction(clip).play()
mixer.update(1 / 60)
}
}
catch (error) {
console.warn('Failed to load VRM idle animation for preview.', error)
}
await new Promise<void>((resolve) => {
const start = performance.now()
const step = (time: number) => {
if (time - start >= 2000) {
resolve()
return
}
requestAnimationFrame(step)
}
requestAnimationFrame(step)
})
renderer.render(scene, camera)
const croppedCanvas = cropImg(offscreenCanvas)
// padding to 12:16
const paddingCanvas = document.createElement('canvas')
paddingCanvas.width = croppedCanvas.width > croppedCanvas.height / 16 * 12 ? croppedCanvas.width : croppedCanvas.height / 16 * 12
paddingCanvas.height = paddingCanvas.width / 12 * 16
const paddingCanvasCtx = paddingCanvas.getContext('2d')!
paddingCanvasCtx.drawImage(croppedCanvas, (paddingCanvas.width - croppedCanvas.width) / 2, (paddingCanvas.height - croppedCanvas.height) / 2, croppedCanvas.width, croppedCanvas.height)
return paddingCanvas.toDataURL()
}
catch (error) {
console.error(error)
return
}
finally {
if (vrmInstance && 'dispose' in vrmInstance) {
(vrmInstance as { dispose: () => void }).dispose()
}
renderer.dispose()
document.body.removeChild(offscreenCanvas)
URL.revokeObjectURL(objUrl)
}
return generateVrmPreview(file)
}
async function addDisplayModel(format: DisplayModelFormat, file: File) {
+1 -94
View File
@@ -1,94 +1 @@
import { useBroadcastChannel } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { createResettableLocalStorage, createResettableRef } from '../utils/resettable'
type BroadcastChannelEvents
= | BroadcastChannelEventShouldUpdateView
interface BroadcastChannelEventShouldUpdateView {
type: 'should-update-view'
}
export const defaultModelParameters = {
angleX: 0,
angleY: 0,
angleZ: 0,
leftEyeOpen: 1,
rightEyeOpen: 1,
leftEyeSmile: 0,
rightEyeSmile: 0,
leftEyebrowLR: 0,
rightEyebrowLR: 0,
leftEyebrowY: 0,
rightEyebrowY: 0,
leftEyebrowAngle: 0,
rightEyebrowAngle: 0,
leftEyebrowForm: 0,
rightEyebrowForm: 0,
mouthOpen: 0,
mouthForm: 0,
cheek: 0,
bodyAngleX: 0,
bodyAngleY: 0,
bodyAngleZ: 0,
breath: 0,
}
export const useLive2d = defineStore('live2d', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-live2d' })
const shouldUpdateViewHooks = ref<Array<() => void>>([])
const onShouldUpdateView = (hook: () => void) => {
shouldUpdateViewHooks.value.push(hook)
}
function shouldUpdateView() {
post({ type: 'should-update-view' })
shouldUpdateViewHooks.value.forEach(hook => hook())
}
watch(data, (event) => {
if (event.type === 'should-update-view') {
shouldUpdateViewHooks.value.forEach(hook => hook())
}
})
const [position, resetPosition] = createResettableLocalStorage('settings/live2d/position', { x: 0, y: 0 }) // position is relative to the center of the screen, units are %
const positionInPercentageString = computed(() => ({
x: `${position.value.x}%`,
y: `${position.value.y}%`,
}))
const [currentMotion, resetCurrentMotion] = createResettableRef<{ group: string, index?: number }>({ group: 'Idle', index: 0 })
const [availableMotions, resetAvailableMotions] = createResettableRef<{ motionName: string, motionIndex: number, fileName: string }[]>([])
const [motionMap, resetMotionMap] = createResettableLocalStorage<Record<string, string>>('settings/live2d/motion-map', {})
const [scale, resetScale] = createResettableLocalStorage('settings/live2d/scale', 1)
// Live2D model parameters
const [modelParameters, resetModelParameters] = createResettableLocalStorage('settings/live2d/parameters', defaultModelParameters)
function resetState() {
resetPosition()
resetCurrentMotion()
resetAvailableMotions()
resetMotionMap()
resetScale()
resetModelParameters()
shouldUpdateView()
}
return {
position,
positionInPercentageString,
currentMotion,
availableMotions,
motionMap,
scale,
modelParameters,
onShouldUpdateView,
shouldUpdateView,
resetState,
}
})
export * from '@proj-airi/stage-ui-live2d/stores/live2d'
+1 -1
View File
@@ -1 +1 @@
export { randomSaccadeInterval } from './eye-motions'
export { randomSaccadeInterval } from '@proj-airi/stage-ui-live2d/utils/eye-motions'
+100 -72
View File
@@ -2133,60 +2133,9 @@ importers:
'@huggingface/transformers':
specifier: ^3.8.1
version: 3.8.1
'@lemonneko/crop-empty-pixels':
specifier: 0.1.1
version: 0.1.1
'@moeru/eventa':
specifier: ^1.0.0-alpha.10
version: 1.0.0-alpha.10(electron@39.2.7)(h3@2.0.1-rc.5(crossws@0.4.1(srvx@0.9.8(patch_hash=f0151386fdcbcb6f53833cf8f66926ef2eb31b71a2782d6e0960dec832ef108d))))
'@pixi/app':
specifier: ^6.5.10
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/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/constants':
specifier: ^6.5.10
version: 6.5.10
'@pixi/core':
specifier: ^6.5.10
version: 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':
specifier: ^6.5.10
version: 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/extensions':
specifier: ^6.5.10
version: 6.5.10
'@pixi/interaction':
specifier: ^6.5.10
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.5.10
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)))
'@pixi/math':
specifier: ^6.5.10
version: 6.5.10
'@pixi/runner':
specifier: ^6.5.10
version: 6.5.10
'@pixi/settings':
specifier: ^6.5.10
version: 6.5.10(@pixi/constants@6.5.10)
'@pixi/sprite':
specifier: ^6.5.10
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/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/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/ticker':
specifier: ^6.5.10
version: 6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@pixi/utils':
specifier: '6'
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@pixiv/three-vrm':
specifier: ^3.4.4
version: 3.4.4(three@0.182.0)
'@pixiv/three-vrm-animation':
specifier: ^3.4.4
version: 3.4.4(three@0.182.0)
'@pixiv/three-vrm-core':
specifier: ^3.4.4
version: 3.4.4(three@0.182.0)
'@proj-airi/audio':
specifier: workspace:^
version: link:../audio
@@ -2217,6 +2166,9 @@ importers:
'@proj-airi/stage-shared':
specifier: workspace:^
version: link:../stage-shared
'@proj-airi/stage-ui-live2d':
specifier: workspace:^
version: link:../stage-ui-live2d
'@proj-airi/stage-ui-three':
specifier: workspace:^
version: link:../stage-ui-three
@@ -2310,9 +2262,6 @@ importers:
html2canvas:
specifier: ^1.4.1
version: 1.4.1
jszip:
specifier: ^3.10.1
version: 3.10.1
localforage:
specifier: ^1.10.0
version: 1.10.0
@@ -2331,18 +2280,9 @@ importers:
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pixi-filters:
specifier: '4'
version: 4.2.0(5af797a0b5bd919a06578c24a7479046)
pixi-live2d-display:
specifier: ^0.4.0
version: 0.4.0(0736f5bd67d2e4de7e9fff36ad63fa2e)
posthog-js:
specifier: 'catalog:'
version: 1.306.1
postprocessing:
specifier: ^6.38.2
version: 6.38.2(three@0.182.0)
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
@@ -2370,9 +2310,6 @@ importers:
splitpanes:
specifier: 'catalog:'
version: 4.0.4(vue@3.5.25(typescript@5.9.3))
three:
specifier: ^0.182.0
version: 0.182.0
unified:
specifier: ^11.0.5
version: 11.0.5
@@ -2497,9 +2434,6 @@ importers:
'@types/splitpanes':
specifier: 'catalog:'
version: 2.2.6
'@types/three':
specifier: ^0.182.0
version: 0.182.0
'@types/unist':
specifier: 'catalog:'
version: 3.0.3
@@ -2540,6 +2474,94 @@ importers:
specifier: ^3.1.8
version: 3.2.1(typescript@5.9.3)
packages/stage-ui-live2d:
dependencies:
'@lemonneko/crop-empty-pixels':
specifier: 0.1.1
version: 0.1.1
'@pixi/app':
specifier: ^6.5.10
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/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/constants':
specifier: ^6.5.10
version: 6.5.10
'@pixi/core':
specifier: ^6.5.10
version: 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':
specifier: ^6.5.10
version: 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/extensions':
specifier: ^6.5.10
version: 6.5.10
'@pixi/interaction':
specifier: ^6.5.10
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.5.10
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)))
'@pixi/math':
specifier: ^6.5.10
version: 6.5.10
'@pixi/runner':
specifier: ^6.5.10
version: 6.5.10
'@pixi/settings':
specifier: ^6.5.10
version: 6.5.10(@pixi/constants@6.5.10)
'@pixi/sprite':
specifier: ^6.5.10
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/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/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/ticker':
specifier: ^6.5.10
version: 6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@pixi/utils':
specifier: ^6.5.10
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
'@proj-airi/stage-shared':
specifier: workspace:^
version: link:../stage-shared
'@proj-airi/ui':
specifier: workspace:^
version: link:../ui
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
culori:
specifier: ^4.0.2
version: 4.0.2
es-toolkit:
specifier: 'catalog:'
version: 1.43.0
jszip:
specifier: ^3.10.1
version: 3.10.1
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pixi-filters:
specifier: '4'
version: 4.2.0(5af797a0b5bd919a06578c24a7479046)
pixi-live2d-display:
specifier: ^0.4.0
version: 0.4.0(0736f5bd67d2e4de7e9fff36ad63fa2e)
three:
specifier: ^0.182.0
version: 0.182.0
vue:
specifier: ^3.5.25
version: 3.5.25(typescript@5.9.3)
devDependencies:
'@types/culori':
specifier: ^4.0.1
version: 4.0.1
'@types/three':
specifier: ^0.182.0
version: 0.182.0
vue-tsc:
specifier: ^3.1.8
version: 3.2.1(typescript@5.9.3)
packages/stage-ui-three:
dependencies:
'@pixiv/three-vrm':
@@ -2847,7 +2869,7 @@ importers:
version: 66.5.11
'@wxt-dev/module-vue':
specifier: ^1.0.3
version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
vue-tsc:
specifier: ^3.1.8
version: 3.2.1(typescript@5.9.3)
@@ -22068,6 +22090,12 @@ snapshots:
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53
vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
dependencies:
'@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
@@ -22813,9 +22841,9 @@ snapshots:
'@types/filesystem': 0.0.36
'@types/har-format': 1.2.16
'@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- vite
+20 -16
View File
@@ -1,6 +1,3 @@
catalogMode: prefer
shellEmulator: true
packages:
- packages/**
- plugins/**
@@ -10,18 +7,6 @@ packages:
- apps/**
- '!**/dist/**'
overrides:
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
axios: npm:feaxios@^0.0.23
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
isarray: npm:@nolyfill/isarray@^1.0.44
safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
patchedDependencies:
srvx@0.9.8: patches/srvx@0.9.8.patch
catalog:
'@capacitor/cli': ^8.0.0
'@capacitor/core': ^8.0.0
@@ -74,6 +59,8 @@ catalog:
xsschema: 0.4.0-beta.13
zod: ^4.2.1
catalogMode: prefer
catalogs:
rolldown-vite:
vite: npm:rolldown-vite@^7.2.11
@@ -90,8 +77,9 @@ ignoredBuiltDependencies:
onlyBuiltDependencies:
- '@discordjs/opus'
- '@napi-rs/image'
- '@ffmpeg-installer/darwin-arm64'
- '@ffmpeg-installer/linux-x64'
- '@napi-rs/image'
- '@parcel/watcher'
- bufferutil
- core-js
@@ -101,6 +89,7 @@ onlyBuiltDependencies:
- es5-ext
- esbuild
- ffmpeg-static
- less
- msw
- onnxruntime-node
- protobufjs
@@ -109,3 +98,18 @@ onlyBuiltDependencies:
- spawn-sync
- utf-8-validate
- vue-demi
overrides:
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
axios: npm:feaxios@^0.0.23
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
isarray: npm:@nolyfill/isarray@^1.0.44
safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
patchedDependencies:
srvx@0.9.8: patches/srvx@0.9.8.patch
shellEmulator: true