7 changed files with 260 additions and 14 deletions
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,135 @@ |
|||
<script setup lang="ts"> |
|||
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v'; |
|||
import { computed, ref, watch } from 'vue'; |
|||
|
|||
interface ShinyTextProps { |
|||
text: string; |
|||
disabled?: boolean; |
|||
speed?: number; |
|||
className?: string; |
|||
color?: string; |
|||
shineColor?: string; |
|||
spread?: number; |
|||
yoyo?: boolean; |
|||
pauseOnHover?: boolean; |
|||
direction?: 'left' | 'right'; |
|||
delay?: number; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<ShinyTextProps>(), { |
|||
disabled: false, |
|||
speed: 2, |
|||
className: '', |
|||
color: '#b5b5b5', |
|||
shineColor: '#ffffff', |
|||
spread: 120, |
|||
yoyo: false, |
|||
pauseOnHover: false, |
|||
direction: 'left', |
|||
delay: 0 |
|||
}); |
|||
|
|||
const isPaused = ref(false); |
|||
const progress = useMotionValue(0); |
|||
const elapsedRef = ref(0); |
|||
const lastTimeRef = ref<number | null>(null); |
|||
const directionRef = ref(props.direction === 'left' ? 1 : -1); |
|||
|
|||
const animationDuration = computed(() => props.speed * 1000); |
|||
const delayDuration = computed(() => props.delay * 1000); |
|||
|
|||
useAnimationFrame(time => { |
|||
if (props.disabled || 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; |
|||
|
|||
// Animation goes from 0 to 100 |
|||
if (props.yoyo) { |
|||
const cycleDuration = animationDuration.value + delayDuration.value; |
|||
const fullCycle = cycleDuration * 2; |
|||
const cycleTime = elapsedRef.value % fullCycle; |
|||
|
|||
if (cycleTime < animationDuration.value) { |
|||
// Forward animation: 0 -> 100 |
|||
const p = (cycleTime / animationDuration.value) * 100; |
|||
progress.set(directionRef.value === 1 ? p : 100 - p); |
|||
} else if (cycleTime < cycleDuration) { |
|||
// Delay at end |
|||
progress.set(directionRef.value === 1 ? 100 : 0); |
|||
} else if (cycleTime < cycleDuration + animationDuration.value) { |
|||
// Reverse animation: 100 -> 0 |
|||
const reverseTime = cycleTime - cycleDuration; |
|||
const p = 100 - (reverseTime / animationDuration.value) * 100; |
|||
progress.set(directionRef.value === 1 ? p : 100 - p); |
|||
} else { |
|||
// Delay at start |
|||
progress.set(directionRef.value === 1 ? 0 : 100); |
|||
} |
|||
} else { |
|||
const cycleDuration = animationDuration.value + delayDuration.value; |
|||
const cycleTime = elapsedRef.value % cycleDuration; |
|||
|
|||
if (cycleTime < animationDuration.value) { |
|||
// Animation phase: 0 -> 100 |
|||
const p = (cycleTime / animationDuration.value) * 100; |
|||
progress.set(directionRef.value === 1 ? p : 100 - p); |
|||
} else { |
|||
// Delay phase - hold at end (shine off-screen) |
|||
progress.set(directionRef.value === 1 ? 100 : 0); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
watch( |
|||
() => props.direction, |
|||
() => { |
|||
directionRef.value = props.direction === 'left' ? 1 : -1; |
|||
elapsedRef.value = 0; |
|||
progress.set(0); |
|||
}, |
|||
{ |
|||
immediate: true |
|||
} |
|||
); |
|||
|
|||
const backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`); |
|||
|
|||
const handleMouseEnter = () => { |
|||
if (props.pauseOnHover) isPaused.value = true; |
|||
}; |
|||
|
|||
const handleMouseLeave = () => { |
|||
if (props.pauseOnHover) isPaused.value = false; |
|||
}; |
|||
|
|||
const gradientStyle = computed(() => ({ |
|||
backgroundImage: `linear-gradient(${props.spread}deg, ${props.color} 0%, ${props.color} 35%, ${props.shineColor} 50%, ${props.color} 65%, ${props.color} 100%)`, |
|||
backgroundSize: '200% auto', |
|||
WebkitBackgroundClip: 'text', |
|||
backgroundClip: 'text', |
|||
WebkitTextFillColor: 'transparent' |
|||
})); |
|||
</script> |
|||
|
|||
<template> |
|||
<Motion |
|||
tag="span" |
|||
:class="['inline-block', className]" |
|||
:style="{ ...gradientStyle, backgroundPosition }" |
|||
@mouseenter="handleMouseEnter" |
|||
@mouseleave="handleMouseLeave" |
|||
> |
|||
{{ text }} |
|||
</Motion> |
|||
</template> |
|||
@ -0,0 +1,94 @@ |
|||
<template> |
|||
<component |
|||
:is="as" |
|||
:class="['relative inline-block overflow-hidden !bg-transparent !border-none !rounded-[20px]', customClass]" |
|||
v-bind="restAttrs" |
|||
:style="componentStyle" |
|||
> |
|||
<div |
|||
class="absolute w-[300%] h-[50%] opacity-70 bottom-[-11px] right-[-250%] rounded-full animate-star-movement-bottom z-0" |
|||
:style="{ |
|||
background: `radial-gradient(circle, ${color}, transparent 10%)`, |
|||
animationDuration: speed |
|||
}" |
|||
></div> |
|||
|
|||
<div |
|||
class="absolute w-[300%] h-[50%] opacity-70 top-[-10px] left-[-250%] rounded-full animate-star-movement-top z-0" |
|||
:style="{ |
|||
background: `radial-gradient(circle, ${color}, transparent 10%)`, |
|||
animationDuration: speed |
|||
}" |
|||
></div> |
|||
|
|||
<div |
|||
class="relative z-10 border border-[#333] bg-[#0b0b0b] text-white text-[16px] text-center px-[64px] py-[24px] rounded-[20px]" |
|||
> |
|||
<slot /> |
|||
</div> |
|||
</component> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { computed, defineProps, useAttrs } from 'vue'; |
|||
|
|||
interface StarBorderProps { |
|||
as?: string; |
|||
customClass?: string; |
|||
color?: string; |
|||
speed?: string; |
|||
thickness?: number; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<StarBorderProps>(), { |
|||
as: 'button', |
|||
customClass: '', |
|||
color: 'white', |
|||
speed: '6s', |
|||
thickness: 1 |
|||
}); |
|||
|
|||
const restAttrs = useAttrs(); |
|||
|
|||
const componentStyle = computed(() => { |
|||
const base = { |
|||
padding: `${props.thickness}px 0` |
|||
}; |
|||
const userStyle = (restAttrs.style as Record<string, string>) || {}; |
|||
return { ...base, ...userStyle }; |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
@keyframes star-movement-bottom { |
|||
0% { |
|||
transform: translate(0%, 0%); |
|||
opacity: 1; |
|||
} |
|||
|
|||
100% { |
|||
transform: translate(-100%, 0%); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
@keyframes star-movement-top { |
|||
0% { |
|||
transform: translate(0%, 0%); |
|||
opacity: 1; |
|||
} |
|||
|
|||
100% { |
|||
transform: translate(100%, 0%); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
.animate-star-movement-bottom { |
|||
animation: star-movement-bottom linear infinite alternate; |
|||
} |
|||
|
|||
.animate-star-movement-top { |
|||
animation: star-movement-top linear infinite alternate; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue