feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
124
apps/web/src/components/GlitchText.tsx
Normal file
124
apps/web/src/components/GlitchText.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useInView } from "framer-motion";
|
||||
|
||||
/**
|
||||
* GlitchText: Binary scramble reveal effect
|
||||
*
|
||||
* Text characters scramble through random binary (0/1) before settling
|
||||
* into the final text. Creates a "decoding" feel.
|
||||
*
|
||||
* Can also be triggered on hover for buttons.
|
||||
*/
|
||||
|
||||
interface GlitchTextProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
trigger?: "inView" | "hover" | "mount";
|
||||
as?: "span" | "div" | "h1" | "h2" | "p";
|
||||
}
|
||||
|
||||
const CHARS = "01";
|
||||
|
||||
export const GlitchText: React.FC<GlitchTextProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
delay = 0,
|
||||
duration = 0.8,
|
||||
trigger = "inView",
|
||||
as: Tag = "span",
|
||||
}) => {
|
||||
const text = children;
|
||||
const [display, setDisplay] = React.useState(text);
|
||||
const [isRevealed, setIsRevealed] = React.useState(false);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const ref = React.useRef<any>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
||||
|
||||
// Initial scramble reveal (on mount or in-view)
|
||||
React.useEffect(() => {
|
||||
if (isRevealed) return;
|
||||
|
||||
const shouldStart =
|
||||
trigger === "mount" || (trigger === "inView" && isInView);
|
||||
if (!shouldStart) return;
|
||||
|
||||
const chars = text.split("");
|
||||
const totalFrames = Math.ceil(duration * 60); // ~60fps
|
||||
const staggerPerChar = totalFrames / chars.length;
|
||||
let frame = 0;
|
||||
|
||||
const delayMs = delay * 1000;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const interval = setInterval(() => {
|
||||
frame++;
|
||||
const revealed = chars.map((char, i) => {
|
||||
if (char === " ") return " ";
|
||||
const charFrame = i * staggerPerChar;
|
||||
if (frame > charFrame + staggerPerChar * 2) return char; // Settled
|
||||
if (frame > charFrame) {
|
||||
// Scrambling phase
|
||||
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
}
|
||||
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
});
|
||||
setDisplay(revealed.join(""));
|
||||
|
||||
if (frame >= totalFrames + staggerPerChar * 2) {
|
||||
setDisplay(text);
|
||||
setIsRevealed(true);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000 / 60);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, delayMs);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [isInView, trigger, text, delay, duration, isRevealed]);
|
||||
|
||||
// Hover re-scramble (for buttons)
|
||||
React.useEffect(() => {
|
||||
if (!isHovering || trigger !== "hover") return;
|
||||
|
||||
const chars = text.split("");
|
||||
const totalFrames = 20; // Quick scramble
|
||||
let frame = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
frame++;
|
||||
const scrambled = chars.map((char, i) => {
|
||||
if (char === " ") return " ";
|
||||
const settle = (frame / totalFrames) * chars.length;
|
||||
if (i < settle) return char;
|
||||
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
});
|
||||
setDisplay(scrambled.join(""));
|
||||
|
||||
if (frame >= totalFrames) {
|
||||
setDisplay(text);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000 / 60);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isHovering, trigger, text]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
className={`${className}`}
|
||||
onMouseEnter={trigger === "hover" ? () => setIsHovering(true) : undefined}
|
||||
onMouseLeave={
|
||||
trigger === "hover" ? () => setIsHovering(false) : undefined
|
||||
}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }} // Prevent layout shift during scramble
|
||||
>
|
||||
{display}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user