optimization and some fixes v3

This commit is contained in:
2026-04-26 12:31:58 +07:00
parent c669d2e175
commit 56651f587f
3 changed files with 166 additions and 127 deletions
+1 -1
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.3"; const ENGINE_VERSION = "2.0.4";
interface NetrunnerMatrixProps { interface NetrunnerMatrixProps {
isSecure?: boolean; isSecure?: boolean;
+92 -50
View File
@@ -13,6 +13,8 @@ let isMobile = false,
isVpnOn = false, isVpnOn = false,
isDarkMode = true; isDarkMode = true;
let lastDrawTime = 0;
// Ссылка на класс, которую мы получим после импорта модуля // Ссылка на класс, которую мы получим после импорта модуля
let MatrixEngineClass; let MatrixEngineClass;
@@ -24,8 +26,11 @@ const chars =
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥"; "0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
function buildAtlas() { function buildAtlas() {
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 2); // Увеличиваем атлас до 11 строк
atlasCtx = atlasCanvas.getContext("2d", { alpha: false }); atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
// ВАЖНО: alpha: true, чтобы фон мог быть полупрозрачным!
atlasCtx = atlasCanvas.getContext("2d", { alpha: true });
atlasCtx.clearRect(0, 0, atlasCanvas.width, atlasCanvas.height);
let r, g, b; let r, g, b;
if (isVpnOn && isDarkMode) { if (isVpnOn && isDarkMode) {
@@ -46,21 +51,40 @@ function buildAtlas() {
b = 199; b = 199;
} }
atlasCtx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)"; const bgR = isDarkMode ? 10 : 250;
atlasCtx.fillRect(0, 0, atlasCanvas.width, atlasCanvas.height); const bgG = isDarkMode ? 10 : 250;
const bgB = isDarkMode ? 12 : 252;
atlasCtx.font = `bold ${fontSize}px monospace`; atlasCtx.font = `bold ${fontSize}px monospace`;
atlasCtx.textBaseline = "top"; atlasCtx.textBaseline = "top";
atlasCtx.textAlign = "center"; atlasCtx.textAlign = "center";
const halfFs = fontSize / 2; const halfFs = fontSize / 2;
atlasCtx.fillStyle = `rgb(${r}, ${g}, ${b})`; // Строки 0..9: Хвост (запекаем альфу прямо в картинку)
for (let i = 0; i < chars.length; i++) for (let row = 0; row <= 9; row++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, 0); const alpha = (row + 1) / 10;
const yOffset = row * fontSize;
// Рисуем полупрозрачный фон
atlasCtx.fillStyle = `rgba(${bgR}, ${bgG}, ${bgB}, ${alpha})`;
atlasCtx.fillRect(0, yOffset, atlasCanvas.width, fontSize);
// Рисуем полупрозрачный текст
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
}
}
// Строка 10: Белая "Голова" (Альфа 1.0)
const headY = 10 * fontSize;
atlasCtx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`;
atlasCtx.fillRect(0, headY, atlasCanvas.width, fontSize);
atlasCtx.fillStyle = "rgb(255, 255, 255)"; atlasCtx.fillStyle = "rgb(255, 255, 255)";
for (let i = 0; i < chars.length; i++) for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, fontSize); atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
}
} }
function getGlitchColor(type) { function getGlitchColor(type) {
@@ -194,7 +218,6 @@ 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;
// ОПТИМИЗАЦИЯ: На мобилках тени убивают FPS. Отключаем их полностью для контуров.
const blurMultiplier = isMobile ? 0 : 1.0; const blurMultiplier = isMobile ? 0 : 1.0;
ctx.save(); ctx.save();
@@ -228,7 +251,7 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
ctx.save(); ctx.save();
ctx.translate(cx + gazeX, cy + gazeY); ctx.translate(cx + gazeX, cy + gazeY);
const t = performance.now() * 0.0015; const t = performance.now() * 0.003; // Ускорено вращение колец
ctx.save(); ctx.save();
ctx.rotate(t); ctx.rotate(t);
@@ -236,7 +259,7 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
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.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();
@@ -249,7 +272,6 @@ 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();
// ЗРАЧОК: Кибер-дыхание
const pulse = Math.sin(t * 5) * 0.15 + 0.85; const pulse = Math.sin(t * 5) * 0.15 + 0.85;
ctx.save(); ctx.save();
ctx.scale(pulse, pulse); ctx.scale(pulse, pulse);
@@ -305,15 +327,14 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
ctx.restore(); ctx.restore();
} }
function render() { // === РАЗДЕЛЕНИЕ ЛОГИКИ ОТРИСОВКИ ===
if (!engine || !wasm) return requestAnimationFrame(render);
engine.tick();
function drawMatrix() {
// Очистка экрана с небольшим шлейфом
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.25)"
: "rgba(250, 250, 252, 0.2)"; : "rgba(250, 250, 252, 0.25)";
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
const renderLen = engine.render_len(); const renderLen = engine.render_len();
@@ -323,24 +344,19 @@ function render() {
engine.render_ptr(), engine.render_ptr(),
renderLen, renderLen,
); );
for (let i = 0; i < renderLen; i += 5) {
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 atlasRow = data[i + 3];
const scale = data[i + 5]; const scale = data[i + 4];
const scaledSize = fontSize * scale; const scaledSize = fontSize * scale;
ctx.globalAlpha = alpha;
ctx.drawImage( ctx.drawImage(
atlasCanvas, atlasCanvas,
charIdx * fontSize, charIdx * fontSize,
isHead * fontSize, atlasRow * fontSize,
fontSize, fontSize,
fontSize, fontSize,
x, x,
@@ -350,27 +366,10 @@ function render() {
); );
} }
} }
}
const eyeLen = engine.eye_len(); function drawEffects() {
if (eyeLen >= 6) { // Рендер Глитч-блоков
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
const cx = eyeData[0];
const cy = eyeData[1];
const gazeX = eyeData[2];
const gazeY = eyeData[3];
const dir = eyeData[4];
const isClosed = eyeData[5];
const gap = isMobile ? 85 : 85;
ctx.globalAlpha = 1.0;
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;
@@ -385,6 +384,7 @@ function render() {
} }
} }
// Рендер Искр
const gsLen = engine.glass_len(); const gsLen = engine.glass_len();
if (gsLen > 0) { if (gsLen > 0) {
const gsData = new Float32Array( const gsData = new Float32Array(
@@ -407,6 +407,48 @@ function render() {
); );
} }
} }
}
requestAnimationFrame(render);
function drawEyesBuffers() {
const eyeLen = engine.eye_len();
if (eyeLen >= 6) {
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
const cx = eyeData[0];
const cy = eyeData[1];
const gazeX = eyeData[2];
const gazeY = eyeData[3];
const dir = eyeData[4];
const isClosed = eyeData[5];
const gap = isMobile ? 85 : 85;
ctx.globalAlpha = 1.0;
drawEye(ctx, cx - gap, cy, gazeX, gazeY, dir, isClosed);
drawEye(ctx, cx + gap, cy, gazeX, gazeY, dir, isClosed);
}
}
// === ГЛАВНЫЙ ЦИКЛ ===
function render(currentTime) {
requestAnimationFrame(render);
if (!engine || !wasm) return;
const now = currentTime || performance.now();
// Адаптивный FPS
const fpsLimit = isMobile ? 33 : 16;
if (now - lastDrawTime < fpsLimit) return;
lastDrawTime = now;
// 1. Считаем физику (только 1 раз за кадр!)
engine.tick();
// 2. Рисуем слои
drawMatrix();
drawEffects();
drawEyesBuffers();
} }
+73 -76
View File
@@ -43,12 +43,12 @@ impl FastRng {
} }
struct Drop { struct Drop {
base_x: f32, // Якорная позиция по X (с учетом сноса ветром/гироскопом) base_x: f32,
y: f32, y: f32,
offset_x: f32, // Текущее смещение для пружинной анимации offset_x: f32,
vel_x: f32, // Текущая скорость смещения vel_x: f32,
speed: f32, speed: f32,
layer: u8, // 0 = Дальний (Far), 1 = Средний (Mid), 2 = Ближний (Near) layer: u8,
glitch_timer: f32, glitch_timer: f32,
} }
@@ -117,7 +117,10 @@ pub struct MatrixEngine {
glass_symbols: Vec<GlassSymbol>, glass_symbols: Vec<GlassSymbol>,
glitch_blocks: Vec<GlitchBlock>, glitch_blocks: Vec<GlitchBlock>,
render_buffer: Vec<f32>, // Единый буфер рендера [x, y, char_idx, is_head, alpha, scale] // Буфер рендера: [x, y, char_idx, atlas_row, scale] (Stride = 5)
render_buffer: Vec<f32>,
visible_drops: usize, // Счетчик видимых элементов
glitch_buffer: Vec<f32>, glitch_buffer: Vec<f32>,
glass_buffer: Vec<f32>, glass_buffer: Vec<f32>,
eye_buffer: Vec<f32>, eye_buffer: Vec<f32>,
@@ -134,7 +137,6 @@ pub struct MatrixEngine {
tear_y: f32, tear_y: f32,
tear_timer: f32, tear_timer: f32,
// Eye state
blink_timer: f32, blink_timer: f32,
last_interaction: f32, last_interaction: f32,
gaze_target_x: f32, gaze_target_x: f32,
@@ -146,7 +148,6 @@ pub struct MatrixEngine {
eye_anchor_y: f32, eye_anchor_y: f32,
mobile_wander_timer: f32, mobile_wander_timer: f32,
// Mobile & Physics state
tilt_x: f32, tilt_x: f32,
tilt_y: f32, tilt_y: f32,
swipe_x: f32, swipe_x: f32,
@@ -182,6 +183,7 @@ impl MatrixEngine {
glass_symbols: Vec::new(), glass_symbols: Vec::new(),
glitch_blocks: Vec::new(), glitch_blocks: Vec::new(),
render_buffer: Vec::new(), render_buffer: Vec::new(),
visible_drops: 0,
glitch_buffer: Vec::new(), glitch_buffer: Vec::new(),
glass_buffer: Vec::new(), glass_buffer: Vec::new(),
eye_buffer: Vec::new(), eye_buffer: Vec::new(),
@@ -219,18 +221,17 @@ impl MatrixEngine {
fn init_drops(&mut self) { fn init_drops(&mut self) {
self.drops.clear(); self.drops.clear();
// 3 слоя параллакса: (Layer ID, Scale, Min Speed, Max Speed) // Ускоренный в 2 раза дождь:
let layers = [ let layers = [
(0, 0.4, 0.005, 0.02), // Far (бывший bg_drops) (0, 0.4, 0.01, 0.04), // Far: было 0.005-0.02
(1, 0.7, 0.02, 0.06), // Mid (1, 0.7, 0.04, 0.12), // Mid: было 0.02-0.06
(2, 1.0, 0.05, 0.15), // Near (бывший fg_drops) (2, 1.0, 0.10, 0.30), // Near: было 0.05-0.15
]; ];
let base_spacing = if self.is_mobile { 1.5 } else { 1.0 } * self.font_size as f32; let base_spacing = self.font_size as f32;
for (layer, scale, min_spd, max_spd) in layers.iter() { for (layer, scale, min_spd, max_spd) in layers.iter() {
let spacing = base_spacing * scale; let spacing = base_spacing * scale;
// Делаем запас колонок по бокам для параллакса и наклона
let columns = (self.width as f32 / spacing).ceil() as u32 + 20; let columns = (self.width as f32 / spacing).ceil() as u32 + 20;
for i in 0..columns { for i in 0..columns {
@@ -247,8 +248,8 @@ impl MatrixEngine {
} }
} }
// Выделяем память: размер вектора = кол-во капель * 6 параметров // Stride 5: [x, y, char_idx, atlas_row, scale]
self.render_buffer.resize(self.drops.len() * 6, 0.0); self.render_buffer.resize(self.drops.len() * 5, 0.0);
} }
pub fn resize(&mut self, width: u32, height: u32) { pub fn resize(&mut self, width: u32, height: u32) {
@@ -264,7 +265,6 @@ impl MatrixEngine {
self.gaze_target_y = y; self.gaze_target_y = y;
self.last_interaction = 0.0; self.last_interaction = 0.0;
// Автоматически запускаем магнитный свайп (Kinetic Swipe) при любом движении!
self.swipe_x = x; self.swipe_x = x;
self.swipe_y = y; self.swipe_y = y;
self.swipe_timer = 15.0; self.swipe_timer = 15.0;
@@ -273,28 +273,23 @@ impl MatrixEngine {
pub fn set_drawing(&mut self, is_drawing: bool) { pub fn set_drawing(&mut self, is_drawing: bool) {
self.is_drawing = is_drawing; self.is_drawing = is_drawing;
} }
pub fn set_mobile(&mut self, is_mobile: bool) { pub fn set_mobile(&mut self, is_mobile: bool) {
self.is_mobile = is_mobile; self.is_mobile = is_mobile;
} }
pub fn set_eye_anchor(&mut self, cy: f32) { pub fn set_eye_anchor(&mut self, cy: f32) {
self.eye_anchor_y = cy; self.eye_anchor_y = cy;
} }
pub fn set_vpn_status(&mut self, is_vpn_on: bool) { pub fn set_vpn_status(&mut self, is_vpn_on: bool) {
self.is_vpn_on = is_vpn_on; self.is_vpn_on = is_vpn_on;
} }
pub fn set_tilt(&mut self, tilt_x: f32, tilt_y: f32) { pub fn set_tilt(&mut self, tilt_x: f32, tilt_y: f32) {
self.tilt_x = tilt_x; self.tilt_x = tilt_x;
self.tilt_y = tilt_y; self.tilt_y = tilt_y;
} }
pub fn trigger_shockwave(&mut self, x: f32, y: f32) { pub fn trigger_shockwave(&mut self, x: f32, y: f32) {
// 1. Заставляем глаза смотреть на точку клика и обновляем "магнит"
self.update_mouse(x, y); self.update_mouse(x, y);
// 2. Запускаем физику ударной волны
if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) { if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) {
sw.x = x; sw.x = x;
sw.y = y; sw.y = y;
@@ -304,7 +299,6 @@ impl MatrixEngine {
sw.active = true; sw.active = true;
} }
// 3. Добавляем небольшой цифровой всплеск (глитч) при ударе
let blocks_count = if self.is_mobile { 4 } else { 12 }; let blocks_count = if self.is_mobile { 4 } else { 12 };
for _ in 0..blocks_count { for _ in 0..blocks_count {
let block = GlitchBlock::new(x, y, &mut self.rng); let block = GlitchBlock::new(x, y, &mut self.rng);
@@ -316,8 +310,6 @@ impl MatrixEngine {
self.swipe_x = x; self.swipe_x = x;
self.swipe_y = y; self.swipe_y = y;
self.swipe_timer = 15.0; self.swipe_timer = 15.0;
// Передаем координаты свайпа глазам, чтобы они чётко следили за движением
self.update_mouse(x, y); self.update_mouse(x, y);
} }
@@ -326,7 +318,6 @@ impl MatrixEngine {
self.emp_x = x; self.emp_x = x;
self.emp_y = y; self.emp_y = y;
// 1. Запускаем Ударную Волну (Shockwave) прямо по клику!
if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) { if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) {
sw.x = x; sw.x = x;
sw.y = y; sw.y = y;
@@ -336,27 +327,25 @@ impl MatrixEngine {
sw.active = true; sw.active = true;
} }
// 2. Спавним прямоугольные глитчи вместе с волной
let blocks_count = if self.is_mobile { 5 } else { 25 }; let blocks_count = if self.is_mobile { 5 } else { 25 };
for _ in 0..blocks_count { for _ in 0..blocks_count {
let block = GlitchBlock::new(x, y, &mut self.rng); let block = GlitchBlock::new(x, y, &mut self.rng);
self.glitch_blocks.push(block); self.glitch_blocks.push(block);
} }
self.update_mouse(x, y); self.update_mouse(x, y);
} }
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.tick_count = self.tick_count.wrapping_add(1); self.tick_count = self.tick_count.wrapping_add(1);
self.last_interaction += 1.0; self.last_interaction += 1.0;
self.visible_drops = 0; // СБРОС СЧЕТЧИКА ВИДИМЫХ КАПЕЛЬ
self.glitch_buffer.clear(); self.glitch_buffer.clear();
self.glass_buffer.clear(); self.glass_buffer.clear();
self.eye_buffer.clear(); self.eye_buffer.clear();
// --- Ударные Волны ---
for sw in self.shockwaves.iter_mut().filter(|s| s.active) { for sw in self.shockwaves.iter_mut().filter(|s| s.active) {
sw.radius += 25.0; // Расширяем кольцо sw.radius += 25.0;
sw.life -= 1.0; sw.life -= 1.0;
if sw.life <= 0.0 { if sw.life <= 0.0 {
sw.active = false; sw.active = false;
@@ -408,15 +397,16 @@ impl MatrixEngine {
} }
let distance = (dx * dx + dy * dy).sqrt(); let distance = (dx * dx + dy * dy).sqrt();
let target_dist = (distance * 0.1).min(20.0); let target_dist = (distance * 0.1_f32).min(20.0_f32);
let angle = dy.atan2(dx); let angle = dy.atan2(dx);
self.gaze_x += (angle.cos() * target_dist - self.gaze_x) * 0.12;
self.gaze_y += (angle.sin() * target_dist - self.gaze_y) * 0.12; self.gaze_x += (angle.cos() * target_dist - self.gaze_x) * 0.35_f32;
self.gaze_y += (angle.sin() * target_dist - self.gaze_y) * 0.35_f32;
if self.blink_timer > 0.0 { if self.blink_timer > 0.0 {
self.blink_timer -= 1.0; self.blink_timer -= 1.0;
} else if !self.is_vpn_on && self.rng.gen_bool(0.0015) { } else if !self.is_vpn_on && self.rng.gen_bool(0.0006) {
self.blink_timer = 16.0; self.blink_timer = 8.0;
} }
let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 { let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 {
@@ -424,9 +414,10 @@ impl MatrixEngine {
} else { } else {
0.0 0.0
}; };
self.eye_closedness_vel += (target_closedness - self.eye_closedness) * 0.06; self.eye_closedness_vel += (target_closedness - self.eye_closedness) * 0.15_f32;
self.eye_closedness_vel *= 0.88; self.eye_closedness_vel *= 0.75_f32;
self.eye_closedness = (self.eye_closedness + self.eye_closedness_vel).clamp(-0.15, 1.15); self.eye_closedness =
(self.eye_closedness + self.eye_closedness_vel).clamp(-0.15_f32, 1.15_f32);
self.eye_buffer.extend_from_slice(&[ self.eye_buffer.extend_from_slice(&[
cx, cx,
@@ -437,7 +428,6 @@ impl MatrixEngine {
self.eye_closedness, self.eye_closedness,
]); ]);
// --- Глобальные эффекты (EMP, Слезы) ---
if !self.is_mobile { if !self.is_mobile {
if self.rng.gen_bool(0.01) { if self.rng.gen_bool(0.01) {
let rx = self.rng.gen_range(0.0, self.width as f32); let rx = self.rng.gen_range(0.0, self.width as f32);
@@ -471,7 +461,8 @@ impl MatrixEngine {
0.0 0.0
}; };
// --- Итерация по единому массиву капель --- // --- Итерация по каплям ---
// --- Итерация по каплям ---
for i in 0..self.drops.len() { for i in 0..self.drops.len() {
let drop = &mut self.drops[i]; let drop = &mut self.drops[i];
@@ -481,7 +472,8 @@ impl MatrixEngine {
_ => 1.0, _ => 1.0,
}; };
// Бесконечный экран // Гироскоп и бесконечный экран по горизонтали
drop.base_x += self.tilt_x * parallax_factor * 5.0;
let wrap_margin = 150.0; let wrap_margin = 150.0;
if drop.base_x < -wrap_margin { if drop.base_x < -wrap_margin {
drop.base_x += self.width as f32 + wrap_margin * 2.0; drop.base_x += self.width as f32 + wrap_margin * 2.0;
@@ -494,23 +486,40 @@ impl MatrixEngine {
let current_x = drop.base_x + drop.offset_x; let current_x = drop.base_x + drop.offset_x;
let current_y = drop.y * (self.font_size as f32 * parallax_factor); let current_y = drop.y * (self.font_size as f32 * parallax_factor);
// === ИСПРАВЛЕННЫЙ FRUSTUM CULLING + РЕСПАУН ===
// Проверяем смерть капли ПЕРЕД отсечением
if current_y > self.height as f32 + 50.0 {
if self.rng.gen_bool(0.03) {
// 3% шанс возродиться каждый кадр (дает плотный поток)
drop.y = self.rng.gen_range(-20.0, -5.0);
drop.speed = match drop.layer {
0 => self.rng.gen_range(0.01, 0.04),
1 => self.rng.gen_range(0.04, 0.12),
_ => self.rng.gen_range(0.10, 0.30),
};
} else {
drop.y += drop.speed; // Если не возродилась, продолжает падать в пустоте
}
continue; // Не рендерим, она за экраном
} else if current_y < -50.0 {
drop.y += drop.speed;
continue; // Не рендерим, она еще летит сверху
}
let mut is_scrambled = false; let mut is_scrambled = false;
let mut is_void = false; let mut is_void = false;
let mut is_emp_ring = false; let mut is_emp_ring = false;
let mut force_white = false; let mut force_white = false;
// 2. УДАРНЫЕ ВОЛНЫ (по клику)
for sw in self.shockwaves.iter().filter(|s| s.active) { for sw in self.shockwaves.iter().filter(|s| s.active) {
let dx = current_x - sw.x; let dx = current_x - sw.x;
let dy = current_y - sw.y; let dy = current_y - sw.y;
// ОПТИМИЗАЦИЯ: Отсекаем капли вне зоны действия волны до вычисления sqrt()
if dx.abs() > sw.radius + 40.0 || dy.abs() > sw.radius + 40.0 { if dx.abs() > sw.radius + 40.0 || dy.abs() > sw.radius + 40.0 {
continue; continue;
} }
let dist = (dx * dx + dy * dy).sqrt(); let dist = (dx * dx + dy * dy).sqrt();
if (dist - sw.radius).abs() < 40.0 { if (dist - sw.radius).abs() < 40.0 {
let force = (sw.life / sw.max_life) * 18.0 * parallax_factor; let force = (sw.life / sw.max_life) * 18.0 * parallax_factor;
drop.vel_x += (dx / dist.max(1.0_f32)) * force; drop.vel_x += (dx / dist.max(1.0_f32)) * force;
@@ -518,10 +527,8 @@ impl MatrixEngine {
} }
} }
// 3. КИНЕТИЧЕСКИЙ СВАЙП И МАГНИТ (Палец/Мышь)
let check_swipe = self.swipe_timer > 0.0; let check_swipe = self.swipe_timer > 0.0;
let check_mouse = !self.is_mobile && self.last_interaction < 10.0; let check_mouse = !self.is_mobile && self.last_interaction < 10.0;
if check_swipe || check_mouse { if check_swipe || check_mouse {
let (ix, iy) = if check_swipe { let (ix, iy) = if check_swipe {
(self.swipe_x, self.swipe_y) (self.swipe_x, self.swipe_y)
@@ -534,13 +541,10 @@ impl MatrixEngine {
let rad = if self.is_mobile { 100.0 } else { 140.0 }; let rad = if self.is_mobile { 100.0 } else { 140.0 };
if dist_sq < rad * rad { if dist_sq < rad * rad {
let dist = dist_sq.sqrt().max(1.0); let dist = dist_sq.sqrt().max(1.0_f32);
let force = (rad - dist) / rad; let force = (rad - dist) / rad;
// Отталкивание пружины
drop.vel_x += (dx / dist) * force * 15.0 * parallax_factor; drop.vel_x += (dx / dist) * force * 15.0 * parallax_factor;
// Замедление падения ("Вязкость")
drop.y -= drop.speed * force * 0.6; drop.y -= drop.speed * force * 0.6;
is_scrambled = true; is_scrambled = true;
if dist < rad * 0.5 { if dist < rad * 0.5 {
force_white = true; force_white = true;
@@ -548,12 +552,10 @@ impl MatrixEngine {
} }
} }
// EMP Волна от включения VPN (только для передних слоев)
if self.emp_timer > 0.0 && drop.layer > 0 { if self.emp_timer > 0.0 && drop.layer > 0 {
let dx = current_x - self.emp_x; let dx = current_x - self.emp_x;
let dy = current_y - self.emp_y; let dy = current_y - self.emp_y;
let dist_sq = dx * dx + dy * dy; let dist_sq = dx * dx + dy * dy;
if dist_sq < emp_rad_sq { if dist_sq < emp_rad_sq {
if dist_sq > emp_r20_sq { if dist_sq > emp_r20_sq {
is_emp_ring = true; is_emp_ring = true;
@@ -572,13 +574,11 @@ impl MatrixEngine {
if is_void { if is_void {
drop.y += drop.speed; drop.y += drop.speed;
self.render_buffer[i * 6 + 4] = 0.0; // Alpha = 0 continue; // Не пишем в буфер вообще
continue;
} }
// 4. ПРУЖИННАЯ ФИЗИКА drop.vel_x += -0.12 * drop.offset_x;
drop.vel_x += -0.12 * drop.offset_x; // Сила возврата (Гука) drop.vel_x *= 0.85;
drop.vel_x *= 0.85; // Трение (Затухание)
drop.offset_x += drop.vel_x; drop.offset_x += drop.vel_x;
if self.rng.gen_bool(0.001) { if self.rng.gen_bool(0.001) {
@@ -606,7 +606,7 @@ impl MatrixEngine {
char_idx = (hash.wrapping_add(self.tick_count) as usize) % self.charset_len; char_idx = (hash.wrapping_add(self.tick_count) as usize) % self.charset_len;
} else if is_scrambled || drop.glitch_timer > 0.0 { } else if is_scrambled || drop.glitch_timer > 0.0 {
char_idx = (hash.wrapping_add(self.tick_count) as usize) % self.charset_len; char_idx = (hash.wrapping_add(self.tick_count) as usize) % self.charset_len;
base_alpha = (base_alpha + 0.2_f32).min(1.0_f32); base_alpha = (base_alpha + 0.2_f32).clamp(0.0_f32, 1.0_f32);
is_head = (hash % 10) < 2; is_head = (hash % 10) < 2;
if drop.glitch_timer > 0.0 { if drop.glitch_timer > 0.0 {
drop.glitch_timer -= 1.0; drop.glitch_timer -= 1.0;
@@ -615,38 +615,34 @@ impl MatrixEngine {
if force_white { if force_white {
is_head = true; is_head = true;
base_alpha = 1.0;
} }
if self.is_mobile && !force_white && drop.layer == 2 { // ОПТИМИЗАЦИЯ: Расчет строки атласа вместо прозрачности (0..10)
base_alpha = 0.8; let atlas_row = if is_head {
} 10.0 // Строка 10 - полностью белая голова
} else {
(base_alpha * 9.0).clamp(0.0_f32, 9.0_f32).floor() // От 0 до 9 для цвета
};
let atlas_row = if is_head { 1.0 } else { 0.0 };
let scale = match drop.layer { let scale = match drop.layer {
0 => 0.4, 0 => 0.4,
1 => 0.7, 1 => 0.7,
_ => 1.0, _ => 1.0,
}; };
// Добавляем красивый 3D гироскоп
let final_x = drop.base_x + drop.offset_x + (self.tilt_x * 60.0 * parallax_factor); let final_x = drop.base_x + drop.offset_x + (self.tilt_x * 60.0 * parallax_factor);
let base_idx = i * 6; // Пишем в буфер (Stride = 5)
let base_idx = self.visible_drops * 5;
self.render_buffer[base_idx] = final_x; self.render_buffer[base_idx] = final_x;
self.render_buffer[base_idx + 1] = current_y; self.render_buffer[base_idx + 1] = current_y;
self.render_buffer[base_idx + 2] = char_idx as f32; self.render_buffer[base_idx + 2] = char_idx as f32;
self.render_buffer[base_idx + 3] = atlas_row; self.render_buffer[base_idx + 3] = atlas_row;
self.render_buffer[base_idx + 4] = base_alpha; self.render_buffer[base_idx + 4] = scale;
self.render_buffer[base_idx + 5] = scale;
self.visible_drops += 1;
drop.y += drop.speed; drop.y += drop.speed;
if current_y > self.height as f32 + 50.0 && self.rng.gen_bool(0.01) {
drop.y = self.rng.gen_range(-20.0, 0.0);
drop.speed = match drop.layer {
0 => self.rng.gen_range(0.005, 0.02),
1 => self.rng.gen_range(0.02, 0.06),
_ => self.rng.gen_range(0.05, 0.15),
};
}
} }
// --- Глитчи и Осколки (Пылинки) --- // --- Глитчи и Осколки (Пылинки) ---
@@ -684,12 +680,13 @@ impl MatrixEngine {
self.glass_symbols.retain(|s| s.life > 0.0); self.glass_symbols.retain(|s| s.life > 0.0);
} }
// Обрати внимание: Вместо fg_ptr и bg_ptr теперь один render_ptr
pub fn render_ptr(&self) -> *const f32 { pub fn render_ptr(&self) -> *const f32 {
self.render_buffer.as_ptr() self.render_buffer.as_ptr()
} }
// Возвращаем ТОЛЬКО размер видимых капель * 5
pub fn render_len(&self) -> usize { pub fn render_len(&self) -> usize {
self.render_buffer.len() self.visible_drops * 5
} }
pub fn glitch_ptr(&self) -> *const f32 { pub fn glitch_ptr(&self) -> *const f32 {