Files
vue-bits/public/r/DecryptedText.json
2026-01-21 16:08:55 +05:30

1 line
7.2 KiB
JSON

{"name":"DecryptedText","title":"DecryptedText","description":"Hacker-style decryption cycling random glyphs until resolving to real text.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';\n\ninterface DecryptedTextProps {\n text: string;\n speed?: number;\n maxIterations?: number;\n sequential?: boolean;\n revealDirection?: 'start' | 'end' | 'center';\n useOriginalCharsOnly?: boolean;\n characters?: string;\n className?: string;\n encryptedClassName?: string;\n parentClassName?: string;\n animateOn?: 'view' | 'hover';\n}\n\nconst props = withDefaults(defineProps<DecryptedTextProps>(), {\n text: '',\n speed: 50,\n maxIterations: 10,\n sequential: false,\n revealDirection: 'start',\n useOriginalCharsOnly: false,\n characters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className: '',\n parentClassName: '',\n encryptedClassName: '',\n animateOn: 'hover'\n});\n\nconst emit = defineEmits<{\n animationComplete: [];\n}>();\n\nconst containerRef = useTemplateRef<HTMLSpanElement>('containerRef');\nconst displayText = ref(props.text);\nconst isHovering = ref(false);\nconst isScrambling = ref(false);\nconst revealedIndices = ref(new Set<number>());\nconst hasAnimated = ref(false);\n\nlet interval: ReturnType<typeof setInterval> | null = null;\nlet intersectionObserver: IntersectionObserver | null = null;\n\nwatch(\n [\n () => isHovering.value,\n () => props.text,\n () => props.speed,\n () => props.maxIterations,\n () => props.sequential,\n () => props.revealDirection,\n () => props.characters,\n () => props.useOriginalCharsOnly\n ],\n () => {\n let currentIteration = 0;\n\n const getNextIndex = (revealedSet: Set<number>): number => {\n const textLength = props.text.length;\n switch (props.revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n const availableChars = props.useOriginalCharsOnly\n ? Array.from(new Set(props.text.split(''))).filter(char => char !== ' ')\n : props.characters.split('');\n\n const shuffleText = (originalText: string, currentRevealed: Set<number>): string => {\n if (props.useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n }\n };\n\n if (interval) {\n clearInterval(interval);\n interval = null;\n }\n\n if (isHovering.value) {\n isScrambling.value = true;\n interval = setInterval(() => {\n if (props.sequential) {\n if (revealedIndices.value.size < props.text.length) {\n const nextIndex = getNextIndex(revealedIndices.value);\n const newRevealed = new Set(revealedIndices.value);\n newRevealed.add(nextIndex);\n revealedIndices.value = newRevealed;\n displayText.value = shuffleText(props.text, newRevealed);\n } else {\n clearInterval(interval!);\n interval = null;\n isScrambling.value = false;\n emit('animationComplete');\n }\n } else {\n displayText.value = shuffleText(props.text, revealedIndices.value);\n currentIteration++;\n if (currentIteration >= props.maxIterations) {\n clearInterval(interval!);\n interval = null;\n isScrambling.value = false;\n displayText.value = props.text;\n emit('animationComplete');\n }\n }\n }, props.speed);\n } else {\n displayText.value = props.text;\n revealedIndices.value = new Set();\n isScrambling.value = false;\n }\n }\n);\n\nconst handleMouseEnter = () => {\n if (props.animateOn === 'hover') {\n isHovering.value = true;\n }\n};\n\nconst handleMouseLeave = () => {\n if (props.animateOn === 'hover') {\n isHovering.value = false;\n }\n};\n\nonMounted(async () => {\n if (props.animateOn === 'view') {\n await nextTick();\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated.value) {\n isHovering.value = true;\n hasAnimated.value = true;\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n intersectionObserver = new IntersectionObserver(observerCallback, observerOptions);\n if (containerRef.value) {\n intersectionObserver.observe(containerRef.value);\n }\n }\n});\n\nonUnmounted(() => {\n if (interval) {\n clearInterval(interval);\n }\n if (intersectionObserver && containerRef.value) {\n intersectionObserver.unobserve(containerRef.value);\n }\n});\n</script>\n\n<template>\n <span\n ref=\"containerRef\"\n :class=\"`inline-block whitespace-pre-wrap ${props.parentClassName}`\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n >\n <span class=\"sr-only\">{{ displayText }}</span>\n\n <span aria-hidden=\"true\">\n <span\n v-for=\"(char, index) in displayText.split('')\"\n :key=\"index\"\n :class=\"revealedIndices.has(index) || !isScrambling || !isHovering ? props.className : props.encryptedClassName\"\n >\n {{ char }}\n </span>\n </span>\n </span>\n</template>\n","path":"DecryptedText/DecryptedText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}