Merge branch 'main' into add-ascii-texts

This commit is contained in:
David
2025-07-12 19:49:51 +03:00
committed by GitHub
5 changed files with 268 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ export const CATEGORIES = [
'Text Cursor', 'Text Cursor',
'Decrypted Text', 'Decrypted Text',
'Ascii Text', 'Ascii Text',
'Scramble Text',
'True Focus', 'True Focus',
'Scroll Float', 'Scroll Float',
'Scroll Reveal', 'Scroll Reveal',

View File

@@ -27,6 +27,7 @@ const textAnimations = {
'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"), 'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"),
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"), 'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
'ascii-text': () => import("../demo/TextAnimations/AsciiTextDemo.vue"), 'ascii-text': () => import("../demo/TextAnimations/AsciiTextDemo.vue"),
'scramble-text': () => import("../demo/TextAnimations/ScrambleTextDemo.vue"),
'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"),

View File

@@ -0,0 +1,28 @@
import code from '@content/TextAnimations/ScrambleText/ScrambleText.vue?raw'
import type { CodeObject } from '../../../types/code'
export const scrambleTextCode: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ScrambleText`,
installation: `npm install gsap`,
usage: `// Component inspired by Tom Miller from the GSAP community
// https://codepen.io/creativeocean/pen/NPWLwJM
<template>
<ScrambleText
:className="'m-[7vw] max-w-[800px] font-mono font-medium text-[clamp(14px,4vw,32px)] text-white'"
:radius="100"
:duration="1.2"
:speed="0.5"
scrambleChars=".:"
>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Similique pariatur dignissimos porro eius quam doloremque
et enim velit nobis maxime.
</ScrambleText>
</template>
<script setup lang="ts">
import ScrambleText from "./ScrambleText.vue";
</script>`,
code
}

View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { gsap } from 'gsap'
import { SplitText } from 'gsap/SplitText'
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin'
gsap.registerPlugin(SplitText, ScrambleTextPlugin)
interface ScrambleTextProps {
radius?: number
duration?: number
speed?: number
scrambleChars?: string
className?: string
style?: Record<string, string | number>
}
const props = withDefaults(defineProps<ScrambleTextProps>(), {
radius: 100,
duration: 1.2,
speed: 0.5,
scrambleChars: ".:",
className: "",
style: () => ({})
})
const rootRef = ref<HTMLDivElement | null>(null)
let splitText: SplitText | null = null
let handleMove: ((e: PointerEvent) => void) | null = null
const initializeScrambleText = () => {
if (!rootRef.value) return
const pElement = rootRef.value.querySelector('p')
if (!pElement) return
splitText = new SplitText(pElement, {
type: 'chars',
charsClass: 'inline-block will-change-transform'
})
splitText.chars.forEach((el) => {
const c = el as HTMLElement
gsap.set(c, { attr: { 'data-content': c.innerHTML } })
})
handleMove = (e: PointerEvent) => {
if (!splitText) return
splitText.chars.forEach((el) => {
const c = el as HTMLElement
const { left, top, width, height } = c.getBoundingClientRect()
const dx = e.clientX - (left + width / 2)
const dy = e.clientY - (top + height / 2)
const dist = Math.hypot(dx, dy)
if (dist < props.radius) {
gsap.to(c, {
overwrite: true,
duration: props.duration * (1 - dist / props.radius),
scrambleText: {
text: c.dataset.content || '',
chars: props.scrambleChars,
speed: props.speed
},
ease: 'none'
})
}
})
}
rootRef.value.addEventListener('pointermove', handleMove)
}
const cleanup = () => {
if (rootRef.value && handleMove) {
rootRef.value.removeEventListener('pointermove', handleMove)
}
if (splitText) {
splitText.revert()
splitText = null
}
handleMove = null
}
onMounted(() => {
initializeScrambleText()
})
onUnmounted(() => {
cleanup()
})
watch(
[() => props.radius, () => props.duration, () => props.speed, () => props.scrambleChars],
() => {
cleanup()
initializeScrambleText()
}
)
</script>
<template>
<div
ref="rootRef"
:class="`scramble-text ${className}`"
:style="style"
>
<p>
<slot></slot>
</p>
</div>
</template>

View File

@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref } from 'vue'
import TabbedLayout from '../../components/common/TabbedLayout.vue'
import PropTable from '../../components/common/PropTable.vue'
import Dependencies from '../../components/code/Dependencies.vue'
import CliInstallation from '../../components/code/CliInstallation.vue'
import CodeExample from '../../components/code/CodeExample.vue'
import Customize from '../../components/common/Customize.vue'
import PreviewSlider from '../../components/common/PreviewSlider.vue'
import ScrambleText from '../../content/TextAnimations/ScrambleText/ScrambleText.vue'
import { scrambleTextCode } from '@/constants/code/TextAnimations/scrambleTextCode'
const radius = ref(100)
const duration = ref(1.2)
const speed = ref(0.5)
const scrambleChars = ref(".:")
const propData = [
{
name: "radius",
type: "number",
default: "100",
description: "The radius around the mouse pointer within which characters will scramble."
},
{
name: "duration",
type: "number",
default: "1.2",
description: "The duration of the scramble effect on a character."
},
{
name: "speed",
type: "number",
default: "0.5",
description: "The speed of the scramble animation."
},
{
name: "scrambleChars",
type: "string",
default: "'.:'",
description: "The characters used for scrambling."
},
{
name: "className",
type: "string",
default: '""',
description: "Additional CSS classes for the component."
},
{
name: "style",
type: "Record<string, string | number>",
default: "{}",
description: "Inline styles for the component."
}
]
</script>
<template>
<div class="scramble-text-demo">
<TabbedLayout>
<template #preview>
<div class="demo-container relative h-[500px] overflow-hidden">
<ScrambleText
:className="'m-[7vw] max-w-[800px] font-mono font-medium text-[clamp(14px,4vw,32px)] text-white'"
:radius="radius"
:duration="duration"
:speed="speed"
:scrambleChars="scrambleChars"
>
Once you hover over me, you will see the effect in action! You can customize the radius, duration, and speed of the scramble effect.
</ScrambleText>
</div>
<Customize>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Scramble Characters</label>
<input
v-model="scrambleChars"
type="text"
placeholder="Enter text..."
maxlength="5"
class="w-[160px] px-3 py-2 bg-[#0b0b0b] border border-[#333] rounded-md text-white focus:outline-none focus:border-[#666]"
/>
</div>
<PreviewSlider
title="Radius"
v-model="radius"
:min="10"
:max="300"
:step="10"
/>
<PreviewSlider
title="Duration"
v-model="duration"
:min="0.1"
:max="5"
:step="0.1"
/>
<PreviewSlider
title="Speed"
v-model="speed"
:min="0.1"
:max="2"
:step="0.1"
/>
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['gsap']" />
</template>
<template #code>
<CodeExample :code-object="scrambleTextCode" />
</template>
<template #cli>
<CliInstallation :command="scrambleTextCode.cli" />
</template>
</TabbedLayout>
</div>
</template>