Files
netrunner-landing/components/graphics/NetrunnerMatrix.tsx
T

155 lines
5.4 KiB
TypeScript

"use client";
import React, { useRef, useEffect, useState } from "react";
import { useTheme } from "next-themes";
type MatrixEngine = {
tick: () => void;
update_mouse: (x: number, y: number) => void;
trigger_glitch: (x: number, y: number) => void;
set_drawing: (is_drawing: boolean) => void;
set_theme: (is_vpn_on: boolean, is_dark_mode: boolean) => void;
resize: (width: number, height: number) => void;
};
interface NetrunnerMatrixProps {
isSecure?: boolean;
}
export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
isSecure = false,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<MatrixEngine | null>(null);
const animationFrameId = useRef<number>(null);
const [wasmLoaded, setWasmLoaded] = useState(false);
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const loadWasm = async () => {
try {
const wasmPath = "/wasm-matrix/matrix_engine.js";
const wasmModule = await import(
/* @vite-ignore */ /* webpackIgnore: true */ wasmPath
);
await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm");
// 1:1 Разрешение без мыльного скейлинга
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, 16);
engineRef.current = engine;
engine.set_theme(isSecure, isDarkMode);
setWasmLoaded(true);
// ОПТИМИЗАЦИЯ: Ограничиваем до 35 FPS для устранения лагов
let lastTime = 0;
const fpsInterval = 1000 / 35; // ~28ms на кадр
const render = (time: number) => {
animationFrameId.current = requestAnimationFrame(render);
const elapsed = time - lastTime;
if (elapsed > fpsInterval) {
lastTime = time - (elapsed % fpsInterval);
engine.tick();
}
};
requestAnimationFrame(render);
} catch (e) {
console.error("Wasm Engine Load Failed:", e);
}
};
loadWasm();
const handleResize = () => {
if (canvas && container && engineRef.current) {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
engineRef.current.resize(canvas.width, canvas.height);
}
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
if (animationFrameId.current)
cancelAnimationFrame(animationFrameId.current);
};
}, []);
useEffect(() => {
if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode);
}, [isSecure, isDarkMode]);
// ГЛОБАЛЬНЫЙ ПЕРЕХВАТ (Убраны костыли со скейлингом)
useEffect(() => {
const triggerAction = (clientX: number, clientY: number) => {
if (!canvasRef.current || !engineRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
if (y < 0 || y > rect.height) return;
engineRef.current.set_drawing(true);
engineRef.current.trigger_glitch(x, y);
};
const moveAction = (clientX: number, clientY: number) => {
if (!canvasRef.current || !engineRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
engineRef.current.update_mouse(clientX - rect.left, clientY - rect.top);
};
const handlePointerDown = (e: PointerEvent) =>
triggerAction(e.clientX, e.clientY);
const handlePointerMove = (e: PointerEvent) =>
moveAction(e.clientX, e.clientY);
const handlePointerUp = () => engineRef.current?.set_drawing(false);
// Поддержка пальцев на мобилках
const handleTouchStart = (e: TouchEvent) =>
triggerAction(e.touches[0].clientX, e.touches[0].clientY);
const handleTouchMove = (e: TouchEvent) =>
moveAction(e.touches[0].clientX, e.touches[0].clientY);
window.addEventListener("pointerdown", handlePointerDown);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handlePointerUp);
return () => {
window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handlePointerUp);
};
}, []);
return (
<div ref={containerRef} className="absolute inset-0 overflow-hidden z-0">
<canvas
ref={canvasRef}
className={`w-full h-full transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`}
// Убрали imageRendering: pixelated, чтобы текст был гладким
/>
</div>
);
};