feat:加上手动调节与表情回放

This commit is contained in:
Edom
2026-08-20 00:17:43 +08:00
parent 851f2b8c6c
commit e880350f62
31 changed files with 3577 additions and 104 deletions
+165
View File
@@ -0,0 +1,165 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import Hero from '@/components/Hero.vue'
import ManualCanvas, { type HeroHandle } from '@/components/ManualCanvas.vue'
import {
DEFAULT_EYE,
DEFAULT_EYES,
type EyeSide,
type EyeTune,
type ManualPart,
type ManualState
} from '@/ui/pose/model'
import type { HeadPose } from '@/ui/pose/math'
const props = withDefaults(
defineProps<{
size?: number
shape?: string
color?: string
state?: string
follow?: boolean
paper?: string
manual?: ManualState | null
tools?: boolean
}>(),
{
size: 440,
shape: 'blob',
color: 'black',
state: 'idle',
follow: false,
paper: '#f9f9f9',
manual: null,
tools: false
}
)
const emit = defineEmits<{
ready: [ok: boolean]
'update:manual': [ManualState]
}>()
const hero = ref<InstanceType<typeof Hero> | null>(null)
const liveHero = computed<HeroHandle | null>(() => hero.value)
const active = computed(() => !!props.manual?.on && props.tools)
function patch(next: Partial<ManualState>) {
if (!props.manual) return
emit('update:manual', { ...props.manual, ...next })
}
function applyEngine() {
const bot = hero.value
const manual = props.manual
if (!bot) return
if (!manual?.on || !props.tools) {
bot.setManualHold(false)
return
}
bot.setManualHold(true)
bot.setPose(manual.pose)
bot.setManualOffset(manual.offset)
bot.setEyeTune(engineEyes(manual.eyes))
}
function near(a: number, b: number) {
return Math.abs(a - b) < 1e-6
}
function sameSide(a: EyeSide, b: EyeSide) {
return (
near(a.width, b.width) &&
near(a.height, b.height) &&
near(a.size, b.size) &&
near(a.angle, b.angle) &&
near(a.x, b.x) &&
near(a.y, b.y)
)
}
function sameEyes(a: EyeTune, b: EyeTune) {
return sameSide(a.left, b.left) && sameSide(a.right, b.right) && near(a.spacing, b.spacing)
}
function engineSide(side: EyeSide): EyeSide {
return { ...side, x: side.x - DEFAULT_EYE.x, y: side.y - DEFAULT_EYE.y }
}
function engineEyes(eyes: EyeTune): EyeTune | null {
if (sameEyes(eyes, DEFAULT_EYES)) return null
return { left: engineSide(eyes.left), right: engineSide(eyes.right), spacing: eyes.spacing }
}
function clearSelection() {
if (props.manual?.selected != null) emit('update:manual', { ...props.manual, selected: null })
}
watch(
() => [props.tools, props.shape],
() => clearSelection()
)
watch(
() => [
props.manual?.on,
props.manual?.pose,
props.manual?.offset,
props.manual?.eyes,
props.tools,
hero.value
],
() => applyEngine(),
{ deep: true }
)
function onReady(ok: boolean) {
emit('ready', ok)
if (ok) void nextTick(applyEngine)
}
onBeforeUnmount(() => {
clearSelection()
hero.value?.setManualHold(false)
})
defineExpose({
spin: () => hero.value?.spin(),
orbitGaze: (ms?: number) => hero.value?.orbitGaze(ms),
svg: () => hero.value?.svg() ?? null,
seekEye: (index: number, opts?: { snap?: boolean }) => hero.value?.seekEye(index, opts),
flushPerformance: () => hero.value?.flushPerformance(),
setPlaylistHold: (on: boolean) => hero.value?.setPlaylistHold(on),
setPaused: (on: boolean | 'hold-pose') => hero.value?.setPaused(on),
holdFrame: (at?: number) => hero.value?.holdFrame(at),
freezeNow: (opts?: { settle?: boolean }) => hero.value?.freezeNow(opts) ?? null,
playback: () => hero.value?.playback() ?? null
})
</script>
<template>
<div
class="avatar-stage relative inline-block aspect-square max-w-full"
:style="{ width: `${props.size}px` }"
>
<Hero
ref="hero"
class="h-auto max-w-full"
:size="size"
:shape="shape"
:color="color"
:state="state"
:follow="follow && !active"
:paper="paper"
@ready="onReady"
/>
<ManualCanvas
v-if="active && manual"
:manual="manual"
:hero="liveHero"
@update:pose="(pose: HeadPose) => patch({ pose })"
@update:selected="(selected: ManualPart | null) => patch({ selected })"
@update:eyes="(eyes: EyeTune) => patch({ eyes })"
/>
</div>
</template>