mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-30 17:07:23 +08:00
feat(stage-ui-live2d): offset model with cursor
This commit is contained in:
@@ -3,12 +3,12 @@ import type { Live2DEyeFocusSource } from '../../composables/live2d'
|
||||
|
||||
import { Screen } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import Live2DCanvas from './live2d/Canvas.vue'
|
||||
import Live2DModel from './live2d/Model.vue'
|
||||
|
||||
import { useLive2DEyeFocusFor, useSettingsLive2d } from '../../composables/live2d'
|
||||
import { getLive2DModelMouseOffset, useLive2DEyeFocusFor, useSettingsLive2d } from '../../composables/live2d'
|
||||
|
||||
import '../../utils/live2d-zip-loader'
|
||||
import '../../utils/live2d-opfs-registration'
|
||||
@@ -64,6 +64,15 @@ const mouseFocus = useLive2DEyeFocusFor({
|
||||
}),
|
||||
source: activeCursorPosition,
|
||||
})
|
||||
const mouseModelOffset = computed(() => {
|
||||
if (!live2dEyeTracking.value)
|
||||
return { x: 0, y: 0 }
|
||||
|
||||
return getLive2DModelMouseOffset(
|
||||
activeCursorPosition.value,
|
||||
live2dCanvasRef.value?.canvasElement()?.getBoundingClientRect(),
|
||||
)
|
||||
})
|
||||
|
||||
watch(() => props.cursorPosition, (cursorPosition) => {
|
||||
activeCursorPosition.value = cursorPosition ? { ...cursorPosition } : null
|
||||
@@ -120,6 +129,7 @@ defineExpose({
|
||||
:height="height"
|
||||
:paused="paused"
|
||||
:focus-at="mouseFocus"
|
||||
:mouse-offset="mouseModelOffset"
|
||||
:eye-tracking="live2dEyeTracking"
|
||||
:eye-focus-source-active="!!activeCursorPosition"
|
||||
:theme-colors-hue="themeColorsHue"
|
||||
|
||||
@@ -42,6 +42,7 @@ const props = withDefaults(defineProps<{
|
||||
height: number
|
||||
paused?: boolean
|
||||
focusAt?: { x: number, y: number }
|
||||
mouseOffset?: { x: number, y: number }
|
||||
eyeTracking?: boolean
|
||||
eyeFocusSourceActive?: boolean
|
||||
themeColorsHue?: number
|
||||
@@ -57,6 +58,7 @@ const props = withDefaults(defineProps<{
|
||||
nowSpeaking: false,
|
||||
paused: false,
|
||||
focusAt: () => ({ x: 0, y: 0 }),
|
||||
mouseOffset: () => ({ x: 0, y: 0 }),
|
||||
eyeTracking: false,
|
||||
eyeFocusSourceActive: false,
|
||||
disableFocusAt: false,
|
||||
@@ -88,8 +90,8 @@ let isUnmounted = false
|
||||
const modelLoadMutex = new Mutex()
|
||||
|
||||
const offset = computed(() => ({
|
||||
x: (position.value.x / 100) * props.width,
|
||||
y: -(position.value.y / 100) * props.height,
|
||||
x: (position.value.x / 100) * props.width + props.mouseOffset.x,
|
||||
y: -(position.value.y / 100) * props.height + props.mouseOffset.y,
|
||||
}))
|
||||
|
||||
const pixiApp = toRef(() => props.app)
|
||||
|
||||
@@ -76,4 +76,27 @@ describe('useLive2DEyeFocusFor', () => {
|
||||
|
||||
expect(focus.value).toEqual({ x: 1000, y: 1000 })
|
||||
})
|
||||
|
||||
it('maps cursor displacement to a small model offset with twice the vertical rate', async () => {
|
||||
const { getLive2DModelMouseOffset } = await import('./eye-tracking')
|
||||
|
||||
expect(getLive2DModelMouseOffset(
|
||||
{ x: 750, y: 750 },
|
||||
{ left: 0, top: 0, width: 1000, height: 1000 },
|
||||
)).toEqual({ x: -10, y: 20 })
|
||||
expect(getLive2DModelMouseOffset(
|
||||
{ x: 500, y: 500 },
|
||||
{ left: 0, top: 0, width: 1000, height: 1000 },
|
||||
)).toEqual({ x: 0, y: 0 })
|
||||
})
|
||||
|
||||
it('does not offset the model without valid tracking geometry', async () => {
|
||||
const { getLive2DModelMouseOffset } = await import('./eye-tracking')
|
||||
|
||||
expect(getLive2DModelMouseOffset(null, undefined)).toEqual({ x: 0, y: 0 })
|
||||
expect(getLive2DModelMouseOffset(
|
||||
{ x: 500, y: 500 },
|
||||
{ left: 0, top: 0, width: 0, height: 1000 },
|
||||
)).toEqual({ x: 0, y: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,31 @@ export interface Live2DEyeFocusSource {
|
||||
y: number
|
||||
}
|
||||
|
||||
const live2dModelMouseOffsetRatio = 0.04
|
||||
|
||||
/**
|
||||
* Maps cursor displacement from the canvas center to a small model offset.
|
||||
*
|
||||
* @example
|
||||
* getLive2DModelMouseOffset({ x: 750, y: 750 }, { left: 0, top: 0, width: 1000, height: 1000 })
|
||||
* // => { x: -10, y: 20 }
|
||||
*/
|
||||
export function getLive2DModelMouseOffset(
|
||||
source: Live2DEyeFocusSource | null | undefined,
|
||||
canvasRect: Pick<DOMRect, 'left' | 'top' | 'width' | 'height'> | undefined,
|
||||
): { x: number, y: number } {
|
||||
if (!source || !canvasRect || canvasRect.width <= 0 || canvasRect.height <= 0)
|
||||
return { x: 0, y: 0 }
|
||||
|
||||
const horizontalOffset = source.x - canvasRect.left - canvasRect.width / 2
|
||||
const verticalOffset = source.y - canvasRect.top - canvasRect.height / 2
|
||||
|
||||
return {
|
||||
x: horizontalOffset === 0 ? 0 : -horizontalOffset * live2dModelMouseOffsetRatio,
|
||||
y: verticalOffset * live2dModelMouseOffsetRatio * 2,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a cursor position into the Live2D model driver's eye focus coordinates.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user