124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
export default function MatrixBackground({
|
|
isSecure,
|
|
theme,
|
|
}: {
|
|
isSecure: boolean;
|
|
theme?: string;
|
|
}) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const mousePos = useRef({ x: -1000, y: -1000 });
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d", { alpha: false });
|
|
if (!ctx) return;
|
|
|
|
let width = (canvas.width = window.innerWidth);
|
|
let height = (canvas.height = window.innerHeight);
|
|
|
|
const fontSize = 15;
|
|
const columns = Math.floor(width / fontSize);
|
|
const drops = Array(columns).fill(1);
|
|
|
|
// Хакерский набор символов
|
|
const chars = "01ヲアウエオカキケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン".split("");
|
|
|
|
const isDark = theme === "dark";
|
|
let animationFrameId: number;
|
|
let lastTime = 0;
|
|
const fps = 22;
|
|
const interval = 1000 / fps;
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
mousePos.current = { x: e.clientX, y: e.clientY };
|
|
};
|
|
|
|
const handleResize = () => {
|
|
width = canvas.width = window.innerWidth;
|
|
height = canvas.height = window.innerHeight;
|
|
};
|
|
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
const draw = (currentTime: number) => {
|
|
animationFrameId = requestAnimationFrame(draw);
|
|
|
|
const deltaTime = currentTime - lastTime;
|
|
if (deltaTime < interval) return;
|
|
lastTime = currentTime - (deltaTime % interval);
|
|
|
|
// Рисуем шлейф: чем меньше alpha, тем длиннее "хвосты" у цифр
|
|
ctx.fillStyle = isDark
|
|
? "rgba(11, 13, 23, 0.25)"
|
|
: "rgba(248, 250, 252, 0.30)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
ctx.font = `${fontSize}px monospace`;
|
|
|
|
for (let i = 0; i < drops.length; i++) {
|
|
const x = i * fontSize;
|
|
const y = drops[i] * fontSize;
|
|
|
|
const dx = x - mousePos.current.x;
|
|
const dy = y - mousePos.current.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
const radius = 350; // Радиус подсветки за курсором
|
|
|
|
const highlight = Math.max(0, 1 - distance / radius);
|
|
|
|
// Базовая яркость очень низкая (0.05), чтобы фон не бил в глаза
|
|
const baseOpacity = isDark ? 0.15 : 0.2;
|
|
const finalOpacity = baseOpacity + highlight * 0.6;
|
|
|
|
// Основной цвет
|
|
if (isSecure) {
|
|
ctx.fillStyle = isDark
|
|
? `rgba(0, 240, 255, ${finalOpacity})`
|
|
: `rgba(2, 132, 199, ${finalOpacity})`;
|
|
} else {
|
|
ctx.fillStyle = isDark
|
|
? `rgba(139, 61, 255, ${finalOpacity})`
|
|
: `rgba(109, 40, 217, ${finalOpacity})`;
|
|
}
|
|
|
|
// Мягкое свечение только там, где "взгляд" (highlight)
|
|
if (isDark && highlight > 0.8) {
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = isSecure ? "#00F0FF" : "#8B3DFF";
|
|
} else {
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
const char = chars[Math.floor(Math.random() * chars.length)];
|
|
ctx.fillText(char, x, y);
|
|
|
|
if (y > height && Math.random() > 0.985) {
|
|
drops[i] = 0;
|
|
}
|
|
drops[i]++;
|
|
}
|
|
};
|
|
|
|
animationFrameId = requestAnimationFrame(draw);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationFrameId);
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
window.removeEventListener("resize", handleResize);
|
|
};
|
|
}, [isSecure, theme]);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="absolute inset-0 w-full h-full pointer-events-none"
|
|
/>
|
|
);
|
|
}
|