mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-04-21 17:44:39 -06:00
c5d66b6279
Co-authored-by: DavidHDev <48634587+DavidHDev@users.noreply.github.com>
1 line
7.5 KiB
JSON
1 line
7.5 KiB
JSON
{"name":"TrueFocus","title":"TrueFocus","description":"Applies dynamic blur / clarity based over a series of words in order.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { motion } from 'motion-v';\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch, useTemplateRef, type Ref } from 'vue';\n\ninterface TrueFocusProps {\n sentence?: string;\n manualMode?: boolean;\n blurAmount?: number;\n borderColor?: string;\n glowColor?: string;\n animationDuration?: number;\n pauseBetweenAnimations?: number;\n index?: Array<number>;\n syncGroup?: string;\n defaultBlur?: boolean;\n}\n\nconst props = withDefaults(defineProps<TrueFocusProps>(), {\n sentence: 'True Focus',\n manualMode: false,\n blurAmount: 5,\n borderColor: 'green',\n glowColor: 'rgba(0, 255, 0, 0.6)',\n animationDuration: 0.5,\n pauseBetweenAnimations: 1,\n defaultBlur: true\n});\n\nconst words = computed(() => props.sentence.split(' '));\n\nconst currentIndex = props.syncGroup ? getSyncGroupIndex(props.syncGroup) : ref(-1);\nconst lastActiveIndex = ref<number | null>(null);\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst wordRefs = ref<HTMLSpanElement[]>([]);\nconst focusRect = ref({ x: 0, y: 0, width: 0, height: 0 });\n\nlet interval: ReturnType<typeof setInterval> | null = null;\n\nwatch(\n [currentIndex, () => words.value.length],\n async () => {\n if (currentIndex.value === null || currentIndex.value === -1) return;\n\n let actualWordIndex = currentIndex.value;\n if (props.index) {\n actualWordIndex = props.index.findIndex(val => val === currentIndex.value);\n if (actualWordIndex === -1) return;\n }\n\n if (!wordRefs.value[actualWordIndex] || !containerRef.value) return;\n\n await nextTick();\n\n const parentRect = containerRef.value.getBoundingClientRect();\n const activeRect = wordRefs.value[actualWordIndex].getBoundingClientRect();\n\n focusRect.value = {\n x: activeRect.left - parentRect.left,\n y: activeRect.top - parentRect.top,\n width: activeRect.width,\n height: activeRect.height\n };\n },\n { immediate: true }\n);\n\nconst handleMouseEnter = (index: number) => {\n if (props.manualMode) {\n const mappedIndex = props.index ? props.index[index] : index;\n lastActiveIndex.value = mappedIndex;\n currentIndex.value = mappedIndex;\n }\n};\n\nconst handleMouseLeave = () => {\n if (props.manualMode) {\n if (props.defaultBlur) {\n currentIndex.value = lastActiveIndex.value || 0;\n return;\n }\n currentIndex.value = -1;\n }\n};\n\nconst setWordRef = (el: HTMLSpanElement | null, index: number) => {\n if (el) wordRefs.value[index] = el;\n};\n\nconst startInterval = () => {\n if (interval) clearInterval(interval);\n if (!props.manualMode) {\n interval = setInterval(\n () => {\n currentIndex.value = (currentIndex.value + 1) % words.value.length;\n },\n (props.animationDuration + props.pauseBetweenAnimations) * 1000\n );\n }\n};\n\nonMounted(async () => {\n await nextTick();\n\n const isOwner = props.syncGroup ? registerSyncGroup(props.syncGroup) : true;\n\n let initialWordIndex = 0;\n if (props.index && currentIndex.value !== null && currentIndex.value !== undefined) {\n const foundIndex = props.index.findIndex(val => val === currentIndex.value);\n if (foundIndex !== -1) initialWordIndex = foundIndex;\n }\n\n if (wordRefs.value[initialWordIndex] && containerRef.value) {\n const parentRect = containerRef.value.getBoundingClientRect();\n const activeRect = wordRefs.value[initialWordIndex].getBoundingClientRect();\n focusRect.value = {\n x: activeRect.left - parentRect.left,\n y: activeRect.top - parentRect.top,\n width: activeRect.width,\n height: activeRect.height\n };\n }\n\n if (isOwner) {\n watch(\n [() => props.manualMode, () => props.animationDuration, () => props.pauseBetweenAnimations, () => words.value],\n () => startInterval(),\n { immediate: true }\n );\n }\n});\n\nonUnmounted(() => {\n if (interval) clearInterval(interval);\n if (props.syncGroup) unregisterSyncGroup(props.syncGroup);\n});\n</script>\n\n<script lang=\"ts\">\ninterface SyncGroupState {\n index: Ref<number>;\n count: number;\n}\n\nexport const syncGroupMap = new Map<string, SyncGroupState>();\n\nexport function getSyncGroupIndex(group: string): Ref<number> {\n if (!syncGroupMap.has(group)) {\n syncGroupMap.set(group, { index: ref(-1), count: 0 });\n }\n return syncGroupMap.get(group)!.index;\n}\n\nexport function registerSyncGroup(group: string): boolean {\n const state = syncGroupMap.get(group)!;\n state.count += 1;\n return state.count === 1;\n}\n\nexport function unregisterSyncGroup(group: string): void {\n if (!syncGroupMap.has(group)) return;\n const state = syncGroupMap.get(group)!;\n state.count -= 1;\n if (state.count <= 0) {\n syncGroupMap.delete(group);\n }\n}\n</script>\n\n<template>\n <div class=\"relative flex flex-wrap justify-center items-center gap-[1em]\" ref=\"containerRef\">\n <span\n v-for=\"(word, index) in words\"\n :key=\"props.index ? props.index[index] : index\"\n :ref=\"el => setWordRef(el as HTMLSpanElement, index)\"\n class=\"relative font-black text-7xl transition-[filter,color] duration-300 ease-in-out cursor-pointer\"\n :style=\"{\n filter:\n currentIndex === -1 || (props.index ? props.index[index] : index) === currentIndex\n ? 'blur(0px)'\n : `blur(${blurAmount}px)`,\n '--border-color': borderColor,\n '--glow-color': glowColor,\n transition: `filter ${animationDuration}s ease`\n }\"\n @mouseenter=\"handleMouseEnter(index)\"\n @mouseleave=\"handleMouseLeave\"\n >\n {{ word }}\n </span>\n\n <motion.div\n class=\"top-0 left-0 box-content absolute border-none pointer-events-none\"\n :animate=\"{\n x: focusRect.x,\n y: focusRect.y,\n width: focusRect.width,\n height: focusRect.height,\n opacity: currentIndex >= 0 ? 1 : 0\n }\"\n :transition=\"{\n duration: animationDuration\n }\"\n :style=\"{\n '--border-color': borderColor,\n '--glow-color': glowColor\n }\"\n >\n <span\n class=\"top-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-r-0 border-b-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n\n <span\n class=\"top-[-10px] right-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-b-0 border-l-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n\n <span\n class=\"bottom-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-t-0 border-r-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n\n <span\n class=\"right-[-10px] bottom-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-t-0 border-l-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n </motion.div>\n </div>\n</template>\n","path":"TrueFocus/TrueFocus.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.10.2"}],"devDependencies":[],"categories":["TextAnimations"]} |