move to webgl

This commit is contained in:
2026-04-26 17:16:40 +07:00
parent f98629ce42
commit f22b04df33
4 changed files with 644 additions and 385 deletions
+100 -82
View File
@@ -1,9 +1,10 @@
"use client";
import React, { useRef, useEffect, useState } from "react";
import React, { useRef, useEffect, useState, useCallback } from "react";
import { useTheme } from "next-themes";
const ENGINE_VERSION = "2.0.4";
// Версионирование для сброса кэша Cloudflare при деплое
const ENGINE_VERSION = "3.0.2";
interface NetrunnerMatrixProps {
isSecure?: boolean;
@@ -13,9 +14,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
isSecure = false,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const workerRef = useRef<Worker | null>(null);
const dimensionsRef = useRef({ width: 0, height: 0 });
const [isLoaded, setIsLoaded] = useState(false);
const hasTransferred = useRef(false);
@@ -31,11 +30,24 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
? resolvedTheme === "dark" || theme === "dark"
: true;
const checkIsMobile = () => {
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. ИНИЦИАЛИЗАЦИЯ WEBGL ===
useEffect(() => {
if (
!mounted ||
@@ -44,34 +56,21 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
)
return;
const canvas = canvasRef.current;
const container = containerRef.current;
if (!container || hasTransferred.current) return;
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 };
// Создаем canvas вручную, чтобы обойти баг React DOM reuse
const canvas = document.createElement("canvas");
canvas.className =
"absolute inset-0 w-full h-full block transition-opacity duration-1000 opacity-0 z-0";
container.insertBefore(canvas, container.firstChild);
try {
// ФИЗИЧЕСКИЙ размер Canvas (для резкости)
const isMobile = checkIsMobile();
const pixelScale = Math.min(window.devicePixelRatio || 1, 2.0);
const logicalWidth = container.clientWidth;
const logicalHeight = container.clientHeight;
canvas.width = logicalWidth * pixelScale;
canvas.height = logicalHeight * pixelScale;
@@ -83,6 +82,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const offscreen = canvas.transferControlToOffscreen();
hasTransferred.current = true;
const eyeScale = isMobile ? 0.6 : 0.8;
worker.postMessage(
{
@@ -92,59 +92,65 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
logicalWidth,
logicalHeight,
pixelScale,
eyeY: getEyeY(), // Без scale, так как движок теперь работает в логических координатах
eyeY: getEyeY(),
eyeScale,
isMobile,
isVpnOn: isSecure,
isDarkMode,
fontSize: isMobile ? 10 : 16,
fontSize: isMobile ? 12 : 16,
},
},
[offscreen],
);
setIsLoaded(true);
// Плавное появление
setTimeout(() => {
canvas.classList.remove("opacity-0");
canvas.classList.add("opacity-100");
}, 150);
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
if (width === 0 || height === 0) continue;
let resizeTimeout: ReturnType<typeof setTimeout>;
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 };
const isMob = width <= 768;
workerRef.current.postMessage({
workerRef.current?.postMessage({
type: "RESIZE",
payload: {
logicalWidth: lw,
logicalHeight: lh,
logicalWidth: width,
logicalHeight: height,
pixelScale: pScale,
eyeY: getEyeY(),
eyeScale: isMob ? 0.6 : 0.8,
isMobile: isMob,
fontSize: isMob ? 10 : 16,
fontSize: isMob ? 12 : 16,
},
});
}, 150);
};
}
});
window.addEventListener("resize", handleResize);
resizeObserver.observe(container);
return () => {
window.removeEventListener("resize", handleResize);
clearTimeout(resizeTimeout);
resizeObserver.disconnect();
if (workerRef.current) {
workerRef.current.terminate();
workerRef.current = null;
}
if (container.contains(canvas)) {
container.removeChild(canvas);
}
hasTransferred.current = false;
};
} catch (e) {
console.error("Matrix Worker Bridge Failed:", e);
console.error("Matrix WebGL Bridge Failed:", e);
}
}, [mounted]);
}, [mounted, checkIsMobile, getEyeY]);
// === 2. РЕАКЦИЯ НА ТЕМУ ===
useEffect(() => {
if (workerRef.current && mounted) {
workerRef.current.postMessage({
@@ -154,8 +160,11 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}
}, [isSecure, isDarkMode, mounted]);
// === 3. ИНТЕРАКТИВНОСТЬ (МЫШЬ, КЛИКИ, ГИРОСКОП) ===
useEffect(() => {
if (!mounted || !workerRef.current) return;
if (!mounted || !workerRef.current || !containerRef.current) return;
const container = containerRef.current;
const handleOrientation = (e: DeviceOrientationEvent) => {
let gamma = e.gamma || 0;
@@ -168,75 +177,84 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
});
};
if (
typeof (DeviceOrientationEvent as any).requestPermission === "function"
) {
// iOS permission logic omitted for brevity
} else {
window.addEventListener("deviceorientation", handleOrientation);
}
window.addEventListener("deviceorientation", handleOrientation);
const handleEvents = (e: any) => {
const handleEvents = (e: PointerEvent | TouchEvent) => {
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;
if (e.target instanceof Element && e.target.closest("button, a, input"))
return;
let clientX = e.touches ? e.touches[0].clientX : e.clientX;
let clientY = e.touches ? e.touches[0].clientY : e.clientY;
let clientX, clientY;
if ("touches" in e) {
clientX = e.touches[0]?.clientX;
clientY = e.touches[0]?.clientY;
} else {
clientX = (e as PointerEvent).clientX;
clientY = (e as PointerEvent).clientY;
}
if (clientX === undefined) return;
if (clientX === undefined || clientY === 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;
const rect = container.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 handlePointerUp = () => {
workerRef.current?.postMessage({ type: "DRAW_END", payload: {} });
};
window.addEventListener("pointerdown", handleEvents, { passive: true });
window.addEventListener("pointermove", handleEvents, { passive: true });
window.addEventListener("pointerup", handlePointerUp, { passive: true });
window.addEventListener("touchstart", handleEvents, { passive: true });
window.addEventListener("touchmove", handleEvents, { passive: true });
window.addEventListener("touchend", handlePointerUp, { passive: true });
return () => {
window.removeEventListener("deviceorientation", handleOrientation);
window.removeEventListener("pointerdown", handleEvents);
window.removeEventListener("pointermove", handleEvents);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("touchstart", handleEvents);
window.removeEventListener("touchmove", handleEvents);
window.removeEventListener("touchend", handlePointerUp);
};
}, [mounted]);
return (
<div
ref={containerRef}
className="absolute inset-0 overflow-hidden z-0 bg-background"
className="absolute inset-0 overflow-hidden z-0"
style={{ backgroundColor: isDarkMode ? "#0a0a0c" : "#fafafc" }}
>
<canvas
key="matrix-canvas"
ref={canvasRef}
className={`w-full h-full block transition-opacity duration-1000 ${isLoaded ? "opacity-100" : "opacity-0"}`}
/>
<div
className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25] hidden md:block"
className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25] hidden md:block z-10"
style={{
background: `linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06))`,
backgroundSize: "100% 4px, 3px 100%",