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

155 lines
5.3 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 {
// НАДЕЖНЫЙ ПУТЬ ДЛЯ ПРОДАКШЕНА:
// Форсируем абсолютные URL, чтобы Webpack не пытался их бандлить
const baseUrl = window.location.origin;
const jsUrl = new URL("/wasm-matrix/matrix_engine.js", baseUrl).href;
const wasmUrl = new URL("/wasm-matrix/matrix_engine_bg.wasm", baseUrl)
.href;
const wasmModule = await import(/* webpackIgnore: true */ jsUrl);
await wasmModule.default(wasmUrl);
const fontSize = 16;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, fontSize);
engineRef.current = engine;
engine.set_theme(isSecure, isDarkMode);
setWasmLoaded(true);
const render = () => {
engine.tick();
if (animationFrameId.current !== null) {
animationFrameId.current = requestAnimationFrame(render);
}
};
animationFrameId.current = 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 !== null) {
cancelAnimationFrame(animationFrameId.current);
animationFrameId.current = null;
}
};
}, []);
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: MouseEvent) =>
triggerAction(e.clientX, e.clientY);
const handlePointerMove = (e: MouseEvent) =>
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("mousedown", handlePointerDown);
window.addEventListener("mousemove", handlePointerMove);
window.addEventListener("mouseup", handlePointerUp);
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handlePointerUp);
return () => {
window.removeEventListener("mousedown", handlePointerDown);
window.removeEventListener("mousemove", handlePointerMove);
window.removeEventListener("mouseup", 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"}`}
style={{ imageRendering: "pixelated" }}
/>
</div>
);
};