diff --git a/src/content/TextAnimations/RotatingText/RotatingText.vue b/src/content/TextAnimations/RotatingText/RotatingText.vue index ff00e25..bc71ef0 100644 --- a/src/content/TextAnimations/RotatingText/RotatingText.vue +++ b/src/content/TextAnimations/RotatingText/RotatingText.vue @@ -2,38 +2,47 @@ import { AnimatePresence, Motion, type Target, type Transition, type VariantLabels } from 'motion-v'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; -function cn(...classes: (string | undefined | null | boolean)[]): string { - return classes.filter(Boolean).join(' '); +type StaggerFrom = 'first' | 'last' | 'center' | 'random' | number; +type SplitBy = 'characters' | 'words' | 'lines'; + +interface WordElement { + characters: string[]; + needsSpace: boolean; } interface RotatingTextProps { texts: string[]; transition?: Transition; initial?: boolean | Target | VariantLabels; - animate?: any; + animate?: Target | VariantLabels; exit?: Target | VariantLabels; animatePresenceMode?: 'sync' | 'wait'; animatePresenceInitial?: boolean; rotationInterval?: number; staggerDuration?: number; - staggerFrom?: 'first' | 'last' | 'center' | 'random' | number; + staggerFrom?: StaggerFrom; loop?: boolean; auto?: boolean; - splitBy?: string; + splitBy?: SplitBy; onNext?: (index: number) => void; mainClassName?: string; splitLevelClassName?: string; elementLevelClassName?: string; } +const cn = (...classes: (string | undefined | null | boolean)[]): string => { + return classes.filter(Boolean).join(' '); +}; + const props = withDefaults(defineProps(), { - transition: () => ({ - type: 'spring', - damping: 25, - stiffness: 300 - }), + transition: () => + ({ + type: 'spring', + damping: 25, + stiffness: 300 + }) as Transition, initial: () => ({ y: '100%', opacity: 0 }) as Target, - animate: () => ({ y: 0, opacity: 1 }), + animate: () => ({ y: 0, opacity: 1 }) as Target, exit: () => ({ y: '-120%', opacity: 0 }) as Target, animatePresenceMode: 'wait', animatePresenceInitial: false, @@ -45,105 +54,128 @@ const props = withDefaults(defineProps(), { splitBy: 'characters' }); -const currentTextIndex = ref(0); -let intervalId: number | null = null; +const currentTextIndex = ref(0); +let intervalId: ReturnType | null = null; const splitIntoCharacters = (text: string): string[] => { - if (typeof Intl !== 'undefined' && Intl.Segmenter) { - const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); - return Array.from(segmenter.segment(text), segment => segment.segment); + if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const segmenter = new (Intl as any).Segmenter('en', { granularity: 'grapheme' }); + return [...segmenter.segment(text)].map(({ segment }) => segment); } - return Array.from(text); + + return [...text]; }; -const elements = computed(() => { - const currentText: string = props.texts[currentTextIndex.value]; +const elements = computed((): WordElement[] => { + const currentText = props.texts[currentTextIndex.value]; - if (props.splitBy === 'characters') { - const words = currentText.split(' '); - return words.map((word, i) => ({ - characters: splitIntoCharacters(word), - needsSpace: i !== words.length - 1 - })); + switch (props.splitBy) { + case 'characters': { + const words = currentText.split(' '); + return words.map((word, i) => ({ + characters: splitIntoCharacters(word), + needsSpace: i !== words.length - 1 + })); + } + case 'words': { + const words = currentText.split(' '); + return words.map((word, i) => ({ + characters: [word], + needsSpace: i !== words.length - 1 + })); + } + case 'lines': { + const lines = currentText.split('\n'); + return lines.map((line, i) => ({ + characters: [line], + needsSpace: i !== lines.length - 1 + })); + } + default: { + const parts = currentText.split(props.splitBy!); + return parts.map((part, i) => ({ + characters: [part], + needsSpace: i !== parts.length - 1 + })); + } } - if (props.splitBy === 'words') { - return currentText.split(' ').map((word, i, arr) => ({ - characters: [word], - needsSpace: i !== arr.length - 1 - })); - } - if (props.splitBy === 'lines') { - return currentText.split('\n').map((line, i, arr) => ({ - characters: [line], - needsSpace: i !== arr.length - 1 - })); - } - - return currentText.split(props.splitBy).map((part, i, arr) => ({ - characters: [part], - needsSpace: i !== arr.length - 1 - })); }); const getStaggerDelay = (index: number, totalChars: number): number => { - const total = totalChars; + const { staggerDuration, staggerFrom } = props; - if (props.staggerFrom === 'first') return index * props.staggerDuration; - if (props.staggerFrom === 'last') return (total - 1 - index) * props.staggerDuration; - if (props.staggerFrom === 'center') { - const center = Math.floor(total / 2); - return Math.abs(center - index) * props.staggerDuration; + switch (staggerFrom) { + case 'first': + return index * staggerDuration; + case 'last': + return (totalChars - 1 - index) * staggerDuration; + case 'center': { + const center = Math.floor(totalChars / 2); + return Math.abs(center - index) * staggerDuration; + } + case 'random': { + const randomIndex = Math.floor(Math.random() * totalChars); + return Math.abs(randomIndex - index) * staggerDuration; + } + default: + return Math.abs((staggerFrom as number) - index) * staggerDuration; } - if (props.staggerFrom === 'random') { - const randomIndex = Math.floor(Math.random() * total); - return Math.abs(randomIndex - index) * props.staggerDuration; - } - - return Math.abs((props.staggerFrom as number) - index) * props.staggerDuration; }; -const handleIndexChange = (newIndex: number) => { +const handleIndexChange = (newIndex: number): void => { currentTextIndex.value = newIndex; - if (props.onNext) props.onNext(newIndex); + props.onNext?.(newIndex); }; -const next = () => { - const nextIndex = - currentTextIndex.value === props.texts.length - 1 - ? props.loop - ? 0 - : currentTextIndex.value - : currentTextIndex.value + 1; +const next = (): void => { + const isAtEnd = currentTextIndex.value === props.texts.length - 1; + const nextIndex = isAtEnd ? (props.loop ? 0 : currentTextIndex.value) : currentTextIndex.value + 1; + if (nextIndex !== currentTextIndex.value) { handleIndexChange(nextIndex); } }; -const previous = () => { - const prevIndex = - currentTextIndex.value === 0 - ? props.loop - ? props.texts.length - 1 - : currentTextIndex.value - : currentTextIndex.value - 1; +const previous = (): void => { + const isAtStart = currentTextIndex.value === 0; + const prevIndex = isAtStart + ? props.loop + ? props.texts.length - 1 + : currentTextIndex.value + : currentTextIndex.value - 1; + if (prevIndex !== currentTextIndex.value) { handleIndexChange(prevIndex); } }; -const jumpTo = (index: number) => { +const jumpTo = (index: number): void => { const validIndex = Math.max(0, Math.min(index, props.texts.length - 1)); if (validIndex !== currentTextIndex.value) { handleIndexChange(validIndex); } }; -const reset = () => { +const reset = (): void => { if (currentTextIndex.value !== 0) { handleIndexChange(0); } }; +const cleanupInterval = (): void => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } +}; + +const startInterval = (): void => { + if (props.auto) { + intervalId = setInterval(next, props.rotationInterval); + } +}; + defineExpose({ next, previous, @@ -152,30 +184,20 @@ defineExpose({ }); watch( - () => [props.auto, props.rotationInterval], + () => [props.auto, props.rotationInterval] as const, () => { - if (intervalId) { - clearInterval(intervalId); - intervalId = null; - } - - if (props.auto) { - intervalId = setInterval(next, props.rotationInterval); - } + cleanupInterval(); + startInterval(); }, { immediate: true } ); onMounted(() => { - if (props.auto) { - intervalId = setInterval(next, props.rotationInterval); - } + startInterval(); }); onUnmounted(() => { - if (intervalId) { - clearInterval(intervalId); - } + cleanupInterval(); }); @@ -201,9 +223,9 @@ onUnmounted(() => { > { > {{ char }} - + diff --git a/src/demo/TextAnimations/RotatingTextDemo.vue b/src/demo/TextAnimations/RotatingTextDemo.vue index 5c0ff2b..26efea5 100644 --- a/src/demo/TextAnimations/RotatingTextDemo.vue +++ b/src/demo/TextAnimations/RotatingTextDemo.vue @@ -17,7 +17,7 @@