"use client"; import React, { useRef, useEffect, useState, useCallback } from "react"; const ENGINE_VERSION = "3.0.3"; export interface NetrunnerMatrixProps { isSecure?: boolean; assetsPath?: string; isDarkMode?: boolean; showEyes?: boolean; // Добавлен флаг глаз } export const NetrunnerMatrix: React.FC = ({ isSecure = false, assetsPath = "/wasm-matrix", isDarkMode = true, showEyes = true, // По умолчанию глаза включены }) => { const containerRef = useRef(null); const workerRef = useRef(null); const hasTransferred = useRef(false); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); return () => setMounted(false); }, []); const checkIsMobile = useCallback(() => { if (typeof window === "undefined") return false; return window.innerWidth <= 768 || "ontouchstart" in window; }, []); const getEyeY = useCallback(() => { const anchor = document.getElementById("eye-anchor"); const container = containerRef.current; if (anchor && container) { const ar = anchor.getBoundingClientRect(); const cr = container.getBoundingClientRect(); return ar.top - cr.top + ar.height / 2; } return typeof window !== "undefined" ? window.innerHeight * 0.4 : 300; }, []); // === 1. ИНИЦИАЛИЗАЦИЯ И УПРАВЛЕНИЕ ВОРКЕРОМ === useEffect(() => { if ( !mounted || typeof window === "undefined" || !("OffscreenCanvas" in window) ) return; const container = containerRef.current; if (!container || hasTransferred.current) return; const canvas = document.createElement("canvas"); Object.assign(canvas.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", display: "block", zIndex: "0", pointerEvents: "none", transition: "opacity 1.5s ease-in-out", opacity: "0", }); canvas.id = "matrix-canvas"; container.insertBefore(canvas, container.firstChild); try { const isMobile = checkIsMobile(); const isLowQuality = isMobile || (navigator.hardwareConcurrency || 4) <= 4; const currentDPR = window.devicePixelRatio || 1; const optimalScale = isMobile ? Math.min(currentDPR, 1.25) : currentDPR; const logicalWidth = container.clientWidth; const logicalHeight = container.clientHeight; const workerUrl = new URL( `${assetsPath}/matrix_worker.js?v=${ENGINE_VERSION}`, window.location.origin, ).href; const worker = new Worker(workerUrl, { type: "module" }); workerRef.current = worker; const offscreen = canvas.transferControlToOffscreen(); hasTransferred.current = true; worker.postMessage( { type: "INIT", payload: { canvas: offscreen, assetsPath, logicalWidth, logicalHeight, pixelScale: optimalScale, eyeY: getEyeY(), isMobile, isVpnOn: isSecure, isDarkMode, fontSize: isMobile ? 12 : 16, isLowQuality, showEyes, // Передаем флаг в воркер }, }, [offscreen], ); worker.onmessage = (e) => { if (e.data.type === "READY") { requestAnimationFrame(() => { canvas.style.opacity = "1"; }); } }; // --- ПРОСТАЯ И НАДЕЖНАЯ ОПТИМИЗАЦИЯ ПАУЗЫ --- // Мы не трогаем IntersectionObserver, который мог конфликтовать с z-index, // а просто ставим на паузу, когда приложение/вкладка свернуто. const onVisibilityChange = () => { if (!workerRef.current) return; if (document.hidden) { workerRef.current.postMessage({ type: "PAUSE" }); } else { workerRef.current.postMessage({ type: "RESUME" }); } }; document.addEventListener("visibilitychange", onVisibilityChange); // -------------------------------------------- const resizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { const { width: newWidth, height: newHeight } = entry.contentRect; if (newWidth === 0 || newHeight === 0) continue; workerRef.current?.postMessage({ type: "RESIZE", payload: { logicalWidth: newWidth, logicalHeight: newHeight, pixelScale: optimalScale, assetsPath, eyeY: getEyeY(), isMobile: newWidth <= 768, fontSize: newWidth <= 768 ? 12 : 16, isLowQuality: newWidth <= 768 || isLowQuality, }, }); } }); resizeObserver.observe(container); return () => { resizeObserver.disconnect(); document.removeEventListener("visibilitychange", onVisibilityChange); if (workerRef.current) { workerRef.current.terminate(); workerRef.current = null; } if (container.contains(canvas)) container.removeChild(canvas); hasTransferred.current = false; }; } catch (e) { console.error("Matrix WebGL Bridge Failed:", e); } }, [mounted, checkIsMobile, getEyeY, assetsPath, showEyes]); // === 2. СИНХРОНИЗАЦИЯ ТЕМЫ И VPN === useEffect(() => { if (workerRef.current && mounted) { workerRef.current.postMessage({ type: "THEME", payload: { isVpnOn: isSecure, isDarkMode: isDarkMode, }, }); } }, [isSecure, isDarkMode, mounted]); // === 3. ИНТЕРАКТИВНОСТЬ (ОРИГИНАЛЬНЫЙ РАБОЧИЙ КОД) === useEffect(() => { if (!mounted || !workerRef.current || !containerRef.current) return; const handleOrientation = (e: DeviceOrientationEvent) => { let gamma = e.gamma || 0; if (gamma > 45) gamma = 45; if (gamma < -45) gamma = -45; workerRef.current?.postMessage({ type: "TILT", payload: { x: gamma / 45.0, y: 0 }, }); }; window.addEventListener("deviceorientation", handleOrientation); const handleEvents = (e: any) => { if ( e.target instanceof Element && e.target.closest("button, a, input, textarea, label") ) return; const isDown = e.type === "pointerdown" || e.type === "touchstart"; const isMove = e.type === "pointermove" || e.type === "touchmove"; const clientX = e.touches ? e.touches[0]?.clientX : e.clientX; const clientY = e.touches ? e.touches[0]?.clientY : e.clientY; if (clientX === undefined || clientY === undefined) return; const rect = containerRef.current!.getBoundingClientRect(); const px = clientX - rect.left; const py = clientY - rect.top; if (isDown) { workerRef.current?.postMessage({ type: "SHOCKWAVE", payload: { x: px, y: py }, }); workerRef.current?.postMessage({ type: "DRAW_START", payload: { x: px, y: py }, }); } if (isMove) { workerRef.current?.postMessage({ type: "KINETIC_SWIPE", payload: { x: px, y: py }, }); workerRef.current?.postMessage({ type: "MOUSE_MOVE", payload: { x: px, y: py }, }); } }; const handleStop = () => workerRef.current?.postMessage({ type: "DRAW_END", payload: {} }); const options = { passive: true }; window.addEventListener("pointerdown", handleEvents, options); window.addEventListener("pointermove", handleEvents, options); window.addEventListener("pointerup", handleStop, options); window.addEventListener("touchstart", handleEvents, options); window.addEventListener("touchmove", handleEvents, options); window.addEventListener("touchend", handleStop, options); return () => { window.removeEventListener("deviceorientation", handleOrientation); window.removeEventListener("pointerdown", handleEvents); window.removeEventListener("pointermove", handleEvents); window.removeEventListener("pointerup", handleStop); window.removeEventListener("touchstart", handleEvents); window.removeEventListener("touchmove", handleEvents); window.removeEventListener("touchend", handleStop); }; }, [mounted]); return ( // Оставляем pointer-events-none, раз он стабильно работает в твоем окружении
); };