[ REFACT ] : GradientText Text Animation

This commit is contained in:
Utkarsh-Singhal-26
2026-02-01 17:24:20 +05:30
parent dbcaa6f488
commit 80c42e7af4
3 changed files with 244 additions and 120 deletions

View File

@@ -1,75 +1,140 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';
import { computed, ref, useSlots } from 'vue';
interface GradientTextProps {
text: string;
className?: string;
colors?: string[];
animationSpeed?: number;
showBorder?: boolean;
direction?: 'horizontal' | 'vertical' | 'diagonal';
pauseOnHover?: boolean;
yoyo?: boolean;
}
const props = withDefaults(defineProps<GradientTextProps>(), {
text: '',
className: '',
colors: () => ['#ffaa40', '#9c40ff', '#ffaa40'],
colors: () => ['#27FF64', '#27FF64', '#A0FFBC'],
animationSpeed: 8,
showBorder: false
showBorder: false,
direction: 'horizontal',
pauseOnHover: false,
yoyo: true
});
const slots = useSlots();
const text = computed(() => (slots.default?.() ?? []).map(v => v.children).join(''));
const isPaused = ref(false);
const progress = useMotionValue(0);
const elapsedRef = ref(0);
const lastTimeRef = ref<number | null>(null);
const animationDuration = props.animationSpeed * 1000;
useAnimationFrame(time => {
if (isPaused.value) {
lastTimeRef.value = null;
return;
}
if (lastTimeRef.value === null) {
lastTimeRef.value = time;
return;
}
const deltaTime = time - lastTimeRef.value;
lastTimeRef.value = time;
elapsedRef.value += deltaTime;
if (props.yoyo) {
const fullCycle = animationDuration * 2;
const cycleTime = elapsedRef.value % fullCycle;
if (cycleTime < animationDuration) {
progress.set((cycleTime / animationDuration) * 100);
} else {
progress.set(100 - ((cycleTime - animationDuration) / animationDuration) * 100);
}
} else {
// Continuously increase position for seamless looping
progress.set((elapsedRef.value / animationDuration) * 100);
}
});
const backgroundPosition = useTransform(progress, p => {
if (props.direction === 'horizontal') {
return `${p}% 50%`;
} else if (props.direction === 'vertical') {
return `50% ${p}%`;
} else {
// For diagonal, move only horizontally to avoid interference patterns
return `${p}% 50%`;
}
});
const handleMouseEnter = () => {
if (props.pauseOnHover) isPaused.value = true;
};
const handleMouseLeave = () => {
if (props.pauseOnHover) isPaused.value = false;
};
const gradientAngle = computed(() =>
props.direction === 'horizontal' ? 'to right' : props.direction === 'vertical' ? 'to bottom' : 'to bottom right'
);
// Duplicate first color at the end for seamless looping
const gradientColors = computed(() => [...props.colors, props.colors[0]].join(', '));
const gradientStyle = computed(() => ({
backgroundImage: `linear-gradient(to right, ${props.colors.join(', ')})`,
animationDuration: `${props.animationSpeed}s`,
backgroundSize: '300% 100%',
'--animation-duration': `${props.animationSpeed}s`
}));
const borderStyle = computed(() => ({
...gradientStyle.value
}));
const textStyle = computed(() => ({
...gradientStyle.value,
backgroundClip: 'text',
WebkitBackgroundClip: 'text'
backgroundImage: `linear-gradient(${gradientAngle.value}, ${gradientColors.value})`,
backgroundSize:
props.direction === 'horizontal' ? '300% 100%' : props.direction === 'vertical' ? '100% 300%' : '300% 300%',
backgroundRepeat: 'repeat'
}));
</script>
<template>
<div
:class="`relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer ${className}`"
<Motion
tag="div"
:class="[
'relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 overflow-hidden cursor-pointer',
className,
showBorder && 'py-1 px-2'
]"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div
<Motion
tag="div"
v-if="showBorder"
class="absolute inset-0 bg-cover z-0 pointer-events-none animate-gradient"
:style="borderStyle"
class="z-0 absolute inset-0 rounded-[1.25rem] pointer-events-none"
:style="{ ...gradientStyle, backgroundPosition }"
>
<div
class="absolute inset-0 bg-black rounded-[1.25rem] z-[-1]"
style="width: calc(100% - 2px); height: calc(100% - 2px); left: 50%; top: 50%; transform: translate(-50%, -50%)"
class="z-[-1] absolute bg-black rounded-[1.25rem]"
:style="{
width: 'calc(100% - 2px)',
height: 'calc(100% - 2px)',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}"
/>
</div>
</Motion>
<div class="inline-block relative z-2 text-transparent bg-cover animate-gradient" :style="textStyle">
<Motion
tag="div"
class="inline-block z-2 relative bg-clip-text text-transparent"
:style="{
...gradientStyle,
backgroundPosition,
WebkitBackgroundClip: 'text'
}"
>
{{ text }}
</div>
</div>
</Motion>
</Motion>
</template>
<style scoped>
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animate-gradient {
animation: gradient var(--animation-duration, 8s) linear infinite;
}
</style>