mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Merge pull request #50 from purshottam-jain24/main
[ADDED: TEXT TYPE ANIMATION]
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy'];
|
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type'];
|
||||||
export const UPDATED = [];
|
export const UPDATED = [];
|
||||||
|
|
||||||
// Used for main sidebar navigation
|
// Used for main sidebar navigation
|
||||||
@@ -26,7 +26,8 @@ export const CATEGORIES = [
|
|||||||
'Scroll Reveal',
|
'Scroll Reveal',
|
||||||
'Rotating Text',
|
'Rotating Text',
|
||||||
'Glitch Text',
|
'Glitch Text',
|
||||||
'Scroll Velocity'
|
'Scroll Velocity',
|
||||||
|
'Text Type'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const textAnimations = {
|
|||||||
'rotating-text': () => import("../demo/TextAnimations/RotatingTextDemo.vue"),
|
'rotating-text': () => import("../demo/TextAnimations/RotatingTextDemo.vue"),
|
||||||
'glitch-text': () => import("../demo/TextAnimations/GlitchTextDemo.vue"),
|
'glitch-text': () => import("../demo/TextAnimations/GlitchTextDemo.vue"),
|
||||||
'scroll-velocity': () => import("../demo/TextAnimations/ScrollVelocityDemo.vue"),
|
'scroll-velocity': () => import("../demo/TextAnimations/ScrollVelocityDemo.vue"),
|
||||||
|
'text-type': () => import("../demo/TextAnimations/TextTypeDemo.vue"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
|||||||
19
src/constants/code/TextAnimations/textTypeCode.ts
Normal file
19
src/constants/code/TextAnimations/textTypeCode.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import code from '@/content/TextAnimations/TextType/TextType.vue?raw';
|
||||||
|
import { createCodeObject } from '@/types/code';
|
||||||
|
|
||||||
|
export const textType = createCodeObject(code, 'TextAnimations/TextType', {
|
||||||
|
installation: `npm install gsap`,
|
||||||
|
usage: `<template>
|
||||||
|
<TextType
|
||||||
|
:text="['Text typing effect', 'for your websites', 'Happy coding!']"
|
||||||
|
:typingSpeed="75"
|
||||||
|
:pauseDuration="1500"
|
||||||
|
:showCursor="true"
|
||||||
|
cursorCharacter="|"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TextType from "./TextType.vue";
|
||||||
|
</script>`
|
||||||
|
});
|
||||||
171
src/content/TextAnimations/TextType/TextType.vue
Normal file
171
src/content/TextAnimations/TextType/TextType.vue
Normal 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>
|
||||||
228
src/demo/TextAnimations/TextTypeDemo.vue
Normal file
228
src/demo/TextAnimations/TextTypeDemo.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container">
|
||||||
|
<TextType
|
||||||
|
:key="key"
|
||||||
|
:text="texts"
|
||||||
|
:typingSpeed="typingSpeed"
|
||||||
|
:pauseDuration="pauseDuration"
|
||||||
|
:deletingSpeed="deletingSpeed"
|
||||||
|
:showCursor="showCursor"
|
||||||
|
:cursorCharacter="cursorCharacter"
|
||||||
|
:cursorBlinkDuration="cursorBlinkDuration"
|
||||||
|
:variableSpeed="variableSpeedEnabled ? { min: variableSpeedMin, max: variableSpeedMax } : undefined"
|
||||||
|
className="custom-text-type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSelect v-model="cursorCharacter" :options="['|', '_', '█', '▌', '▐']" title="Cursor Character" />
|
||||||
|
<PreviewSlider v-model="typingSpeed" title="Typing Speed" :min="10" :max="200" :step="5" value-unit="ms" />
|
||||||
|
<PreviewSlider
|
||||||
|
v-model="pauseDuration"
|
||||||
|
title="Pause Duration"
|
||||||
|
:min="500"
|
||||||
|
:max="5000"
|
||||||
|
:step="100"
|
||||||
|
value-unit="ms"
|
||||||
|
/>
|
||||||
|
<PreviewSlider v-model="deletingSpeed" title="Deleting Speed" :min="10" :max="100" :step="5" value-unit="ms" />
|
||||||
|
<PreviewSlider
|
||||||
|
v-model="cursorBlinkDuration"
|
||||||
|
title="Cursor Blink Duration"
|
||||||
|
:min="0.1"
|
||||||
|
:max="2"
|
||||||
|
:step="0.1"
|
||||||
|
value-unit="s"
|
||||||
|
/>
|
||||||
|
<PreviewSwitch v-model="showCursor" title="Show Cursor" />
|
||||||
|
<PreviewSwitch v-model="variableSpeedEnabled" title="Variable Speed" />
|
||||||
|
<PreviewSlider
|
||||||
|
v-model="variableSpeedMin"
|
||||||
|
title="Variable Speed Min"
|
||||||
|
:min="10"
|
||||||
|
:max="150"
|
||||||
|
:step="5"
|
||||||
|
value-unit="ms"
|
||||||
|
/>
|
||||||
|
<PreviewSlider
|
||||||
|
v-model="variableSpeedMax"
|
||||||
|
title="Variable Speed Max"
|
||||||
|
:min="50"
|
||||||
|
:max="300"
|
||||||
|
:step="5"
|
||||||
|
value-unit="ms"
|
||||||
|
/>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
|
||||||
|
<Dependencies :dependency-list="['gsap']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="textType" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="textType.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import { textType } from '../../constants/code/TextAnimations/textTypeCode';
|
||||||
|
import TextType from '../../content/TextAnimations/TextType/TextType.vue';
|
||||||
|
|
||||||
|
const texts = ref(["Welcome to Vue Bits! It's great to have you here!", 'Build some amazing experiences!']);
|
||||||
|
const typingSpeed = ref(75);
|
||||||
|
const pauseDuration = ref(1500);
|
||||||
|
const deletingSpeed = ref(50);
|
||||||
|
const showCursor = ref(true);
|
||||||
|
const cursorCharacter = ref('_');
|
||||||
|
const variableSpeedEnabled = ref(false);
|
||||||
|
const variableSpeedMin = ref(60);
|
||||||
|
const variableSpeedMax = ref(120);
|
||||||
|
const cursorBlinkDuration = ref(0.5);
|
||||||
|
|
||||||
|
const { rerenderKey: key } = useForceRerender();
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'string | string[]',
|
||||||
|
default: '-',
|
||||||
|
description: 'Text or array of texts to type out'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'as',
|
||||||
|
type: 'string',
|
||||||
|
default: 'div',
|
||||||
|
description: 'HTML tag to render the component as'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'typingSpeed',
|
||||||
|
type: 'number',
|
||||||
|
default: '50',
|
||||||
|
description: 'Speed of typing in milliseconds'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'initialDelay',
|
||||||
|
type: 'number',
|
||||||
|
default: '0',
|
||||||
|
description: 'Initial delay before typing starts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pauseDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '2000',
|
||||||
|
description: 'Time to wait between typing and deleting'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deletingSpeed',
|
||||||
|
type: 'number',
|
||||||
|
default: '30',
|
||||||
|
description: 'Speed of deleting characters'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'loop',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Whether to loop through texts array'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'Optional class name for styling'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showCursor',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Whether to show the cursor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hideCursorWhileTyping',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Hide cursor while typing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cursorCharacter',
|
||||||
|
type: 'string',
|
||||||
|
default: '|',
|
||||||
|
description: 'Character to use as cursor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cursorBlinkDuration',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Animation duration for cursor blinking'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cursorClassName',
|
||||||
|
type: 'string',
|
||||||
|
default: "''",
|
||||||
|
description: 'Optional class name for cursor styling'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textColors',
|
||||||
|
type: 'string[]',
|
||||||
|
default: '[]',
|
||||||
|
description: 'Array of colors for each sentence'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'variableSpeed',
|
||||||
|
type: '{min: number, max: number}',
|
||||||
|
default: 'undefined',
|
||||||
|
description: 'Random typing speed within range for human-like feel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onSentenceComplete',
|
||||||
|
type: '(sentence: string, index: number) => void',
|
||||||
|
default: 'undefined',
|
||||||
|
description: 'Callback fired after each sentence is finished'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'startOnVisible',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Start typing when component is visible in viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reverseMode',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Type backwards (right to left)'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-text-type {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 4rem);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.demo-container {
|
||||||
|
padding: 64px;
|
||||||
|
min-height: 350px;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user