mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
[ REFACT ] : GradientText Text Animation
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
|
|
||||||
import { createCodeObject } from '@/types/code';
|
import { createCodeObject } from '@/types/code';
|
||||||
|
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
|
||||||
|
|
||||||
export const gradientText = createCodeObject(code, 'TextAnimations/GradientText', {
|
export const gradientText = createCodeObject(code, 'TextAnimations/GradientText', {
|
||||||
usage: `<template>
|
installation: `npm install motion-v`,
|
||||||
|
usage: `// For a smoother animation, the gradient should start and end with the same color
|
||||||
|
<template>
|
||||||
<GradientText
|
<GradientText
|
||||||
text="Add a splash of color!"
|
:colors="['#27FF64', '#27FF64', '#A0FFBC']"
|
||||||
:colors="['#ffaa40', '#9c40ff', '#ffaa40']"
|
:animation-speed="3"
|
||||||
:animation-speed="8"
|
|
||||||
:show-border="false"
|
:show-border="false"
|
||||||
class-name="your-custom-class"
|
class-name="your-custom-class"
|
||||||
/>
|
>
|
||||||
|
Add a splash of color!
|
||||||
|
</GradientText>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,75 +1,140 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';
|
||||||
|
import { computed, ref, useSlots } from 'vue';
|
||||||
|
|
||||||
interface GradientTextProps {
|
interface GradientTextProps {
|
||||||
text: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
colors?: string[];
|
colors?: string[];
|
||||||
animationSpeed?: number;
|
animationSpeed?: number;
|
||||||
showBorder?: boolean;
|
showBorder?: boolean;
|
||||||
|
direction?: 'horizontal' | 'vertical' | 'diagonal';
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
yoyo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<GradientTextProps>(), {
|
const props = withDefaults(defineProps<GradientTextProps>(), {
|
||||||
text: '',
|
|
||||||
className: '',
|
className: '',
|
||||||
colors: () => ['#ffaa40', '#9c40ff', '#ffaa40'],
|
colors: () => ['#27FF64', '#27FF64', '#A0FFBC'],
|
||||||
animationSpeed: 8,
|
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(() => ({
|
const gradientStyle = computed(() => ({
|
||||||
backgroundImage: `linear-gradient(to right, ${props.colors.join(', ')})`,
|
backgroundImage: `linear-gradient(${gradientAngle.value}, ${gradientColors.value})`,
|
||||||
animationDuration: `${props.animationSpeed}s`,
|
backgroundSize:
|
||||||
backgroundSize: '300% 100%',
|
props.direction === 'horizontal' ? '300% 100%' : props.direction === 'vertical' ? '100% 300%' : '300% 300%',
|
||||||
'--animation-duration': `${props.animationSpeed}s`
|
backgroundRepeat: 'repeat'
|
||||||
}));
|
|
||||||
|
|
||||||
const borderStyle = computed(() => ({
|
|
||||||
...gradientStyle.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
const textStyle = computed(() => ({
|
|
||||||
...gradientStyle.value,
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text'
|
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Motion
|
||||||
: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}`"
|
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"
|
v-if="showBorder"
|
||||||
class="absolute inset-0 bg-cover z-0 pointer-events-none animate-gradient"
|
class="z-0 absolute inset-0 rounded-[1.25rem] pointer-events-none"
|
||||||
:style="borderStyle"
|
:style="{ ...gradientStyle, backgroundPosition }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black rounded-[1.25rem] z-[-1]"
|
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%)"
|
: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 }}
|
{{ text }}
|
||||||
</div>
|
</Motion>
|
||||||
</div>
|
</Motion>
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
@@ -2,69 +2,77 @@
|
|||||||
<div class="gradient-text-demo">
|
<div class="gradient-text-demo">
|
||||||
<TabbedLayout>
|
<TabbedLayout>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
<h2 class="demo-title-extra">Default</h2>
|
<div class="relative h-[400px] text-5xl demo-container">
|
||||||
|
<GradientText
|
||||||
<div class="demo-container h-[200px]">
|
:colors="colors"
|
||||||
<div class="text-[2rem]">
|
:animation-speed="animationSpeed"
|
||||||
<GradientText
|
:direction="direction"
|
||||||
text="Add a splash of color!"
|
:pause-on-hover="pauseOnHover"
|
||||||
:colors="gradientPreview"
|
:yoyo="yoyo"
|
||||||
:animation-speed="speed"
|
:show-border="showBorder"
|
||||||
:show-border="false"
|
>
|
||||||
/>
|
Gradient Magic
|
||||||
</div>
|
</GradientText>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="demo-title-extra">Border Animation</h2>
|
|
||||||
|
|
||||||
<div class="demo-container h-[200px]">
|
|
||||||
<div class="text-[2rem]">
|
|
||||||
<GradientText
|
|
||||||
text="Now with a cool border!"
|
|
||||||
:colors="gradientPreview"
|
|
||||||
:animation-speed="speed"
|
|
||||||
:show-border="true"
|
|
||||||
class-name="custom-gradient-class"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<PreviewSlider title="Loop Duration" v-model="speed" :min="1" :max="10" :step="0.5" value-unit="s" />
|
<PreviewSlider
|
||||||
|
title="Animation Speed"
|
||||||
|
v-model="animationSpeed"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
:step="0.5"
|
||||||
|
value-unit="s"
|
||||||
|
/>
|
||||||
|
<PreviewSelect title="Direction" v-model="direction" :options="directionOptions" />
|
||||||
|
<PreviewSwitch title="Yoyo Mode" v-model="yoyo" />
|
||||||
|
<PreviewSwitch title="Pause on Hover" v-model="pauseOnHover" />
|
||||||
|
<PreviewSwitch title="Show Border" v-model="showBorder" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<div class="mb-4">
|
<label class="block mb-2 font-medium text-sm">Colors</label>
|
||||||
<label class="block text-sm font-medium mb-2">Colors</label>
|
|
||||||
|
|
||||||
<input
|
<div class="flex flex-wrap gap-2 px-1 pt-1">
|
||||||
v-model="colors"
|
<div v-for="(color, index) in colors" :key="index" class="relative w-8 h-8">
|
||||||
type="text"
|
<div
|
||||||
placeholder="Enter colors separated by commas"
|
class="relative border-[#222] border-2 rounded-md w-8 h-8 overflow-hidden"
|
||||||
maxlength="100"
|
:style="{ backgroundColor: color }"
|
||||||
class="w-[300px] px-3 py-2 bg-[#0b0b0b] border border-[#333] rounded-md text-white focus:outline-none focus:border-[#666]"
|
>
|
||||||
/>
|
<input
|
||||||
|
type="color"
|
||||||
|
:value="color"
|
||||||
|
@input="updateColor(index, ($event.target as HTMLInputElement).value)"
|
||||||
|
class="absolute inset-0 opacity-0 w-8 h-8 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="colors.length > 2"
|
||||||
|
@click="removeColor(index)"
|
||||||
|
class="-top-1.5 -right-1.5 absolute flex justify-center items-center bg-[#170D27] border border-[#222] rounded-full w-4 h-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="text-[#8BC79A] text-[8px]! pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="colors.length < 8"
|
||||||
|
@click="addColor"
|
||||||
|
class="flex justify-center items-center border-[#392e4e] border-2 hover:border-[#27FF64] border-dashed rounded-md w-8 h-8 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="text-[#8BC79A] text-sm pi pi-plus" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-[300px] h-3 rounded-md border border-[#333333]"
|
class="mt-3 border border-[#333333] rounded-md w-[300px] h-3"
|
||||||
:style="{
|
:style="{
|
||||||
background: `linear-gradient(to right, ${gradientPreview.join(', ')})`
|
background: `linear-gradient(to right, ${gradientPreview})`
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Customize>
|
</Customize>
|
||||||
|
|
||||||
<p class="demo-extra-info mt-4 flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
For a smoother animation, the gradient should start and end with the same color.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<PropTable :data="propData" />
|
<PropTable :data="propData" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,27 +88,57 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
import CodeExample from '@/components/code/CodeExample.vue';
|
||||||
import PropTable from '../../components/common/PropTable.vue';
|
import Customize from '@/components/common/Customize.vue';
|
||||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
import PreviewSelect from '@/components/common/PreviewSelect.vue';
|
||||||
import CodeExample from '../../components/code/CodeExample.vue';
|
import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||||
import Customize from '../../components/common/Customize.vue';
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
|
||||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
import PropTable from '@/components/common/PropTable.vue';
|
||||||
import GradientText from '../../content/TextAnimations/GradientText/GradientText.vue';
|
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||||
import { gradientText } from '@/constants/code/TextAnimations/gradientTextCode';
|
import { gradientText } from '@/constants/code/TextAnimations/gradientTextCode';
|
||||||
|
import GradientText from '@/content/TextAnimations/GradientText/GradientText.vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const colors = ref('#40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa');
|
const colors = ref(['#27FF64', '#27FF64', '#A0FFBC']);
|
||||||
const speed = ref(3);
|
const animationSpeed = ref(8);
|
||||||
|
const direction = ref<'horizontal' | 'vertical' | 'diagonal'>('horizontal');
|
||||||
|
const pauseOnHover = ref(false);
|
||||||
|
const yoyo = ref(true);
|
||||||
|
const showBorder = ref(false);
|
||||||
|
|
||||||
const gradientPreview = computed(() => colors.value.split(',').map(color => color.trim()));
|
const directionOptions = [
|
||||||
|
{ value: 'horizontal', label: 'Horizontal' },
|
||||||
|
{ value: 'vertical', label: 'Vertical' },
|
||||||
|
{ value: 'diagonal', label: 'Diagonal' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const gradientPreview = computed(() => [...colors.value, colors.value[0]].join(', '));
|
||||||
|
|
||||||
|
const updateColor = (index: number, newColor: string) => {
|
||||||
|
const newColors = [...colors.value];
|
||||||
|
newColors[index] = newColor;
|
||||||
|
colors.value = newColors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addColor = () => {
|
||||||
|
if (colors.value.length < 8) {
|
||||||
|
colors.value.push('#ffffff');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeColor = (index: number) => {
|
||||||
|
if (colors.value.length > 2) {
|
||||||
|
colors.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const propData = [
|
const propData = [
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'slot',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '""',
|
default: '-',
|
||||||
description: 'The text content to be displayed with gradient effect.'
|
description: 'The content to be displayed inside the gradient text.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'className',
|
name: 'className',
|
||||||
@@ -111,20 +149,38 @@ const propData = [
|
|||||||
{
|
{
|
||||||
name: 'colors',
|
name: 'colors',
|
||||||
type: 'string[]',
|
type: 'string[]',
|
||||||
default: '["#ffaa40", "#9c40ff", "#ffaa40"]',
|
default: `["#5227FF", "#FF9FFC", "#B19EEF"]`,
|
||||||
description: 'Defines the gradient colors for the text or border.'
|
description: 'Array of colors for the gradient effect.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'animationSpeed',
|
name: 'animationSpeed',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: '8',
|
default: '8',
|
||||||
description: 'The duration of the gradient animation in seconds.'
|
description: 'Duration of one animation cycle in seconds.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direction',
|
||||||
|
type: `'horizontal' | 'vertical' | 'diagonal'`,
|
||||||
|
default: `'horizontal'`,
|
||||||
|
description: 'Direction of the gradient animation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pauseOnHover',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Pauses the animation when hovering over the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yoyo',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Reverses animation direction at the end instead of looping.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showBorder',
|
name: 'showBorder',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: 'false',
|
default: 'false',
|
||||||
description: 'Determines whether a border with the gradient effect is displayed.'
|
description: 'Displays a gradient border around the text.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user