2 changed files with 270 additions and 53 deletions
@ -0,0 +1,203 @@ |
|||
<template> |
|||
<div :class="computedClasses" :style="inlineStyles" :data-text="children"> |
|||
{{ children }} |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { computed } from 'vue'; |
|||
import type { CSSProperties } from 'vue'; |
|||
|
|||
interface GlitchTextProps { |
|||
children: string; |
|||
speed?: number; |
|||
enableShadows?: boolean; |
|||
enableOnHover?: boolean; |
|||
className?: string; |
|||
} |
|||
|
|||
interface CustomCSSProperties extends CSSProperties { |
|||
'--after-duration': string; |
|||
'--before-duration': string; |
|||
'--after-shadow': string; |
|||
'--before-shadow': string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<GlitchTextProps>(), { |
|||
speed: 0.5, |
|||
enableShadows: true, |
|||
enableOnHover: false, |
|||
className: '' |
|||
}); |
|||
|
|||
const inlineStyles = computed( |
|||
(): CustomCSSProperties => ({ |
|||
'--after-duration': `${props.speed * 3}s`, |
|||
'--before-duration': `${props.speed * 2}s`, |
|||
'--after-shadow': props.enableShadows ? '-5px 0 red' : 'none', |
|||
'--before-shadow': props.enableShadows ? '5px 0 cyan' : 'none' |
|||
}) |
|||
); |
|||
|
|||
const baseClasses = [ |
|||
'text-white', |
|||
'font-black', |
|||
'whitespace-nowrap', |
|||
'relative', |
|||
'mx-auto', |
|||
'select-none', |
|||
'cursor-pointer', |
|||
'text-[clamp(2rem,10vw,8rem)]', |
|||
|
|||
'before:content-[attr(data-text)]', |
|||
'before:absolute', |
|||
'before:top-0', |
|||
'before:text-white', |
|||
'before:bg-[#0b0b0b]', |
|||
'before:overflow-hidden', |
|||
'before:[clip-path:inset(0_0_0_0)]', |
|||
|
|||
'after:content-[attr(data-text)]', |
|||
'after:absolute', |
|||
'after:top-0', |
|||
'after:text-white', |
|||
'after:bg-[#0b0b0b]', |
|||
'after:overflow-hidden', |
|||
'after:[clip-path:inset(0_0_0_0)]' |
|||
]; |
|||
|
|||
const normalGlitchClasses = [ |
|||
'after:left-[10px]', |
|||
'after:[text-shadow:var(--after-shadow,-10px_0_red)]', |
|||
'after:[animation:animate-glitch_var(--after-duration,3s)_infinite_linear_alternate-reverse]', |
|||
|
|||
'before:left-[-10px]', |
|||
'before:[text-shadow:var(--before-shadow,10px_0_cyan)]', |
|||
'before:[animation:animate-glitch_var(--before-duration,2s)_infinite_linear_alternate-reverse]' |
|||
]; |
|||
|
|||
const hoverOnlyClasses = [ |
|||
'before:content-[""]', |
|||
'before:opacity-0', |
|||
'before:[animation:none]', |
|||
'after:content-[""]', |
|||
'after:opacity-0', |
|||
'after:[animation:none]', |
|||
|
|||
'hover:before:content-[attr(data-text)]', |
|||
'hover:before:opacity-100', |
|||
'hover:before:left-[-10px]', |
|||
'hover:before:[text-shadow:var(--before-shadow,10px_0_cyan)]', |
|||
'hover:before:[animation:animate-glitch_var(--before-duration,2s)_infinite_linear_alternate-reverse]', |
|||
|
|||
'hover:after:content-[attr(data-text)]', |
|||
'hover:after:opacity-100', |
|||
'hover:after:left-[10px]', |
|||
'hover:after:[text-shadow:var(--after-shadow,-10px_0_red)]', |
|||
'hover:after:[animation:animate-glitch_var(--after-duration,3s)_infinite_linear_alternate-reverse]' |
|||
]; |
|||
|
|||
const computedClasses = computed(() => { |
|||
const classes = [...baseClasses]; |
|||
|
|||
if (props.enableOnHover) { |
|||
classes.push(...hoverOnlyClasses); |
|||
} else { |
|||
classes.push(...normalGlitchClasses); |
|||
} |
|||
|
|||
if (props.className) { |
|||
classes.push(props.className); |
|||
} |
|||
|
|||
return classes.join(' '); |
|||
}); |
|||
</script> |
|||
|
|||
<style> |
|||
@keyframes animate-glitch { |
|||
0% { |
|||
clip-path: inset(20% 0 50% 0); |
|||
} |
|||
|
|||
5% { |
|||
clip-path: inset(10% 0 60% 0); |
|||
} |
|||
|
|||
10% { |
|||
clip-path: inset(15% 0 55% 0); |
|||
} |
|||
|
|||
15% { |
|||
clip-path: inset(25% 0 35% 0); |
|||
} |
|||
|
|||
20% { |
|||
clip-path: inset(30% 0 40% 0); |
|||
} |
|||
|
|||
25% { |
|||
clip-path: inset(40% 0 20% 0); |
|||
} |
|||
|
|||
30% { |
|||
clip-path: inset(10% 0 60% 0); |
|||
} |
|||
|
|||
35% { |
|||
clip-path: inset(15% 0 55% 0); |
|||
} |
|||
|
|||
40% { |
|||
clip-path: inset(25% 0 35% 0); |
|||
} |
|||
|
|||
45% { |
|||
clip-path: inset(30% 0 40% 0); |
|||
} |
|||
|
|||
50% { |
|||
clip-path: inset(20% 0 50% 0); |
|||
} |
|||
|
|||
55% { |
|||
clip-path: inset(10% 0 60% 0); |
|||
} |
|||
|
|||
60% { |
|||
clip-path: inset(15% 0 55% 0); |
|||
} |
|||
|
|||
65% { |
|||
clip-path: inset(25% 0 35% 0); |
|||
} |
|||
|
|||
70% { |
|||
clip-path: inset(30% 0 40% 0); |
|||
} |
|||
|
|||
75% { |
|||
clip-path: inset(40% 0 20% 0); |
|||
} |
|||
|
|||
80% { |
|||
clip-path: inset(20% 0 50% 0); |
|||
} |
|||
|
|||
85% { |
|||
clip-path: inset(10% 0 60% 0); |
|||
} |
|||
|
|||
90% { |
|||
clip-path: inset(15% 0 55% 0); |
|||
} |
|||
|
|||
95% { |
|||
clip-path: inset(25% 0 35% 0); |
|||
} |
|||
|
|||
100% { |
|||
clip-path: inset(30% 0 40% 0); |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue