"use client"; import React, { useRef, useEffect, useState } from "react"; import { useTheme } from "next-themes"; 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 = (scale: number) => { 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) * scale; } return 0; }; const isMobile = checkIsMobile(); const pixelScale = isMobile ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5); const initialWidth = container.clientWidth * pixelScale; const initialHeight = container.clientHeight * pixelScale; dimensionsRef.current = { width: initialWidth, height: initialHeight }; try { canvas.width = initialWidth; canvas.height = initialHeight; const worker = new Worker("/wasm-matrix/matrix_worker.js?v=1.0", { type: "module", }); workerRef.current = worker; const offscreen = canvas.transferControlToOffscreen(); hasTransferred.current = true; worker.postMessage( { type: "INIT", payload: { canvas: offscreen, width: initialWidth, height: initialHeight, eyeY: getEyeY(pixelScale), 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 scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5); const w = container.clientWidth * scale; const h = container.clientHeight * scale; dimensionsRef.current = { width: w, height: h }; workerRef.current.postMessage({ type: "RESIZE", payload: { width: w, height: h, eyeY: getEyeY(scale), 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) return; const handleEvents = (e: any) => { if (!workerRef.current || !canvasRef.current) return; const isInteractive = e.target.closest( 'button, a, input, [role="button"], [role="switch"], .data-\\[state\\]', ); const isDown = e.type === "pointerdown" || e.type === "touchstart"; const isUp = e.type === "pointerup" || e.type === "touchend" || e.type === "pointercancel"; if (isInteractive && isDown) return; const type = isDown ? "DRAW_START" : isUp ? "DRAW_END" : "MOUSE_MOVE"; let clientX: number | null = null; let clientY: number | null = null; if (e.touches && e.touches.length > 0) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else if (e.clientX !== undefined) { clientX = e.clientX; clientY = e.clientY; } if (clientX === null || clientY === null) { if (isUp) workerRef.current.postMessage({ type }); return; } const rect = canvasRef.current.getBoundingClientRect(); const scaleX = dimensionsRef.current.width / rect.width; const scaleY = dimensionsRef.current.height / rect.height; workerRef.current.postMessage({ type, payload: { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY, }, }); }; const options = { passive: true }; if (window.PointerEvent) { window.addEventListener("pointerdown", handleEvents, options as any); window.addEventListener("pointermove", handleEvents, options as any); window.addEventListener("pointerup", handleEvents, options as any); window.addEventListener("pointercancel", handleEvents, options as any); } else { window.addEventListener("touchstart", handleEvents, options); window.addEventListener("touchmove", handleEvents, options); window.addEventListener("touchend", handleEvents, options); } return () => { if (window.PointerEvent) { window.removeEventListener("pointerdown", handleEvents); window.removeEventListener("pointermove", handleEvents); window.removeEventListener("pointerup", handleEvents); window.removeEventListener("pointercancel", handleEvents); } else { window.removeEventListener("touchstart", handleEvents); window.removeEventListener("touchmove", handleEvents); window.removeEventListener("touchend", handleEvents); } }; }, [mounted]); return (
); };