optimization and some fixes

This commit is contained in:
2026-04-26 12:04:01 +07:00
parent 5bb5c4e75e
commit c669d2e175
3 changed files with 78 additions and 105 deletions
+40 -39
View File
@@ -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]);
+37 -65
View File
@@ -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],
+1 -1
View File
@@ -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;