feat(stage-ui): introduce poppin'text
This commit is contained in:
@@ -63,6 +63,7 @@ words:
|
||||
- demodel
|
||||
- devlogs
|
||||
- directml
|
||||
- dokidoki
|
||||
- dotenvx
|
||||
- DownloadLive2DSDK
|
||||
- dtolnay
|
||||
@@ -100,6 +101,7 @@ words:
|
||||
- Kawaii
|
||||
- kebabcase
|
||||
- Keyyable
|
||||
- kirakira
|
||||
- kwaa
|
||||
- lemonnekogh
|
||||
- libsamplerate
|
||||
@@ -159,6 +161,7 @@ words:
|
||||
- pixi
|
||||
- pixiv
|
||||
- popmotion
|
||||
- poppin
|
||||
- PREFLIGHTS
|
||||
- prereleased
|
||||
- pretrained
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PoppinText from './PoppinText.web.vue'
|
||||
|
||||
import { popupAnimator } from './animators'
|
||||
|
||||
const text = ref('キラキラドキドキ')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Poppin'Text"
|
||||
group="widgets"
|
||||
:layout="{ type: 'grid', width: '100%' }"
|
||||
>
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant
|
||||
id="basic-english"
|
||||
title="English Text"
|
||||
>
|
||||
<div h-50 w-50 p-4>
|
||||
<PoppinText :text="text" :animator="popupAnimator" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- Poppin'Text - Makes your text "kirakira dokidoki"!! -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Animator } from './animators'
|
||||
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
animator?: Animator
|
||||
}>()
|
||||
|
||||
const graphemeClusters = computed(() => [...props.text]) // Safely split text into grapheme clusters with UTF-8 awareness
|
||||
const elements = ref<HTMLElement[]>([])
|
||||
|
||||
const animatorCleanupFn = shallowRef<() => void>()
|
||||
|
||||
onMounted(() => {
|
||||
animatorCleanupFn.value = props.animator?.(elements.value)
|
||||
})
|
||||
|
||||
watch([graphemeClusters, () => props.animator], ([_, animator]) => {
|
||||
animatorCleanupFn.value?.()
|
||||
animatorCleanupFn.value = animator?.(elements.value)
|
||||
}, { flush: 'post' }) // <- Ensure post-update refs
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-hidden">
|
||||
<span
|
||||
v-for="(cluster, index) in graphemeClusters"
|
||||
:key="index"
|
||||
ref="elements"
|
||||
class="inline-block"
|
||||
>
|
||||
{{ cluster }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export type Animator = (elements: HTMLElement[]) => (() => void)
|
||||
|
||||
export * from './popup'
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Animator } from '.'
|
||||
|
||||
import { createTimeline } from 'animejs'
|
||||
|
||||
export const popupAnimator: Animator = (elements: HTMLElement[]) => {
|
||||
const timeline = createTimeline({ loop: true })
|
||||
.add(elements, {
|
||||
translateY: ['1.1em', 0],
|
||||
translateZ: 0,
|
||||
duration: 750,
|
||||
delay: (el, i) => 50 * i,
|
||||
})
|
||||
|
||||
return () => {
|
||||
timeline.remove(elements)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './Dialogs'
|
||||
export { default as PoppingSubtitles } from './PoppingSubtitles.web.vue'
|
||||
export { default as PoppinText } from './PoppinText/PoppinText.web.vue'
|
||||
export * from './Toasters'
|
||||
|
||||
Reference in New Issue
Block a user