# 乱码解码 Scramble Text > 标题先显示一串乱跳的字符,再从左到右一个个定格成正确的字。 分类:动效 · 角色:点缀(贴在某个元素上,一页两三个) [全部组件和流程](https://www.dachuiai.com/llms.txt) · [动效选型规则](https://www.dachuiai.com/guides/motion.md) ## 给 AI:用户把这个组件发给了你 1. 先看用户的项目:框架、页面类型、现在的风格、手机还是电脑为主。 2. 对照下面的「适合 / 不适合」判断合不合适。不合适就直接说,并从[动效选型规则](https://www.dachuiai.com/guides/motion.md)或[全部组件](https://www.dachuiai.com/llms.txt)里另挑一个。 3. 合适就按「装进项目」装好,示例里的文字和图片换成用户自己的。 4. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:科技、开发者、安全、AI 产品的标题或关键数字。 - 不适合:正文和长句;温暖、手作、亲子风格;同一屏已经有打字机。 - 适合的网站:SaaS、官网、作品集、活动页 - 放在:标题 - 手机上:好用:出现时播一次;悬停重播在手机上没有。 - 性能代价:低:只在出场那一两秒换文字。 - 常搭配:[点阵波纹 Dot Grid](https://www.dachuiai.com/c/dot-grid.md)、[粒子聚合 Particle Morph](https://www.dachuiai.com/c/particle-morph.md)、[光标跟随 Custom Cursor](https://www.dachuiai.com/c/custom-cursor.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react`(lucide-react 只有最后的演示示例用到;不留示例的话,删掉对应的 import 就不用装) - 文件:原样存成 `scramble-text.tsx`(比如 `src/components/scramble-text.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/scramble-text.tsx),比手抄准:`curl -fsSL -o src/components/scramble-text.tsx https://www.dachuiai.com/code/scramble-text.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`useScramble`、`ScrambleText` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 保留代码里 `prefers-reduced-motion` 的处理:用户开了「减弱动态效果」时内容照样完整显示。 - Next.js(有 app 文件夹):文件第一行加 `'use client';`。 - 用 JavaScript 不用 TypeScript:Vite 和 Next.js 都能直接用 .tsx 文件;也可以删掉类型标注,改名 .jsx。 - 没有 Tailwind:先装 Tailwind CSS v4(Vite 项目 `npm i tailwindcss @tailwindcss/vite`,vite 配置的 plugins 里加 `tailwindcss()`,入口引入的那个 CSS 文件(一般是 `src/index.css`)第一行写 `@import "tailwindcss";`),或者把类名改写成项目用的样式。 - 已经装了 Tailwind v3:别为了一个组件升级。类名大多通用,少数 v4 写法要换,比如焦点框 `outline-2` 在 v3 里要写成 `outline outline-2`。 - 不是 React(Vue、Svelte、小程序、纯 HTML):照着代码的做法改写,时长、曲线、参数保持一样。Wix、有赞这类建站平台装不进代码,告诉用户去平台自带的动效设置里找。 ## 参数 **useScramble** | 参数 | 默认值 | 作用 | | --- | --- | --- | | `text` | | 要显示的文字 | | `delay` | `0` | 延迟多久才开始(毫秒) | | `speed` | `45` | 多少毫秒换一次乱码,越小越快 | | `settle` | `6` | 每个字乱跳多少次才定格,越大越久 | **ScrambleText** | 参数 | 默认值 | 作用 | | --- | --- | --- | | `text` | | 要显示的文字 | | `className` | `''` | 加在最外层的类名,用来调大小、位置、间距 | | `delay` | `0` | 延迟多久才开始(毫秒) | ## 你可以说 > 标题加 Scramble Text 乱码解码效果:先显示一串乱跳的字符,再从左到右一个个定格成正确的字,鼠标移上去再播一次。 ## 完整代码 ```tsx import {useCallback, useEffect, useRef, useState} from 'react'; import {RotateCcw} from 'lucide-react'; /* --------------------------------------------------------------- * ScrambleText 乱码解码 * 文字先是一串乱跳的字符,再从左到右一个个定格成正确的字。 * 出现时播放一次;鼠标移上去会再解码一次。 * ------------------------------------------------------------- */ const GLYPHS = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#$%&@*'; export function useScramble(text: string, {delay = 0, speed = 45, settle = 6} = {}) { const [out, setOut] = useState(text); const [done, setDone] = useState(true); const raf = useRef(0); const play = useCallback(() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return setOut(text); cancelAnimationFrame(raf.current); setDone(false); const start = performance.now() + delay; let last = 0; const tick = (now: number) => { raf.current = requestAnimationFrame(tick); if (now < start || now - last < speed) return; last = now; const fixed = Math.floor((now - start) / speed / settle * 2); // 已经定格的字数 const s = text.split('').map((ch, i) => (ch === ' ' || i < fixed ? ch : GLYPHS[(Math.random() * GLYPHS.length) | 0])).join(''); setOut(s); if (fixed >= text.length) { cancelAnimationFrame(raf.current); setDone(true); } }; raf.current = requestAnimationFrame(tick); }, [text, delay, speed, settle]); useEffect(() => { play(); return () => cancelAnimationFrame(raf.current); }, [play]); return {text: out, done, play}; } export function ScrambleText({text, className = '', delay = 0}: {text: string; className?: string; delay?: number}) { const s = useScramble(text, {delay}); return ( s.done && s.play()} className={className}> {s.text.split('').map((ch, i) => ( {ch} ))} ); } /* ---------------- 示例:终端窗口里的标题 ---------------- */ export default function ScrambleTextDemo() { const [run, setRun] = useState(0); return (
{['#F87171', '#FBBF24', '#34D399'].map((c) => )} decode.ts
); } ```