filter update, hero optimization

This commit is contained in:
2026-04-24 12:10:38 +07:00
parent 487e75a405
commit 8bf72addb2
6 changed files with 281 additions and 115 deletions
+30 -7
View File
@@ -9,6 +9,7 @@ type MatrixEngine = {
trigger_glitch: (x: number, y: number) => void; trigger_glitch: (x: number, y: number) => void;
set_drawing: (is_drawing: boolean) => void; set_drawing: (is_drawing: boolean) => void;
set_theme: (is_vpn_on: boolean, is_dark_mode: boolean) => void; set_theme: (is_vpn_on: boolean, is_dark_mode: boolean) => void;
set_mobile: (is_mobile: boolean) => void;
resize: (width: number, height: number) => void; resize: (width: number, height: number) => void;
}; };
@@ -28,6 +29,15 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark"; const isDarkMode = resolvedTheme === "dark";
const checkIsMobile = () => {
if (typeof window === "undefined") return false;
return (
window.innerWidth <= 768 ||
"ontouchstart" in window ||
navigator.maxTouchPoints > 0
);
};
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
@@ -35,8 +45,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const loadWasm = async () => { const loadWasm = async () => {
try { try {
// НАДЕЖНЫЙ ПУТЬ ДЛЯ ПРОДАКШЕНА:
// Форсируем абсолютные URL, чтобы Webpack не пытался их бандлить
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
const jsUrl = new URL("/wasm-matrix/matrix_engine.js", baseUrl).href; const jsUrl = new URL("/wasm-matrix/matrix_engine.js", baseUrl).href;
const wasmUrl = new URL("/wasm-matrix/matrix_engine_bg.wasm", baseUrl) const wasmUrl = new URL("/wasm-matrix/matrix_engine_bg.wasm", baseUrl)
@@ -52,6 +60,8 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const engine = new wasmModule.MatrixEngine(canvas, fontSize); const engine = new wasmModule.MatrixEngine(canvas, fontSize);
engineRef.current = engine; engineRef.current = engine;
engine.set_theme(isSecure, isDarkMode); engine.set_theme(isSecure, isDarkMode);
engine.set_mobile(checkIsMobile());
setWasmLoaded(true); setWasmLoaded(true);
const render = () => { const render = () => {
@@ -73,6 +83,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
canvas.width = container.clientWidth; canvas.width = container.clientWidth;
canvas.height = container.clientHeight; canvas.height = container.clientHeight;
engineRef.current.resize(canvas.width, canvas.height); engineRef.current.resize(canvas.width, canvas.height);
engineRef.current.set_mobile(checkIsMobile());
} }
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
@@ -87,12 +98,9 @@ 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]);
// ГЛОБАЛЬНЫЙ ПЕРЕХВАТ (ВКЛЮЧАЯ ТАЧ-СОБЫТИЯ)
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;
@@ -143,12 +151,27 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}, []); }, []);
return ( return (
<div ref={containerRef} className="absolute inset-0 overflow-hidden z-0"> <div
ref={containerRef}
className="absolute inset-0 overflow-hidden z-0 bg-background"
>
<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"}`}
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
{/* Секретный соус: CRT Scanlines (Аппаратное ускорение, 0 CPU) */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25]"
style={{
background: `
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06))
`,
backgroundSize: "100% 4px, 3px 100%",
}}
/>
</div> </div>
); );
}; };
+85 -21
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useRef } from "react"; import { useState, useRef, useEffect } from "react";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { motion, Variants } from "framer-motion"; import { motion, Variants } from "framer-motion";
import { Shield, EyeOff } from "lucide-react"; import { Shield, EyeOff } from "lucide-react";
@@ -9,18 +9,29 @@ import { CyberButton } from "../ui/cyber-button";
import { NetrunnerMatrix } from "../graphics/NetrunnerMatrix"; import { NetrunnerMatrix } from "../graphics/NetrunnerMatrix";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
function CyberEye({ isClosed, vpnOn }: { isClosed: boolean; vpnOn: boolean }) { function CyberEye({
isClosed,
vpnOn,
targetX,
targetY,
}: {
isClosed: boolean;
vpnOn: boolean;
targetX: number;
targetY: number;
}) {
const eyeRef = useRef<HTMLDivElement>(null); const eyeRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ x: 0, y: 0 }); const [offset, setOffset] = useState({ x: 0, y: 0 });
const [direction, setDirection] = useState("center"); const [direction, setDirection] = useState("center");
const handleMouseMove = (e: MouseEvent) => { // Теперь глаз только вычисляет углы на основе переданных ему координат
useEffect(() => {
if (isClosed || !eyeRef.current) return; if (isClosed || !eyeRef.current) return;
const rect = eyeRef.current.getBoundingClientRect(); const rect = eyeRef.current.getBoundingClientRect();
const eyeCenterX = rect.left + rect.width / 2; const eyeCenterX = rect.left + rect.width / 2;
const eyeCenterY = rect.top + rect.height / 2; const eyeCenterY = rect.top + rect.height / 2;
const dx = e.clientX - eyeCenterX; const dx = targetX - eyeCenterX;
const dy = e.clientY - eyeCenterY; const dy = targetY - eyeCenterY;
if (Math.abs(dx) < 30 && Math.abs(dy) < 30) { if (Math.abs(dx) < 30 && Math.abs(dy) < 30) {
setDirection("center"); setDirection("center");
@@ -32,16 +43,10 @@ function CyberEye({ isClosed, vpnOn }: { isClosed: boolean; vpnOn: boolean }) {
const angle = Math.atan2(dy, dx); const angle = Math.atan2(dy, dx);
const maxRadius = 22; const maxRadius = 22;
const distance = Math.min(maxRadius, Math.hypot(dx, dy) * 0.12); // Чуть усилили множитель (0.15), чтобы глаза были более "отзывчивыми"
const distance = Math.min(maxRadius, Math.hypot(dx, dy) * 0.15);
setOffset({ x: Math.cos(angle) * distance, y: Math.sin(angle) * distance }); setOffset({ x: Math.cos(angle) * distance, y: Math.sin(angle) * distance });
}; }, [targetX, targetY, isClosed]);
useState(() => {
if (typeof window !== "undefined") {
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}
});
const eyeVariants: Variants = { const eyeVariants: Variants = {
open: { open: {
@@ -115,9 +120,12 @@ function CyberEye({ isClosed, vpnOn }: { isClosed: boolean; vpnOn: boolean }) {
function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) { function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
const [isBlinking, setIsBlinking] = useState(false); const [isBlinking, setIsBlinking] = useState(false);
const [targetCoords, setTargetCoords] = useState({ x: 0, y: 0 });
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const lastInteraction = useRef<number>(0);
useState(() => { // Логика моргания
useEffect(() => {
const schedule = () => { const schedule = () => {
timerRef.current = setTimeout( timerRef.current = setTimeout(
() => { () => {
@@ -132,14 +140,73 @@ function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
}; };
if (!vpnOn) schedule(); if (!vpnOn) schedule();
return () => clearTimeout(timerRef.current); return () => clearTimeout(timerRef.current);
}); }, [vpnOn]);
// Логика слежения и случайного блуждания
useEffect(() => {
// Изначально смотрим в центр
setTargetCoords({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
const handleInteract = (e: MouseEvent | TouchEvent) => {
lastInteraction.current = Date.now();
let clientX, clientY;
if ("touches" in e && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = (e as MouseEvent).clientX;
clientY = (e as MouseEvent).clientY;
}
setTargetCoords({ x: clientX, y: clientY });
};
window.addEventListener("mousemove", handleInteract);
window.addEventListener("touchstart", handleInteract, { passive: true });
window.addEventListener("pointerdown", handleInteract);
// Интервал для "живого" взгляда на мобилках
const randomLookInterval = setInterval(() => {
const isMobile = window.innerWidth <= 768 || "ontouchstart" in window;
// Если на мобилке и юзер не трогал экран последние 2.5 секунды
if (isMobile && Date.now() - lastInteraction.current > 2500) {
// Выбираем случайную точку, но не слишком близко к краям экрана
setTargetCoords({
x:
window.innerWidth * 0.2 + Math.random() * (window.innerWidth * 0.6),
y:
window.innerHeight * 0.2 +
Math.random() * (window.innerHeight * 0.6),
});
}
}, 2000); // Раз в 2 секунды глаза могут сменить цель
return () => {
window.removeEventListener("mousemove", handleInteract);
window.removeEventListener("touchstart", handleInteract);
window.removeEventListener("pointerdown", handleInteract);
clearInterval(randomLookInterval);
};
}, []);
const shouldBeClosed = vpnOn || isBlinking; const shouldBeClosed = vpnOn || isBlinking;
return ( return (
<div className="flex gap-6 md:gap-10 justify-center mb-12 h-20 items-center select-none pointer-events-none"> <div className="flex gap-6 md:gap-10 justify-center mb-12 h-20 items-center select-none pointer-events-none">
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} /> <CyberEye
<CyberEye isClosed={shouldBeClosed} vpnOn={vpnOn} /> isClosed={shouldBeClosed}
vpnOn={vpnOn}
targetX={targetCoords.x}
targetY={targetCoords.y}
/>
<CyberEye
isClosed={shouldBeClosed}
vpnOn={vpnOn}
targetX={targetCoords.x}
targetY={targetCoords.y}
/>
</div> </div>
); );
} }
@@ -159,8 +226,6 @@ export function Hero({
{/* --- RUST BACKGROUND --- */} {/* --- RUST BACKGROUND --- */}
<div className="absolute inset-0 -z-10 bg-background pointer-events-auto"> <div className="absolute inset-0 -z-10 bg-background pointer-events-auto">
<NetrunnerMatrix isSecure={isVpnOn} /> <NetrunnerMatrix isSecure={isVpnOn} />
{/* Прозрачные слои для виньетки — теперь не мешают кликам */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_120%)] opacity-80 pointer-events-none" /> <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_120%)] opacity-80 pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-background via-transparent to-transparent opacity-90 pointer-events-none" /> <div className="absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-background via-transparent to-transparent opacity-90 pointer-events-none" />
</div> </div>
@@ -171,7 +236,6 @@ export function Hero({
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20 pointer-events-none" className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20 pointer-events-none"
> >
{/* Оборачиваем интерактивные элементы в pointer-events-auto, чтобы они работали на фоне некликабельного motion.div */}
<div className="pointer-events-auto flex flex-col items-center"> <div className="pointer-events-auto flex flex-col items-center">
<motion.div <motion.div
layout layout
+100 -60
View File
@@ -81,20 +81,19 @@ pub struct MatrixEngine {
width: u32, width: u32,
height: u32, height: u32,
font_size: u32, font_size: u32,
drops: Vec<Drop>, fg_drops: Vec<Drop>,
bg_drops: Vec<Drop>, // Разделили на два слоя
glass_symbols: Vec<GlassSymbol>, glass_symbols: Vec<GlassSymbol>,
glitch_blocks: Vec<GlitchBlock>, glitch_blocks: Vec<GlitchBlock>,
charset: Vec<char>, charset: Vec<char>,
ctx: CanvasRenderingContext2d, ctx: CanvasRenderingContext2d,
// Тот самый секретный холст для спрайтов
atlas_canvas: HtmlCanvasElement, atlas_canvas: HtmlCanvasElement,
mouse_x: f32, mouse_x: f32,
mouse_y: f32, mouse_y: f32,
is_drawing: bool, is_drawing: bool,
is_vpn_on: bool, is_vpn_on: bool,
is_dark_mode: bool, is_dark_mode: bool,
is_mobile: bool,
tick_count: u32, tick_count: u32,
emp_timer: f32, emp_timer: f32,
emp_x: f32, emp_x: f32,
@@ -113,36 +112,22 @@ 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 document = web_sys::window().unwrap().document().unwrap(); let document = web_sys::window().unwrap().document().unwrap();
let atlas_canvas = document let atlas_canvas = document
.create_element("canvas")? .create_element("canvas")?
.dyn_into::<HtmlCanvasElement>()?; .dyn_into::<HtmlCanvasElement>()?;
// Ширина = количество символов * размер. Высота = 2 ряда (цветной и белый)
atlas_canvas.set_width((charset.len() as u32) * font_size); atlas_canvas.set_width((charset.len() as u32) * font_size);
atlas_canvas.set_height(font_size * 2); atlas_canvas.set_height(font_size * 2);
let mut drops = Vec::new();
let mut rng = rand::thread_rng();
for _ in 0..columns {
drops.push(Drop {
y: rng.gen_range(-20.0..0.0),
speed: rng.gen_range(0.15..0.45),
glitch_timer: 0.0,
});
}
let mut engine = MatrixEngine { let mut engine = MatrixEngine {
width, width,
height, height,
font_size, font_size,
drops, fg_drops: Vec::new(),
bg_drops: Vec::new(),
glass_symbols: Vec::new(), glass_symbols: Vec::new(),
glitch_blocks: Vec::new(), glitch_blocks: Vec::new(),
charset, charset,
@@ -153,6 +138,7 @@ impl MatrixEngine {
is_drawing: false, is_drawing: false,
is_vpn_on: false, is_vpn_on: false,
is_dark_mode: true, is_dark_mode: true,
is_mobile: false,
tick_count: 0, tick_count: 0,
emp_timer: 0.0, emp_timer: 0.0,
emp_x: 0.0, emp_x: 0.0,
@@ -161,13 +147,37 @@ impl MatrixEngine {
tear_timer: 0.0, tear_timer: 0.0,
}; };
// Генерируем спрайты первый раз engine.init_drops();
engine.build_atlas(); engine.build_atlas();
Ok(engine) Ok(engine)
} }
// МЕТОД ГЕНЕРАЦИИ СПРАЙТОВ (Вызывается 1 раз при загрузке или смене темы) fn init_drops(&mut self) {
let mut rng = rand::thread_rng();
self.fg_drops.clear();
self.bg_drops.clear();
// Передний план (крупные)
let fg_columns = self.width / self.font_size;
for _ in 0..fg_columns {
self.fg_drops.push(Drop {
y: rng.gen_range(-20.0..self.height as f32),
speed: rng.gen_range(0.15..0.45),
glitch_timer: 0.0,
});
}
// Задний план (мелкие, больше колонок)
let bg_columns = (self.width as f32 / (self.font_size as f32 * 0.6)) as u32;
for _ in 0..bg_columns {
self.bg_drops.push(Drop {
y: rng.gen_range(-20.0..self.height as f32),
speed: rng.gen_range(0.05..0.15),
glitch_timer: 0.0,
});
}
}
fn build_atlas(&self) { fn build_atlas(&self) {
let atlas_ctx = self let atlas_ctx = self
.atlas_canvas .atlas_canvas
@@ -177,7 +187,6 @@ impl MatrixEngine {
.dyn_into::<CanvasRenderingContext2d>() .dyn_into::<CanvasRenderingContext2d>()
.unwrap(); .unwrap();
let (r, g, b) = self.get_theme_colors(); let (r, g, b) = self.get_theme_colors();
atlas_ctx.clear_rect( atlas_ctx.clear_rect(
0.0, 0.0,
0.0, 0.0,
@@ -186,18 +195,16 @@ impl MatrixEngine {
); );
atlas_ctx.set_font(&format!("bold {}px monospace", self.font_size)); atlas_ctx.set_font(&format!("bold {}px monospace", self.font_size));
atlas_ctx.set_text_baseline("top"); atlas_ctx.set_text_baseline("top");
atlas_ctx.set_text_align("center"); // Рисуем строго по центру клетки atlas_ctx.set_text_align("center");
let half_fs = self.font_size as f64 / 2.0; let half_fs = self.font_size as f64 / 2.0;
// 1 РЯД: Базовый цвет (тема)
atlas_ctx.set_fill_style_str(&format!("rgb({}, {}, {})", r, g, b)); atlas_ctx.set_fill_style_str(&format!("rgb({}, {}, {})", r, g, b));
for (i, c) in self.charset.iter().enumerate() { for (i, c) in self.charset.iter().enumerate() {
let x = (i as u32 * self.font_size) as f64 + half_fs; let x = (i as u32 * self.font_size) as f64 + half_fs;
atlas_ctx.fill_text(&c.to_string(), x, 0.0).unwrap_or(()); atlas_ctx.fill_text(&c.to_string(), x, 0.0).unwrap_or(());
} }
// 2 РЯД: Белый цвет (головки капель и EMP вспышки)
atlas_ctx.set_fill_style_str("rgb(255, 255, 255)"); atlas_ctx.set_fill_style_str("rgb(255, 255, 255)");
for (i, c) in self.charset.iter().enumerate() { for (i, c) in self.charset.iter().enumerate() {
let x = (i as u32 * self.font_size) as f64 + half_fs; let x = (i as u32 * self.font_size) as f64 + half_fs;
@@ -209,16 +216,7 @@ impl MatrixEngine {
pub fn resize(&mut self, width: u32, height: u32) { pub fn resize(&mut self, width: u32, height: u32) {
self.width = width; self.width = width;
self.height = height; self.height = height;
let columns = width / self.font_size; self.init_drops();
let mut rng = rand::thread_rng();
self.drops.clear();
for _ in 0..columns {
self.drops.push(Drop {
y: rng.gen_range(-20.0..height as f32),
speed: rng.gen_range(0.15..0.45),
glitch_timer: 0.0,
});
}
} }
pub fn update_mouse(&mut self, x: f32, y: f32) { pub fn update_mouse(&mut self, x: f32, y: f32) {
@@ -228,16 +226,21 @@ impl MatrixEngine {
pub fn set_drawing(&mut self, is_drawing: bool) { pub fn set_drawing(&mut self, is_drawing: bool) {
self.is_drawing = is_drawing; self.is_drawing = is_drawing;
} }
pub fn set_theme(&mut self, is_vpn_on: bool, is_dark_mode: bool) { pub fn set_theme(&mut self, is_vpn_on: bool, is_dark_mode: bool) {
if self.is_vpn_on != is_vpn_on || self.is_dark_mode != is_dark_mode { if self.is_vpn_on != is_vpn_on || self.is_dark_mode != is_dark_mode {
self.is_vpn_on = is_vpn_on; self.is_vpn_on = is_vpn_on;
self.is_dark_mode = is_dark_mode; self.is_dark_mode = is_dark_mode;
self.build_atlas(); // Перерисовываем спрайты под новые цвета! self.build_atlas();
} }
} }
pub fn set_mobile(&mut self, is_mobile: bool) {
self.is_mobile = is_mobile;
}
pub fn trigger_glitch(&mut self, x: f32, y: f32) { pub fn trigger_glitch(&mut self, x: f32, y: f32) {
if self.is_mobile {
return;
}
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;
@@ -283,16 +286,17 @@ 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_rgba(); let bg_solid = self.get_bg_color_rgba();
if rng.gen_bool(0.02) { if !self.is_mobile {
let rx = rng.gen_range(0.0..self.width as f32); if rng.gen_bool(0.02) {
let ry = rng.gen_range(0.0..self.height as f32); let rx = rng.gen_range(0.0..self.width as f32);
self.glitch_blocks let ry = rng.gen_range(0.0..self.height as f32);
.push(GlitchBlock::new(rx, ry, r, g, b, &bg_solid)); self.glitch_blocks
} .push(GlitchBlock::new(rx, ry, r, g, b, &bg_solid));
}
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);
}
} }
if self.emp_timer > 0.0 { if self.emp_timer > 0.0 {
@@ -302,8 +306,49 @@ impl MatrixEngine {
self.tear_timer -= 1.0; self.tear_timer -= 1.0;
} }
for i in 0..self.drops.len() { // --- 1. РИСУЕМ ЗАДНИЙ ПЛАН (Глубина) ---
let drop = &mut self.drops[i]; // Рисуем только на ПК, чтобы поберечь телефоны
if !self.is_mobile {
let bg_f = self.font_size as f64 * 0.6; // Уменьшенный размер
for i in 0..self.bg_drops.len() {
let drop = &mut self.bg_drops[i];
let x = (i as f32) * (self.font_size as f32 * 0.6);
let cell_y = drop.y.floor();
let y = cell_y * (self.font_size as f32 * 0.6);
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();
// Задний слой очень тусклый
self.ctx.set_global_alpha(rng.gen_range(0.05..0.15));
let src_x = (char_idx as u32 * self.font_size) as f64;
self.ctx
.draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
&self.atlas_canvas,
src_x,
0.0,
self.font_size as f64,
self.font_size as f64,
x as f64,
y as f64,
bg_f,
bg_f,
)
.unwrap_or(());
drop.y += drop.speed;
if y > self.height as f32 && rng.gen_bool(0.005) {
drop.y = rng.gen_range(-10.0..0.0);
}
}
}
// --- 2. РИСУЕМ ПЕРЕДНИЙ ПЛАН ---
for i in 0..self.fg_drops.len() {
let drop = &mut self.fg_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 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);
@@ -338,7 +383,6 @@ impl MatrixEngine {
drop.y += drop.speed; drop.y += drop.speed;
continue; continue;
} }
if rng.gen_bool(0.001) { if rng.gen_bool(0.001) {
drop.glitch_timer = 5.0; drop.glitch_timer = 5.0;
} }
@@ -366,12 +410,9 @@ impl MatrixEngine {
} }
} }
// --- БЫСТРЕЙШАЯ В МИРЕ ОТРИСОВКА (Копирование пикселей) ---
self.ctx.set_global_alpha(target_alpha as f64); self.ctx.set_global_alpha(target_alpha as f64);
// Вычисляем координаты куска спрайта из атласа
let src_x = (char_idx as u32 * self.font_size) as f64; let src_x = (char_idx as u32 * self.font_size) as f64;
let src_y = if is_head { self.font_size as f64 } else { 0.0 }; // 0-й ряд цветной, 1-й ряд белый let src_y = if is_head { self.font_size as f64 } else { 0.0 };
let f = self.font_size as f64; let f = self.font_size as f64;
self.ctx self.ctx
@@ -380,11 +421,11 @@ impl MatrixEngine {
src_x, src_x,
src_y, src_y,
f, f,
f, // Откуда берем (X, Y, Ширина, Высота) f,
x as f64, x as f64,
y as f64, y as f64,
f, f,
f, // Куда кладем f,
) )
.unwrap_or(()); .unwrap_or(());
@@ -408,8 +449,7 @@ 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 && 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,
self.mouse_y, self.mouse_y,
+60 -27
View File
@@ -1,10 +1,9 @@
use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post}; use axum::{http::StatusCode, response::IntoResponse, routing::post, Json, Router};
use regex::Regex; use regex::Regex;
use reqwest::Client; use reqwest::Client;
use rustrict::CensorStr; use rustrict::CensorStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::LazyLock; use std::sync::LazyLock;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct CommentPayload { struct CommentPayload {
@@ -12,31 +11,59 @@ struct CommentPayload {
message: String, message: String,
} }
// Расширенный список КОРНЕЙ. Регулярка будет искать эти корни внутри слов. // 1. Расширенный список КОРНЕЙ.
// Используем \p{L}* (любые буквы) вокруг корня, чтобы ловить склонения.
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| { static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(хуй|хуя|пизд|ебат|ебан|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш)").unwrap() Regex::new(r"(?i)(хуй|хуя|хуе|пизд|ебат|ебан|ебл|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш|спайс|cpam|scam|crypto|binance)").unwrap()
}); });
// 2. БЕЛЫЙ СПИСОК (Исключения). Защищает нормальные слова, содержащие плохие корни.
const SAFE_WORDS: &[&str] = &[
"колебат",
"оскорблят",
"употреблят",
"расслаблят",
"потреблят",
"страхуй",
"рубл",
"сабл",
"хлебат",
"греб",
"истреб",
"щебет",
"мебел",
];
// Заменяет легальные слова-исключения на плейсхолдер перед проверкой
fn apply_allowlist(text: &str) -> String {
let mut safe_text = text.to_string();
for &safe_word in SAFE_WORDS {
safe_text = safe_text.replace(safe_word, "[SAFE]");
}
safe_text
}
// Функция жесткой нормализации текста перед проверкой // Функция жесткой нормализации текста перед проверкой
fn normalize_for_filter(input: &str) -> String { fn normalize_for_filter(input: &str) -> String {
let mut normalized = String::new(); let mut normalized = String::new();
let mut prev_char: Option<char> = None; let mut prev_char: Option<char> = None;
// 1. Приводим к нижнему регистру
let lower_input = input.to_lowercase(); let lower_input = input.to_lowercase();
// 2. Идем по символам (с учетом юникода) for ch in lower_input.chars() {
for g in lower_input.graphemes(true) { // СОХРАНЯЕМ ПРОБЕЛЫ, чтобы не склеивать соседние слова ("нас укачало" != "насукачало")
let mut ch = g.chars().next().unwrap_or(' '); if ch.is_whitespace() {
normalized.push(' ');
prev_char = Some(' ');
continue;
}
// Пропускаем символы, которые не являются буквами или цифрами (запятые, точки, пробелы) // Вырезаем пунктуацию (чтобы ловить х.у.й), но пропускаем дальше только буквы/цифры
if !ch.is_alphanumeric() { if !ch.is_alphanumeric() {
continue; continue;
} }
// 3. Замена "leetspeak" и похожих английских букв на русские (базовая защита) // Замена "leetspeak" (англо-русская подмена и цифры)
ch = match ch { let mapped = match ch {
'a' | '@' => 'а', 'a' | '@' => 'а',
'b' => 'в', 'b' => 'в',
'c' => 'с', 'c' => 'с',
@@ -48,43 +75,45 @@ fn normalize_for_filter(input: &str) -> String {
'm' => 'м', 'm' => 'м',
't' => 'т', 't' => 'т',
'k' => 'к', 'k' => 'к',
'h' => 'н',
'4' => 'ч',
_ => ch, _ => ch,
}; };
// 4. Сжатие повторяющихся символов (хххуууууууй -> хуй) // Сжатие повторяющихся символов (хххуууууууй -> хуй)
// Не сжимаем пробелы, только буквы
if let Some(p) = prev_char { if let Some(p) = prev_char {
if p == ch { if p == mapped && mapped != ' ' {
continue; // Пропускаем дубликат continue;
} }
} }
normalized.push(ch); normalized.push(mapped);
prev_char = Some(ch); prev_char = Some(mapped);
} }
normalized normalized
} }
// Функция проверки на спам (КАПС или слишком длинные слова) // Функция проверки на спам (КАПС или слишком длинные бессмысленные слова)
fn is_spam(input: &str) -> bool { fn is_spam(input: &str) -> bool {
let alpha_chars: String = input.chars().filter(|c| c.is_alphabetic()).collect(); let alpha_chars: String = input.chars().filter(|c| c.is_alphabetic()).collect();
if alpha_chars.is_empty() { if alpha_chars.is_empty() {
return false; return false;
} }
// Считаем процент заглавных букв // Проверка на КАПС (> 70% заглавных при длине > 10)
let uppercase_count = alpha_chars.chars().filter(|c| c.is_uppercase()).count(); let uppercase_count = alpha_chars.chars().filter(|c| c.is_uppercase()).count();
let uppercase_ratio = uppercase_count as f64 / alpha_chars.len() as f64; let uppercase_ratio = uppercase_count as f64 / alpha_chars.len() as f64;
// Если больше 70% текста - КАПС (при длине текста больше 10 символов)
if input.len() > 10 && uppercase_ratio > 0.7 { if input.len() > 10 && uppercase_ratio > 0.7 {
return true; return true;
} }
// Проверка на слишком длинные "слова" без пробелов (защита от aaaaaaaaaaaaaa) // Проверка на зажатую клавиатуру (aaaaaaaaaaaa)
for word in input.split_whitespace() { for word in input.split_whitespace() {
if word.len() > 35 { // Игнорируем длинные ссылки (http/https)
// Нормальных слов длиннее 35 символов почти нет if word.len() > 35 && !word.starts_with("http") {
return true; return true;
} }
} }
@@ -102,7 +131,7 @@ async fn main() {
} }
async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse { async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse {
// --- 1. Проверка на спам (КАПС, длинные слова) --- // --- 1. Проверка на спам ---
if is_spam(&payload.message) || is_spam(&payload.nickname) { if is_spam(&payload.message) || is_spam(&payload.nickname) {
println!("🗑️ Blocked by SPAM filter: {}", payload.nickname); println!("🗑️ Blocked by SPAM filter: {}", payload.nickname);
return ( return (
@@ -120,14 +149,18 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
); );
} }
// --- 3. Русский хардкорный фильтр --- // --- 3. Русский хардкорный фильтр с защитой от ложных срабатываний ---
let normalized_msg = normalize_for_filter(&payload.message); let normalized_msg = normalize_for_filter(&payload.message);
let normalized_nick = normalize_for_filter(&payload.nickname); let normalized_nick = normalize_for_filter(&payload.nickname);
if BAD_WORDS_REGEX.is_match(&normalized_msg) || BAD_WORDS_REGEX.is_match(&normalized_nick) { // Сначала защищаем нормальные слова, затем проверяем регуляркой
let safe_msg = apply_allowlist(&normalized_msg);
let safe_nick = apply_allowlist(&normalized_nick);
if BAD_WORDS_REGEX.is_match(&safe_msg) || BAD_WORDS_REGEX.is_match(&safe_nick) {
println!( println!(
"🛡️ Blocked by Deep Regex: [Original: {}] -> [Normalized: {}]", "🛡️ Blocked by Deep Regex: [Original: {}] -> [Normalized: {}]",
payload.message, normalized_msg payload.message, safe_msg
); );
return ( return (
StatusCode::NOT_ACCEPTABLE, StatusCode::NOT_ACCEPTABLE,
+6
View File
@@ -37,6 +37,12 @@ export class MatrixEngine {
set_drawing(is_drawing) { set_drawing(is_drawing) {
wasm.matrixengine_set_drawing(this.__wbg_ptr, is_drawing); wasm.matrixengine_set_drawing(this.__wbg_ptr, is_drawing);
} }
/**
* @param {boolean} is_mobile
*/
set_mobile(is_mobile) {
wasm.matrixengine_set_mobile(this.__wbg_ptr, is_mobile);
}
/** /**
* @param {boolean} is_vpn_on * @param {boolean} is_vpn_on
* @param {boolean} is_dark_mode * @param {boolean} is_dark_mode
Binary file not shown.