文字解码
悬停时,每个字会快速闪过随机字符,再从左到右依次定格,像屏幕在解码。
用 React 写一个不依赖第三方库的文字解码效果,在悬停和键盘聚焦时播放。保存目标文字,开始时运行约 700ms 的 requestAnimationFrame 循环。每一帧中,进度位置之前的字显示最终文字,之后的字显示从字符池“动效光影节奏速度弧线弹性渐变层次空间时间”中随机选出的汉字。空格保持为空格。组件卸载或开始新一轮时取消循环。中文字宽一致,因此宽度不会跳动;把真实文字放在 aria-label 中,避免屏幕阅读器读出乱码。在 prefers-reduced-motion 下直接显示最终文字,不做解码。文字为“信号正在解码”。
源代码
'use client'
import { useEffect, useRef, useState } from 'react'
import s from './demo.module.css'
const DURATION = 700
export default function TextScramble({ labels }: { labels: Record<string, string> }) {
const target = labels.text
const pool = Array.from(labels.glyphs)
const [shown, setShown] = useState(target)
const frame = useRef(0)
useEffect(() => () => cancelAnimationFrame(frame.current), [])
const run = () => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
cancelAnimationFrame(frame.current)
const chars = Array.from(target)
const start = performance.now()
const tick = (now: number) => {
const progress = Math.min((now - start) / DURATION, 1)
const locked = Math.floor(progress * chars.length)
setShown(
chars
.map((c, i) => (i < locked || c === ' ' ? c : pool[Math.floor(Math.random() * pool.length)]))
.join(''),
)
if (progress < 1) frame.current = requestAnimationFrame(tick)
}
frame.current = requestAnimationFrame(tick)
}
return (
<span className={s.text} tabIndex={0} aria-label={target} onPointerEnter={run} onFocus={run}>
<span aria-hidden="true">{shown}</span>
</span>
)
}
.text {
padding: 8px 12px;
border-radius: var(--radius-sm);
font: 600 var(--text-2xl) / 1.2 var(--font-mono);
letter-spacing: -0.01em;
color: var(--stage-ink);
white-space: nowrap;
cursor: default;
}
.text:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}