155 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
};
|