mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
1 line
3.9 KiB
JSON
1 line
3.9 KiB
JSON
{"name":"CircularText","title":"CircularText","description":"Layouts characters around a circle with optional rotation animation.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { computed, ref, watchEffect, onUnmounted } from 'vue';\nimport { Motion } from 'motion-v';\n\ninterface CircularTextProps {\n text: string;\n spinDuration?: number;\n onHover?: 'slowDown' | 'speedUp' | 'pause' | 'goBonkers';\n className?: string;\n}\n\nconst props = withDefaults(defineProps<CircularTextProps>(), {\n text: '',\n spinDuration: 20,\n onHover: 'speedUp',\n className: ''\n});\n\nconst letters = computed(() => Array.from(props.text));\nconst isHovered = ref(false);\n\nconst currentRotation = ref(0);\nconst animationId = ref<number | null>(null);\nconst lastTime = ref<number>(Date.now());\nconst rotationSpeed = ref<number>(0);\n\nconst getCurrentSpeed = () => {\n if (isHovered.value && props.onHover === 'pause') return 0;\n\n const baseDuration = props.spinDuration;\n const baseSpeed = 360 / baseDuration;\n\n if (!isHovered.value) return baseSpeed;\n\n switch (props.onHover) {\n case 'slowDown':\n return baseSpeed / 2;\n case 'speedUp':\n return baseSpeed * 4;\n case 'goBonkers':\n return baseSpeed * 20;\n default:\n return baseSpeed;\n }\n};\n\nconst getCurrentScale = () => {\n return isHovered.value && props.onHover === 'goBonkers' ? 0.8 : 1;\n};\n\nconst animate = () => {\n const now = Date.now();\n const deltaTime = (now - lastTime.value) / 1000;\n lastTime.value = now;\n\n const targetSpeed = getCurrentSpeed();\n\n const speedDiff = targetSpeed - rotationSpeed.value;\n const smoothingFactor = Math.min(1, deltaTime * 5);\n rotationSpeed.value += speedDiff * smoothingFactor;\n\n currentRotation.value = (currentRotation.value + rotationSpeed.value * deltaTime) % 360;\n\n animationId.value = requestAnimationFrame(animate);\n};\n\nconst startAnimation = () => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n lastTime.value = Date.now();\n rotationSpeed.value = getCurrentSpeed();\n animate();\n};\n\nwatchEffect(() => {\n startAnimation();\n});\n\nstartAnimation();\n\nonUnmounted(() => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n }\n});\n\nconst handleHoverStart = () => {\n isHovered.value = true;\n};\n\nconst handleHoverEnd = () => {\n isHovered.value = false;\n};\n\nconst getLetterTransform = (index: number) => {\n const rotationDeg = (360 / letters.value.length) * index;\n const factor = Math.PI / letters.value.length;\n const x = factor * index;\n const y = factor * index;\n return `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`;\n};\n</script>\n\n<template>\n <Motion\n :animate=\"{\n rotate: currentRotation,\n scale: getCurrentScale()\n }\"\n :transition=\"{\n rotate: {\n duration: 0\n },\n scale: {\n type: 'spring',\n damping: 20,\n stiffness: 300\n }\n }\"\n :class=\"`m-0 mx-auto rounded-full w-[200px] h-[200px] relative font-black text-white text-center cursor-pointer origin-center ${props.className}`\"\n @mouseenter=\"handleHoverStart\"\n @mouseleave=\"handleHoverEnd\"\n >\n <span\n v-for=\"(letter, i) in letters\"\n :key=\"i\"\n class=\"absolute inline-block inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]\"\n :style=\"{\n transform: getLetterTransform(i),\n WebkitTransform: getLetterTransform(i)\n }\"\n >\n {{ letter }}\n </span>\n </Motion>\n</template>\n","path":"CircularText/CircularText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]} |