|
|
@@ -1,6 +1,6 @@
|
|
|
<script setup lang="ts">
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
-import { decodeAgentAvatar, defaultAgentIdentity } from '@shared/agentIdentity'
|
|
|
+import { decodeAgentAvatar } from '@shared/agentIdentity'
|
|
|
import { mountCursorAvatar, type CursorState } from './maus/cursor-engine'
|
|
|
import { MASCOT_SILHOUETTES, LAOA_BODIES } from './maus/silhouettes'
|
|
|
type MascotState = 'idle' | 'working' | 'loading' | 'notifying' | 'waiting' | 'success' | 'failure'
|
|
|
@@ -13,19 +13,22 @@ let beat: ReturnType<typeof setTimeout> | undefined
|
|
|
let observer: IntersectionObserver | undefined
|
|
|
let media: MediaQueryList | undefined
|
|
|
let renderer: ReturnType<typeof mountCursorAvatar> | undefined
|
|
|
-const identity = computed(() => decodeAgentAvatar(props.avatar) ?? defaultAgentIdentity(props.name))
|
|
|
-const hasImage = computed(() => identity.value.avatarMode === 'image' && !!identity.value.imageDataURL && !imageFailed.value)
|
|
|
-const radius = computed(() => identity.value.imageCrop === 'circle' ? '50%' : identity.value.imageCrop === 'square' ? '0' : '22%')
|
|
|
+// Missing metadata during refresh is not a configured green mascot.
|
|
|
+const identity = computed(() => decodeAgentAvatar(props.avatar))
|
|
|
+const hasImage = computed(() => identity.value?.avatarMode === 'image' && !!identity.value.imageDataURL && !imageFailed.value)
|
|
|
+const hasMascot = computed(() => identity.value?.avatarMode === 'mascot')
|
|
|
+const hasVisual = computed(() => hasImage.value || hasMascot.value)
|
|
|
+const radius = computed(() => identity.value?.imageCrop === 'circle' ? '50%' : identity.value?.imageCrop === 'square' ? '0' : '22%')
|
|
|
const states: Record<MascotState, CursorState> = {idle:'idle',working:'working',loading:'loading',waiting:'alerting',notifying:'notifying',success:'happy',failure:'sad'}
|
|
|
const runtimeActive = computed(() => props.state !== 'idle' && !beatEnded.value)
|
|
|
const imageActivity = computed(() => hasImage.value && runtimeActive.value && ['working', 'loading'].includes(props.state))
|
|
|
-const state = computed<CursorState>(() => runtimeActive.value ? states[props.state] : oneShot.value ?? identity.value.expression)
|
|
|
-const live = computed(() => visible.value && foreground.value && !reduced.value && props.animated !== false && (props.animated === true || props.size >= 56 || runtimeActive.value || !!oneShot.value || hover.value))
|
|
|
-const options = computed(() => ({ state: state.value, paused: !live.value, color: identity.value.color, gaze: gaze.value,
|
|
|
+const state = computed<CursorState>(() => runtimeActive.value ? states[props.state] : oneShot.value ?? identity.value?.expression ?? 'idle')
|
|
|
+const live = computed(() => hasVisual.value && visible.value && foreground.value && !reduced.value && props.animated !== false && (props.animated === true || props.size >= 56 || runtimeActive.value || !!oneShot.value || hover.value))
|
|
|
+const options = computed(() => identity.value && hasVisual.value ? ({ state: state.value, paused: !live.value, color: identity.value.color, gaze: gaze.value,
|
|
|
silhouette: hasImage.value ? MASCOT_SILHOUETTES.circle : identity.value.bodyId ? LAOA_BODIES[identity.value.bodyId] : MASCOT_SILHOUETTES[identity.value.shape === 'ellipse' ? 'oval' : identity.value.shape], fixedTime: props.fixedTime, expression: props.expressionIndex,
|
|
|
- effectsOnly: hasImage.value, ribbons: runtimeActive.value || state.value !== 'working' }))
|
|
|
+ effectsOnly: hasImage.value, ribbons: runtimeActive.value || state.value !== 'working' }) : undefined)
|
|
|
function sync() {
|
|
|
- if (!svg.value || (hasImage.value && !imageActivity.value)) { renderer?.dispose();renderer=undefined;return }
|
|
|
+ if (!svg.value || !options.value || (hasImage.value && !imageActivity.value)) { renderer?.dispose();renderer=undefined;return }
|
|
|
if (!renderer) renderer=mountCursorAvatar(svg.value, options.value)
|
|
|
else renderer.update(options.value)
|
|
|
}
|
|
|
@@ -54,11 +57,13 @@ onBeforeUnmount(()=>{renderer?.dispose();observer?.disconnect();clearTimeout(bea
|
|
|
</script>
|
|
|
<template>
|
|
|
<span ref="host" class="agent-avatar" :class="[`agent-avatar--${props.state}`, {'agent-avatar--image':hasImage, 'agent-avatar--image-active': imageActivity && live}]" :style="{width:`${size}px`,height:`${size}px`}" role="img" :aria-label="`${name} 的头像`" @pointermove="pointer" @pointerleave="leave" :data-animated="live && (!hasImage || imageActivity)" :data-expression="state">
|
|
|
- <img @error="imageFailed = true" v-if="hasImage" :src="identity.imageDataURL" alt="" :style="{borderRadius:radius}" />
|
|
|
- <svg v-show="!hasImage || imageActivity" ref="svg" aria-hidden="true" />
|
|
|
+ <span v-if="!hasVisual" class="agent-avatar__placeholder" aria-hidden="true" />
|
|
|
+ <img @error="imageFailed = true" v-if="hasImage" :src="identity?.imageDataURL" alt="" :style="{borderRadius:radius}" />
|
|
|
+ <svg v-if="hasMascot || imageActivity" ref="svg" aria-hidden="true" />
|
|
|
</span>
|
|
|
</template>
|
|
|
<style scoped>
|
|
|
.agent-avatar{display:block;position:relative;flex:0 0 auto;overflow:visible;line-height:1;background:transparent}.agent-avatar img,.agent-avatar svg{display:block;width:100%;height:100%;object-fit:cover}
|
|
|
+.agent-avatar__placeholder{display:block;width:100%;height:100%;border-radius:50%;background:var(--surface-soft);box-shadow:inset 0 0 0 1px var(--line)}
|
|
|
.agent-avatar svg{overflow:visible;pointer-events:none}.agent-avatar--image svg{position:absolute;inset:0}.agent-avatar--image-active img{transform:scale(.72)}
|
|
|
</style>
|