"use client" import React, { useMemo } from "react" import { createSeededRNG } from "../../utils/random" import { useCountWithInterval, useTick } from "./hooks" import { tween } from "./utils" type Layer = { text: string type: "base" | "random" pattern: boolean[] } export type CipherTextProps = { children: string active?: boolean paused?: boolean interval?: number charSet?: string infinite?: boolean progress?: number initialProgress?: number } export function CipherText({ children: text, active = true, paused = false, charSet = "-_~`!@#$%^&*()+=[]{}|;:,.<>?", interval: intervalMs = 50, infinite = false, progress: forcedProgress, initialProgress = 0, }: CipherTextProps) { const size = text.length /** * Progress for the text layer */ const autoProgress = useCountWithInterval(initialProgress, { max: size, paused, increase: active, intervalMs, }) const progress = forcedProgress ?? autoProgress /** * Progress for the infinite layer */ const infiniteProgress = useCountWithInterval(infinite ? size : 0, { max: size, increase: infinite, intervalMs, }) useTick(infiniteProgress > 0, intervalMs) /** * Determining layers */ const random = useMemo(() => createSeededRNG(text), [text]) const percentComplete = progress / size const baseRandomLayer = new Array(size).fill("") const layers = [ // Base text layer { text, pattern: new Array(size).fill(false).map((_, i) => progress > i), }, // Cipher layer { text: baseRandomLayer.map( () => charSet[Math.floor(random() * charSet.length)], ), pattern: baseRandomLayer.map((_, i) => progress > i ? random() > tween(0.25, 1, percentComplete) : false, ), }, // Infinite layer (just random cipher if enabled) infiniteProgress > 0 ? { text: baseRandomLayer.map( () => charSet[Math.floor(random() * charSet.length)], ), pattern: new Array(size) .fill(false) .map(() => infiniteProgress / size > random()), } : undefined, ].filter(Boolean) as Layer[] /** * Merging layers into a single string */ const mergedLayers = useMemo(() => { const text = new Array(size).fill("") for (const layer of layers) { for (let i = 0; i < layer.text.length; i++) { if (layer.pattern?.[i] === false) { continue } text[i] = layer.text[i] } } return text.join("").trim() }, [layers, size]) return <>{mergedLayers} }