fix(stage-ui-live2d): animate model position and scale on resize with animejs (#1395)

---------

Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
Stardust
2026-03-17 15:24:13 +08:00
committed by GitHub
co-authored by Neko
parent 09f2470c22
commit b97c7a8826
3 changed files with 55 additions and 11 deletions
+1
View File
@@ -52,6 +52,7 @@
"@proj-airi/stage-shared": "workspace:^",
"@proj-airi/ui": "workspace:^",
"@vueuse/core": "^14.2.1",
"animejs": "^4.3.6",
"culori": "^4.0.2",
"es-toolkit": "catalog:",
"jszip": "^3.10.1",
@@ -5,7 +5,8 @@ import type { PixiLive2DInternalModel } from '../../../composables/live2d'
import { listenBeatSyncBeatSignal } from '@proj-airi/stage-shared/beat-sync'
import { useTheme } from '@proj-airi/ui'
import { breakpointsTailwind, until, useBreakpoints, useDebounceFn } from '@vueuse/core'
import { breakpointsTailwind, until, useBreakpoints } from '@vueuse/core'
import { animate } from 'animejs'
import { formatHex } from 'culori'
import { Mutex } from 'es-toolkit'
import { storeToRefs } from 'pinia'
@@ -116,10 +117,9 @@ function getCoreModel() {
return model.value!.internalModel.coreModel as any
}
function setScaleAndPosition() {
if (!model.value)
return
let resizeAnimation: ReturnType<typeof animate> | undefined
function computeScaleAndPosition() {
let offsetFactor = 2.2
if (isMobile.value) {
offsetFactor = 2.2
@@ -129,15 +129,52 @@ function setScaleAndPosition() {
const widthScale = (props.width * 0.95 / initialModelWidth.value * offsetFactor)
let scale = Math.min(heightScale, widthScale)
// Prevent zero or NaN values to fix the "headless" model issue.
if (Number.isNaN(scale) || scale <= 0) {
scale = 1e-6
}
model.value.scale.set(scale * props.scale, scale * props.scale)
return {
scale: scale * props.scale,
x: (props.width / 2) + offset.value.xOffset,
y: props.height + offset.value.yOffset,
}
}
model.value.x = (props.width / 2) + offset.value.xOffset
model.value.y = props.height + offset.value.yOffset
function setScaleAndPosition(animated = false) {
if (!model.value)
return
const target = computeScaleAndPosition()
if (!animated) {
model.value.scale.set(target.scale, target.scale)
model.value.x = target.x
model.value.y = target.y
return
}
resizeAnimation?.pause()
const current = {
scale: model.value.scale.x,
x: model.value.x,
y: model.value.y,
}
resizeAnimation = animate(current, {
scale: target.scale,
x: target.x,
y: target.y,
duration: 200,
ease: 'outQuad',
onUpdate: () => {
if (!model.value)
return
model.value.scale.set(current.scale, current.scale)
model.value.x = current.x
model.value.y = current.y
},
})
}
const live2dStore = useLive2d()
@@ -398,7 +435,9 @@ async function setMotion(motionName: string, index?: number) {
}
}
const handleResize = useDebounceFn(setScaleAndPosition, 100)
function handleResize() {
setScaleAndPosition(true)
}
const dropShadowColorComputer = ref<HTMLDivElement>()
const dropShadowAnimationId = ref(0)
@@ -425,8 +464,8 @@ watch(modelSrcRef, async () => await loadModel(), { immediate: true })
watch(dark, updateDropShadowFilter, { immediate: true })
watch([model, themeColorsHue], updateDropShadowFilter)
watch(live2dShadowEnabled, updateDropShadowFilter)
watch(offset, setScaleAndPosition)
watch(() => props.scale, setScaleAndPosition)
watch(offset, () => setScaleAndPosition())
watch(() => props.scale, () => setScaleAndPosition())
// TODO: This is hacky!
function updateDropShadowFilterLoop() {
@@ -625,6 +664,7 @@ onMounted(async () => {
onUnmounted(() => {
isUnmounted = true
resizeAnimation?.pause()
disposeShouldUpdateView?.()
})
+3
View File
@@ -2910,6 +2910,9 @@ importers:
'@vueuse/core':
specifier: ^14.2.1
version: 14.2.1(vue@3.5.29(typescript@5.9.3))
animejs:
specifier: ^4.3.6
version: 4.3.6
culori:
specifier: ^4.0.2
version: 4.0.2