fix(stage-ui-live2d): better blink interval

This commit is contained in:
Neko Ayaka
2026-05-21 02:53:02 +08:00
parent 92b20b0dd2
commit b2dfe03d11
2 changed files with 50 additions and 2 deletions
@@ -135,4 +135,48 @@ describe('live2d motion manager plugins', () => {
expect(context.internalModel.eyeBlink?.updateParameters).not.toHaveBeenCalled()
expect(context.handled).toBe(true)
})
/**
* @example
* expect(context.model.getParameterValueById('ParamEyeLOpen')).toBeLessThan(1)
*/
it('opens force blink over a randomized 150ms to 300ms duration', () => {
const randomSpy = vi.spyOn(Math, 'random')
.mockReturnValueOnce(0)
.mockReturnValueOnce(0.5)
const context = createContext({
live2dAutoBlinkEnabled: ref(true),
live2dForceAutoBlinkEnabled: ref(true),
timeDelta: 3,
})
const plugin = useMotionUpdatePluginAutoEyeBlink(ref(false))
// ROOT CAUSE:
//
// Force blink previously reopened at one fixed speed.
// That made closed eyes feel either too quick or too slow depending on
// the model's eye-open parameter range.
//
// We fixed this by randomizing each opening phase between 150ms and 300ms.
plugin(context)
context.timeDelta = 0.075
context.handled = false
plugin(context)
context.timeDelta = 0.15
context.handled = false
plugin(context)
expect(context.model.getParameterValueById('ParamEyeLOpen')).toBeCloseTo(4 / 9)
expect(context.model.getParameterValueById('ParamEyeROpen')).toBeCloseTo(4 / 9)
context.timeDelta = 0.075
context.handled = false
plugin(context)
expect(context.model.getParameterValueById('ParamEyeLOpen')).toBe(1)
expect(context.model.getParameterValueById('ParamEyeROpen')).toBe(1)
randomSpy.mockRestore()
})
})
@@ -255,6 +255,7 @@ export function useMotionUpdatePluginAutoEyeBlink(
startLeft: 1,
startRight: 1,
delayMs: 0,
openDurationMs: 300,
}
// Eye values captured at blink start. Used as the base during
@@ -263,11 +264,13 @@ export function useMotionUpdatePluginAutoEyeBlink(
let preBlinkLeft = 1.0
let preBlinkRight = 1.0
const blinkCloseDuration = 75 // ms
const blinkOpenDuration = 75 // ms
const minBlinkOpenDuration = 150 // ms
const maxBlinkOpenDuration = 300 // ms
const minDelay = 3000
const maxDelay = 8000
const clamp01 = (value: number) => Math.min(1, Math.max(0, value))
const randomBlinkOpenDuration = () => minBlinkOpenDuration + Math.random() * (maxBlinkOpenDuration - minBlinkOpenDuration)
function resetBlinkState() {
blinkState.phase = 'idle'
@@ -307,13 +310,14 @@ export function useMotionUpdatePluginAutoEyeBlink(
if (blinkState.progress >= 1) {
blinkState.phase = 'opening'
blinkState.progress = 0
blinkState.openDurationMs = randomBlinkOpenDuration()
}
return { eyeLOpen, eyeROpen }
}
// Opening: move back to the base with ease-in.
blinkState.progress = Math.min(1, blinkState.progress + dt / blinkOpenDuration)
blinkState.progress = Math.min(1, blinkState.progress + dt / blinkState.openDurationMs)
const eased = easeInQuad(blinkState.progress)
const eyeLOpen = clamp01(blinkState.startLeft * eased)
const eyeROpen = clamp01(blinkState.startRight * eased)