文字解码

motion · text-decode · 灵感来源:Vercel / Linear hero 的标题解码动效

Liquid Glass亲手写一遍,才是研究
左→右扫描锁定 · 空格不参与乱码

复用卡

Text Decode / Scramble · 文字解码 · 动效 / Motion

规格 · 术语提示词(给 AI 用)

做一个 Text Decode / Scramble(文字解码):
- 字符逐个替换成随机字符再落回
- 每字符速率固定,速率与总时长分开定义
- 对可访问性名做处理:屏幕阅读器读最终文本
用这些 CSS 变量:--color-surface --color-text --color-text-muted --color-text-subtle --glass-border --shadow-md --shadow-sm
灵感参考:Vercel / Linear hero 的标题解码动效

参数(CSS 变量)

--color-surface --color-text --color-text-muted --color-text-subtle --glass-border --shadow-md --shadow-sm

必缺状态

(无必缺状态)

整页场景

(尚未接进整页场景)

安装(一条链接)

参考(本站可核对)

调研笔记

文字解码

拆解

  • 单向扫描锁定:左→右,指针左侧锁定为定稿,右侧持续乱码翻涌
  • 乱码从字符池随机抽取;空格不参与乱码(保持词形)
  • 进度 = elapsed / duration,线性推进,终帧精确落在原文
  • 时长建议 15~20ms/字符

实现取舍

  • 零依赖:rAF 驱动,每帧拼 定稿前缀 + 乱码后缀
  • SSR 输出终稿:首屏不闪空白、SEO/读屏拿到真文本;客户端挂载后打乱再解码
  • prefers-reduced-motion 直接跳过,静态呈现
  • 重播靠外部改 key 重挂载(组件保持无状态对外)

关键参数

  • 默认字符池 A-Z0-9@#$%&*+=/;中文场景传 CJK 字符池避免字形违和
  • 默认时长 900ms

实现

motion/text-decode
import { useEffect, useRef, useState } from 'react';

const DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*+=/';

interface Props {
  text: string;
  /** 乱码字符池;中文文本建议传 CJK 字符池 */
  charset?: string;
  /** 解码总时长 ms */
  duration?: number;
  className?: string;
}

export function TextDecode({
  text,
  charset = DEFAULT_CHARSET,
  duration = 900,
  className = '',
}: Props) {
  // SSR 输出终稿:首屏不闪空白,挂载后再打乱解码
  const [display, setDisplay] = useState(text);
  const raf = useRef(0);

  useEffect(() => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    const chars = charset.split('');
    const rand = () => chars[Math.floor(Math.random() * chars.length)];
    let start: number | null = null;

    const tick = (now: number) => {
      if (start === null) start = now;
      const t = Math.min(1, (now - start) / duration);
      const locked = Math.floor(t * text.length);
      let out = text.slice(0, locked);
      for (let i = locked; i < text.length; i++) {
        out += text[i] === ' ' ? ' ' : rand();
      }
      setDisplay(out);
      if (t < 1) {
        raf.current = requestAnimationFrame(tick);
      } else {
        setDisplay(text);
      }
    };

    raf.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf.current);
  }, [text, charset, duration]);

  return <span className={className}>{display}</span>;
}

元信息

标识
motion/text-decode
状态
done
最后提交
2026-10-30
风格轴
text-decode
依赖 token
--color-surface --color-text --color-text-muted --color-text-subtle --glass-border --shadow-md --shadow-sm
在工作台打开 源码与 notes 都在 src/components/motion/text-decode/