"use client"; import React, { useRef, useEffect, useState } from "react"; import { useTheme } from "next-themes"; const ENGINE_VERSION = "2.0.4"; interface NetrunnerMatrixProps { isSecure?: boolean; } export const NetrunnerMatrix: React.FC = ({ isSecure = false, }) => { const containerRef = useRef(null); const canvasRef = useRef(null); const workerRef = useRef(null); const dimensionsRef = useRef({ width: 0, height: 0 }); const [isLoaded, setIsLoaded] = useState(false); const hasTransferred = useRef(false); const { resolvedTheme, theme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); return () => setMounted(false); }, []); const isDarkMode = mounted ? resolvedTheme === "dark" || theme === "dark" : true; const checkIsMobile = () => { if (typeof window === "undefined") return false; return window.innerWidth <= 768 || "ontouchstart" in window; }; useEffect(() => { if ( !mounted || typeof window === "undefined" || !("OffscreenCanvas" in window) ) return; const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container || hasTransferred.current) return; const getEyeY = () => { const anchor = document.getElementById("eye-anchor"); const canvas = canvasRef.current; if (anchor && canvas) { const ar = anchor.getBoundingClientRect(); const cr = canvas.getBoundingClientRect(); return ar.top - cr.top + ar.height / 2; } return 0; }; const isMobile = checkIsMobile(); // Берем плотность пикселей экрана, ограничиваем до 2.0 для баланса резкости и FPS const pixelScale = Math.min(window.devicePixelRatio || 1, 2.0); // ЛОГИЧЕСКИЙ размер (в CSS пикселях) - для физики Rust const logicalWidth = container.clientWidth; const logicalHeight = container.clientHeight; dimensionsRef.current = { width: logicalWidth, height: logicalHeight }; try { // ФИЗИЧЕСКИЙ размер Canvas (для резкости) canvas.width = logicalWidth * pixelScale; canvas.height = logicalHeight * pixelScale; const worker = new Worker( `/wasm-matrix/matrix_worker.js?v=${ENGINE_VERSION}`, { type: "module" }, ); workerRef.current = worker; const offscreen = canvas.transferControlToOffscreen(); hasTransferred.current = true; worker.postMessage( { type: "INIT", payload: { canvas: offscreen, logicalWidth, logicalHeight, pixelScale, eyeY: getEyeY(), // Без scale, так как движок теперь работает в логических координатах isMobile, isVpnOn: isSecure, isDarkMode, fontSize: isMobile ? 10 : 16, }, }, [offscreen], ); setIsLoaded(true); let resizeTimeout: ReturnType; const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (!container || !workerRef.current) return; const isMob = checkIsMobile(); const pScale = Math.min(window.devicePixelRatio || 1, 2.0); const lw = container.clientWidth; const lh = container.clientHeight; dimensionsRef.current = { width: lw, height: lh }; workerRef.current.postMessage({ type: "RESIZE", payload: { logicalWidth: lw, logicalHeight: lh, pixelScale: pScale, eyeY: getEyeY(), isMobile: isMob, fontSize: isMob ? 10 : 16, }, }); }, 150); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); clearTimeout(resizeTimeout); if (workerRef.current) { workerRef.current.terminate(); workerRef.current = null; } hasTransferred.current = false; }; } catch (e) { console.error("Matrix Worker Bridge Failed:", e); } }, [mounted]); useEffect(() => { if (workerRef.current && mounted) { workerRef.current.postMessage({ type: "THEME", payload: { isVpnOn: isSecure, isDarkMode }, }); } }, [isSecure, isDarkMode, mounted]); useEffect(() => { if (!mounted || !workerRef.current) return; const handleOrientation = (e: DeviceOrientationEvent) => { let gamma = e.gamma || 0; if (gamma > 45) gamma = 45; if (gamma < -45) gamma = -45; const tiltX = gamma / 45.0; workerRef.current?.postMessage({ type: "TILT", payload: { x: tiltX, y: 0 }, }); }; if ( typeof (DeviceOrientationEvent as any).requestPermission === "function" ) { // iOS permission logic omitted for brevity } else { window.addEventListener("deviceorientation", handleOrientation); } const handleEvents = (e: any) => { const isDown = e.type === "pointerdown" || e.type === "touchstart"; const isMove = e.type === "pointermove" || e.type === "touchmove"; const isInteractive = e.target.closest( 'button, a, input, [role="button"], [role="switch"]', ); if (isDown && isInteractive) return; let clientX = e.touches ? e.touches[0].clientX : e.clientX; let clientY = e.touches ? e.touches[0].clientY : e.clientY; if (clientX === undefined) return; const rect = canvasRef.current!.getBoundingClientRect(); // Scale теперь всегда 1.0, так как dimensionsRef равен логическому CSS размеру const scaleX = dimensionsRef.current.width / rect.width; const scaleY = dimensionsRef.current.height / rect.height; const px = (clientX - rect.left) * scaleX; const py = (clientY - rect.top) * scaleY; if (isDown) { workerRef.current?.postMessage({ type: "SHOCKWAVE", payload: { x: px, y: py }, }); } if (isMove) { workerRef.current?.postMessage({ type: "KINETIC_SWIPE", payload: { x: px, y: py }, }); } }; window.addEventListener("pointerdown", handleEvents, { passive: true }); window.addEventListener("pointermove", handleEvents, { passive: true }); window.addEventListener("touchstart", handleEvents, { passive: true }); window.addEventListener("touchmove", handleEvents, { passive: true }); return () => { window.removeEventListener("deviceorientation", handleOrientation); window.removeEventListener("pointerdown", handleEvents); window.removeEventListener("pointermove", handleEvents); window.removeEventListener("touchstart", handleEvents); window.removeEventListener("touchmove", handleEvents); }; }, [mounted]); return (
); };