Files
personal_website/src/components/GlitchText.tsx
T

83 lines
2.2 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
const MATRIX_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*!?<>[]{}アイウエオカキクケコサシスセソタチツテトナニヌネノ';
interface GlitchTextProps {
text: string;
className?: string;
glitchOnHover?: boolean;
as?: 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'p';
}
const GlitchText = ({
text,
className = '',
glitchOnHover = true,
as: Component = 'span'
}: GlitchTextProps) => {
const [displayText, setDisplayText] = useState(text);
const [isGlitching, setIsGlitching] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const iterationRef = useRef(0);
const getRandomChar = () => {
return MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
};
const startGlitch = useCallback(() => {
if (!glitchOnHover || isGlitching) return;
setIsGlitching(true);
iterationRef.current = 0;
intervalRef.current = setInterval(() => {
setDisplayText(prev => {
return text
.split('')
.map((char, index) => {
if (char === ' ') return ' ';
// Gradually resolve characters from left to right
if (index < iterationRef.current) {
return text[index];
}
// Random chance to show original or glitch
return Math.random() > 0.5 ? getRandomChar() : char;
})
.join('');
});
iterationRef.current += 1;
// Complete after all characters resolved
if (iterationRef.current > text.length) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
setDisplayText(text);
setIsGlitching(false);
}
}, 50);
}, [text, glitchOnHover, isGlitching]);
const stopGlitch = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
setDisplayText(text);
setIsGlitching(false);
}, [text]);
return (
<Component
className={`${className} ${isGlitching ? 'glitch-active' : ''}`}
onMouseEnter={startGlitch}
onMouseLeave={stopGlitch}
style={{ display: 'inline-block' }}
>
{displayText}
</Component>
);
};
export default GlitchText;