mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
feat(text-animations): add GlitchText component
This commit is contained in:
33
src/components/common/PreviewText.vue
Normal file
33
src/components/common/PreviewText.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="preview-text">
|
||||||
|
<span class="text-label">{{ title }}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="handleChange"
|
||||||
|
class="w-[300px] px-3 py-2 bg-[#0b0b0b] border border-[#333] rounded-md text-white focus:outline-none focus:border-[#666]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
defineProps<{
|
||||||
|
title: string;
|
||||||
|
modelValue: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit('update:modelValue', target.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,7 +22,8 @@ export const CATEGORIES = [
|
|||||||
'True Focus',
|
'True Focus',
|
||||||
'Scroll Float',
|
'Scroll Float',
|
||||||
'Scroll Reveal',
|
'Scroll Reveal',
|
||||||
'Rotating Text'
|
'Rotating Text',
|
||||||
|
'Glitch Text'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ const textAnimations = {
|
|||||||
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
|
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
|
||||||
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
|
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
|
||||||
'scroll-reveal': ()=> import("../demo/TextAnimations/ScrollRevealDemo.vue"),
|
'scroll-reveal': ()=> import("../demo/TextAnimations/ScrollRevealDemo.vue"),
|
||||||
'rotating-text': ()=> import("../demo/TextAnimations/RotatingTextDemo.vue")
|
'rotating-text': ()=> import("../demo/TextAnimations/RotatingTextDemo.vue"),
|
||||||
|
'glitch-text': () => import("../demo/TextAnimations/GlitchTextDemo.vue"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
|||||||
19
src/constants/code/TextAnimations/glitchTextCode.ts
Normal file
19
src/constants/code/TextAnimations/glitchTextCode.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import code from '@/content/TextAnimations/GlitchText/GlitchText.vue?raw';
|
||||||
|
import type { CodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const glitchText: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/GlitchText`,
|
||||||
|
usage: `<template>
|
||||||
|
<GlitchText
|
||||||
|
children="Vue Bits"
|
||||||
|
:speed="0.5"
|
||||||
|
:enable-shadows="true"
|
||||||
|
:enable-on-hover="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import GlitchText from "./GlitchText.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
};
|
||||||
120
src/content/TextAnimations/GlitchText/GlitchText.css
Normal file
120
src/content/TextAnimations/GlitchText/GlitchText.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
.glitch {
|
||||||
|
color: #fff;
|
||||||
|
font-size: clamp(2rem, 10vw, 8rem);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 900;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch::after,
|
||||||
|
.glitch::before {
|
||||||
|
content: attr(data-text);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #060010;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch:not(.enable-on-hover)::after {
|
||||||
|
left: 10px;
|
||||||
|
text-shadow: var(--after-shadow, -10px 0 red);
|
||||||
|
animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;
|
||||||
|
}
|
||||||
|
.glitch:not(.enable-on-hover)::before {
|
||||||
|
left: -10px;
|
||||||
|
text-shadow: var(--before-shadow, 10px 0 cyan);
|
||||||
|
animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch.enable-on-hover::after,
|
||||||
|
.glitch.enable-on-hover::before {
|
||||||
|
content: '';
|
||||||
|
opacity: 0;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch.enable-on-hover:hover::after {
|
||||||
|
content: attr(data-text);
|
||||||
|
opacity: 1;
|
||||||
|
left: 10px;
|
||||||
|
text-shadow: var(--after-shadow, -10px 0 red);
|
||||||
|
animation: animate-glitch var(--after-duration, 3s) infinite linear alternate-reverse;
|
||||||
|
}
|
||||||
|
.glitch.enable-on-hover:hover::before {
|
||||||
|
content: attr(data-text);
|
||||||
|
opacity: 1;
|
||||||
|
left: -10px;
|
||||||
|
text-shadow: var(--before-shadow, 10px 0 cyan);
|
||||||
|
animation: animate-glitch var(--before-duration, 2s) infinite linear alternate-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/content/TextAnimations/GlitchText/GlitchText.vue
Normal file
47
src/content/TextAnimations/GlitchText/GlitchText.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<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';
|
||||||
|
import './GlitchText.css';
|
||||||
|
|
||||||
|
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 computedClasses = computed(() => {
|
||||||
|
const hoverClass = props.enableOnHover ? 'enable-on-hover' : '';
|
||||||
|
return `glitch ${hoverClass} ${props.className}`.trim();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
90
src/demo/TextAnimations/GlitchTextDemo.vue
Normal file
90
src/demo/TextAnimations/GlitchTextDemo.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="glitchtext-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container relative h-[500px] overflow-hidden">
|
||||||
|
<GlitchText
|
||||||
|
:children="text"
|
||||||
|
:speed="speed"
|
||||||
|
:enable-shadows="enableShadows"
|
||||||
|
:enable-on-hover="enableOnHover"
|
||||||
|
class-name="demo-glitch-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<div class="mb-4">
|
||||||
|
<PreviewText title="Text" v-model="text" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Refresh Delay" v-model="speed" :min="0.1" :max="5" :step="0.1" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Glitch Colors" v-model="enableShadows" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Glitch On Hover" v-model="enableOnHover" />
|
||||||
|
</div>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="glitchText" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="glitchText.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import GlitchText from '../../content/TextAnimations/GlitchText/GlitchText.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import PreviewText from '../../components/common/PreviewText.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import { glitchText } from '@/constants/code/TextAnimations/glitchTextCode';
|
||||||
|
|
||||||
|
const text = ref('Vue Bits');
|
||||||
|
const speed = ref(0.5);
|
||||||
|
const enableShadows = ref(true);
|
||||||
|
const enableOnHover = ref(false);
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'children',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The text content that will display the glitch effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'speed',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Multiplier for the animation speed. Higher values slow down the glitch effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableShadows',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Toggle the colored text shadows on the glitch pseudo-elements.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableOnHover',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'If true, the glitch animation is only activated on hover.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Additional custom classes to apply to the component.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user