文字解密
文字以随机字符快速滚动后逐字解密为最终内容,常见于黑客终端和科技品牌网站。
交互预览
动效定义与代码
{
"slug": "text-scramble",
"name": "文字解密",
"nameEn": "Text Scramble",
"description": "文字以随机字符快速滚动后逐字解密为最终内容,常见于黑客终端和科技品牌网站。",
"descriptionEn": "Characters cycle through random glyphs before resolving into the final text, one character at a time. A staple of hacker terminals and tech brand hero sections.",
"category": "text",
"tags": [
"text",
"scramble",
"decode",
"hacker",
"sequential"
],
"trigger": "on-mount",
"difficulty": "intermediate",
"duration": "1.5s",
"easing": "linear",
"cssProperties": [
"content"
],
"isGPUAccelerated": false,
"previewBg": "dark",
"keywords": [
"scramble",
"decode",
"cipher",
"hacker",
"terminal",
"reveal text",
"random characters"
],
"useCases": [
"Hero headline reveal",
"Hacker/tech aesthetic landing pages",
"Portfolio name entrance",
"Navigation link hover effect"
],
"relatedAnimations": [
"typewriter",
"glitch-text",
"text-reveal"
],
"recommendedStyles": [
"cyberpunk-neon",
"sci-fi-hud",
"dark-mode",
"github-style"
],
"codeSnippets": [
{
"label": "CSS Keyframes",
"language": "css",
"code": "/* Text scramble relies on JavaScript to swap characters.\n CSS handles the smooth cursor and container styling. */\n\n.text-scramble {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n display: inline-block;\n white-space: pre;\n}\n\n.text-scramble-char {\n display: inline-block;\n will-change: opacity;\n transition: opacity 80ms ease-out;\n}\n\n.text-scramble-char[data-resolved=\"false\"] {\n opacity: 0.5;\n color: var(--scramble-color, #00ffff);\n}\n\n.text-scramble-char[data-resolved=\"true\"] {\n opacity: 1;\n color: inherit;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .text-scramble-char {\n transition: none;\n opacity: 1 !important;\n color: inherit !important;\n }\n}"
},
{
"label": "Tailwind CSS",
"language": "css",
"code": "/* Text scramble — JS-driven, styled with Tailwind utilities */\n\n@utility text-scramble-base {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n display: inline-block;\n white-space: pre;\n}\n\n/* Usage:\n <span class=\"text-scramble-base font-mono text-lg\">\n <!-- JS swaps inner chars -->\n </span>\n*/"
},
{
"label": "AnimeJS",
"language": "tsx",
"code": "\"use client\";\n\nimport { animate, scrambleText } from \"animejs\";\nimport { useEffect, useRef } from \"react\";\n\ninterface TextScrambleProps {\n text: string;\n className?: string;\n}\n\nexport function TextScramble({ text, className }: TextScrambleProps) {\n const ref = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n const element = ref.current;\n if (!element) return;\n\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n element.textContent = text;\n return;\n }\n\n element.textContent = text;\n\n const animation = animate(element, {\n textContent: scrambleText({\n text,\n chars: \"A-Z0-9!%#_\",\n override: \"A-Z0-9!%#_\",\n from: \"left\",\n cursor: \"_\",\n revealRate: 42,\n settleDuration: 320,\n settleRate: 28,\n }),\n });\n\n return () => {\n animation.revert();\n element.textContent = text;\n };\n }, [text]);\n\n return (\n <span ref={ref} className={className}>\n {text}\n </span>\n );\n}"
},
{
"label": "Framer Motion",
"language": "tsx",
"code": "\"use client\";\n\nimport { useEffect, useState, useCallback } from \"react\";\n\nconst CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*\";\n\ninterface TextScrambleProps {\n text: string;\n /** ms per character resolve */\n speed?: number;\n className?: string;\n}\n\nexport function TextScramble({ text, speed = 50, className }: TextScrambleProps) {\n const [display, setDisplay] = useState(text.split(\"\").map(() => CHARS[Math.floor(Math.random() * CHARS.length)]));\n const [resolved, setResolved] = useState<boolean[]>(new Array(text.length).fill(false));\n\n const scramble = useCallback(() => {\n let frame = 0;\n const interval = setInterval(() => {\n setDisplay((prev) =>\n prev.map((ch, i) =>\n i <= frame ? text[i] : CHARS[Math.floor(Math.random() * CHARS.length)]\n )\n );\n setResolved((prev) => prev.map((_, i) => i <= frame));\n frame++;\n if (frame >= text.length) clearInterval(interval);\n }, speed);\n return () => clearInterval(interval);\n }, [text, speed]);\n\n useEffect(() => {\n const cleanup = scramble();\n return cleanup;\n }, [scramble]);\n\n return (\n <span className={className} style={{ fontFamily: \"monospace\" }}>\n {display.map((ch, i) => (\n <span key={i} style={{ opacity: resolved[i] ? 1 : 0.5, color: resolved[i] ? \"inherit\" : \"#00ffff\" }}>\n {ch}\n </span>\n ))}\n </span>\n );\n}"
}
]
}