mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Component Boom
This commit is contained in:
181
src/content/TextAnimations/TextCursor/TextCursor.vue
Normal file
181
src/content/TextAnimations/TextCursor/TextCursor.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Motion } from 'motion-v'
|
||||
|
||||
interface TextCursorProps {
|
||||
text?: string
|
||||
delay?: number
|
||||
spacing?: number
|
||||
followMouseDirection?: boolean
|
||||
randomFloat?: boolean
|
||||
exitDuration?: number
|
||||
removalInterval?: number
|
||||
maxPoints?: number
|
||||
}
|
||||
|
||||
interface TrailItem {
|
||||
id: number
|
||||
x: number
|
||||
y: number
|
||||
angle: number
|
||||
randomX?: number
|
||||
randomY?: number
|
||||
randomRotate?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TextCursorProps>(), {
|
||||
text: '⚛️',
|
||||
delay: 0.01,
|
||||
spacing: 100,
|
||||
followMouseDirection: true,
|
||||
randomFloat: true,
|
||||
exitDuration: 0.5,
|
||||
removalInterval: 30,
|
||||
maxPoints: 5
|
||||
})
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const trail = ref<TrailItem[]>([])
|
||||
const lastMoveTime = ref(Date.now())
|
||||
const idCounter = ref(0)
|
||||
|
||||
let removalIntervalId: number | null = null
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
const mouseX = e.clientX - rect.left
|
||||
const mouseY = e.clientY - rect.top
|
||||
|
||||
let newTrail = [...trail.value]
|
||||
|
||||
if (newTrail.length === 0) {
|
||||
newTrail.push({
|
||||
id: idCounter.value++,
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
angle: 0,
|
||||
...(props.randomFloat && {
|
||||
randomX: Math.random() * 10 - 5,
|
||||
randomY: Math.random() * 10 - 5,
|
||||
randomRotate: Math.random() * 10 - 5
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const last = newTrail[newTrail.length - 1]
|
||||
const dx = mouseX - last.x
|
||||
const dy = mouseY - last.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (distance >= props.spacing) {
|
||||
let rawAngle = (Math.atan2(dy, dx) * 180) / Math.PI
|
||||
if (rawAngle > 90) rawAngle -= 180
|
||||
else if (rawAngle < -90) rawAngle += 180
|
||||
const computedAngle = props.followMouseDirection ? rawAngle : 0
|
||||
const steps = Math.floor(distance / props.spacing)
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const t = (props.spacing * i) / distance
|
||||
const newX = last.x + dx * t
|
||||
const newY = last.y + dy * t
|
||||
newTrail.push({
|
||||
id: idCounter.value++,
|
||||
x: newX,
|
||||
y: newY,
|
||||
angle: computedAngle,
|
||||
...(props.randomFloat && {
|
||||
randomX: Math.random() * 10 - 5,
|
||||
randomY: Math.random() * 10 - 5,
|
||||
randomRotate: Math.random() * 10 - 5
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTrail.length > props.maxPoints) {
|
||||
newTrail = newTrail.slice(newTrail.length - props.maxPoints)
|
||||
}
|
||||
|
||||
trail.value = newTrail
|
||||
lastMoveTime.value = Date.now()
|
||||
}
|
||||
|
||||
const startRemovalInterval = () => {
|
||||
if (removalIntervalId) {
|
||||
clearInterval(removalIntervalId)
|
||||
}
|
||||
|
||||
removalIntervalId = setInterval(() => {
|
||||
if (Date.now() - lastMoveTime.value > 100) {
|
||||
if (trail.value.length > 0) {
|
||||
trail.value = trail.value.slice(1)
|
||||
}
|
||||
}
|
||||
}, props.removalInterval)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.addEventListener('mousemove', handleMouseMove)
|
||||
startRemovalInterval()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
if (removalIntervalId) {
|
||||
clearInterval(removalIntervalId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="w-full h-full relative">
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<Motion
|
||||
v-for="item in trail"
|
||||
:key="item.id"
|
||||
:initial="{ opacity: 0, scale: 1, rotate: item.angle }"
|
||||
:animate="{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
x: props.randomFloat ? [0, item.randomX || 0, 0] : 0,
|
||||
y: props.randomFloat ? [0, item.randomY || 0, 0] : 0,
|
||||
rotate: props.randomFloat
|
||||
? [item.angle, item.angle + (item.randomRotate || 0), item.angle]
|
||||
: item.angle
|
||||
}"
|
||||
:transition="{
|
||||
duration: props.randomFloat ? 2 : props.exitDuration,
|
||||
repeat: props.randomFloat ? Infinity : 0,
|
||||
repeatType: props.randomFloat ? 'mirror' : 'loop'
|
||||
}"
|
||||
class="absolute select-none whitespace-nowrap text-3xl"
|
||||
:style="{ left: `${item.x}px`, top: `${item.y}px` }"
|
||||
>
|
||||
{{ props.text }}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trail-enter-active,
|
||||
.trail-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.trail-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.trail-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user