hero matrix engine update

This commit is contained in:
2026-04-26 11:42:21 +07:00
parent 9df999af94
commit 166d0e7f08
3 changed files with 489 additions and 276 deletions
+60 -62
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.1"; const ENGINE_VERSION = "2.0.2";
interface NetrunnerMatrixProps { interface NetrunnerMatrixProps {
isSecure?: boolean; isSecure?: boolean;
@@ -151,80 +151,78 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}); });
} }
}, [isSecure, isDarkMode, mounted]); }, [isSecure, isDarkMode, mounted]);
useEffect(() => { useEffect(() => {
if (!mounted) return; if (!mounted || !workerRef.current) return;
const handleEvents = (e: any) => { // --- 1. Гироскоп (Наклон телефона) ---
if (!workerRef.current || !canvasRef.current) return; const handleOrientation = (e: DeviceOrientationEvent) => {
let gamma = e.gamma || 0; // Наклон влево/вправо (-90 до 90)
const isInteractive = e.target.closest( // Ограничиваем угол для комфорта
'button, a, input, [role="button"], [role="switch"], .data-\\[state\\]', if (gamma > 45) gamma = 45;
); if (gamma < -45) gamma = -45;
const isDown = e.type === "pointerdown" || e.type === "touchstart"; // Нормализуем от -1.0 до 1.0
const isUp = const tiltX = gamma / 45.0;
e.type === "pointerup" ||
e.type === "touchend" ||
e.type === "pointercancel";
if (isInteractive && isDown) return; workerRef.current?.postMessage({
type: "TILT",
const type = isDown ? "DRAW_START" : isUp ? "DRAW_END" : "MOUSE_MOVE"; payload: { x: tiltX, y: 0 },
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 }; // Запрос прав для iOS 13+ (если нужно, можно вынести в кнопку включения VPN)
if (
if (window.PointerEvent) { typeof (DeviceOrientationEvent as any).requestPermission === "function"
window.addEventListener("pointerdown", handleEvents, options as any); ) {
window.addEventListener("pointermove", handleEvents, options as any); // Это должно вызываться по клику пользователя, здесь оставлено для примера
window.addEventListener("pointerup", handleEvents, options as any); // (DeviceOrientationEvent as any).requestPermission().then(...)
window.addEventListener("pointercancel", handleEvents, options as any);
} else { } else {
window.addEventListener("touchstart", handleEvents, options); window.addEventListener("deviceorientation", handleOrientation);
window.addEventListener("touchmove", handleEvents, options);
window.addEventListener("touchend", handleEvents, options);
} }
// --- 2. Ударные волны и Кинетический свайп ---
const handleEvents = (e: any) => {
const isDown = e.type === "pointerdown" || e.type === "touchstart";
const isMove = e.type === "pointermove" || e.type === "touchmove";
let clientX = e.touches ? e.touches[0].clientX : e.clientX;
let clientY = e.touches ? e.touches[0].clientY : e.clientY;
if (clientX === undefined) return;
const rect = canvasRef.current!.getBoundingClientRect();
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;
// Ударная волна при клике/тапе
if (isDown) {
workerRef.current?.postMessage({
type: "SHOCKWAVE",
payload: { x: px, y: py },
});
}
// Магнитный свайп
if (isMove) {
workerRef.current?.postMessage({
type: "KINETIC_SWIPE",
payload: { x: px, y: py },
});
}
};
window.addEventListener("pointerdown", handleEvents, { passive: true });
window.addEventListener("pointermove", handleEvents, { passive: true });
window.addEventListener("touchstart", handleEvents, { passive: true });
window.addEventListener("touchmove", handleEvents, { passive: true });
return () => { return () => {
if (window.PointerEvent) { window.removeEventListener("deviceorientation", handleOrientation);
window.removeEventListener("pointerdown", handleEvents); 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]); }, [mounted]);
+91 -60
View File
@@ -167,11 +167,16 @@ self.onmessage = async (e) => {
const eyeData = new Float32Array(wasm.memory.buffer, eyePtr, eyeLen); const eyeData = new Float32Array(wasm.memory.buffer, eyePtr, eyeLen);
self.postMessage({ type: "EYE_DATA", payload: eyeData.slice() }); self.postMessage({ type: "EYE_DATA", payload: eyeData.slice() });
} }
} else if (type === "SHOCKWAVE") {
if (engine) engine.trigger_shockwave(payload.x, payload.y);
} else if (type === "KINETIC_SWIPE") {
if (engine) engine.kinetic_swipe(payload.x, payload.y);
} else if (type === "TILT") {
if (engine && typeof engine.set_tilt === "function") {
engine.set_tilt(payload.x, payload.y);
}
} }
}; };
// ... функция drawEye без изменений ...
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
@@ -219,46 +224,70 @@ 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);
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.6)"; // Добавляем плавное вращение для кибер-интерфейса зрачка
const t = performance.now() * 0.0015;
// 1. Внешнее кибер-кольцо (вращающееся, прерывистое)
ctx.save();
ctx.rotate(t);
ctx.strokeStyle = color;
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.7 * alphaMult;
ctx.setLineDash([8, 6, 2, 6]); // Техно-паттерн
ctx.beginPath();
ctx.arc(0, 0, 16, 0, Math.PI * 2);
ctx.stroke();
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.globalAlpha = alphaMult;
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.arc(0, 0, 10, 0, Math.PI * 2);
ctx.stroke(); ctx.stroke();
ctx.fillStyle = isDarkMode ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.8)"; // 3. Зрачок (Острый кибер-ромб вместо скучной точки)
ctx.shadowBlur = 5; ctx.fillStyle = isDarkMode ? "#fff" : "#000";
ctx.shadowColor = isDarkMode ? "white" : "black"; ctx.shadowBlur = 10;
ctx.shadowColor = color; // Легкое свечение зрачка цветом темы
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, 3.5, 0, Math.PI * 2); ctx.moveTo(0, -5);
ctx.lineTo(4, 0);
ctx.lineTo(0, 5);
ctx.lineTo(-4, 0);
ctx.fill(); ctx.fill();
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)"; // 4. Техно-перекрестие (HUD)
ctx.lineWidth = 1; ctx.strokeStyle = color;
ctx.shadowBlur = 0; ctx.lineWidth = 1.5;
ctx.shadowBlur = 5;
ctx.globalAlpha = 0.4 * alphaMult;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(-16, 0); ctx.moveTo(-22, 0);
ctx.lineTo(-8, 0); ctx.lineTo(-12, 0);
ctx.moveTo(16, 0); ctx.moveTo(22, 0);
ctx.lineTo(8, 0); ctx.lineTo(12, 0);
ctx.moveTo(0, -16); ctx.moveTo(0, -22);
ctx.lineTo(0, -8); ctx.lineTo(0, -12);
ctx.moveTo(0, 16); ctx.moveTo(0, 22);
ctx.lineTo(0, 8); ctx.lineTo(0, 12);
ctx.stroke(); ctx.stroke();
ctx.fillStyle = isDarkMode ? "white" : "black"; // 5. Индикаторы направления (теперь тоже в цвет интерфейса)
ctx.fillStyle = color;
ctx.font = "10px monospace"; ctx.font = "10px monospace";
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.2) * alphaMult; ctx.shadowBlur = 8;
ctx.fillText("▴", 0, -20); ctx.globalAlpha = (dir === 1 ? 1.0 : 0.15) * alphaMult;
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.2) * alphaMult; ctx.fillText("▴", 0, -26);
ctx.fillText("▾", 0, 22); ctx.globalAlpha = (dir === 2 ? 1.0 : 0.15) * alphaMult;
ctx.globalAlpha = (dir === 3 ? 1.0 : 0.2) * alphaMult; ctx.fillText("▾", 0, 26);
ctx.fillText("◂", -22, 1); ctx.globalAlpha = (dir === 3 ? 1.0 : 0.15) * alphaMult;
ctx.globalAlpha = (dir === 4 ? 1.0 : 0.2) * alphaMult; ctx.fillText("◂", -26, 1);
ctx.fillText("▸", 22, 1); ctx.globalAlpha = (dir === 4 ? 1.0 : 0.15) * alphaMult;
ctx.fillText("▸", 26, 1);
ctx.restore(); ctx.restore();
} }
@@ -266,53 +295,53 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
} }
function render() { function render() {
if (!engine) return requestAnimationFrame(render); if (!engine || !wasm) return requestAnimationFrame(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 bgLen = engine.bg_len(); // --- ЕДИНЫЙ РЕНДЕР МАТРИЦЫ (Параллакс + Ударные волны) ---
if (bgLen > 0) { const renderLen = engine.render_len();
const bgData = new Float32Array(wasm.memory.buffer, engine.bg_ptr(), bgLen); if (renderLen > 0) {
const bgF = fontSize * 0.6; const data = new Float32Array(
for (let i = 0; i < bgLen; i += 4) { wasm.memory.buffer,
ctx.globalAlpha = bgData[i + 3]; engine.render_ptr(),
renderLen,
);
// Шаг 6, так как в Rust мы пишем [x, y, char_idx, is_head, alpha, scale]
for (let i = 0; i < renderLen; i += 6) {
const x = data[i];
const y = data[i + 1];
const charIdx = data[i + 2];
const isHead = data[i + 3];
const alpha = data[i + 4];
const scale = data[i + 5];
const scaledSize = fontSize * scale;
ctx.globalAlpha = alpha;
ctx.drawImage( ctx.drawImage(
atlasCanvas, atlasCanvas,
bgData[i + 2] * fontSize, charIdx * fontSize,
0, isHead * fontSize, // Выбираем строку атласа: 0 (цвет) или 1 (белый)
fontSize, fontSize,
fontSize, fontSize,
bgData[i], x,
bgData[i + 1], y,
bgF, scaledSize,
bgF, scaledSize,
); );
} }
} }
const fgLen = engine.fg_len(); // --- РЕНДЕР ГЛАЗ ---
const fgData = new Float32Array(wasm.memory.buffer, engine.fg_ptr(), fgLen);
for (let i = 0; i < fgLen; i += 5) {
ctx.globalAlpha = fgData[i + 4];
ctx.drawImage(
atlasCanvas,
fgData[i + 2] * fontSize,
fgData[i + 3] * fontSize,
fontSize,
fontSize,
fgData[i],
fgData[i + 1],
fontSize,
fontSize,
);
}
const eyeLen = engine.eye_len(); const eyeLen = engine.eye_len();
if (eyeLen >= 6) { if (eyeLen >= 6) {
const eyeData = new Float32Array( const eyeData = new Float32Array(
@@ -327,12 +356,13 @@ function render() {
const dir = eyeData[4]; const dir = eyeData[4];
const isClosed = eyeData[5]; const isClosed = eyeData[5];
const gap = isMobile ? 65 : 75; 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;
@@ -347,6 +377,7 @@ 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(
@@ -359,7 +390,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],
+333 -149
View File
@@ -43,8 +43,12 @@ impl FastRng {
} }
struct Drop { struct Drop {
base_x: f32, // Якорная позиция по X (с учетом сноса ветром/гироскопом)
y: f32, y: f32,
offset_x: f32, // Текущее смещение для пружинной анимации
vel_x: f32, // Текущая скорость смещения
speed: f32, speed: f32,
layer: u8, // 0 = Дальний (Far), 1 = Средний (Mid), 2 = Ближний (Near)
glitch_timer: f32, glitch_timer: f32,
} }
@@ -82,6 +86,15 @@ impl GlitchBlock {
} }
} }
struct Shockwave {
x: f32,
y: f32,
radius: f32,
life: f32,
max_life: f32,
active: bool,
}
struct GlassSymbol { struct GlassSymbol {
x: f32, x: f32,
y: f32, y: f32,
@@ -98,13 +111,13 @@ pub struct MatrixEngine {
font_size: u32, font_size: u32,
charset_len: usize, charset_len: usize,
rng: FastRng, rng: FastRng,
fg_drops: Vec<Drop>,
bg_drops: Vec<Drop>, drops: Vec<Drop>,
shockwaves: Vec<Shockwave>,
glass_symbols: Vec<GlassSymbol>, glass_symbols: Vec<GlassSymbol>,
glitch_blocks: Vec<GlitchBlock>, glitch_blocks: Vec<GlitchBlock>,
fg_buffer: Vec<f32>, render_buffer: Vec<f32>, // Единый буфер рендера [x, y, char_idx, is_head, alpha, scale]
bg_buffer: Vec<f32>,
glitch_buffer: Vec<f32>, glitch_buffer: Vec<f32>,
glass_buffer: Vec<f32>, glass_buffer: Vec<f32>,
eye_buffer: Vec<f32>, eye_buffer: Vec<f32>,
@@ -132,6 +145,13 @@ pub struct MatrixEngine {
eye_closedness_vel: f32, eye_closedness_vel: f32,
eye_anchor_y: f32, eye_anchor_y: f32,
mobile_wander_timer: f32, mobile_wander_timer: f32,
// Mobile & Physics state
tilt_x: f32,
tilt_y: f32,
swipe_x: f32,
swipe_y: f32,
swipe_timer: f32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -140,18 +160,28 @@ impl MatrixEngine {
pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine { pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let shockwaves = (0..5)
.map(|_| Shockwave {
x: 0.0,
y: 0.0,
radius: 0.0,
life: 0.0,
max_life: 0.0,
active: false,
})
.collect();
let mut engine = MatrixEngine { let mut engine = MatrixEngine {
width, width,
height, height,
font_size, font_size,
charset_len: 84, charset_len: 84,
rng: FastRng::new(1337), rng: FastRng::new(1337),
fg_drops: Vec::new(), drops: Vec::new(),
bg_drops: Vec::new(), shockwaves,
glass_symbols: Vec::new(), glass_symbols: Vec::new(),
glitch_blocks: Vec::new(), glitch_blocks: Vec::new(),
fg_buffer: Vec::new(), render_buffer: Vec::new(),
bg_buffer: Vec::new(),
glitch_buffer: Vec::new(), glitch_buffer: Vec::new(),
glass_buffer: Vec::new(), glass_buffer: Vec::new(),
eye_buffer: Vec::new(), eye_buffer: Vec::new(),
@@ -176,40 +206,49 @@ impl MatrixEngine {
eye_closedness_vel: 0.0, eye_closedness_vel: 0.0,
eye_anchor_y: 0.0, eye_anchor_y: 0.0,
mobile_wander_timer: 0.0, mobile_wander_timer: 0.0,
tilt_x: 0.0,
tilt_y: 0.0,
swipe_x: 0.0,
swipe_y: 0.0,
swipe_timer: 0.0,
}; };
engine.init_drops(); engine.init_drops();
engine engine
} }
fn init_drops(&mut self) { fn init_drops(&mut self) {
self.fg_drops.clear(); self.drops.clear();
self.bg_drops.clear();
let fg_spacing = if self.is_mobile { 2.0 } else { 1.0 } * self.font_size as f32; // 3 слоя параллакса: (Layer ID, Scale, Min Speed, Max Speed)
let fg_columns = (self.width as f32 / fg_spacing).ceil() as u32; let layers = [
(0, 0.4, 0.005, 0.02), // Far (бывший bg_drops)
(1, 0.7, 0.02, 0.06), // Mid
(2, 1.0, 0.05, 0.15), // Near (бывший fg_drops)
];
for _ in 0..fg_columns { let base_spacing = if self.is_mobile { 1.5 } else { 1.0 } * self.font_size as f32;
self.fg_drops.push(Drop {
y: self.rng.gen_range(-20.0, self.height as f32), for (layer, scale, min_spd, max_spd) in layers.iter() {
speed: self.rng.gen_range(0.02, 0.15), let spacing = base_spacing * scale;
// Делаем запас колонок по бокам для параллакса и наклона
let columns = (self.width as f32 / spacing).ceil() as u32 + 20;
for i in 0..columns {
let start_x = (i as f32 * spacing) - (10.0 * spacing);
self.drops.push(Drop {
base_x: start_x,
y: self.rng.gen_range(-50.0, self.height as f32),
offset_x: 0.0,
vel_x: 0.0,
speed: self.rng.gen_range(*min_spd, *max_spd),
layer: *layer as u8,
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
let bg_density = if self.is_mobile { 4.0 } else { 1.5 };
let bg_spacing = self.font_size as f32 * bg_density;
let bg_columns = (self.width as f32 / bg_spacing).ceil() as u32;
for _ in 0..bg_columns {
self.bg_drops.push(Drop {
y: self.rng.gen_range(-20.0, self.height as f32),
speed: self.rng.gen_range(0.005, 0.04),
glitch_timer: 0.0,
});
} }
self.fg_buffer.resize(self.fg_drops.len() * 5, 0.0); // Выделяем память: размер вектора = кол-во капель * 6 параметров
self.bg_buffer.resize(self.bg_drops.len() * 4, 0.0); self.render_buffer.resize(self.drops.len() * 6, 0.0);
} }
pub fn resize(&mut self, width: u32, height: u32) { pub fn resize(&mut self, width: u32, height: u32) {
@@ -224,6 +263,11 @@ impl MatrixEngine {
self.gaze_target_x = x; self.gaze_target_x = x;
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_y = y;
self.swipe_timer = 15.0;
} }
pub fn set_drawing(&mut self, is_drawing: bool) { pub fn set_drawing(&mut self, is_drawing: bool) {
@@ -234,7 +278,6 @@ impl MatrixEngine {
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;
} }
@@ -243,16 +286,63 @@ impl MatrixEngine {
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) {
self.tilt_x = tilt_x;
self.tilt_y = tilt_y;
}
pub fn trigger_shockwave(&mut self, x: f32, y: f32) {
// 1. Заставляем глаза смотреть на точку клика и обновляем "магнит"
self.update_mouse(x, y);
// 2. Запускаем физику ударной волны
if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) {
sw.x = x;
sw.y = y;
sw.radius = 0.0;
sw.life = 40.0;
sw.max_life = 40.0;
sw.active = true;
}
// 3. Добавляем небольшой цифровой всплеск (глитч) при ударе
let blocks_count = if self.is_mobile { 4 } else { 12 };
for _ in 0..blocks_count {
let block = GlitchBlock::new(x, y, &mut self.rng);
self.glitch_blocks.push(block);
}
}
pub fn kinetic_swipe(&mut self, x: f32, y: f32) {
self.swipe_x = x;
self.swipe_y = y;
self.swipe_timer = 15.0;
// Передаем координаты свайпа глазам, чтобы они чётко следили за движением
self.update_mouse(x, y);
}
pub fn trigger_glitch(&mut self, x: f32, y: f32) { pub fn trigger_glitch(&mut self, x: f32, y: f32) {
self.emp_timer = 60.0; self.emp_timer = 60.0;
self.emp_x = x; self.emp_x = x;
self.emp_y = y; self.emp_y = y;
let blocks_count = if self.is_mobile { 3 } else { 25 }; // 1. Запускаем Ударную Волну (Shockwave) прямо по клику!
if let Some(sw) = self.shockwaves.iter_mut().find(|s| !s.active) {
sw.x = x;
sw.y = y;
sw.radius = 0.0;
sw.life = 40.0;
sw.max_life = 40.0;
sw.active = true;
}
// 2. Спавним прямоугольные глитчи вместе с волной
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);
} }
@@ -262,22 +352,42 @@ impl MatrixEngine {
self.glitch_buffer.clear(); self.glitch_buffer.clear();
self.glass_buffer.clear(); self.glass_buffer.clear();
self.eye_buffer.clear();
// --- Eye Logic --- // --- Ударные Волны ---
// Calculate container center for sw in self.shockwaves.iter_mut().filter(|s| s.active) {
sw.radius += 25.0; // Расширяем кольцо
sw.life -= 1.0;
if sw.life <= 0.0 {
sw.active = false;
}
}
if self.swipe_timer > 0.0 {
self.swipe_timer -= 1.0;
}
// --- Логика Глаз ---
let cx = self.width as f32 / 2.0; let cx = self.width as f32 / 2.0;
let cy = if self.eye_anchor_y > 0.0 { self.eye_anchor_y } else { if self.is_mobile { 150.0 } else { 180.0 } }; let cy = if self.eye_anchor_y > 0.0 {
self.eye_anchor_y
} else {
if self.is_mobile {
150.0
} else {
180.0
}
};
// Random wander for mobile
if self.is_mobile && self.last_interaction > 150.0 { if self.is_mobile && self.last_interaction > 150.0 {
self.mobile_wander_timer += 1.0; self.mobile_wander_timer += 1.0;
if self.mobile_wander_timer > 108.0 { // ~1.8s if self.mobile_wander_timer > 108.0 {
self.mobile_wander_timer = 0.0; self.mobile_wander_timer = 0.0;
if self.rng.gen_bool(0.8) { if self.rng.gen_bool(0.8) {
self.gaze_target_x = cx + self.rng.gen_range(-100.0, 100.0); self.gaze_target_x = cx + self.rng.gen_range(-100.0, 100.0);
self.gaze_target_y = cy + self.rng.gen_range(-100.0, 100.0); self.gaze_target_y = cy + self.rng.gen_range(-100.0, 100.0);
if self.rng.gen_bool(0.4) { if self.rng.gen_bool(0.4) {
self.blink_timer = 12.0; // micro-blink self.blink_timer = 12.0;
} }
} else { } else {
self.gaze_target_x = cx; self.gaze_target_x = cx;
@@ -288,64 +398,52 @@ impl MatrixEngine {
let dx = self.gaze_target_x - cx; let dx = self.gaze_target_x - cx;
let dy = self.gaze_target_y - cy; let dy = self.gaze_target_y - cy;
let mut dir = 0.0; let mut dir = 0.0;
if dx.abs() < 60.0 && dy.abs() < 60.0 { if dx.abs() < 60.0 && dy.abs() < 60.0 {
dir = 0.0; // center dir = 0.0;
} else if dx.abs() > dy.abs() { } else if dx.abs() > dy.abs() {
dir = if dx > 0.0 { 4.0 } else { 3.0 }; // 4=right, 3=left dir = if dx > 0.0 { 4.0 } else { 3.0 };
} else { } else {
dir = if dy > 0.0 { 2.0 } else { 1.0 }; // 2=down, 1=up dir = if dy > 0.0 { 2.0 } else { 1.0 };
} }
let distance = (dx * dx + dy * dy).sqrt(); let distance = (dx * dx + dy * dy).sqrt();
let max_radius = 20.0; let target_dist = (distance * 0.1).min(20.0);
let target_dist = (distance * 0.1).min(max_radius);
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;
let target_gaze_x = angle.cos() * target_dist;
let target_gaze_y = angle.sin() * target_dist;
// Spring physics interpolation (smoother gaze)
self.gaze_x += (target_gaze_x - self.gaze_x) * 0.12;
self.gaze_y += (target_gaze_y - self.gaze_y) * 0.12;
// Blinking
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 { } else if !self.is_vpn_on && self.rng.gen_bool(0.0015) {
if self.rng.gen_bool(0.0015) { // random blink
self.blink_timer = 16.0; self.blink_timer = 16.0;
} }
}
let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 { 1.0 } else { 0.0 }; let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 {
1.0
} else {
0.0
};
self.eye_closedness_vel += (target_closedness - self.eye_closedness) * 0.06;
self.eye_closedness_vel *= 0.88;
self.eye_closedness = (self.eye_closedness + self.eye_closedness_vel).clamp(-0.15, 1.15);
// Springy blink physics (slower, smoother) self.eye_buffer.extend_from_slice(&[
let diff = target_closedness - self.eye_closedness; cx,
self.eye_closedness_vel += diff * 0.06; // lower stiffness (slower acceleration) cy,
self.eye_closedness_vel *= 0.88; // higher damping (longer slide, less snappy) self.gaze_x,
self.eye_closedness += self.eye_closedness_vel; self.gaze_y,
dir,
// Clamp to prevent extreme glitches, but allow small spring bounce self.eye_closedness,
if self.eye_closedness < -0.15 { self.eye_closedness = -0.15; } ]);
if self.eye_closedness > 1.15 { self.eye_closedness = 1.15; }
self.eye_buffer.clear();
self.eye_buffer.push(cx);
self.eye_buffer.push(cy);
self.eye_buffer.push(self.gaze_x);
self.eye_buffer.push(self.gaze_y);
self.eye_buffer.push(dir);
self.eye_buffer.push(self.eye_closedness);
// --- End Eye Logic ---
// --- Глобальные эффекты (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);
let ry = self.rng.gen_range(0.0, self.height as f32); let ry = self.rng.gen_range(0.0, self.height as f32);
let block = GlitchBlock::new(rx, ry, &mut self.rng); self.glitch_blocks
self.glitch_blocks.push(block); .push(GlitchBlock::new(rx, ry, &mut self.rng));
} }
if self.rng.gen_bool(0.002) { if self.rng.gen_bool(0.002) {
self.tear_y = self.rng.gen_range(0.0, self.height as f32); self.tear_y = self.rng.gen_range(0.0, self.height as f32);
@@ -360,59 +458,97 @@ impl MatrixEngine {
self.tear_timer -= 1.0; self.tear_timer -= 1.0;
} }
let bg_density = if self.is_mobile { 4.0 } else { 1.5 };
let bg_x_spacing = self.font_size as f32 * bg_density;
let bg_y_spacing = self.font_size as f32 * 0.6;
for i in 0..self.bg_drops.len() {
let drop = &mut self.bg_drops[i];
let x = (i as f32) * bg_x_spacing;
let cell_y = drop.y as i32 as f32;
let y = drop.y * bg_y_spacing;
let hash = (i as u32)
.wrapping_mul(12345)
.wrapping_add((cell_y as u32).wrapping_mul(67890));
let char_idx = (hash as usize) % self.charset_len;
let base_idx = i * 4;
self.bg_buffer[base_idx] = x;
self.bg_buffer[base_idx + 1] = y;
self.bg_buffer[base_idx + 2] = char_idx as f32;
self.bg_buffer[base_idx + 3] = 0.02 + ((hash % 100) as f32 * 0.0008);
drop.y += drop.speed;
if y > self.height as f32 && self.rng.gen_bool(0.005) {
drop.y = self.rng.gen_range(-10.0, 0.0);
drop.speed = self.rng.gen_range(0.005, 0.04);
}
}
let emp_rad = (60.0 - self.emp_timer) * 15.0; let emp_rad = (60.0 - self.emp_timer) * 15.0;
let emp_rad_sq = emp_rad * emp_rad; let emp_rad_sq = emp_rad * emp_rad;
let emp_r20_sq = if emp_rad > 20.0 { (emp_rad - 20.0) * (emp_rad - 20.0) } else { 0.0 }; let emp_r20_sq = if emp_rad > 20.0 {
let emp_r80_sq = if emp_rad > 80.0 { (emp_rad - 80.0) * (emp_rad - 80.0) } else { 0.0 }; (emp_rad - 20.0).powi(2)
} else {
0.0
};
let emp_r80_sq = if emp_rad > 80.0 {
(emp_rad - 80.0).powi(2)
} else {
0.0
};
let fg_spacing = if self.is_mobile { 2.0 } else { 1.0 } * self.font_size as f32; // --- Итерация по единому массиву капель ---
for i in 0..self.drops.len() {
let drop = &mut self.drops[i];
for i in 0..self.fg_drops.len() { let parallax_factor = match drop.layer {
let drop = &mut self.fg_drops[i]; 0 => 0.3,
let mut x = (i as f32) * fg_spacing; 1 => 0.6,
let cell_y = drop.y as i32 as f32; _ => 1.0,
let y = drop.y * (self.font_size as f32); };
// 1. ГИРОСКОП: Постоянный снос
drop.base_x += self.tilt_x * parallax_factor * 5.0;
// Бесконечный экран
let wrap_margin = 150.0;
if drop.base_x < -wrap_margin {
drop.base_x += self.width as f32 + wrap_margin * 2.0;
drop.y = 0.0;
} else if drop.base_x > self.width as f32 + wrap_margin {
drop.base_x -= self.width as f32 + wrap_margin * 2.0;
drop.y = 0.0;
}
let current_x = drop.base_x + drop.offset_x;
let current_y = drop.y * (self.font_size as f32 * parallax_factor);
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;
if !self.is_mobile && self.tear_timer > 0.0 && (y - self.tear_y).abs() < 60.0 { // 2. УДАРНЫЕ ВОЛНЫ (по клику)
x += self.rng.gen_range(-20.0, 20.0); for sw in self.shockwaves.iter().filter(|s| s.active) {
is_scrambled = true; let dx = current_x - sw.x;
let dy = current_y - sw.y;
let dist = (dx * dx + dy * dy).sqrt();
if (dist - sw.radius).abs() < 40.0 {
let force = (sw.life / sw.max_life) * 18.0 * parallax_factor;
drop.vel_x += (dx / dist.max(1.0)) * force;
force_white = true;
}
} }
if !self.is_mobile && self.emp_timer > 0.0 { // 3. КИНЕТИЧЕСКИЙ СВАЙП И МАГНИТ (Палец/Мышь)
let dx = x - self.emp_x; let check_swipe = self.swipe_timer > 0.0;
let dy = y - self.emp_y; let check_mouse = !self.is_mobile && self.last_interaction < 10.0;
if check_swipe || check_mouse {
let (ix, iy) = if check_swipe {
(self.swipe_x, self.swipe_y)
} else {
(self.mouse_x, self.mouse_y)
};
let dx = current_x - ix;
let dy = current_y - iy;
let dist_sq = dx * dx + dy * dy;
let rad = if self.is_mobile { 100.0 } else { 140.0 };
if dist_sq < rad * rad {
let dist = dist_sq.sqrt().max(1.0);
let force = (rad - dist) / rad;
// Отталкивание пружины
drop.vel_x += (dx / dist) * force * 15.0 * parallax_factor;
// Замедление падения ("Вязкость")
drop.y -= drop.speed * force * 0.6;
is_scrambled = true;
if dist < rad * 0.5 {
force_white = true;
}
}
}
// EMP Волна от включения VPN (только для передних слоев)
if self.emp_timer > 0.0 && drop.layer > 0 {
let dx = current_x - self.emp_x;
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 {
@@ -426,60 +562,91 @@ impl MatrixEngine {
} }
} }
if !self.is_mobile && self.tear_timer > 0.0 && (current_y - self.tear_y).abs() < 60.0 {
drop.vel_x += self.rng.gen_range(-10.0, 10.0);
is_scrambled = true;
}
if is_void { if is_void {
drop.y += drop.speed; drop.y += drop.speed;
let base_idx = i * 5; self.render_buffer[i * 6 + 4] = 0.0; // Alpha = 0
self.fg_buffer[base_idx + 4] = 0.0;
continue; continue;
} }
// 4. ПРУЖИННАЯ ФИЗИКА
drop.vel_x += -0.12 * drop.offset_x; // Сила возврата (Гука)
drop.vel_x *= 0.85; // Трение (Затухание)
drop.offset_x += drop.vel_x;
if self.rng.gen_bool(0.001) { if self.rng.gen_bool(0.001) {
drop.glitch_timer = 5.0; drop.glitch_timer = 5.0;
} }
let cell_y = drop.y as i32 as u32;
let time_shift = self.tick_count / 10; let time_shift = self.tick_count / 10;
let hash = (i as u32) let hash = (i as u32)
.wrapping_mul(73856093) .wrapping_mul(73856093)
.wrapping_add((cell_y as u32).wrapping_mul(19349663)) .wrapping_add(cell_y.wrapping_mul(19349663))
.wrapping_add(time_shift); .wrapping_add(time_shift);
let mut char_idx = (hash as usize) % self.charset_len; let mut char_idx = (hash as usize) % self.charset_len;
let mut is_head = (hash % 100) < 15; let mut is_head = (hash % 100) < 15;
let mut target_alpha = if is_head { 1.0 } else { 0.6 }; let mut base_alpha = match drop.layer {
0 => 0.15,
1 => 0.45,
_ => 0.8,
};
if is_emp_ring { if is_emp_ring {
is_head = true; is_head = true;
target_alpha = 1.0; base_alpha = 1.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;
} 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;
target_alpha = 0.6 + ((hash % 100) as f32 * 0.004); base_alpha = (base_alpha + 0.2_f32).min(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;
} }
} }
if self.is_mobile { if force_white {
target_alpha = 0.8; is_head = true;
base_alpha = 1.0;
}
if self.is_mobile && !force_white && drop.layer == 2 {
base_alpha = 0.8;
} }
let atlas_row = if is_head { 1.0 } else { 0.0 }; let atlas_row = if is_head { 1.0 } else { 0.0 };
let scale = match drop.layer {
0 => 0.4,
1 => 0.7,
_ => 1.0,
};
let final_x = drop.base_x + drop.offset_x;
let base_idx = i * 5; let base_idx = i * 6;
self.fg_buffer[base_idx] = x; self.render_buffer[base_idx] = final_x;
self.fg_buffer[base_idx + 1] = y; self.render_buffer[base_idx + 1] = current_y;
self.fg_buffer[base_idx + 2] = char_idx as f32; self.render_buffer[base_idx + 2] = char_idx as f32;
self.fg_buffer[base_idx + 3] = atlas_row; self.render_buffer[base_idx + 3] = atlas_row;
self.fg_buffer[base_idx + 4] = target_alpha; self.render_buffer[base_idx + 4] = base_alpha;
self.render_buffer[base_idx + 5] = scale;
drop.y += drop.speed; drop.y += drop.speed;
if y > self.height as f32 && self.rng.gen_bool(0.01) { if current_y > self.height as f32 + 50.0 && self.rng.gen_bool(0.01) {
drop.y = self.rng.gen_range(-10.0, 0.0); drop.y = self.rng.gen_range(-20.0, 0.0);
drop.speed = self.rng.gen_range(0.02, 0.15); 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),
};
} }
} }
// --- Глитчи и Осколки (Пылинки) ---
for block in self.glitch_blocks.iter_mut() { for block in self.glitch_blocks.iter_mut() {
self.glitch_buffer.extend_from_slice(&[ self.glitch_buffer.extend_from_slice(&[
block.x, block.x,
@@ -492,19 +659,20 @@ impl MatrixEngine {
} }
self.glitch_blocks.retain(|b| b.life > 0.0); self.glitch_blocks.retain(|b| b.life > 0.0);
if !self.is_mobile && self.is_drawing && self.rng.gen_bool(0.3) { if !self.is_mobile && self.last_interaction < 2.0 && self.rng.gen_bool(0.2) {
self.glass_symbols.push(GlassSymbol { self.glass_symbols.push(GlassSymbol {
x: self.mouse_x + self.rng.gen_range(-15.0, 15.0), x: self.mouse_x + self.rng.gen_range(-20.0, 20.0),
y: self.mouse_y + self.rng.gen_range(-15.0, 15.0), y: self.mouse_y + self.rng.gen_range(-20.0, 20.0),
char_idx: self.rng.gen_usize(self.charset_len), char_idx: self.rng.gen_usize(self.charset_len),
life: 1.0, life: 1.0,
max_life: self.rng.gen_range(30.0, 80.0), max_life: self.rng.gen_range(20.0, 50.0),
velocity_y: self.rng.gen_range(0.1, 0.5), velocity_y: self.rng.gen_range(-1.5, 1.5),
}); });
} }
for sym in self.glass_symbols.iter_mut() { for sym in self.glass_symbols.iter_mut() {
sym.y += sym.velocity_y; sym.y += sym.velocity_y;
sym.x += self.rng.gen_range(-0.5, 0.5);
sym.life -= 1.0; sym.life -= 1.0;
let alpha = (sym.life / sym.max_life).max(0.0); let alpha = (sym.life / sym.max_life).max(0.0);
self.glass_buffer self.glass_buffer
@@ -513,14 +681,30 @@ impl MatrixEngine {
self.glass_symbols.retain(|s| s.life > 0.0); self.glass_symbols.retain(|s| s.life > 0.0);
} }
pub fn fg_ptr(&self) -> *const f32 { self.fg_buffer.as_ptr() } // Обрати внимание: Вместо fg_ptr и bg_ptr теперь один render_ptr
pub fn fg_len(&self) -> usize { self.fg_buffer.len() } pub fn render_ptr(&self) -> *const f32 {
pub fn bg_ptr(&self) -> *const f32 { self.bg_buffer.as_ptr() } self.render_buffer.as_ptr()
pub fn bg_len(&self) -> usize { self.bg_buffer.len() } }
pub fn glitch_ptr(&self) -> *const f32 { self.glitch_buffer.as_ptr() } pub fn render_len(&self) -> usize {
pub fn glitch_len(&self) -> usize { self.glitch_buffer.len() } self.render_buffer.len()
pub fn glass_ptr(&self) -> *const f32 { self.glass_buffer.as_ptr() } }
pub fn glass_len(&self) -> usize { self.glass_buffer.len() }
pub fn eye_ptr(&self) -> *const f32 { self.eye_buffer.as_ptr() } pub fn glitch_ptr(&self) -> *const f32 {
pub fn eye_len(&self) -> usize { self.eye_buffer.len() } self.glitch_buffer.as_ptr()
}
pub fn glitch_len(&self) -> usize {
self.glitch_buffer.len()
}
pub fn glass_ptr(&self) -> *const f32 {
self.glass_buffer.as_ptr()
}
pub fn glass_len(&self) -> usize {
self.glass_buffer.len()
}
pub fn eye_ptr(&self) -> *const f32 {
self.eye_buffer.as_ptr()
}
pub fn eye_len(&self) -> usize {
self.eye_buffer.len()
}
} }