Component Boom

This commit is contained in:
David Haz
2025-07-10 15:36:38 +03:00
parent a4982577ad
commit 9b3465b04d
135 changed files with 16697 additions and 60 deletions

View 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>