optimization of wasm and component v3

This commit is contained in:
2026-04-24 05:32:58 +07:00
parent 9d22618140
commit 59141fe227
3 changed files with 92 additions and 113 deletions
+33 -33
View File
@@ -35,36 +35,32 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const loadWasm = async () => { const loadWasm = async () => {
try { try {
const wasmPath = "/wasm-matrix/matrix_engine.js"; // НАДЕЖНЫЙ ПУТЬ ДЛЯ ПРОДАКШЕНА:
const wasmModule = await import( // Форсируем абсолютные URL, чтобы Webpack не пытался их бандлить
/* @vite-ignore */ /* webpackIgnore: true */ wasmPath const baseUrl = window.location.origin;
); const jsUrl = new URL("/wasm-matrix/matrix_engine.js", baseUrl).href;
await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm"); const wasmUrl = new URL("/wasm-matrix/matrix_engine_bg.wasm", baseUrl)
.href;
// 1:1 Разрешение без мыльного скейлинга const wasmModule = await import(/* webpackIgnore: true */ jsUrl);
await wasmModule.default(wasmUrl);
const fontSize = 16;
canvas.width = container.clientWidth; canvas.width = container.clientWidth;
canvas.height = container.clientHeight; canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, 16); const engine = new wasmModule.MatrixEngine(canvas, fontSize);
engineRef.current = engine; engineRef.current = engine;
engine.set_theme(isSecure, isDarkMode); engine.set_theme(isSecure, isDarkMode);
setWasmLoaded(true); setWasmLoaded(true);
// ОПТИМИЗАЦИЯ: Ограничиваем до 35 FPS для устранения лагов const render = () => {
let lastTime = 0; engine.tick();
const fpsInterval = 1000 / 35; // ~28ms на кадр if (animationFrameId.current !== null) {
animationFrameId.current = requestAnimationFrame(render);
const render = (time: number) => {
animationFrameId.current = requestAnimationFrame(render);
const elapsed = time - lastTime;
if (elapsed > fpsInterval) {
lastTime = time - (elapsed % fpsInterval);
engine.tick();
} }
}; };
animationFrameId.current = requestAnimationFrame(render);
requestAnimationFrame(render);
} catch (e) { } catch (e) {
console.error("Wasm Engine Load Failed:", e); console.error("Wasm Engine Load Failed:", e);
} }
@@ -83,16 +79,20 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
if (animationFrameId.current) if (animationFrameId.current !== null) {
cancelAnimationFrame(animationFrameId.current); cancelAnimationFrame(animationFrameId.current);
animationFrameId.current = null;
}
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode); if (engineRef.current) {
engineRef.current.set_theme(isSecure, isDarkMode);
}
}, [isSecure, isDarkMode]); }, [isSecure, isDarkMode]);
// ГЛОБАЛЬНЫЙ ПЕРЕХВАТ (Убраны костыли со скейлингом) // ГЛОБАЛЬНЫЙ ПЕРЕХВАТ (ВКЛЮЧАЯ ТАЧ-СОБЫТИЯ)
useEffect(() => { useEffect(() => {
const triggerAction = (clientX: number, clientY: number) => { const triggerAction = (clientX: number, clientY: number) => {
if (!canvasRef.current || !engineRef.current) return; if (!canvasRef.current || !engineRef.current) return;
@@ -112,30 +112,30 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
engineRef.current.update_mouse(clientX - rect.left, clientY - rect.top); engineRef.current.update_mouse(clientX - rect.left, clientY - rect.top);
}; };
const handlePointerDown = (e: PointerEvent) => const handlePointerDown = (e: MouseEvent) =>
triggerAction(e.clientX, e.clientY); triggerAction(e.clientX, e.clientY);
const handlePointerMove = (e: PointerEvent) => const handlePointerMove = (e: MouseEvent) =>
moveAction(e.clientX, e.clientY); moveAction(e.clientX, e.clientY);
const handlePointerUp = () => engineRef.current?.set_drawing(false); const handlePointerUp = () => engineRef.current?.set_drawing(false);
// Поддержка пальцев на мобилках
const handleTouchStart = (e: TouchEvent) => const handleTouchStart = (e: TouchEvent) =>
triggerAction(e.touches[0].clientX, e.touches[0].clientY); triggerAction(e.touches[0].clientX, e.touches[0].clientY);
const handleTouchMove = (e: TouchEvent) => const handleTouchMove = (e: TouchEvent) =>
moveAction(e.touches[0].clientX, e.touches[0].clientY); moveAction(e.touches[0].clientX, e.touches[0].clientY);
window.addEventListener("pointerdown", handlePointerDown); window.addEventListener("mousedown", handlePointerDown);
window.addEventListener("pointermove", handlePointerMove); window.addEventListener("mousemove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp); window.addEventListener("mouseup", handlePointerUp);
window.addEventListener("touchstart", handleTouchStart, { passive: true }); window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true }); window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handlePointerUp); window.addEventListener("touchend", handlePointerUp);
return () => { return () => {
window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("mousedown", handlePointerDown);
window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("mousemove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("mouseup", handlePointerUp);
window.removeEventListener("touchstart", handleTouchStart); window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove); window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handlePointerUp); window.removeEventListener("touchend", handlePointerUp);
@@ -147,7 +147,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className={`w-full h-full transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`} className={`w-full h-full transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`}
// Убрали imageRendering: pixelated, чтобы текст был гладким style={{ imageRendering: "pixelated" }}
/> />
</div> </div>
); );
+59 -80
View File
@@ -21,24 +21,22 @@ 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..150.0); // Сделал глитчи чуть компактнее для телефонов let w = rng.gen_range(20.0..250.0);
let h = rng.gen_range(5.0..40.0); let h = rng.gen_range(5.0..50.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!("rgb({}, {}, {})", r, g, b), // Убрал альфу 16..=50 => format!("rgba({}, {}, {}, 0.9)", r, g, b),
51..=75 => format!("rgb({}, {}, {})", 255 - r, 255 - g, 255 - b), 51..=75 => format!("rgba({}, {}, {}, 0.7)", 255 - r, 255 - g, 255 - b),
76..=95 => "rgb(255, 255, 255)".to_string(), 76..=95 => "rgba(255, 255, 255, 0.95)".to_string(),
_ => "rgb(255, 20, 80)".to_string(), _ => "rgba(255, 20, 80, 0.95)".to_string(),
}; };
Self { Self {
x: x - w / 2.0 + rng.gen_range(-100.0..100.0), x: x - w / 2.0 + rng.gen_range(-120.0..120.0),
y: y - h / 2.0 + rng.gen_range(-100.0..100.0), y: y - h / 2.0 + rng.gen_range(-120.0..120.0),
w, w,
h, h,
life: rng.gen_range(5.0..20.0), life: rng.gen_range(5.0..25.0),
color, color,
} }
} }
@@ -63,14 +61,13 @@ impl GlassSymbol {
(true, false) => "rgb(90, 30, 200)", (true, false) => "rgb(90, 30, 200)",
(false, false) => "rgb(0, 140, 160)", (false, false) => "rgb(0, 140, 160)",
}; };
Self { Self {
x: x + rng.gen_range(-10.0..10.0), x: x + rng.gen_range(-15.0..15.0),
y: y + rng.gen_range(-10.0..10.0), y: y + rng.gen_range(-15.0..15.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(20.0..50.0), // Быстрее исчезают max_life: rng.gen_range(30.0..80.0),
velocity_y: rng.gen_range(0.2..0.8), velocity_y: rng.gen_range(0.1..0.5),
color: color.to_string(), color: color.to_string(),
} }
} }
@@ -109,20 +106,18 @@ impl MatrixEngine {
.dyn_into::<CanvasRenderingContext2d>()?; .dyn_into::<CanvasRenderingContext2d>()?;
let width = canvas.width(); let width = canvas.width();
let height = canvas.height(); let height = canvas.height();
let chars_str = "0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン>_±÷×=≠≈≡≤≥"; let chars_str = "0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
let charset: Vec<char> = chars_str.chars().collect(); let charset: Vec<char> = chars_str.chars().collect();
let columns = width / font_size; let columns = width / font_size;
let mut drops = Vec::new(); let mut drops = Vec::new();
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
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.2..0.5), speed: rng.gen_range(0.15..0.45),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
Ok(MatrixEngine { Ok(MatrixEngine {
width, width,
height, height,
@@ -155,7 +150,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.2..0.5), speed: rng.gen_range(0.15..0.45),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
@@ -174,12 +169,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 = 40.0; self.emp_timer = 60.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_rgb(); let bg = self.get_bg_color_rgba();
for _ in 0..15 { for _ in 0..25 {
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 +189,11 @@ impl MatrixEngine {
} }
} }
fn get_bg_color_rgb(&self) -> String { fn get_bg_color_rgba(&self) -> String {
if self.is_dark_mode { if self.is_dark_mode {
"rgb(10, 10, 12)".to_string() "rgba(10, 10, 12, 1.0)".to_string()
} else { } else {
"rgb(250, 250, 252)".to_string() "rgba(250, 250, 252, 1.0)".to_string()
} }
} }
@@ -206,25 +201,22 @@ 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(1.0);
self.ctx.set_global_alpha(0.25);
let bg_fill = if self.is_dark_mode { let bg_fill = if self.is_dark_mode {
"rgb(10, 10, 12)" "rgba(10, 10, 12, 0.2)"
} else { } else {
"rgb(250, 250, 252)" "rgba(250, 250, 252, 0.2)"
}; };
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_rgba();
let color_base = format!("rgb({}, {}, {})", r, g, b); let color_base = format!("rgb({}, {}, {})", r, g, b);
let color_head = "rgb(255, 255, 255)"; let color_head = "rgb(255, 255, 255)";
let bg_solid = self.get_bg_color_rgb();
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);
@@ -235,7 +227,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..10.0); self.tear_timer = rng.gen_range(5.0..15.0);
} }
if self.emp_timer > 0.0 { if self.emp_timer > 0.0 {
@@ -245,10 +237,9 @@ 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 mut x = (i as f32) * (self.font_size as f32); let mut x = (i as f32) * (self.font_size as f32);
let drop = &mut self.drops[i];
let cell_y = drop.y.floor(); let cell_y = drop.y.floor();
let y = cell_y * (self.font_size as f32); let y = cell_y * (self.font_size as f32);
@@ -264,18 +255,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; let dist = (dx * dx + dy * dy).sqrt();
let emp_radius = (60.0 - self.emp_timer) * 15.0;
let emp_radius = (40.0 - self.emp_timer) * 15.0; if dist < emp_radius {
let emp_radius_sq = emp_radius * emp_radius; if dist > emp_radius - 20.0 {
if dist_sq < emp_radius_sq {
let ring_inner = emp_radius - 30.0;
let scramble_inner = emp_radius - 80.0;
if dist_sq > ring_inner * ring_inner {
is_emp_ring = true; is_emp_ring = true;
} else if dist_sq > scramble_inner * scramble_inner { } else if dist > emp_radius - 80.0 {
is_scrambled = true; is_scrambled = true;
} else { } else {
is_void = true; is_void = true;
@@ -292,53 +278,51 @@ 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(self.tick_count / 8); .wrapping_add(time_shift);
let mut char_index = (hash as usize) % self.charset.len(); let mut char_idx = (hash as usize) % self.charset.len();
let is_head = rng.gen_bool(0.12); let is_head = rng.gen_bool(0.15);
let mut target_alpha = if is_head { 1.0 } else { 0.6 };
let mut target_color = if is_head {
color_head
} else {
color_base.as_str()
};
// ОПРЕДЕЛЯЕМ ПРОЗРАЧНОСТЬ И ЦВЕТ БЕЗ FORMAT! if is_emp_ring {
let (target_alpha, target_color) = if is_emp_ring { target_color = color_head;
char_index = rng.gen_range(0..self.charset.len()); target_alpha = 1.0;
(1.0, color_head) char_idx = rng.gen_range(0..self.charset.len());
} 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_idx = rng.gen_range(0..self.charset.len());
target_alpha = rng.gen_range(0.6..1.0);
if rng.gen_bool(0.2) {
target_color = color_head;
}
if drop.glitch_timer > 0.0 { if drop.glitch_timer > 0.0 {
drop.glitch_timer -= 1.0; drop.glitch_timer -= 1.0;
} }
}
if rng.gen_bool(0.3) { self.ctx.set_global_alpha(target_alpha as f64);
(0.9, color_head)
} else {
(0.8, color_base.as_str())
}
} else if is_head {
(1.0, color_head)
} else {
(0.6, color_base.as_str())
};
self.ctx.set_global_alpha(target_alpha);
self.ctx.set_fill_style_str(target_color); self.ctx.set_fill_style_str(target_color);
let char_str = self.charset[char_idx].to_string();
let char_str = self.charset[char_index].to_string();
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(());
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(-5.0..0.0); drop.y = rng.gen_range(-10.0..0.0);
drop.speed = rng.gen_range(0.2..0.5); drop.speed = rng.gen_range(0.15..0.45);
} }
} }
// РИСУЕМ БЛОКИ (АЛЬФА 0.8 ДЛЯ ВСЕХ) self.ctx.set_global_alpha(1.0);
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(
@@ -351,7 +335,6 @@ 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,
@@ -363,20 +346,16 @@ impl MatrixEngine {
} }
for sym in self.glass_symbols.iter_mut() { for sym in self.glass_symbols.iter_mut() {
let char_str = sym.value.to_string();
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_global_alpha(alpha as f64); self.ctx.set_global_alpha(alpha as f64);
self.ctx.set_fill_style_str(&sym.color); 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(&sym.value.to_string(), 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); self.ctx.set_global_alpha(1.0);
} }
} }
Binary file not shown.