Files
grok-icon-study/src/components/Hero.vue
T

111 lines
2.8 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { mountReplica, type ReplicaMount } from '@/replica/host'
const props = withDefaults(
defineProps<{
size?: number
shape?: string
color?: string
state?: string
follow?: boolean
paper?: string
}>(),
{
size: 440,
shape: 'blob',
color: 'black',
state: 'idle',
follow: false,
paper: '#f9f9f9'
}
)
const emit = defineEmits<{ ready: [ok: boolean] }>()
const svg = ref<SVGSVGElement | null>(null)
const live = ref(false)
const tried = ref(false)
let replica: ReplicaMount | null = null
let alive = true
function input() {
return {
state: props.state,
shape: props.shape,
color: props.color,
follow: props.follow,
paper: props.paper,
size: props.size
}
}
async function attach() {
const node = svg.value
if (!node) return
const mount = await mountReplica(node, input(), true)
if (!alive) {
mount?.destroy()
return
}
replica = mount
live.value = !!replica
tried.value = true
replica?.apply(input())
emit('ready', live.value)
}
onMounted(() => {
void attach()
})
onBeforeUnmount(() => {
alive = false
replica?.destroy()
replica = null
})
watch(
() => [props.state, props.shape, props.color, props.follow, props.paper, props.size],
() => replica?.apply(input())
)
defineExpose({
spin: () => replica?.spin(),
orbitGaze: (ms?: number) => replica?.orbitGaze(ms),
svg: () => svg.value,
setPose: (pose: { turn?: number; tilt?: number; roll?: number }) => replica?.setPose(pose),
setManualOffset: (offset: { tx?: number; ty?: number; spin?: number }) =>
replica?.setManualOffset(offset),
setEyeTune: (tune: unknown) => replica?.setEyeTune(tune),
setManualHold: (on: boolean) => replica?.setManualHold(on),
flushPerformance: () => replica?.flushPerformance(),
seekEye: (index: number, opts?: { snap?: boolean }) => replica?.seekEye(index, opts),
setPlaylistHold: (on: boolean) => replica?.setPlaylistHold(on),
setPaused: (on: boolean | 'hold-pose') => replica?.setPaused(on),
holdFrame: (at?: number) => replica?.holdFrame(at),
freezeNow: (opts?: { settle?: boolean }) => replica?.freezeNow(opts) ?? null,
playback: () => replica?.playback() ?? null,
parts: () => replica?.parts() ?? null
})
</script>
<template>
<div class="relative inline-block aspect-square max-w-full" :style="{ width: `${props.size}px` }">
<svg
ref="svg"
class="absolute inset-0 h-full w-full overflow-visible"
:width="props.size"
:height="props.size"
role="img"
:aria-label="t('app.botAria')"
/>
<p
v-if="tried && !live"
class="absolute inset-6 flex items-center justify-center text-center text-xs text-[var(--muted)]"
>
{{ t('app.missingGeo') }}
</p>
</div>
</template>