返回动效库
文字进阶

文字解密

文字以随机字符快速滚动后逐字解密为最终内容,常见于黑客终端和科技品牌网站。

加载时1.5s

交互预览

动效定义与代码

{
  "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}"
    }
  ]
}