mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
1 line
5.0 KiB
JSON
1 line
5.0 KiB
JSON
{"name":"BlurText","title":"BlurText","description":"Text starts blurred then crisply resolves for a soft-focus reveal effect.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <p ref=\"rootRef\" :class=\"['blur-text', className, 'flex', 'flex-wrap']\">\n <Motion\n v-for=\"(segment, index) in elements\"\n :key=\"`${animationKey}-${index}`\"\n tag=\"span\"\n :initial=\"fromSnapshot\"\n :animate=\"inView ? getAnimateKeyframes() : fromSnapshot\"\n :transition=\"getTransition(index)\"\n :style=\"{\n display: 'inline-block',\n willChange: 'transform, filter, opacity'\n }\"\n @animation-complete=\"() => handleAnimationComplete(index)\"\n >\n {{ segment === ' ' ? '\\u00A0' : segment\n }}{{ animateBy === 'words' && index < elements.length - 1 ? '\\u00A0' : '' }}\n </Motion>\n </p>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\nimport { Motion } from 'motion-v';\n\ntype AnimateBy = 'words' | 'letters';\ntype Direction = 'top' | 'bottom';\ntype AnimationSnapshot = Record<string, string | number>;\n\ninterface BlurTextProps {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: AnimateBy;\n direction?: Direction;\n threshold?: number;\n rootMargin?: string;\n animationFrom?: AnimationSnapshot;\n animationTo?: AnimationSnapshot[];\n easing?: (t: number) => number;\n onAnimationComplete?: () => void;\n stepDuration?: number;\n}\n\nconst props = withDefaults(defineProps<BlurTextProps>(), {\n text: '',\n delay: 200,\n className: '',\n animateBy: 'words' as AnimateBy,\n direction: 'top' as Direction,\n threshold: 0.1,\n rootMargin: '0px',\n easing: (t: number) => t,\n stepDuration: 0.35\n});\n\nconst buildKeyframes = (\n from: AnimationSnapshot,\n steps: AnimationSnapshot[]\n): Record<string, Array<string | number>> => {\n const keys = new Set<string>([...Object.keys(from), ...steps.flatMap(step => Object.keys(step))]);\n\n const keyframes: Record<string, Array<string | number>> = {};\n\n for (const key of keys) {\n keyframes[key] = [from[key], ...steps.map(step => step[key])];\n }\n\n return keyframes;\n};\n\nconst elements = computed(() => (props.animateBy === 'words' ? props.text.split(' ') : props.text.split('')));\n\nconst defaultFrom = computed<AnimationSnapshot>(() =>\n props.direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 }\n);\n\nconst defaultTo = computed<AnimationSnapshot[]>(() => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: props.direction === 'top' ? 5 : -5\n },\n {\n filter: 'blur(0px)',\n opacity: 1,\n y: 0\n }\n]);\n\nconst fromSnapshot = computed(() => props.animationFrom ?? defaultFrom.value);\nconst toSnapshots = computed(() => props.animationTo ?? defaultTo.value);\n\nconst stepCount = computed(() => toSnapshots.value.length + 1);\nconst totalDuration = computed(() => props.stepDuration * (stepCount.value - 1));\nconst times = computed(() =>\n Array.from({ length: stepCount.value }, (_, i) => (stepCount.value === 1 ? 0 : i / (stepCount.value - 1)))\n);\n\nconst inView = ref(false);\nconst animationKey = ref(0);\nconst completionFired = ref(false);\nconst rootRef = useTemplateRef<HTMLParagraphElement>('rootRef');\n\nlet observer: IntersectionObserver | null = null;\n\nconst setupObserver = () => {\n if (!rootRef.value) return;\n\n observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n inView.value = true;\n observer?.unobserve(rootRef.value as Element);\n }\n },\n {\n threshold: props.threshold,\n rootMargin: props.rootMargin\n }\n );\n\n observer.observe(rootRef.value);\n};\n\nconst getAnimateKeyframes = () => {\n return buildKeyframes(fromSnapshot.value, toSnapshots.value);\n};\n\nconst getTransition = (index: number) => {\n return {\n duration: totalDuration.value,\n times: times.value,\n delay: (index * props.delay) / 1000,\n ease: props.easing\n };\n};\n\nconst handleAnimationComplete = (index: number) => {\n if (index === elements.value.length - 1 && !completionFired.value && props.onAnimationComplete) {\n completionFired.value = true;\n props.onAnimationComplete();\n }\n};\n\nonMounted(() => {\n setupObserver();\n});\n\nonUnmounted(() => {\n observer?.disconnect();\n});\n\nwatch([() => props.threshold, () => props.rootMargin], () => {\n observer?.disconnect();\n setupObserver();\n});\n\nwatch([() => props.delay, () => props.stepDuration, () => props.animateBy, () => props.direction], () => {\n animationKey.value++;\n completionFired.value = false;\n});\n</script>\n","path":"BlurText/BlurText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]} |