[ADDED: TEXT TYPE ANIMATION]

This commit is contained in:
Purshottam Jain
2025-07-21 12:56:16 +05:30
parent 6f7b18429b
commit d3187f71c7
5 changed files with 417 additions and 2 deletions

View File

@@ -0,0 +1,171 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, computed, useTemplateRef } from 'vue';
import { gsap } from 'gsap';
interface TextTypeProps {
className?: string;
showCursor?: boolean;
hideCursorWhileTyping?: boolean;
cursorCharacter?: string;
cursorBlinkDuration?: number;
cursorClassName?: string;
text: string | string[];
as?: string;
typingSpeed?: number;
initialDelay?: number;
pauseDuration?: number;
deletingSpeed?: number;
loop?: boolean;
textColors?: string[];
variableSpeed?: { min: number; max: number };
onSentenceComplete?: (sentence: string, index: number) => void;
startOnVisible?: boolean;
reverseMode?: boolean;
}
const props = withDefaults(defineProps<TextTypeProps>(), {
as: 'div',
typingSpeed: 50,
initialDelay: 0,
pauseDuration: 2000,
deletingSpeed: 30,
loop: true,
className: '',
showCursor: true,
hideCursorWhileTyping: false,
cursorCharacter: '|',
cursorBlinkDuration: 0.5,
textColors: () => [],
startOnVisible: false,
reverseMode: false
});
const displayedText = ref('');
const currentCharIndex = ref(0);
const isDeleting = ref(false);
const currentTextIndex = ref(0);
const isVisible = ref(!props.startOnVisible);
const cursorRef = useTemplateRef('cursorRef');
const containerRef = useTemplateRef('containerRef');
const textArray = computed(() => (Array.isArray(props.text) ? props.text : [props.text]));
const getRandomSpeed = () => {
if (!props.variableSpeed) return props.typingSpeed;
const { min, max } = props.variableSpeed;
return Math.random() * (max - min) + min;
};
const getCurrentTextColor = () => {
if (!props.textColors.length) return '#ffffff';
return props.textColors[currentTextIndex.value % props.textColors.length];
};
let timeout: ReturnType<typeof setTimeout> | null = null;
const clearTimeoutIfNeeded = () => {
if (timeout) clearTimeout(timeout);
};
const executeTypingAnimation = () => {
const currentText = textArray.value[currentTextIndex.value];
const processedText = props.reverseMode ? currentText.split('').reverse().join('') : currentText;
if (isDeleting.value) {
if (displayedText.value === '') {
isDeleting.value = false;
if (currentTextIndex.value === textArray.value.length - 1 && !props.loop) return;
props.onSentenceComplete?.(textArray.value[currentTextIndex.value], currentTextIndex.value);
currentTextIndex.value = (currentTextIndex.value + 1) % textArray.value.length;
currentCharIndex.value = 0;
timeout = setTimeout(() => {}, props.pauseDuration);
} else {
timeout = setTimeout(() => {
displayedText.value = displayedText.value.slice(0, -1);
}, props.deletingSpeed);
}
} else {
if (currentCharIndex.value < processedText.length) {
timeout = setTimeout(
() => {
displayedText.value += processedText[currentCharIndex.value];
currentCharIndex.value += 1;
},
props.variableSpeed ? getRandomSpeed() : props.typingSpeed
);
} else if (textArray.value.length > 1) {
timeout = setTimeout(() => {
isDeleting.value = true;
}, props.pauseDuration);
}
}
};
watch(
[displayedText, currentCharIndex, isDeleting, isVisible],
() => {
if (!isVisible.value) return;
clearTimeoutIfNeeded();
if (currentCharIndex.value === 0 && !isDeleting.value && displayedText.value === '') {
timeout = setTimeout(() => {
executeTypingAnimation();
}, props.initialDelay);
} else {
executeTypingAnimation();
}
},
{ immediate: true }
);
onMounted(() => {
if (props.showCursor && cursorRef.value) {
gsap.set(cursorRef.value, { opacity: 1 });
gsap.to(cursorRef.value, {
opacity: 0,
duration: props.cursorBlinkDuration,
repeat: -1,
yoyo: true,
ease: 'power2.inOut'
});
}
if (props.startOnVisible && containerRef.value) {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) isVisible.value = true;
});
},
{ threshold: 0.1 }
);
if (containerRef.value instanceof Element) {
observer.observe(containerRef.value);
}
onBeforeUnmount(() => observer.disconnect());
}
});
onBeforeUnmount(() => {
clearTimeoutIfNeeded();
});
</script>
<template>
<component :is="as" ref="containerRef" :class="`inline-block whitespace-pre-wrap tracking-tight ${className}`" v-bind="$attrs">
<span class="inline" :style="{ color: getCurrentTextColor() }">
{{ displayedText }}
</span>
<span
v-if="showCursor"
ref="cursorRef"
:class="`ml-1 inline-block opacity-100 ${
hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting) ? 'hidden' : ''
} ${cursorClassName}`"
>
{{ cursorCharacter }}
</span>
</component>
</template>