mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
5.2 KiB
JSON
1 line
5.2 KiB
JSON
{"name":"TextCursor","title":"TextCursor","description":"Make any text element follow your cursor, leaving a trail of copies behind it.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, useTemplateRef } from 'vue';\nimport { Motion } from 'motion-v';\n\ninterface TextCursorProps {\n text?: string;\n delay?: number;\n spacing?: number;\n followMouseDirection?: boolean;\n randomFloat?: boolean;\n exitDuration?: number;\n removalInterval?: number;\n maxPoints?: number;\n}\n\ninterface TrailItem {\n id: number;\n x: number;\n y: number;\n angle: number;\n randomX?: number;\n randomY?: number;\n randomRotate?: number;\n}\n\nconst props = withDefaults(defineProps<TextCursorProps>(), {\n text: '⚛️',\n delay: 0.01,\n spacing: 100,\n followMouseDirection: true,\n randomFloat: true,\n exitDuration: 0.5,\n removalInterval: 30,\n maxPoints: 5\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst trail = ref<TrailItem[]>([]);\nconst lastMoveTime = ref(Date.now());\nconst idCounter = ref(0);\n\nlet removalIntervalId: ReturnType<typeof setInterval> | null = null;\n\nconst handleMouseMove = (e: MouseEvent) => {\n if (!containerRef.value) return;\n\n const rect = containerRef.value.getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n let newTrail = [...trail.value];\n\n if (newTrail.length === 0) {\n newTrail.push({\n id: idCounter.value++,\n x: mouseX,\n y: mouseY,\n angle: 0,\n ...(props.randomFloat && {\n randomX: Math.random() * 10 - 5,\n randomY: Math.random() * 10 - 5,\n randomRotate: Math.random() * 10 - 5\n })\n });\n } else {\n const last = newTrail[newTrail.length - 1];\n const dx = mouseX - last.x;\n const dy = mouseY - last.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n if (distance >= props.spacing) {\n let rawAngle = (Math.atan2(dy, dx) * 180) / Math.PI;\n if (rawAngle > 90) rawAngle -= 180;\n else if (rawAngle < -90) rawAngle += 180;\n const computedAngle = props.followMouseDirection ? rawAngle : 0;\n const steps = Math.floor(distance / props.spacing);\n\n for (let i = 1; i <= steps; i++) {\n const t = (props.spacing * i) / distance;\n const newX = last.x + dx * t;\n const newY = last.y + dy * t;\n newTrail.push({\n id: idCounter.value++,\n x: newX,\n y: newY,\n angle: computedAngle,\n ...(props.randomFloat && {\n randomX: Math.random() * 10 - 5,\n randomY: Math.random() * 10 - 5,\n randomRotate: Math.random() * 10 - 5\n })\n });\n }\n }\n }\n\n if (newTrail.length > props.maxPoints) {\n newTrail = newTrail.slice(newTrail.length - props.maxPoints);\n }\n\n trail.value = newTrail;\n lastMoveTime.value = Date.now();\n};\n\nconst startRemovalInterval = () => {\n if (removalIntervalId) {\n clearInterval(removalIntervalId);\n }\n\n removalIntervalId = setInterval(() => {\n if (Date.now() - lastMoveTime.value > 100) {\n if (trail.value.length > 0) {\n trail.value = trail.value.slice(1);\n }\n }\n }, props.removalInterval);\n};\n\nonMounted(() => {\n if (containerRef.value) {\n containerRef.value.addEventListener('mousemove', handleMouseMove);\n startRemovalInterval();\n }\n});\n\nonUnmounted(() => {\n if (containerRef.value) {\n containerRef.value.removeEventListener('mousemove', handleMouseMove);\n }\n if (removalIntervalId) {\n clearInterval(removalIntervalId);\n }\n});\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"relative w-full h-full\">\n <div class=\"absolute inset-0 pointer-events-none\">\n <Motion\n v-for=\"item in trail\"\n :key=\"item.id\"\n :initial=\"{ opacity: 0, scale: 1, rotate: item.angle }\"\n :animate=\"{\n opacity: 1,\n scale: 1,\n x: props.randomFloat ? [0, item.randomX || 0, 0] : 0,\n y: props.randomFloat ? [0, item.randomY || 0, 0] : 0,\n rotate: props.randomFloat ? [item.angle, item.angle + (item.randomRotate || 0), item.angle] : item.angle\n }\"\n :transition=\"{\n duration: props.randomFloat ? 2 : props.exitDuration,\n repeat: props.randomFloat ? Infinity : 0,\n repeatType: props.randomFloat ? 'mirror' : 'loop'\n }\"\n class=\"absolute text-3xl whitespace-nowrap select-none\"\n :style=\"{ left: `${item.x}px`, top: `${item.y}px` }\"\n >\n {{ props.text }}\n </Motion>\n </div>\n </div>\n</template>\n\n<style scoped>\n.trail-enter-active,\n.trail-leave-active {\n transition: all 0.5s ease;\n}\n\n.trail-enter-from {\n opacity: 0;\n transform: scale(0);\n}\n\n.trail-leave-to {\n opacity: 0;\n transform: scale(0);\n}\n</style>\n","path":"TextCursor/TextCursor.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]} |