optimization and some fixes
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
const ENGINE_VERSION = "2.0.2";
|
const ENGINE_VERSION = "2.0.3";
|
||||||
|
|
||||||
interface NetrunnerMatrixProps {
|
interface NetrunnerMatrixProps {
|
||||||
isSecure?: boolean;
|
isSecure?: boolean;
|
||||||
@@ -49,32 +49,35 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
|
|
||||||
if (!canvas || !container || hasTransferred.current) return;
|
if (!canvas || !container || hasTransferred.current) return;
|
||||||
|
|
||||||
const getEyeY = (scale: number) => {
|
const getEyeY = () => {
|
||||||
const anchor = document.getElementById("eye-anchor");
|
const anchor = document.getElementById("eye-anchor");
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (anchor && canvas) {
|
if (anchor && canvas) {
|
||||||
const ar = anchor.getBoundingClientRect();
|
const ar = anchor.getBoundingClientRect();
|
||||||
const cr = canvas.getBoundingClientRect();
|
const cr = canvas.getBoundingClientRect();
|
||||||
return (ar.top - cr.top + ar.height / 2) * scale;
|
return ar.top - cr.top + ar.height / 2;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMobile = checkIsMobile();
|
const isMobile = checkIsMobile();
|
||||||
const pixelScale = Math.min(window.devicePixelRatio || 1, 1.5);
|
// Берем плотность пикселей экрана, ограничиваем до 2.0 для баланса резкости и FPS
|
||||||
const initialWidth = container.clientWidth * pixelScale;
|
const pixelScale = Math.min(window.devicePixelRatio || 1, 2.0);
|
||||||
const initialHeight = container.clientHeight * pixelScale;
|
|
||||||
|
|
||||||
dimensionsRef.current = { width: initialWidth, height: initialHeight };
|
// ЛОГИЧЕСКИЙ размер (в CSS пикселях) - для физики Rust
|
||||||
|
const logicalWidth = container.clientWidth;
|
||||||
|
const logicalHeight = container.clientHeight;
|
||||||
|
|
||||||
|
dimensionsRef.current = { width: logicalWidth, height: logicalHeight };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
canvas.width = initialWidth;
|
// ФИЗИЧЕСКИЙ размер Canvas (для резкости)
|
||||||
canvas.height = initialHeight;
|
canvas.width = logicalWidth * pixelScale;
|
||||||
|
canvas.height = logicalHeight * pixelScale;
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
`/wasm-matrix/matrix_worker.js?v=${ENGINE_VERSION}`,
|
`/wasm-matrix/matrix_worker.js?v=${ENGINE_VERSION}`,
|
||||||
{
|
{ type: "module" },
|
||||||
type: "module",
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
workerRef.current = worker;
|
workerRef.current = worker;
|
||||||
|
|
||||||
@@ -86,9 +89,10 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
type: "INIT",
|
type: "INIT",
|
||||||
payload: {
|
payload: {
|
||||||
canvas: offscreen,
|
canvas: offscreen,
|
||||||
width: initialWidth,
|
logicalWidth,
|
||||||
height: initialHeight,
|
logicalHeight,
|
||||||
eyeY: getEyeY(pixelScale),
|
pixelScale,
|
||||||
|
eyeY: getEyeY(), // Без scale, так как движок теперь работает в логических координатах
|
||||||
isMobile,
|
isMobile,
|
||||||
isVpnOn: isSecure,
|
isVpnOn: isSecure,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
@@ -106,18 +110,18 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
resizeTimeout = setTimeout(() => {
|
resizeTimeout = setTimeout(() => {
|
||||||
if (!container || !workerRef.current) return;
|
if (!container || !workerRef.current) return;
|
||||||
const isMob = checkIsMobile();
|
const isMob = checkIsMobile();
|
||||||
const scale = isMob
|
const pScale = Math.min(window.devicePixelRatio || 1, 2.0);
|
||||||
? 1.0
|
const lw = container.clientWidth;
|
||||||
: Math.min(window.devicePixelRatio || 1, 1.5);
|
const lh = container.clientHeight;
|
||||||
const w = container.clientWidth * scale;
|
dimensionsRef.current = { width: lw, height: lh };
|
||||||
const h = container.clientHeight * scale;
|
|
||||||
dimensionsRef.current = { width: w, height: h };
|
|
||||||
workerRef.current.postMessage({
|
workerRef.current.postMessage({
|
||||||
type: "RESIZE",
|
type: "RESIZE",
|
||||||
payload: {
|
payload: {
|
||||||
width: w,
|
logicalWidth: lw,
|
||||||
height: h,
|
logicalHeight: lh,
|
||||||
eyeY: getEyeY(scale),
|
pixelScale: pScale,
|
||||||
|
eyeY: getEyeY(),
|
||||||
isMobile: isMob,
|
isMobile: isMob,
|
||||||
fontSize: isMob ? 10 : 16,
|
fontSize: isMob ? 10 : 16,
|
||||||
},
|
},
|
||||||
@@ -149,61 +153,56 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isSecure, isDarkMode, mounted]);
|
}, [isSecure, isDarkMode, mounted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mounted || !workerRef.current) return;
|
if (!mounted || !workerRef.current) return;
|
||||||
|
|
||||||
// --- 1. Гироскоп (Наклон телефона) ---
|
|
||||||
const handleOrientation = (e: DeviceOrientationEvent) => {
|
const handleOrientation = (e: DeviceOrientationEvent) => {
|
||||||
let gamma = e.gamma || 0; // Наклон влево/вправо (-90 до 90)
|
let gamma = e.gamma || 0;
|
||||||
|
|
||||||
// Ограничиваем угол для комфорта
|
|
||||||
if (gamma > 45) gamma = 45;
|
if (gamma > 45) gamma = 45;
|
||||||
if (gamma < -45) gamma = -45;
|
if (gamma < -45) gamma = -45;
|
||||||
|
|
||||||
// Нормализуем от -1.0 до 1.0
|
|
||||||
const tiltX = gamma / 45.0;
|
const tiltX = gamma / 45.0;
|
||||||
|
|
||||||
workerRef.current?.postMessage({
|
workerRef.current?.postMessage({
|
||||||
type: "TILT",
|
type: "TILT",
|
||||||
payload: { x: tiltX, y: 0 },
|
payload: { x: tiltX, y: 0 },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Запрос прав для iOS 13+ (если нужно, можно вынести в кнопку включения VPN)
|
|
||||||
if (
|
if (
|
||||||
typeof (DeviceOrientationEvent as any).requestPermission === "function"
|
typeof (DeviceOrientationEvent as any).requestPermission === "function"
|
||||||
) {
|
) {
|
||||||
// Это должно вызываться по клику пользователя, здесь оставлено для примера
|
// iOS permission logic omitted for brevity
|
||||||
// (DeviceOrientationEvent as any).requestPermission().then(...)
|
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("deviceorientation", handleOrientation);
|
window.addEventListener("deviceorientation", handleOrientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Ударные волны и Кинетический свайп ---
|
|
||||||
const handleEvents = (e: any) => {
|
const handleEvents = (e: any) => {
|
||||||
const isDown = e.type === "pointerdown" || e.type === "touchstart";
|
const isDown = e.type === "pointerdown" || e.type === "touchstart";
|
||||||
const isMove = e.type === "pointermove" || e.type === "touchmove";
|
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 clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
let clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
let clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
if (clientX === undefined) return;
|
if (clientX === undefined) return;
|
||||||
|
|
||||||
const rect = canvasRef.current!.getBoundingClientRect();
|
const rect = canvasRef.current!.getBoundingClientRect();
|
||||||
|
// Scale теперь всегда 1.0, так как dimensionsRef равен логическому CSS размеру
|
||||||
const scaleX = dimensionsRef.current.width / rect.width;
|
const scaleX = dimensionsRef.current.width / rect.width;
|
||||||
const scaleY = dimensionsRef.current.height / rect.height;
|
const scaleY = dimensionsRef.current.height / rect.height;
|
||||||
const px = (clientX - rect.left) * scaleX;
|
const px = (clientX - rect.left) * scaleX;
|
||||||
const py = (clientY - rect.top) * scaleY;
|
const py = (clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
// Ударная волна при клике/тапе
|
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
workerRef.current?.postMessage({
|
workerRef.current?.postMessage({
|
||||||
type: "SHOCKWAVE",
|
type: "SHOCKWAVE",
|
||||||
payload: { x: px, y: py },
|
payload: { x: px, y: py },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Магнитный свайп
|
|
||||||
if (isMove) {
|
if (isMove) {
|
||||||
workerRef.current?.postMessage({
|
workerRef.current?.postMessage({
|
||||||
type: "KINETIC_SWIPE",
|
type: "KINETIC_SWIPE",
|
||||||
@@ -220,7 +219,9 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("deviceorientation", handleOrientation);
|
window.removeEventListener("deviceorientation", handleOrientation);
|
||||||
window.removeEventListener("pointerdown", handleEvents);
|
window.removeEventListener("pointerdown", handleEvents);
|
||||||
// ... удаляем остальные листенеры ...
|
window.removeEventListener("pointermove", handleEvents);
|
||||||
|
window.removeEventListener("touchstart", handleEvents);
|
||||||
|
window.removeEventListener("touchmove", handleEvents);
|
||||||
};
|
};
|
||||||
}, [mounted]);
|
}, [mounted]);
|
||||||
|
|
||||||
|
|||||||
@@ -89,26 +89,26 @@ function getGlitchColor(type) {
|
|||||||
if (type === 3) return "rgba(255,255,255,0.95)";
|
if (type === 3) return "rgba(255,255,255,0.95)";
|
||||||
return "rgba(255,20,80,0.95)";
|
return "rgba(255,20,80,0.95)";
|
||||||
}
|
}
|
||||||
|
let pixelScale = 1.0;
|
||||||
|
|
||||||
self.onmessage = async (e) => {
|
self.onmessage = async (e) => {
|
||||||
const { type, payload } = e.data;
|
const { type, payload } = e.data;
|
||||||
|
|
||||||
if (type === "INIT") {
|
if (type === "INIT") {
|
||||||
ctx = payload.canvas.getContext("2d", { alpha: false });
|
ctx = payload.canvas.getContext("2d", { alpha: false });
|
||||||
width = payload.width;
|
width = payload.logicalWidth;
|
||||||
height = payload.height;
|
height = payload.logicalHeight;
|
||||||
|
pixelScale = payload.pixelScale;
|
||||||
isMobile = payload.isMobile;
|
isMobile = payload.isMobile;
|
||||||
isVpnOn = payload.isVpnOn;
|
isVpnOn = payload.isVpnOn;
|
||||||
isDarkMode = payload.isDarkMode;
|
isDarkMode = payload.isDarkMode;
|
||||||
fontSize = payload.fontSize || 16;
|
fontSize = payload.fontSize || 16;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. ДИНАМИЧЕСКИЙ ИМПОРТ (позволяет использовать переменные в пути)
|
|
||||||
const module = await import("./matrix_engine.js?v=" + version);
|
const module = await import("./matrix_engine.js?v=" + version);
|
||||||
const init = module.default;
|
const init = module.default;
|
||||||
MatrixEngineClass = module.MatrixEngine;
|
MatrixEngineClass = module.MatrixEngine;
|
||||||
|
|
||||||
// 3. ИНИЦИАЛИЗАЦИЯ WASM с версией
|
|
||||||
wasm = await init("./matrix_engine_bg.wasm?v=" + version);
|
wasm = await init("./matrix_engine_bg.wasm?v=" + version);
|
||||||
|
|
||||||
engine = new MatrixEngineClass(width, height, fontSize);
|
engine = new MatrixEngineClass(width, height, fontSize);
|
||||||
@@ -119,6 +119,9 @@ self.onmessage = async (e) => {
|
|||||||
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
||||||
engine.set_eye_anchor(payload.eyeY);
|
engine.set_eye_anchor(payload.eyeY);
|
||||||
|
|
||||||
|
// АППАРАТНОЕ МАСШТАБИРОВАНИЕ
|
||||||
|
ctx.scale(pixelScale, pixelScale);
|
||||||
|
|
||||||
buildAtlas();
|
buildAtlas();
|
||||||
requestAnimationFrame(render);
|
requestAnimationFrame(render);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -126,14 +129,20 @@ self.onmessage = async (e) => {
|
|||||||
}
|
}
|
||||||
} else if (type === "RESIZE") {
|
} else if (type === "RESIZE") {
|
||||||
if (!engine) return;
|
if (!engine) return;
|
||||||
width = payload.width;
|
width = payload.logicalWidth;
|
||||||
height = payload.height;
|
height = payload.logicalHeight;
|
||||||
|
pixelScale = payload.pixelScale;
|
||||||
fontSize = payload.fontSize || 16;
|
fontSize = payload.fontSize || 16;
|
||||||
ctx.canvas.width = width;
|
|
||||||
ctx.canvas.height = height;
|
// Переустанавливаем физический размер и заново применяем Scale
|
||||||
|
ctx.canvas.width = width * pixelScale;
|
||||||
|
ctx.canvas.height = height * pixelScale;
|
||||||
|
ctx.scale(pixelScale, pixelScale);
|
||||||
|
|
||||||
if (typeof engine.set_mobile === "function")
|
if (typeof engine.set_mobile === "function")
|
||||||
engine.set_mobile(payload.isMobile);
|
engine.set_mobile(payload.isMobile);
|
||||||
engine.resize(width, height);
|
engine.resize(width, height);
|
||||||
|
|
||||||
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
||||||
engine.set_eye_anchor(payload.eyeY);
|
engine.set_eye_anchor(payload.eyeY);
|
||||||
buildAtlas();
|
buildAtlas();
|
||||||
@@ -148,7 +157,7 @@ self.onmessage = async (e) => {
|
|||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
ctx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
ctx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height); // fillRect автоматически растянется из-за ctx.scale!
|
||||||
}
|
}
|
||||||
} else if (type === "MOUSE_MOVE") {
|
} else if (type === "MOUSE_MOVE") {
|
||||||
if (engine) engine.update_mouse(payload.x, payload.y);
|
if (engine) engine.update_mouse(payload.x, payload.y);
|
||||||
@@ -159,24 +168,16 @@ self.onmessage = async (e) => {
|
|||||||
}
|
}
|
||||||
} else if (type === "DRAW_END") {
|
} else if (type === "DRAW_END") {
|
||||||
if (engine) engine.set_drawing(false);
|
if (engine) engine.set_drawing(false);
|
||||||
} else if (type === "GET_EYE_DATA") {
|
|
||||||
if (!engine || !wasm) return;
|
|
||||||
const eyeLen = engine.eye_len();
|
|
||||||
if (eyeLen > 0) {
|
|
||||||
const eyePtr = engine.eye_ptr();
|
|
||||||
const eyeData = new Float32Array(wasm.memory.buffer, eyePtr, eyeLen);
|
|
||||||
self.postMessage({ type: "EYE_DATA", payload: eyeData.slice() });
|
|
||||||
}
|
|
||||||
} else if (type === "SHOCKWAVE") {
|
} else if (type === "SHOCKWAVE") {
|
||||||
if (engine) engine.trigger_shockwave(payload.x, payload.y);
|
if (engine) engine.trigger_shockwave(payload.x, payload.y);
|
||||||
} else if (type === "KINETIC_SWIPE") {
|
} else if (type === "KINETIC_SWIPE") {
|
||||||
if (engine) engine.kinetic_swipe(payload.x, payload.y);
|
if (engine) engine.kinetic_swipe(payload.x, payload.y);
|
||||||
} else if (type === "TILT") {
|
} else if (type === "TILT") {
|
||||||
if (engine && typeof engine.set_tilt === "function") {
|
if (engine && typeof engine.set_tilt === "function")
|
||||||
engine.set_tilt(payload.x, payload.y);
|
engine.set_tilt(payload.x, payload.y);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
||||||
const color = isVpnOn
|
const color = isVpnOn
|
||||||
? isDarkMode
|
? isDarkMode
|
||||||
@@ -193,7 +194,8 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
const eyeRadius = 36 * (1 - isClosed) + 8 * isClosed;
|
const eyeRadius = 36 * (1 - isClosed) + 8 * isClosed;
|
||||||
const laserWidth = 140 * isClosed;
|
const laserWidth = 140 * isClosed;
|
||||||
|
|
||||||
const shadowMult = isMobile ? 0.2 : 1.0;
|
// ОПТИМИЗАЦИЯ: На мобилках тени убивают FPS. Отключаем их полностью для контуров.
|
||||||
|
const blurMultiplier = isMobile ? 0 : 1.0;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = bgFill;
|
ctx.fillStyle = bgFill;
|
||||||
@@ -210,13 +212,13 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 4 * (1 - isClosed) + 3 * isClosed;
|
ctx.lineWidth = 4 * (1 - isClosed) + 3 * isClosed;
|
||||||
ctx.shadowColor = color;
|
ctx.shadowColor = color;
|
||||||
ctx.shadowBlur = (15 * (1 - isClosed) + 8 * isClosed) * shadowMult;
|
ctx.shadowBlur = (15 * (1 - isClosed) + 8 * isClosed) * blurMultiplier;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
if (isVpnOn && isClosed > 0.1) {
|
if (isVpnOn && isClosed > 0.1) {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.shadowColor = color;
|
ctx.shadowColor = color;
|
||||||
ctx.shadowBlur = 8;
|
ctx.shadowBlur = 8 * blurMultiplier;
|
||||||
ctx.globalAlpha = isClosed;
|
ctx.globalAlpha = isClosed;
|
||||||
ctx.fillRect(cx - laserWidth / 2, cy - 1, laserWidth, 2);
|
ctx.fillRect(cx - laserWidth / 2, cy - 1, laserWidth, 2);
|
||||||
}
|
}
|
||||||
@@ -228,19 +230,18 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
|
|
||||||
const t = performance.now() * 0.0015;
|
const t = performance.now() * 0.0015;
|
||||||
|
|
||||||
// 1. Внешнее кибер-кольцо
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.rotate(t);
|
ctx.rotate(t);
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.globalAlpha = 0.7 * alphaMult;
|
ctx.globalAlpha = 0.7 * alphaMult;
|
||||||
ctx.setLineDash([8, 6, 2, 6]);
|
ctx.setLineDash([8, 6, 2, 6]);
|
||||||
|
ctx.shadowBlur = 0; // Строго без теней для сложных путей
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(0, 0, 16, 0, Math.PI * 2);
|
ctx.arc(0, 0, 16, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// 2. Внутреннее сплошное кольцо
|
|
||||||
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.globalAlpha = alphaMult;
|
ctx.globalAlpha = alphaMult;
|
||||||
@@ -248,15 +249,13 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 3. ЗРАЧОК (Теперь он живой: пульсирует и имеет энерго-ядро)
|
// ЗРАЧОК: Кибер-дыхание
|
||||||
const pulse = Math.sin(t * 5) * 0.15 + 0.85; // Пульсация масштаба от 0.7 до 1.0
|
const pulse = Math.sin(t * 5) * 0.15 + 0.85;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.scale(pulse, pulse); // Применяем дыхание
|
ctx.scale(pulse, pulse);
|
||||||
|
|
||||||
// Внешний ромб зрачка
|
|
||||||
ctx.fillStyle = isDarkMode ? "#fff" : "#000";
|
ctx.fillStyle = isDarkMode ? "#fff" : "#000";
|
||||||
ctx.shadowBlur = 10 * shadowMult;
|
ctx.shadowBlur = 10 * blurMultiplier;
|
||||||
ctx.shadowColor = color;
|
ctx.shadowColor = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, -6);
|
ctx.moveTo(0, -6);
|
||||||
@@ -265,38 +264,16 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
ctx.lineTo(-5, 0);
|
ctx.lineTo(-5, 0);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Внутреннее ядро (микро-точка)
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(0, 0, 1.5, 0, Math.PI * 2);
|
ctx.arc(0, 0, 1.5, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// 2. Внутреннее сплошное кольцо для глубины
|
|
||||||
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.globalAlpha = alphaMult;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 3. Зрачок (Острый кибер-ромб вместо скучной точки)
|
|
||||||
ctx.fillStyle = isDarkMode ? "#fff" : "#000";
|
|
||||||
ctx.shadowBlur = 10;
|
|
||||||
ctx.shadowColor = color; // Легкое свечение зрачка цветом темы
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, -5);
|
|
||||||
ctx.lineTo(4, 0);
|
|
||||||
ctx.lineTo(0, 5);
|
|
||||||
ctx.lineTo(-4, 0);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 4. Техно-перекрестие (HUD)
|
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.shadowBlur = 5;
|
ctx.shadowBlur = 5 * blurMultiplier;
|
||||||
ctx.globalAlpha = 0.4 * alphaMult;
|
ctx.globalAlpha = 0.4 * alphaMult;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(-22, 0);
|
ctx.moveTo(-22, 0);
|
||||||
@@ -309,12 +286,11 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|||||||
ctx.lineTo(0, 12);
|
ctx.lineTo(0, 12);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 5. Индикаторы направления (теперь тоже в цвет интерфейса)
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.font = "10px monospace";
|
ctx.font = "10px monospace";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.shadowBlur = 8;
|
ctx.shadowBlur = 8 * blurMultiplier;
|
||||||
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.15) * alphaMult;
|
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.15) * alphaMult;
|
||||||
ctx.fillText("▴", 0, -26);
|
ctx.fillText("▴", 0, -26);
|
||||||
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.15) * alphaMult;
|
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.15) * alphaMult;
|
||||||
@@ -334,14 +310,12 @@ function render() {
|
|||||||
|
|
||||||
engine.tick();
|
engine.tick();
|
||||||
|
|
||||||
// Очистка экрана с небольшим шлейфом (альфа 0.2)
|
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
ctx.fillStyle = isDarkMode
|
ctx.fillStyle = isDarkMode
|
||||||
? "rgba(10, 10, 12, 0.2)"
|
? "rgba(10, 10, 12, 0.2)"
|
||||||
: "rgba(250, 250, 252, 0.2)";
|
: "rgba(250, 250, 252, 0.2)";
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
// --- ЕДИНЫЙ РЕНДЕР МАТРИЦЫ (Параллакс + Ударные волны) ---
|
|
||||||
const renderLen = engine.render_len();
|
const renderLen = engine.render_len();
|
||||||
if (renderLen > 0) {
|
if (renderLen > 0) {
|
||||||
const data = new Float32Array(
|
const data = new Float32Array(
|
||||||
@@ -350,13 +324,14 @@ function render() {
|
|||||||
renderLen,
|
renderLen,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Шаг 6, так как в Rust мы пишем [x, y, char_idx, is_head, alpha, scale]
|
|
||||||
for (let i = 0; i < renderLen; i += 6) {
|
for (let i = 0; i < renderLen; i += 6) {
|
||||||
|
const alpha = data[i + 4];
|
||||||
|
if (alpha <= 0.01) continue; // ОПТИМИЗАЦИЯ: Не рисуем невидимые элементы
|
||||||
|
|
||||||
const x = data[i];
|
const x = data[i];
|
||||||
const y = data[i + 1];
|
const y = data[i + 1];
|
||||||
const charIdx = data[i + 2];
|
const charIdx = data[i + 2];
|
||||||
const isHead = data[i + 3];
|
const isHead = data[i + 3];
|
||||||
const alpha = data[i + 4];
|
|
||||||
const scale = data[i + 5];
|
const scale = data[i + 5];
|
||||||
|
|
||||||
const scaledSize = fontSize * scale;
|
const scaledSize = fontSize * scale;
|
||||||
@@ -365,7 +340,7 @@ function render() {
|
|||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
atlasCanvas,
|
atlasCanvas,
|
||||||
charIdx * fontSize,
|
charIdx * fontSize,
|
||||||
isHead * fontSize, // Выбираем строку атласа: 0 (цвет) или 1 (белый)
|
isHead * fontSize,
|
||||||
fontSize,
|
fontSize,
|
||||||
fontSize,
|
fontSize,
|
||||||
x,
|
x,
|
||||||
@@ -376,7 +351,6 @@ function render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- РЕНДЕР ГЛАЗ ---
|
|
||||||
const eyeLen = engine.eye_len();
|
const eyeLen = engine.eye_len();
|
||||||
if (eyeLen >= 6) {
|
if (eyeLen >= 6) {
|
||||||
const eyeData = new Float32Array(
|
const eyeData = new Float32Array(
|
||||||
@@ -391,13 +365,12 @@ function render() {
|
|||||||
const dir = eyeData[4];
|
const dir = eyeData[4];
|
||||||
const isClosed = eyeData[5];
|
const isClosed = eyeData[5];
|
||||||
|
|
||||||
const gap = isMobile ? 85 : 85; // Используем новые отступы
|
const gap = isMobile ? 85 : 85;
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
drawEye(ctx, cx - gap, cy, gazeX, gazeY, dir, isClosed);
|
drawEye(ctx, cx - gap, cy, gazeX, gazeY, dir, isClosed);
|
||||||
drawEye(ctx, cx + gap, cy, gazeX, gazeY, dir, isClosed);
|
drawEye(ctx, cx + gap, cy, gazeX, gazeY, dir, isClosed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- РЕНДЕР ГЛИТЧ-БЛОКОВ ---
|
|
||||||
const glLen = engine.glitch_len();
|
const glLen = engine.glitch_len();
|
||||||
if (glLen > 0) {
|
if (glLen > 0) {
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
@@ -412,7 +385,6 @@ function render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- РЕНДЕР ИСКР (Glass Symbols) ---
|
|
||||||
const gsLen = engine.glass_len();
|
const gsLen = engine.glass_len();
|
||||||
if (gsLen > 0) {
|
if (gsLen > 0) {
|
||||||
const gsData = new Float32Array(
|
const gsData = new Float32Array(
|
||||||
@@ -425,7 +397,7 @@ function render() {
|
|||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
atlasCanvas,
|
atlasCanvas,
|
||||||
gsData[i + 2] * fontSize,
|
gsData[i + 2] * fontSize,
|
||||||
0, // Искры всегда основного цвета (строка 0)
|
0,
|
||||||
fontSize,
|
fontSize,
|
||||||
fontSize,
|
fontSize,
|
||||||
gsData[i],
|
gsData[i],
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ impl MatrixEngine {
|
|||||||
1 => 0.7,
|
1 => 0.7,
|
||||||
_ => 1.0,
|
_ => 1.0,
|
||||||
};
|
};
|
||||||
let final_x = drop.base_x + drop.offset_x + (self.tilt_x * 30.0 * parallax_factor);
|
let final_x = drop.base_x + drop.offset_x + (self.tilt_x * 60.0 * parallax_factor);
|
||||||
|
|
||||||
let base_idx = i * 6;
|
let base_idx = i * 6;
|
||||||
self.render_buffer[base_idx] = final_x;
|
self.render_buffer[base_idx] = final_x;
|
||||||
|
|||||||
Reference in New Issue
Block a user