optimization of wasm and component v2

This commit is contained in:
2026-04-24 05:22:54 +07:00
parent 14c4ab2c13
commit cf74f12e6e
2 changed files with 112 additions and 93 deletions
+44 -36
View File
@@ -28,23 +28,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark"; const isDarkMode = resolvedTheme === "dark";
// УМНОЕ МАСШТАБИРОВАНИЕ ДЛЯ ТЕЛЕФОНОВ
const applyCanvasScaling = (
canvas: HTMLCanvasElement,
container: HTMLDivElement,
) => {
const dpr = window.devicePixelRatio || 1;
// На мобилках сильно режем разрешение для турбо-производительности
const scale = dpr > 1 ? 0.6 : 1.0;
canvas.width = container.clientWidth * scale;
canvas.height = container.clientHeight * scale;
// CSS растягивает канвас на весь экран
canvas.style.width = `${container.clientWidth}px`;
canvas.style.height = `${container.clientHeight}px`;
};
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
@@ -58,19 +41,30 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
); );
await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm"); await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm");
// Применяем масштаб ПЕРЕД инициализацией движка // 1:1 Разрешение без мыльного скейлинга
applyCanvasScaling(canvas, container); canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, 16); const engine = new wasmModule.MatrixEngine(canvas, 16);
engineRef.current = engine; engineRef.current = engine;
engine.set_theme(isSecure, isDarkMode); engine.set_theme(isSecure, isDarkMode);
setWasmLoaded(true); setWasmLoaded(true);
const render = () => { // ОПТИМИЗАЦИЯ: Ограничиваем до 35 FPS для устранения лагов
engine.tick(); let lastTime = 0;
const fpsInterval = 1000 / 35; // ~28ms на кадр
const render = (time: number) => {
animationFrameId.current = requestAnimationFrame(render); animationFrameId.current = requestAnimationFrame(render);
const elapsed = time - lastTime;
if (elapsed > fpsInterval) {
lastTime = time - (elapsed % fpsInterval);
engine.tick();
}
}; };
render();
requestAnimationFrame(render);
} catch (e) { } catch (e) {
console.error("Wasm Engine Load Failed:", e); console.error("Wasm Engine Load Failed:", e);
} }
@@ -80,7 +74,8 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const handleResize = () => { const handleResize = () => {
if (canvas && container && engineRef.current) { if (canvas && container && engineRef.current) {
applyCanvasScaling(canvas, container); canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
engineRef.current.resize(canvas.width, canvas.height); engineRef.current.resize(canvas.width, canvas.height);
} }
}; };
@@ -97,40 +92,53 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode); if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode);
}, [isSecure, isDarkMode]); }, [isSecure, isDarkMode]);
// ГЛОБАЛЬНЫЙ ПЕРЕХВАТ СОБЫТИЙ (POINTER EVENTS - работает для Мыши и Тача) // ГЛОБАЛЬНЫЙ ПЕРЕХВАТ (Убраны костыли со скейлингом)
useEffect(() => { useEffect(() => {
const handlePointerDown = (e: PointerEvent) => { const triggerAction = (clientX: number, clientY: number) => {
if (!canvasRef.current || !engineRef.current) return; if (!canvasRef.current || !engineRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const scale = canvasRef.current.width / rect.width; // Корректируем координаты из-за скейлинга const x = clientX - rect.left;
const x = (e.clientX - rect.left) * scale; const y = clientY - rect.top;
const y = (e.clientY - rect.top) * scale;
if (y < 0 || y > canvasRef.current.height) return; if (y < 0 || y > rect.height) return;
engineRef.current.set_drawing(true); engineRef.current.set_drawing(true);
engineRef.current.trigger_glitch(x, y); engineRef.current.trigger_glitch(x, y);
}; };
const handlePointerMove = (e: PointerEvent) => { const moveAction = (clientX: number, clientY: number) => {
if (!canvasRef.current || !engineRef.current) return; if (!canvasRef.current || !engineRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const scale = canvasRef.current.width / rect.width; engineRef.current.update_mouse(clientX - rect.left, clientY - rect.top);
const x = (e.clientX - rect.left) * scale;
const y = (e.clientY - rect.top) * scale;
engineRef.current.update_mouse(x, y);
}; };
const handlePointerDown = (e: PointerEvent) =>
triggerAction(e.clientX, e.clientY);
const handlePointerMove = (e: PointerEvent) =>
moveAction(e.clientX, e.clientY);
const handlePointerUp = () => engineRef.current?.set_drawing(false); const handlePointerUp = () => engineRef.current?.set_drawing(false);
// Поддержка пальцев на мобилках
const handleTouchStart = (e: TouchEvent) =>
triggerAction(e.touches[0].clientX, e.touches[0].clientY);
const handleTouchMove = (e: TouchEvent) =>
moveAction(e.touches[0].clientX, e.touches[0].clientY);
window.addEventListener("pointerdown", handlePointerDown); window.addEventListener("pointerdown", handlePointerDown);
window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handlePointerUp);
return () => { return () => {
window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handlePointerUp);
}; };
}, []); }, []);
@@ -138,8 +146,8 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
<div ref={containerRef} className="absolute inset-0 overflow-hidden z-0"> <div ref={containerRef} className="absolute inset-0 overflow-hidden z-0">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className={`transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`} className={`w-full h-full transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`}
style={{ imageRendering: "pixelated" }} // Делает апскейл четким, без "мыла" // Убрали imageRendering: pixelated, чтобы текст был гладким
/> />
</div> </div>
); );
+68 -57
View File
@@ -21,24 +21,24 @@ struct GlitchBlock {
impl GlitchBlock { impl GlitchBlock {
fn new(x: f32, y: f32, r: u8, g: u8, b: u8, bg_color: &str) -> Self { fn new(x: f32, y: f32, r: u8, g: u8, b: u8, bg_color: &str) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let w = rng.gen_range(20.0..250.0); let w = rng.gen_range(20.0..150.0); // Сделал глитчи чуть компактнее для телефонов
let h = rng.gen_range(5.0..50.0); let h = rng.gen_range(5.0..40.0);
let choice = rng.gen_range(0..100); let choice = rng.gen_range(0..100);
let color = match choice { let color = match choice {
0..=15 => bg_color.to_string(), 0..=15 => bg_color.to_string(),
16..=50 => format!("rgba({}, {}, {}, 0.9)", r, g, b), 16..=50 => format!("rgb({}, {}, {})", r, g, b), // Убрал альфу
51..=75 => format!("rgba({}, {}, {}, 0.7)", 255 - r, 255 - g, 255 - b), 51..=75 => format!("rgb({}, {}, {})", 255 - r, 255 - g, 255 - b),
76..=95 => "rgba(255, 255, 255, 0.95)".to_string(), 76..=95 => "rgb(255, 255, 255)".to_string(),
_ => "rgba(255, 20, 80, 0.95)".to_string(), _ => "rgb(255, 20, 80)".to_string(),
}; };
Self { Self {
x: x - w / 2.0 + rng.gen_range(-120.0..120.0), x: x - w / 2.0 + rng.gen_range(-100.0..100.0),
y: y - h / 2.0 + rng.gen_range(-120.0..120.0), y: y - h / 2.0 + rng.gen_range(-100.0..100.0),
w, w,
h, h,
life: rng.gen_range(5.0..25.0), life: rng.gen_range(5.0..20.0),
color, color,
} }
} }
@@ -58,19 +58,19 @@ impl GlassSymbol {
fn new(x: f32, y: f32, charset: &[char], is_vpn_on: bool, is_dark_mode: bool) -> Self { fn new(x: f32, y: f32, charset: &[char], is_vpn_on: bool, is_dark_mode: bool) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let color = match (is_vpn_on, is_dark_mode) { let color = match (is_vpn_on, is_dark_mode) {
(true, true) => "139, 61, 255", (true, true) => "rgb(139, 61, 255)",
(false, true) => "0, 229, 242", (false, true) => "rgb(0, 229, 242)",
(true, false) => "90, 30, 200", (true, false) => "rgb(90, 30, 200)",
(false, false) => "0, 140, 160", (false, false) => "rgb(0, 140, 160)",
}; };
Self { Self {
x: x + rng.gen_range(-15.0..15.0), x: x + rng.gen_range(-10.0..10.0),
y: y + rng.gen_range(-15.0..15.0), y: y + rng.gen_range(-10.0..10.0),
value: charset[rng.gen_range(0..charset.len())], value: charset[rng.gen_range(0..charset.len())],
life: 1.0, life: 1.0,
max_life: rng.gen_range(30.0..80.0), max_life: rng.gen_range(20.0..50.0), // Быстрее исчезают
velocity_y: rng.gen_range(0.1..0.5), velocity_y: rng.gen_range(0.2..0.8),
color: color.to_string(), color: color.to_string(),
} }
} }
@@ -118,7 +118,7 @@ impl MatrixEngine {
for _ in 0..columns { for _ in 0..columns {
drops.push(Drop { drops.push(Drop {
y: rng.gen_range(-20.0..0.0), y: rng.gen_range(-20.0..0.0),
speed: rng.gen_range(0.15..0.45), speed: rng.gen_range(0.2..0.5),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
@@ -155,7 +155,7 @@ impl MatrixEngine {
for _ in 0..columns { for _ in 0..columns {
self.drops.push(Drop { self.drops.push(Drop {
y: rng.gen_range(-20.0..height as f32), y: rng.gen_range(-20.0..height as f32),
speed: rng.gen_range(0.15..0.45), speed: rng.gen_range(0.2..0.5),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
@@ -174,12 +174,12 @@ impl MatrixEngine {
} }
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 = 40.0;
self.emp_x = x; self.emp_x = x;
self.emp_y = y; self.emp_y = y;
let (r, g, b) = self.get_theme_colors(); let (r, g, b) = self.get_theme_colors();
let bg = self.get_bg_color(); let bg = self.get_bg_color_rgb();
for _ in 0..25 { for _ in 0..15 {
self.glitch_blocks self.glitch_blocks
.push(GlitchBlock::new(x, y, r, g, b, &bg)); .push(GlitchBlock::new(x, y, r, g, b, &bg));
} }
@@ -194,11 +194,11 @@ impl MatrixEngine {
} }
} }
fn get_bg_color(&self) -> String { fn get_bg_color_rgb(&self) -> String {
if self.is_dark_mode { if self.is_dark_mode {
"rgba(10, 10, 12, 1.0)".to_string() "rgb(10, 10, 12)".to_string()
} else { } else {
"rgba(250, 250, 252, 1.0)".to_string() "rgb(250, 250, 252)".to_string()
} }
} }
@@ -206,23 +206,25 @@ impl MatrixEngine {
self.tick_count = self.tick_count.wrapping_add(1); self.tick_count = self.tick_count.wrapping_add(1);
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
// 1. Очистка фона (единственная альфа, которая нам нужна)
self.ctx.set_global_alpha(0.25);
let bg_fill = if self.is_dark_mode { let bg_fill = if self.is_dark_mode {
"rgba(10, 10, 12, 0.2)" "rgb(10, 10, 12)"
} else { } else {
"rgba(250, 250, 252, 0.2)" "rgb(250, 250, 252)"
}; };
self.ctx.set_fill_style_str(bg_fill); self.ctx.set_fill_style_str(bg_fill);
self.ctx self.ctx
.fill_rect(0.0, 0.0, self.width as f64, self.height as f64); .fill_rect(0.0, 0.0, self.width as f64, self.height as f64);
self.ctx self.ctx
.set_font(&format!("bold {}px monospace", self.font_size)); .set_font(&format!("bold {}px monospace", self.font_size));
// Кешируем базовые строки цветов на весь кадр (ZERO ALLOCATIONS IN LOOP)
let (r, g, b) = self.get_theme_colors(); let (r, g, b) = self.get_theme_colors();
let bg_solid = self.get_bg_color(); let color_base = format!("rgb({}, {}, {})", r, g, b);
let color_head = "rgb(255, 255, 255)";
// ОПТИМИЗАЦИЯ ПАМЯТИ: Кешируем строки базовых цветов один раз на кадр! let bg_solid = self.get_bg_color_rgb();
let color_base = format!("rgba({}, {}, {}, 0.6)", r, g, b);
let color_head = "rgba(255, 255, 255, 1.0)".to_string();
if rng.gen_bool(0.02) { if rng.gen_bool(0.02) {
let rx = rng.gen_range(0.0..self.width as f32); let rx = rng.gen_range(0.0..self.width as f32);
@@ -233,7 +235,7 @@ impl MatrixEngine {
if rng.gen_bool(0.005) { if rng.gen_bool(0.005) {
self.tear_y = rng.gen_range(0.0..self.height as f32); self.tear_y = rng.gen_range(0.0..self.height as f32);
self.tear_timer = rng.gen_range(5.0..15.0); self.tear_timer = rng.gen_range(5.0..10.0);
} }
if self.emp_timer > 0.0 { if self.emp_timer > 0.0 {
@@ -243,6 +245,7 @@ impl MatrixEngine {
self.tear_timer -= 1.0; self.tear_timer -= 1.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];
let mut x = (i as f32) * (self.font_size as f32); let mut x = (i as f32) * (self.font_size as f32);
@@ -261,13 +264,13 @@ impl MatrixEngine {
if self.emp_timer > 0.0 { if self.emp_timer > 0.0 {
let dx = x - self.emp_x; let dx = x - self.emp_x;
let dy = y - self.emp_y; let dy = y - self.emp_y;
let dist_sq = dx * dx + dy * dy; // ОПТИМИЗАЦИЯ: Убрали .sqrt() let dist_sq = dx * dx + dy * dy;
let emp_radius = (60.0 - self.emp_timer) * 15.0; let emp_radius = (40.0 - self.emp_timer) * 15.0;
let emp_radius_sq = emp_radius * emp_radius; let emp_radius_sq = emp_radius * emp_radius;
if dist_sq < emp_radius_sq { if dist_sq < emp_radius_sq {
let ring_inner = emp_radius - 20.0; let ring_inner = emp_radius - 30.0;
let scramble_inner = emp_radius - 80.0; let scramble_inner = emp_radius - 80.0;
if dist_sq > ring_inner * ring_inner { if dist_sq > ring_inner * ring_inner {
@@ -289,38 +292,39 @@ impl MatrixEngine {
drop.glitch_timer = 5.0; drop.glitch_timer = 5.0;
} }
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 as u32).wrapping_mul(19349663))
.wrapping_add(time_shift); .wrapping_add(self.tick_count / 8);
let mut char_index = (hash as usize) % self.charset.len(); let mut char_index = (hash as usize) % self.charset.len();
let is_head = rng.gen_bool(0.15);
// ОПТИМИЗАЦИЯ: В 95% случаев мы используем ссылки на заранее созданные строки, не забивая память! let is_head = rng.gen_bool(0.12);
let mut custom_color = String::new();
let color_ref = if is_emp_ring { // ОПРЕДЕЛЯЕМ ПРОЗРАЧНОСТЬ И ЦВЕТ БЕЗ FORMAT!
&color_head let (target_alpha, target_color) = if is_emp_ring {
char_index = rng.gen_range(0..self.charset.len());
(1.0, color_head)
} else if is_scrambled || drop.glitch_timer > 0.0 { } else if is_scrambled || drop.glitch_timer > 0.0 {
char_index = rng.gen_range(0..self.charset.len()); char_index = rng.gen_range(0..self.charset.len());
let alpha = rng.gen_range(0.6..1.0);
if rng.gen_bool(0.2) {
custom_color = format!("rgba(255, 255, 255, {})", alpha);
} else {
custom_color = format!("rgba({}, {}, {}, {})", r, g, b, alpha);
}
if drop.glitch_timer > 0.0 { if drop.glitch_timer > 0.0 {
drop.glitch_timer -= 1.0; drop.glitch_timer -= 1.0;
} }
&custom_color
if rng.gen_bool(0.3) {
(0.9, color_head)
} else {
(0.8, color_base.as_str())
}
} else if is_head { } else if is_head {
&color_head (1.0, color_head)
} else { } else {
&color_base (0.6, color_base.as_str())
}; };
self.ctx.set_global_alpha(target_alpha);
self.ctx.set_fill_style_str(target_color);
let char_str = self.charset[char_index].to_string(); let char_str = self.charset[char_index].to_string();
self.ctx.set_fill_style_str(color_ref);
self.ctx self.ctx
.fill_text(&char_str, x as f64, y as f64) .fill_text(&char_str, x as f64, y as f64)
.unwrap_or(()); .unwrap_or(());
@@ -328,11 +332,13 @@ impl MatrixEngine {
drop.y += drop.speed; drop.y += drop.speed;
if y > self.height as f32 && rng.gen_bool(0.01) { if y > self.height as f32 && rng.gen_bool(0.01) {
drop.y = rng.gen_range(-10.0..0.0); drop.y = rng.gen_range(-5.0..0.0);
drop.speed = rng.gen_range(0.15..0.45); drop.speed = rng.gen_range(0.2..0.5);
} }
} }
// РИСУЕМ БЛОКИ (АЛЬФА 0.8 ДЛЯ ВСЕХ)
self.ctx.set_global_alpha(0.8);
for block in self.glitch_blocks.iter_mut() { for block in self.glitch_blocks.iter_mut() {
self.ctx.set_fill_style_str(&block.color); self.ctx.set_fill_style_str(&block.color);
self.ctx.fill_rect( self.ctx.fill_rect(
@@ -345,6 +351,7 @@ impl MatrixEngine {
} }
self.glitch_blocks.retain(|b| b.life > 0.0); self.glitch_blocks.retain(|b| b.life > 0.0);
// РИСУЕМ СТЕКЛО (МЫШЬ)
if self.is_drawing && rng.gen_bool(0.3) { if self.is_drawing && rng.gen_bool(0.3) {
self.glass_symbols.push(GlassSymbol::new( self.glass_symbols.push(GlassSymbol::new(
self.mouse_x, self.mouse_x,
@@ -360,12 +367,16 @@ impl MatrixEngine {
sym.y += sym.velocity_y; sym.y += sym.velocity_y;
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.ctx
.set_fill_style_str(&format!("rgba({}, {})", sym.color, alpha)); self.ctx.set_global_alpha(alpha as f64);
self.ctx.set_fill_style_str(&sym.color);
self.ctx self.ctx
.fill_text(&char_str, sym.x as f64, sym.y as f64) .fill_text(&char_str, sym.x as f64, sym.y as f64)
.unwrap_or(()); .unwrap_or(());
} }
self.glass_symbols.retain(|s| s.life > 0.0); self.glass_symbols.retain(|s| s.life > 0.0);
// СБРОС АЛЬФЫ В КОНЦЕ
self.ctx.set_global_alpha(1.0);
} }
} }