Created
August 30, 2024 11:53
-
-
Save schonert/6c4e1885fa9eb74b333b3e5398a81bc6 to your computer and use it in GitHub Desktop.
TextReveal.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useState } from "react"; | |
| interface TextEncryptedProps { | |
| text: string | number; | |
| className?: string; | |
| duration?: number; | |
| delay?: number; | |
| useTextCharacters?: boolean; | |
| } | |
| const CHARACTERS = | |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
| function randomizeText(text: string, useTextCharacters = true) { | |
| const chars = useTextCharacters | |
| ? text.split("").filter((c) => c != " ") | |
| : CHARACTERS; | |
| return text | |
| .split("") | |
| .map((c) => | |
| c == " " ? " " : chars[Math.floor(Math.random() * chars.length)] | |
| ) | |
| .join(""); | |
| } | |
| function animationRunner( | |
| duration: number, | |
| callback: (progress: number) => void | |
| ) { | |
| let animationFrameId: number; | |
| let startTime = 0; | |
| let previousProgress: number = 0; | |
| const animate = (timestamp: number) => { | |
| startTime = startTime || timestamp; | |
| let progress = (timestamp - startTime) / duration; | |
| progress = cubicInOut(progress); | |
| progress = parseFloat(progress.toFixed(2)); | |
| if (progress > 1) { | |
| return callback(1); | |
| } | |
| // Only callback if progress has changed | |
| if (progress != previousProgress) { | |
| callback(progress); | |
| } | |
| previousProgress = progress; | |
| animationFrameId = requestAnimationFrame(animate); | |
| }; | |
| animationFrameId = requestAnimationFrame(animate); | |
| return () => cancelAnimationFrame(animationFrameId); | |
| } | |
| function revealText( | |
| element: HTMLElement, | |
| text: string, | |
| duration: number, | |
| useTextCharacters = true, | |
| callback: () => void | |
| ) { | |
| const abortAnimation = animationRunner(duration, (progress) => { | |
| const charIndex = Math.floor(progress * text.length); | |
| const newText = text.slice(0, charIndex); | |
| const remainderText = randomizeText( | |
| text.slice(newText.length), | |
| useTextCharacters | |
| ); | |
| const highlightChunkSize = Math.max(Math.ceil(remainderText.length / 3), 3); | |
| const highlightedText = remainderText.slice( | |
| 0, | |
| Math.max(highlightChunkSize, 3) | |
| ); | |
| const dimmedHighlightedText = remainderText.slice( | |
| highlightedText.length, | |
| highlightedText.length + highlightChunkSize | |
| ); | |
| const dimmedText = remainderText.slice( | |
| dimmedHighlightedText.length + highlightedText.length, | |
| -1 | |
| ); | |
| if (!element) { | |
| return abortAnimation(); | |
| } | |
| if (progress === 1) { | |
| callback(); | |
| element.innerHTML = `<span>${text}</span>`; | |
| } else { | |
| element.innerHTML = ` | |
| <span>${newText}</span><span style="opacity: 0.9;">${highlightedText}</span><span style="opacity:0.3; font-size: 0.93em">${dimmedHighlightedText}</span><span style="opacity:0.2; font-size: 0.9em;">${dimmedText}</span>`; | |
| } | |
| }); | |
| return abortAnimation; | |
| } | |
| export function TextReveal({ | |
| text, | |
| duration = 750, | |
| delay = 0, | |
| className, | |
| useTextCharacters = true, | |
| }: TextEncryptedProps) { | |
| const [completed, setCompleted] = useState(""); | |
| function startReveal(element: HTMLParagraphElement) { | |
| const animateText = text.toString(); | |
| if (element && completed !== animateText) { | |
| const box = element.getBoundingClientRect(); | |
| element.style.height = `${box.height}px`; | |
| element.style.width = `${box.width}px`; | |
| element.style.overflow = "clip"; | |
| const placeholderText = randomizeText(animateText, useTextCharacters); | |
| element.innerHTML = `<span style="opacity: 0.15;">${placeholderText}</span>`; | |
| setTimeout(() => { | |
| revealText(element, animateText, duration, useTextCharacters, () => { | |
| setCompleted(animateText); | |
| element.style.height = ""; | |
| element.style.width = ""; | |
| element.style.overflow = ""; | |
| }); | |
| }, delay); | |
| } | |
| } | |
| return ( | |
| <p ref={startReveal} key={text} className={className}> | |
| <span style={{ opacity: 0 }}>{text}</span> | |
| </p> | |
| ); | |
| } | |
| function cubicInOut(progress: number) { | |
| return progress < 0.5 | |
| ? 4 * progress * progress * progress | |
| : 1 - Math.pow(-2 * progress + 2, 3) / 2; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment