optimization of wasm and component
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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.
Reference in New Issue
Block a user