optimization of wasm and component

This commit is contained in:
2026-04-24 05:17:56 +07:00
parent e2ece2d6aa
commit 14c4ab2c13
4 changed files with 129 additions and 143 deletions
+42 -33
View File
@@ -28,6 +28,23 @@ 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;
@@ -39,14 +56,12 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const wasmModule = await import( const wasmModule = await import(
/* @vite-ignore */ /* webpackIgnore: true */ wasmPath /* @vite-ignore */ /* webpackIgnore: true */ wasmPath
); );
await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm"); await wasmModule.default("/wasm-matrix/matrix_engine_bg.wasm");
const fontSize = 16; // Применяем масштаб ПЕРЕД инициализацией движка
canvas.width = container.clientWidth; applyCanvasScaling(canvas, container);
canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, fontSize); 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);
@@ -65,8 +80,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const handleResize = () => { const handleResize = () => {
if (canvas && container && engineRef.current) { if (canvas && container && engineRef.current) {
canvas.width = container.clientWidth; applyCanvasScaling(canvas, container);
canvas.height = container.clientHeight;
engineRef.current.resize(canvas.width, canvas.height); engineRef.current.resize(canvas.width, canvas.height);
} }
}; };
@@ -80,57 +94,52 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (engineRef.current) { if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode);
engineRef.current.set_theme(isSecure, isDarkMode);
}
}, [isSecure, isDarkMode]); }, [isSecure, isDarkMode]);
// ГЛОБАЛЬНЫЙ ПЕРЕХВАТ СОБЫТИЙ МЫШИ // ГЛОБАЛЬНЫЙ ПЕРЕХВАТ СОБЫТИЙ (POINTER EVENTS - работает для Мыши и Тача)
useEffect(() => { useEffect(() => {
const handlePointerDown = (e: MouseEvent) => { const handlePointerDown = (e: PointerEvent) => {
if (!canvasRef.current || !engineRef.current) return; if (!canvasRef.current || !engineRef.current) return;
// Используем getBoundingClientRect для идеальной точности относительно канваса
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left; const scale = canvasRef.current.width / rect.width; // Корректируем координаты из-за скейлинга
const y = e.clientY - rect.top; const x = (e.clientX - rect.left) * scale;
const y = (e.clientY - rect.top) * scale;
// Игнорируем клики, если они были за пределами секции Hero 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: MouseEvent) => { const handlePointerMove = (e: PointerEvent) => {
if (!canvasRef.current || !engineRef.current) return; if (!canvasRef.current || !engineRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left; const scale = canvasRef.current.width / rect.width;
const y = e.clientY - rect.top; const x = (e.clientX - rect.left) * scale;
const y = (e.clientY - rect.top) * scale;
engineRef.current.update_mouse(x, y); engineRef.current.update_mouse(x, y);
}; };
const handlePointerUp = () => { const handlePointerUp = () => engineRef.current?.set_drawing(false);
engineRef.current?.set_drawing(false);
};
// Вешаем слушатели прямо на window (работает сквозь любые слои) window.addEventListener("pointerdown", handlePointerDown);
window.addEventListener("mousedown", handlePointerDown); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("mousemove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("mouseup", handlePointerUp);
return () => { return () => {
window.removeEventListener("mousedown", handlePointerDown); window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("mousemove", handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("mouseup", handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
}; };
}, []); // Пустой массив зависимостей, так как engineRef - это реф }, []);
return ( return (
<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={`w-full h-full transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`} className={`transition-opacity duration-1000 ${wasmLoaded ? "opacity-100" : "opacity-0"}`}
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }} // Делает апскейл четким, без "мыла"
/> />
</div> </div>
); );
+45 -62
View File
@@ -6,9 +6,8 @@ import { LanguageSwitcher } from "@/components/sections/lang/language-switcher";
import { ThemeToggle } from "@/components/sections/theme/theme-toggle"; import { ThemeToggle } from "@/components/sections/theme/theme-toggle";
import { Dictionary } from "@/lib/IDict"; import { Dictionary } from "@/lib/IDict";
import { CyberButton } from "../ui/cyber-button"; import { CyberButton } from "../ui/cyber-button";
import { MenuSquare, X, Terminal, FolderOpen, ArrowRight } from "lucide-react"; import { X, Terminal, FolderOpen, ArrowRight } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
export function Header({ export function Header({
dict, dict,
@@ -19,8 +18,8 @@ export function Header({
}) { }) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
// Ссылки для мобильного меню (расширенный список, как в футере) // Единый список ссылок для ретро-меню
const mobileLinks = [ const menuLinks = [
{ name: dict.features, href: `/${lang}#features`, icon: Terminal }, { name: dict.features, href: `/${lang}#features`, icon: Terminal },
{ name: dict.pricing, href: `/${lang}#pricing`, icon: FolderOpen }, { name: dict.pricing, href: `/${lang}#pricing`, icon: FolderOpen },
{ name: dict.faq, href: `/${lang}#faq`, icon: FolderOpen }, { name: dict.faq, href: `/${lang}#faq`, icon: FolderOpen },
@@ -29,7 +28,7 @@ export function Header({
return ( return (
<> <>
<header className="fixed top-4 md:top-6 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl z-50 bg-card/30 dark:bg-card/20 backdrop-blur-xl border border-border/50 rounded-full shadow-lg transition-all duration-500 hover:bg-card/50 dark:hover:bg-card/40"> <header className="fixed top-4 md:top-6 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl z-40 bg-card/30 dark:bg-card/20 backdrop-blur-xl border border-border/50 rounded-full shadow-lg transition-all duration-500 hover:bg-card/50 dark:hover:bg-card/40">
<div className="px-4 md:px-8 h-14 md:h-16 flex items-center justify-between"> <div className="px-4 md:px-8 h-14 md:h-16 flex items-center justify-between">
{/* ЛЕВЫЙ БЛОК: Адаптивный Логотип */} {/* ЛЕВЫЙ БЛОК: Адаптивный Логотип */}
<Link <Link
@@ -68,30 +67,8 @@ export function Header({
</div> </div>
</Link> </Link>
{/* ЦЕНТРАЛЬНЫЙ БЛОК: Навигация (скрыта на мобилках) */} {/* ПРАВЫЙ БЛОК: Контролы и вызов MENU.EXE */}
<nav className="hidden md:flex items-center gap-8 text-sm font-bold text-muted-foreground"> <div className="flex items-center gap-2 md:gap-4 relative z-50">
<Link
href={`/${lang}#features`}
className="hover:text-primary transition-colors duration-200"
>
{dict.features}
</Link>
<Link
href={`/${lang}#pricing`}
className="hover:text-primary transition-colors duration-200"
>
{dict.pricing}
</Link>
<Link
href={`/${lang}#faq`}
className="hover:text-primary transition-colors duration-200"
>
{dict.faq}
</Link>
</nav>
{/* ПРАВЫЙ БЛОК: Контролы */}
<div className="flex items-center gap-1.5 md:gap-4 relative z-50">
<div className="flex items-center gap-1 md:gap-2"> <div className="flex items-center gap-1 md:gap-2">
<LanguageSwitcher /> <LanguageSwitcher />
<ThemeToggle /> <ThemeToggle />
@@ -105,49 +82,64 @@ export function Header({
{dict.cta} {dict.cta}
</CyberButton> </CyberButton>
{/* КНОПКА МОБИЛЬНОГО МЕНЮ */} {/* ГЛОБАЛЬНАЯ КНОПКА МЕНЮ (Видна везде) */}
<button <button
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(true)}
className="sm:hidden p-2 rounded-xl bg-background/50 border border-border text-foreground hover:bg-muted transition-colors" className="px-3 py-1.5 md:px-4 md:py-2 rounded-xl bg-background/50 border border-border text-foreground hover:bg-primary/20 hover:text-primary hover:border-primary/50 transition-all duration-300 flex items-center gap-2 font-mono text-sm uppercase tracking-wider shadow-sm"
aria-label="Toggle System Menu"
> >
{isMenuOpen ? ( <Terminal className="size-4" />
<X className="size-5" /> <span className="hidden md:inline">Menu.exe</span>
) : (
<MenuSquare className="size-5" />
)}
</button> </button>
</div> </div>
</div> </div>
</header> </header>
{/* ВСПЛЫВАЮЩЕЕ ОКНО (Мобильное меню в стиле ретро-ОС) */} {/* ГЛОБАЛЬНАЯ РЕТРО-МОДАЛКА */}
<AnimatePresence> <AnimatePresence>
{isMenuOpen && ( {isMenuOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6">
{/* Оверлей-подложка (Закрывает меню по клику вне окна) */}
<motion.div <motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="absolute inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-md"
onClick={() => setIsMenuOpen(false)}
/>
{/* Само окно (Retro Window) */}
<motion.div
initial={{ opacity: 0, y: -40, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }} exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }} transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="fixed top-24 left-4 right-4 z-40 sm:hidden origin-top" className="relative w-full max-w-lg bg-[#E5E5E5] dark:bg-[#1E1E1E] border-2 border-t-white border-l-white border-b-gray-500 border-r-gray-500 dark:border-t-gray-600 dark:border-l-gray-600 dark:border-b-black dark:border-r-black p-1 shadow-[8px_8px_0_0_rgba(0,0,0,0.6)]"
> >
{/* Окно-папка */}
<div className="bg-[#E5E5E5] dark:bg-[#1E1E1E] border-2 border-t-white border-l-white border-b-gray-500 border-r-gray-500 dark:border-t-gray-600 dark:border-l-gray-600 dark:border-b-black dark:border-r-black p-1 shadow-[4px_4px_0_0_rgba(0,0,0,0.5)]">
{/* Заголовок окна (Title bar) */} {/* Заголовок окна (Title bar) */}
<div className="bg-blue-800 text-white px-2 py-1 flex items-center justify-between mb-2 font-mono text-xs font-bold tracking-wider"> <div className="bg-blue-800 dark:bg-blue-900 text-white px-2 py-1.5 flex items-center justify-between mb-2 font-mono text-sm font-bold tracking-wider select-none">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Terminal className="size-3" /> C:\NETRUNNER\MENU.EXE <Terminal className="size-4" /> C:\NETRUNNER\SYSTEM.DIR
</span> </span>
{/* Классическая кнопка закрытия Win95 */}
<button
onClick={() => setIsMenuOpen(false)}
className="bg-[#c0c0c0] text-black border-2 border-t-white border-l-white border-b-gray-600 border-r-gray-600 w-6 h-6 flex items-center justify-center active:border-t-gray-600 active:border-l-gray-600 active:border-b-white active:border-r-white"
aria-label="Close"
>
<X className="size-4" strokeWidth={3} />
</button>
</div> </div>
{/* Содержимое папки (Ссылки) */} {/* Содержимое папки (Ссылки) */}
<div className="bg-white dark:bg-black border-2 border-inset border-t-gray-500 border-l-gray-500 border-b-white border-r-white dark:border-t-black dark:border-l-black dark:border-b-gray-700 dark:border-r-gray-700 p-2 font-mono flex flex-col gap-1 max-h-[70vh] overflow-y-auto custom-scrollbar-retro"> <div className="bg-white dark:bg-black border-2 border-inset border-t-gray-500 border-l-gray-500 border-b-white border-r-white dark:border-t-black dark:border-l-black dark:border-b-gray-700 dark:border-r-gray-700 p-2 font-mono flex flex-col gap-1 max-h-[70vh] overflow-y-auto custom-scrollbar-retro">
{mobileLinks.map((link) => ( {menuLinks.map((link) => (
<Link <Link
key={link.name} key={link.name}
href={link.href} href={link.href}
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-sm text-black dark:text-white shrink-0" className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-base text-black dark:text-white shrink-0"
> >
<link.icon className="size-5 text-gray-500 group-hover:text-white dark:group-hover:text-primary transition-colors fill-yellow-200 dark:fill-transparent" /> <link.icon className="size-5 text-gray-500 group-hover:text-white dark:group-hover:text-primary transition-colors fill-yellow-200 dark:fill-transparent" />
<span className="flex-1">{link.name}</span> <span className="flex-1">{link.name}</span>
@@ -164,7 +156,7 @@ export function Header({
<Link <Link
href={`/${lang}/docs`} href={`/${lang}/docs`}
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-xs text-gray-600 dark:text-gray-400 shrink-0" className="flex items-center gap-3 p-3 hover:bg-blue-800 hover:text-white dark:hover:bg-primary/20 dark:hover:text-primary transition-colors group cursor-pointer text-sm text-gray-600 dark:text-gray-400 shrink-0"
> >
<Terminal className="size-4" /> <Terminal className="size-4" />
<span className="flex-1 uppercase tracking-widest"> <span className="flex-1 uppercase tracking-widest">
@@ -172,31 +164,22 @@ export function Header({
</span> </span>
</Link> </Link>
{/* Кнопка теперь идет СРАЗУ за списком, а не прибита к низу h-[60vh] */}
<div className="pt-4 pb-2 px-2 shrink-0"> <div className="pt-4 pb-2 px-2 shrink-0">
<CyberButton <CyberButton
href={`/${lang}/download`} href={`/${lang}/download`}
variant="primary" variant="primary"
className="w-full text-xs" className="w-full text-sm py-3"
> >
<span className="flex items-center justify-center gap-2"> <span className="flex items-center justify-center gap-2">
{dict.cta} <ArrowRight className="size-3" /> {dict.cta} <ArrowRight className="size-4" />
</span> </span>
</CyberButton> </CyberButton>
</div> </div>
</div> </div>
</div>
</motion.div> </motion.div>
</div>
)} )}
</AnimatePresence> </AnimatePresence>
{/* Оверлей для закрытия меню по клику вне его */}
{isMenuOpen && (
<div
className="fixed inset-0 z-30 sm:hidden bg-black/20 backdrop-blur-sm"
onClick={() => setIsMenuOpen(false)}
/>
)}
</> </>
); );
} }
+38 -44
View File
@@ -24,14 +24,13 @@ impl GlitchBlock {
let w = rng.gen_range(20.0..250.0); let w = rng.gen_range(20.0..250.0);
let h = rng.gen_range(5.0..50.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!("rgba({}, {}, {}, 0.9)", r, g, b), // Плотный цвет темы 16..=50 => format!("rgba({}, {}, {}, 0.9)", r, g, b),
51..=75 => format!("rgba({}, {}, {}, 0.7)", 255 - r, 255 - g, 255 - b), // Инверсия 51..=75 => format!("rgba({}, {}, {}, 0.7)", 255 - r, 255 - g, 255 - b),
76..=95 => "rgba(255, 255, 255, 0.95)".to_string(), // Ослепительная вспышка 76..=95 => "rgba(255, 255, 255, 0.95)".to_string(),
_ => "rgba(255, 20, 80, 0.95)".to_string(), // 5% ШАНС: КРИТИЧЕСКАЯ КРАСНАЯ ОШИБКА _ => "rgba(255, 20, 80, 0.95)".to_string(),
}; };
Self { Self {
@@ -92,14 +91,12 @@ pub struct MatrixEngine {
is_drawing: bool, is_drawing: bool,
is_vpn_on: bool, is_vpn_on: bool,
is_dark_mode: bool, is_dark_mode: bool,
// Новые переменные для разнообразия
tick_count: u32, tick_count: u32,
emp_timer: f32, emp_timer: f32,
emp_x: f32, emp_x: f32,
emp_y: f32, emp_y: f32,
tear_y: f32, tear_y: f32,
tear_timer: f32, // Горизонтальный разрыв tear_timer: f32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -177,7 +174,7 @@ 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 = 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();
@@ -223,7 +220,10 @@ impl MatrixEngine {
let (r, g, b) = self.get_theme_colors(); let (r, g, b) = self.get_theme_colors();
let bg_solid = self.get_bg_color(); let bg_solid = self.get_bg_color();
// Рандомные глобальные сбои // ОПТИМИЗАЦИЯ ПАМЯТИ: Кешируем строки базовых цветов один раз на кадр!
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);
let ry = rng.gen_range(0.0..self.height as f32); let ry = rng.gen_range(0.0..self.height as f32);
@@ -231,7 +231,6 @@ impl MatrixEngine {
.push(GlitchBlock::new(rx, ry, r, g, b, &bg_solid)); .push(GlitchBlock::new(rx, ry, r, g, b, &bg_solid));
} }
// CRT Горизонтальный разрыв
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..15.0);
@@ -254,26 +253,29 @@ impl MatrixEngine {
let mut is_void = false; let mut is_void = false;
let mut is_emp_ring = false; let mut is_emp_ring = false;
// Горизонтальный разрыв (Tearing)
if self.tear_timer > 0.0 && (y - self.tear_y).abs() < 60.0 { if self.tear_timer > 0.0 && (y - self.tear_y).abs() < 60.0 {
x += rng.gen_range(-20.0..20.0); // Тряска по X x += rng.gen_range(-20.0..20.0);
is_scrambled = true; is_scrambled = true;
} }
// EMP Волна
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 = (dx * dx + dy * dy).sqrt(); let dist_sq = dx * dx + dy * dy; // ОПТИМИЗАЦИЯ: Убрали .sqrt()
let emp_radius = (60.0 - self.emp_timer) * 15.0; // Быстро расширяется
if dist < emp_radius { let emp_radius = (60.0 - self.emp_timer) * 15.0;
if dist > emp_radius - 20.0 { let emp_radius_sq = emp_radius * emp_radius;
is_emp_ring = true; // Белое кольцо на краю взрыва
} else if dist > emp_radius - 80.0 { if dist_sq < emp_radius_sq {
let ring_inner = emp_radius - 20.0;
let scramble_inner = emp_radius - 80.0;
if dist_sq > ring_inner * ring_inner {
is_emp_ring = true;
} else if dist_sq > scramble_inner * scramble_inner {
is_scrambled = true; is_scrambled = true;
} else { } else {
is_void = true; // Эпицентр выжжен is_void = true;
} }
} }
} }
@@ -287,44 +289,38 @@ impl MatrixEngine {
drop.glitch_timer = 5.0; drop.glitch_timer = 5.0;
} }
// Динамический хэш (Символы мутируют раз в ~10 кадров)
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 as u32).wrapping_mul(19349663))
.wrapping_add(time_shift); .wrapping_add(time_shift);
let mut char_index = (hash as usize) % self.charset.len(); let mut char_index = (hash as usize) % self.charset.len();
// TRACERS: 15% шанс, что символ отрисуется как яркая головка
let is_head = rng.gen_bool(0.15); let is_head = rng.gen_bool(0.15);
let mut alpha = if is_head { 1.0 } else { 0.6 };
let mut char_color = if is_head { // ОПТИМИЗАЦИЯ: В 95% случаев мы используем ссылки на заранее созданные строки, не забивая память!
format!("rgba(255, 255, 255, {})", alpha) // Белый наконечник let mut custom_color = String::new();
} else { let color_ref = if is_emp_ring {
format!("rgba({}, {}, {}, {})", r, g, b, alpha) &color_head
};
// Приоритет визуальных сбоев
if is_emp_ring {
char_color = "rgba(255, 255, 255, 1.0)".to_string();
char_index = 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_index = rng.gen_range(0..self.charset.len());
alpha = rng.gen_range(0.6..1.0); let alpha = rng.gen_range(0.6..1.0);
if rng.gen_bool(0.2) { if rng.gen_bool(0.2) {
char_color = format!("rgba(255, 255, 255, {})", alpha); custom_color = format!("rgba(255, 255, 255, {})", alpha);
} else { } else {
char_color = format!("rgba({}, {}, {}, {})", r, g, b, alpha); 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
} else if is_head {
&color_head
} else {
&color_base
};
let char_str = self.charset[char_index].to_string(); let char_str = self.charset[char_index].to_string();
self.ctx.set_fill_style_str(&char_color); 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(());
@@ -337,7 +333,6 @@ impl MatrixEngine {
} }
} }
// Отрисовка блоков поверх
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(
@@ -350,7 +345,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,
Binary file not shown.