108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
// app/components/MatrixRain.tsx
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useTheme } from 'next-themes';
|
|
|
|
interface MatrixRainProps {
|
|
color?: string;
|
|
fps?: number;
|
|
}
|
|
|
|
export default function MatrixRain({ color, fps = 15 }: MatrixRainProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const lastWidth = useRef<number>(0);
|
|
const { resolvedTheme } = useTheme();
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setMounted(true), 0);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!mounted) return;
|
|
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const isLight = resolvedTheme === 'light';
|
|
// Use slate-50 (#f8fafc) for light mode, #0d1117 for dark mode
|
|
const bgColor = isLight ? '#f8fafc' : '#0d1117';
|
|
// The semi-transparent overlay that creates the fading trail effect
|
|
const fadeColor = isLight ? 'rgba(248, 250, 252, 0.2)' : 'rgba(13, 17, 23, 0.2)';
|
|
// A darker green for light mode to maintain contrast, or the default/prop color for dark mode
|
|
const textColor = color || (isLight ? '#1c2d25' : '#84c0a0');
|
|
|
|
const fontSize = 16;
|
|
let columns = 0;
|
|
let drops: number[] = [];
|
|
|
|
const setCanvasSize = () => {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight + 120;
|
|
ctx.fillStyle = bgColor;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
};
|
|
|
|
const initDrops = () => {
|
|
columns = Math.floor(canvas.width / fontSize);
|
|
drops = [];
|
|
for (let x = 0; x < columns; x++) {
|
|
drops[x] = Math.random() * -100;
|
|
}
|
|
};
|
|
|
|
lastWidth.current = window.innerWidth;
|
|
setCanvasSize();
|
|
initDrops();
|
|
|
|
const draw = () => {
|
|
ctx.fillStyle = fadeColor;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.font = `bold ${fontSize}px monospace`;
|
|
|
|
for (let i = 0; i < drops.length; i++) {
|
|
const text = Math.random() > 0.5 ? '0' : '1';
|
|
ctx.fillStyle = textColor;
|
|
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
|
|
|
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
|
|
drops[i] = 0;
|
|
}
|
|
drops[i]++;
|
|
}
|
|
};
|
|
|
|
const intervalId = setInterval(draw, 1000 / fps);
|
|
|
|
const handleResize = () => {
|
|
if (window.innerWidth !== lastWidth.current) {
|
|
lastWidth.current = window.innerWidth;
|
|
setCanvasSize();
|
|
initDrops();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
return () => {
|
|
clearInterval(intervalId);
|
|
window.removeEventListener('resize', handleResize);
|
|
};
|
|
}, [color, fps, resolvedTheme, mounted]); // Re-run effect if theme changes
|
|
|
|
return (
|
|
<div className="fixed inset-0 pointer-events-none w-screen h-[110vh] bg-slate-50 dark:bg-[#0d1117] transition-colors duration-300" style={{ zIndex: -1 }}>
|
|
{mounted && (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="absolute inset-0 w-full h-full opacity-[0.35] dark:opacity-[0.15] blur-[0.5px]"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
} |