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