dqnamoexperiments / scramble-text
Scramble Text
A small text component that masks changed text with encrypted characters, then reveals the new value one character at a time.
dqnamo.com/invite/7K4F2Q9M-X8NV
"use client";import { useEffect, useState } from "react";import { cn } from "@/helpers/classname-helper";type ScrambleTextProps = { children: string; className?: string; intervalMs?: number;};const ENCRYPTED_TEXT_CHARS = "-_~`!@#$%^&*()+=[]{}|;:,.<>?";const MAX_REVEAL_STEPS = 48;type ScrambleMode = "random" | "stable";function getTextSegments(text: string) { if (typeof Intl !== "undefined" && "Segmenter" in Intl) { const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme", }); return Array.from(segmenter.segment(text), ({ segment }) => segment); } return Array.from(text);}function getRandomEncryptedTextChar() { return ENCRYPTED_TEXT_CHARS[ Math.floor(Math.random() * ENCRYPTED_TEXT_CHARS.length) ];}function getStableEncryptedTextChar(segment: string, index: number) { let hash = index + 1; for (const character of segment) { hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % 2147483647; } return ENCRYPTED_TEXT_CHARS[hash % ENCRYPTED_TEXT_CHARS.length];}function getEncryptedTextChar( segment: string, index: number, mode: ScrambleMode,) { if (mode === "stable") { return getStableEncryptedTextChar(segment, index); } return getRandomEncryptedTextChar();}function shouldPreserveSegment(segment: string) { return segment.trim() === "";}function scrambleSegments( segments: string[], revealedCount: number, mode: ScrambleMode,) { return segments .map((character, index) => { if (shouldPreserveSegment(character) || index < revealedCount) { return character; } return getEncryptedTextChar(character, index, mode); }) .join("");}function scrambleText(text: string, revealedCount: number, mode: ScrambleMode) { return scrambleSegments(getTextSegments(text), revealedCount, mode);}function getRevealStep(segmentCount: number) { return Math.max(1, Math.ceil(segmentCount / MAX_REVEAL_STEPS));}function shouldReduceMotion() { return window.matchMedia("(prefers-reduced-motion: reduce)").matches;}export function ScrambleText({ children, className, intervalMs = 32,}: ScrambleTextProps) { const [displayText, setDisplayText] = useState(() => scrambleText(children, 0, "stable"), ); useEffect(() => { const segments = getTextSegments(children); if (segments.length === 0 || intervalMs <= 0 || shouldReduceMotion()) { setDisplayText(children); return; } let revealedCount = 0; const revealStep = getRevealStep(segments.length); setDisplayText(scrambleSegments(segments, revealedCount, "random")); const timer = window.setInterval(() => { revealedCount = Math.min(segments.length, revealedCount + revealStep); setDisplayText(scrambleSegments(segments, revealedCount, "random")); if (revealedCount >= segments.length) { window.clearInterval(timer); } }, intervalMs); return () => { window.clearInterval(timer); }; }, [children, intervalMs]); return ( <span className={cn("inline-block", className)}> <span aria-hidden="true">{displayText}</span> <span aria-atomic="true" aria-live="polite" className="sr-only"> {children} </span> </span> );}Want to work with me?
I run a lil design engineering studio in London where we do fractional design engineering for startups.
THEINTERFACECOMPANYOF LONDON