179 lines
5.0 KiB
Vue
179 lines
5.0 KiB
Vue
<script setup lang="ts">
|
|
import { animate } from 'animejs'
|
|
import { useData } from 'vitepress'
|
|
import { ref, watchEffect } from 'vue'
|
|
|
|
import CharacterShowcase from './CharacterShowcase.vue'
|
|
|
|
interface Character {
|
|
value: string
|
|
variant?: InstanceType<typeof CharacterShowcase>['variant']
|
|
}
|
|
|
|
function interpolate(characters: string[], initial?: Character[]) {
|
|
return characters.reduce<Character[][]>((chars, c) => {
|
|
return [
|
|
...chars,
|
|
[
|
|
...chars.length > 0 ? chars.at(-1)! : [],
|
|
{ value: c, variant: 'dotted' },
|
|
],
|
|
]
|
|
}, initial ? [initial] : [])
|
|
}
|
|
|
|
const STATES: Character[][] = [
|
|
...interpolate([...'💆🏼♀️'].splice(0, 2)),
|
|
...interpolate([...'💆🏼♀️'].splice(2), [{ value: '💆🏼', variant: 'default' }]),
|
|
...interpolate([...'👩🏻💻'].splice(0, 2), [{ value: '💆🏼♀️', variant: 'default' }]),
|
|
...interpolate([...'👩🏻💻'].splice(2), [{ value: '💆🏼♀️', variant: 'active' }, { value: '👩🏻', variant: 'default' }]),
|
|
[{ value: '💆🏼♀️', variant: 'active' }, { value: '👩🏻💻', variant: 'active' }],
|
|
]
|
|
|
|
const { lang } = useData()
|
|
|
|
const stateIndex = ref(0)
|
|
const isPlaying = ref(true)
|
|
const animationHandle = ref<number>()
|
|
|
|
function enterAnimator(e: Element, done: () => void) {
|
|
return animate(e, {
|
|
opacity: [0, 1],
|
|
scale: [0.5, 1],
|
|
ease: 'outQuad',
|
|
duration: 200,
|
|
onComplete: done,
|
|
})
|
|
}
|
|
|
|
function leaveAnimator(e: Element, done: () => void) {
|
|
return animate(e, {
|
|
opacity: [1, 0],
|
|
scale: [1, 0.5],
|
|
ease: 'outQuad',
|
|
duration: 200,
|
|
onComplete: done,
|
|
})
|
|
}
|
|
|
|
watchEffect(() => {
|
|
if (!import.meta.env.SSR) {
|
|
if (isPlaying.value) {
|
|
animationHandle.value = window.setInterval(() => {
|
|
stateIndex.value = (stateIndex.value + 1) % STATES.length
|
|
}, 1000)
|
|
}
|
|
else {
|
|
if (animationHandle.value) {
|
|
window.clearInterval(animationHandle.value)
|
|
animationHandle.value = undefined
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
function stepForward() {
|
|
isPlaying.value = false
|
|
stateIndex.value = (stateIndex.value + 1) % STATES.length
|
|
}
|
|
|
|
function stepBack() {
|
|
isPlaying.value = false
|
|
stateIndex.value = (stateIndex.value - 1 + STATES.length) % STATES.length
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div flex="~ col items-center justify-start gap-1" bg="primary/5" min-h-80 w-full rounded-lg p-2>
|
|
<div flex="~ row items-stretch gap-2 grow" bg="primary/5" w-full rounded-lg p-2>
|
|
<div flex="~ col items-center justify-start gap-1" py-2>
|
|
<div
|
|
flex="~ row items-center"
|
|
rounded-lg p="2"
|
|
bg="hover:primary/10"
|
|
transition="~ all duration-150 ease-out"
|
|
cursor="pointer"
|
|
@click="isPlaying = !isPlaying"
|
|
>
|
|
<div v-if="!isPlaying" i-lucide:play cursor="pointer" />
|
|
<div v-else i-lucide:pause cursor="pointer" />
|
|
</div>
|
|
|
|
<div
|
|
flex="~ row items-center"
|
|
rounded-lg p="2"
|
|
bg="hover:primary/10"
|
|
transition="~ all duration-150 ease-out"
|
|
cursor="pointer"
|
|
@click="stepForward"
|
|
>
|
|
<div i-lucide:step-forward />
|
|
</div>
|
|
|
|
<div
|
|
flex="~ row items-center"
|
|
rounded-lg p="2"
|
|
bg="hover:primary/10"
|
|
transition="~ all duration-150 ease-out"
|
|
cursor="pointer"
|
|
@click="stepBack"
|
|
>
|
|
<div i-lucide:step-back />
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
flex="~ row items-start gap-1 grow"
|
|
transition="~ all duration-150 ease-out"
|
|
overflow="x-scroll"
|
|
bg="primary/5" w-full rounded-lg p-2
|
|
>
|
|
<TransitionGroup
|
|
:css="false"
|
|
@enter="enterAnimator"
|
|
@leave="leaveAnimator"
|
|
>
|
|
<CharacterShowcase
|
|
v-for="(c, i) in STATES[stateIndex]"
|
|
:key="i"
|
|
:value="c.value"
|
|
:variant="c.variant"
|
|
code-point
|
|
/>
|
|
</TransitionGroup>
|
|
</div>
|
|
</div>
|
|
|
|
<div flex="~ row items-center justify-center gap-1 wrap" py-2 text-xs>
|
|
<div font-semibold w="full md:auto" text="center md:unset">
|
|
{{ lang === 'zh-Hans' ? '图例' : 'Legend' }}
|
|
</div>
|
|
<div
|
|
b="~ 2 dotted primary/20"
|
|
rounded-lg px-2
|
|
flex="~ items-center justify-center shrink-0"
|
|
transition="~ all duration-150 ease-out"
|
|
>
|
|
{{ lang === 'zh-Hans' ? '字符' : 'Character' }}
|
|
</div>
|
|
<div
|
|
b="~ 2 dashed primary/20"
|
|
rounded-lg px-2
|
|
flex="~ items-center justify-center shrink-0"
|
|
transition="~ all duration-150 ease-out"
|
|
>
|
|
{{ lang === 'zh-Hans' ? '不完整字素簇' : 'Incomplete cluster' }}
|
|
</div>
|
|
<div
|
|
b="~ 2 solid primary/50"
|
|
bg="primary/10"
|
|
rounded-lg px-2
|
|
flex="~ items-center justify-center shrink-0"
|
|
transition="~ all duration-150 ease-out"
|
|
>
|
|
{{ lang === 'zh-Hans' ? '完整字素簇' : 'Complete cluster' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|