optimize both devicesa

This commit is contained in:
2026-04-24 13:04:00 +07:00
parent 8bf72addb2
commit 942b5c56e2
6 changed files with 590 additions and 591 deletions
+130 -100
View File
@@ -3,16 +3,6 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
type MatrixEngine = {
tick: () => void;
update_mouse: (x: number, y: number) => void;
trigger_glitch: (x: number, y: number) => void;
set_drawing: (is_drawing: 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;
};
interface NetrunnerMatrixProps { interface NetrunnerMatrixProps {
isSecure?: boolean; isSecure?: boolean;
} }
@@ -22,12 +12,22 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<MatrixEngine | null>(null); const workerRef = useRef<Worker | null>(null);
const animationFrameId = useRef<number>(null); const [isLoaded, setIsLoaded] = useState(false);
const [wasmLoaded, setWasmLoaded] = useState(false); const hasTransferred = useRef(false);
const { resolvedTheme } = useTheme(); // --- БЕЗОПАСНОЕ ЧТЕНИЕ ТЕМЫ NEXT.JS ---
const isDarkMode = resolvedTheme === "dark"; const { resolvedTheme, theme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// Вычисляем актуальную тему
const isDarkMode = mounted
? resolvedTheme === "dark" || theme === "dark"
: true;
const checkIsMobile = () => { const checkIsMobile = () => {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
@@ -39,114 +39,147 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (typeof window === "undefined" || !("OffscreenCanvas" in window)) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
if (!canvas || !container) return; if (!canvas || !container || hasTransferred.current) return;
const loadWasm = async () => {
try { try {
const baseUrl = window.location.origin; const isMobile = checkIsMobile();
const jsUrl = new URL("/wasm-matrix/matrix_engine.js", baseUrl).href; // Используем 1.0 для четкости на мобилках, как ты и хотел
const wasmUrl = new URL("/wasm-matrix/matrix_engine_bg.wasm", baseUrl) const pixelScale = isMobile
.href; ? 1.0
: Math.min(window.devicePixelRatio || 1, 1.5);
const wasmModule = await import(/* webpackIgnore: true */ jsUrl); const width = container.clientWidth * pixelScale;
await wasmModule.default(wasmUrl); const height = container.clientHeight * pixelScale;
const fontSize = 16; canvas.width = width;
canvas.width = container.clientWidth; canvas.height = height;
canvas.height = container.clientHeight;
const engine = new wasmModule.MatrixEngine(canvas, fontSize); const worker = new Worker("/wasm-matrix/matrix_worker.js", {
engineRef.current = engine; type: "module",
engine.set_theme(isSecure, isDarkMode); });
engine.set_mobile(checkIsMobile()); workerRef.current = worker;
setWasmLoaded(true); const offscreen = canvas.transferControlToOffscreen();
hasTransferred.current = true;
const render = () => { worker.postMessage(
engine.tick(); {
if (animationFrameId.current !== null) { type: "INIT",
animationFrameId.current = requestAnimationFrame(render); payload: {
} canvas: offscreen,
}; width,
animationFrameId.current = requestAnimationFrame(render); height,
} catch (e) { isMobile,
console.error("Wasm Engine Load Failed:", e); isVpnOn: isSecure,
} isDarkMode, // Здесь передается начальная тема
}; fontSize: isMobile ? 10 : 16,
},
},
[offscreen],
);
loadWasm(); setIsLoaded(true);
const handleResize = () => { const handleResize = () => {
if (canvas && container && engineRef.current) { if (!container || !workerRef.current) return;
canvas.width = container.clientWidth; const isMob = checkIsMobile();
canvas.height = container.clientHeight; // Держим scale консистентным с INIT (1.0 для мобилок)
engineRef.current.resize(canvas.width, canvas.height); const scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5);
engineRef.current.set_mobile(checkIsMobile());
}
};
window.addEventListener("resize", handleResize);
workerRef.current.postMessage({
type: "RESIZE",
payload: {
width: container.clientWidth * scale,
height: container.clientHeight * scale,
isMobile: isMob,
fontSize: isMob ? 10 : 16,
},
});
};
window.addEventListener("resize", handleResize);
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
if (animationFrameId.current !== null) { worker.terminate();
cancelAnimationFrame(animationFrameId.current); };
animationFrameId.current = null; } catch (e) {
console.error("Matrix Worker Bridge Failed:", e);
} }
}; }, []); // Пустой массив, так как INIT делается один раз
}, []);
// --- ДОБАВЛЕНО: ОБНОВЛЕНИЕ ТЕМЫ И VPN ---
useEffect(() => { useEffect(() => {
if (engineRef.current) engineRef.current.set_theme(isSecure, isDarkMode); if (workerRef.current && mounted) {
}, [isSecure, isDarkMode]); workerRef.current.postMessage({
type: "THEME",
payload: {
isVpnOn: isSecure,
isDarkMode: isDarkMode,
},
});
}
}, [isSecure, isDarkMode, mounted]);
// ФИКС ОШИБКИ clientX (Твой код был верным, оставляем с микро-правкой)
useEffect(() => { useEffect(() => {
const triggerAction = (clientX: number, clientY: number) => { const handleEvents = (e: any) => {
if (!canvasRef.current || !engineRef.current) return; if (!workerRef.current || !canvasRef.current) return;
const isDown = e.type === "pointerdown" || e.type === "touchstart";
const isUp =
e.type === "pointerup" ||
e.type === "touchend" ||
e.type === "pointercancel";
const type = isDown ? "DRAW_START" : isUp ? "DRAW_END" : "MOUSE_MOVE";
let clientX: number | null = null;
let clientY: number | null = null;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else if (e.clientX !== undefined) {
clientX = e.clientX;
clientY = e.clientY;
}
if (clientX === null || clientY === null) {
if (isUp) workerRef.current.postMessage({ type });
return;
}
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = clientX - rect.left; // Используем фактическую ширину буфера канваса для правильных координат в Wasm
const y = clientY - rect.top; const scaleX = canvasRef.current.width / rect.width;
const scaleY = canvasRef.current.height / rect.height;
if (y < 0 || y > rect.height) return; const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
engineRef.current.set_drawing(true); workerRef.current.postMessage({ type, payload: { x, y } });
engineRef.current.trigger_glitch(x, y);
}; };
const moveAction = (clientX: number, clientY: number) => { const options = { passive: true };
if (!canvasRef.current || !engineRef.current) return; window.addEventListener("pointerdown", handleEvents);
const rect = canvasRef.current.getBoundingClientRect(); window.addEventListener("pointermove", handleEvents);
engineRef.current.update_mouse(clientX - rect.left, clientY - rect.top); window.addEventListener("pointerup", handleEvents);
}; window.addEventListener("pointercancel", handleEvents);
window.addEventListener("touchstart", handleEvents, options);
const handlePointerDown = (e: MouseEvent) => window.addEventListener("touchmove", handleEvents, options);
triggerAction(e.clientX, e.clientY); window.addEventListener("touchend", handleEvents, options);
const handlePointerMove = (e: MouseEvent) =>
moveAction(e.clientX, e.clientY);
const handlePointerUp = () => engineRef.current?.set_drawing(false);
const handleTouchStart = (e: TouchEvent) =>
triggerAction(e.touches[0].clientX, e.touches[0].clientY);
const handleTouchMove = (e: TouchEvent) =>
moveAction(e.touches[0].clientX, e.touches[0].clientY);
window.addEventListener("mousedown", handlePointerDown);
window.addEventListener("mousemove", handlePointerMove);
window.addEventListener("mouseup", handlePointerUp);
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handlePointerUp);
return () => { return () => {
window.removeEventListener("mousedown", handlePointerDown); window.removeEventListener("pointerdown", handleEvents);
window.removeEventListener("mousemove", handlePointerMove); window.removeEventListener("pointermove", handleEvents);
window.removeEventListener("mouseup", handlePointerUp); window.removeEventListener("pointerup", handleEvents);
window.removeEventListener("pointercancel", handleEvents);
window.removeEventListener("touchstart", handleTouchStart); window.removeEventListener("touchstart", handleEvents);
window.removeEventListener("touchmove", handleTouchMove); window.removeEventListener("touchmove", handleEvents);
window.removeEventListener("touchend", handlePointerUp); window.removeEventListener("touchend", handleEvents);
}; };
}, []); }, []);
@@ -157,11 +190,8 @@ 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 block transition-opacity duration-1000 ${isLoaded ? "opacity-100" : "opacity-0"}`}
style={{ imageRendering: "pixelated" }}
/> />
{/* Секретный соус: CRT Scanlines (Аппаратное ускорение, 0 CPU) */}
<div <div
className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25]" className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25]"
style={{ style={{
+103 -223
View File
@@ -1,7 +1,5 @@
use rand::Rng; use rand::Rng;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
struct Drop { struct Drop {
y: f32, y: f32,
@@ -15,22 +13,23 @@ struct GlitchBlock {
w: f32, w: f32,
h: f32, h: f32,
life: f32, life: f32,
color: String, color_type: f32,
} }
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) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let w = rng.gen_range(20.0..250.0); let w = rng.gen_range(20.0..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 {
0..=15 => bg_color.to_string(), // 0=bg, 1=theme_dense, 2=theme_invert, 3=white, 4=red
16..=50 => format!("rgba({}, {}, {}, 0.9)", r, g, b), let color_type = match choice {
51..=75 => format!("rgba({}, {}, {}, 0.7)", 255 - r, 255 - g, 255 - b), 0..=15 => 0.0,
76..=95 => "rgba(255, 255, 255, 0.95)".to_string(), 16..=50 => 1.0,
_ => "rgba(255, 20, 80, 0.95)".to_string(), 51..=75 => 2.0,
76..=95 => 3.0,
_ => 4.0,
}; };
Self { Self {
@@ -39,7 +38,7 @@ impl GlitchBlock {
w, w,
h, h,
life: rng.gen_range(5.0..25.0), life: rng.gen_range(5.0..25.0),
color, color_type,
} }
} }
} }
@@ -47,33 +46,10 @@ impl GlitchBlock {
struct GlassSymbol { struct GlassSymbol {
x: f32, x: f32,
y: f32, y: f32,
value: char, char_idx: usize,
life: f32, life: f32,
max_life: f32, max_life: f32,
velocity_y: f32, velocity_y: f32,
color: String,
}
impl GlassSymbol {
fn new(x: f32, y: f32, charset: &[char], is_vpn_on: bool, is_dark_mode: bool) -> Self {
let mut rng = rand::thread_rng();
let color = match (is_vpn_on, is_dark_mode) {
(true, true) => "rgb(139, 61, 255)",
(false, true) => "rgb(0, 229, 242)",
(true, false) => "rgb(90, 30, 200)",
(false, false) => "rgb(0, 140, 160)",
};
Self {
x: x + rng.gen_range(-15.0..15.0),
y: y + rng.gen_range(-15.0..15.0),
value: charset[rng.gen_range(0..charset.len())],
life: 1.0,
max_life: rng.gen_range(30.0..80.0),
velocity_y: rng.gen_range(0.1..0.5),
color: color.to_string(),
}
}
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -81,18 +57,21 @@ pub struct MatrixEngine {
width: u32, width: u32,
height: u32, height: u32,
font_size: u32, font_size: u32,
charset_len: usize,
fg_drops: Vec<Drop>, fg_drops: Vec<Drop>,
bg_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>,
ctx: CanvasRenderingContext2d, // БУФЕРЫ ДЛЯ ПЕРЕДАЧИ В JS
atlas_canvas: HtmlCanvasElement, fg_buffer: Vec<f32>,
bg_buffer: Vec<f32>,
glitch_buffer: Vec<f32>,
glass_buffer: Vec<f32>,
mouse_x: f32, mouse_x: f32,
mouse_y: f32, mouse_y: f32,
is_drawing: bool, is_drawing: bool,
is_vpn_on: bool,
is_dark_mode: bool,
is_mobile: bool, is_mobile: bool,
tick_count: u32, tick_count: u32,
emp_timer: f32, emp_timer: f32,
@@ -105,39 +84,24 @@ pub struct MatrixEngine {
#[wasm_bindgen] #[wasm_bindgen]
impl MatrixEngine { impl MatrixEngine {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(canvas: HtmlCanvasElement, font_size: u32) -> Result<MatrixEngine, JsValue> { pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine {
let ctx = canvas let charset_len = 84; // Длина нашего алфавита
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
let width = canvas.width();
let height = canvas.height();
let chars_str = "0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
let charset: Vec<char> = chars_str.chars().collect();
let document = web_sys::window().unwrap().document().unwrap();
let atlas_canvas = document
.create_element("canvas")?
.dyn_into::<HtmlCanvasElement>()?;
atlas_canvas.set_width((charset.len() as u32) * font_size);
atlas_canvas.set_height(font_size * 2);
let mut engine = MatrixEngine { let mut engine = MatrixEngine {
width, width,
height, height,
font_size, font_size,
charset_len,
fg_drops: Vec::new(), fg_drops: Vec::new(),
bg_drops: Vec::new(), bg_drops: Vec::new(),
glass_symbols: Vec::new(), glass_symbols: Vec::new(),
glitch_blocks: Vec::new(), glitch_blocks: Vec::new(),
charset, fg_buffer: Vec::with_capacity(2000),
ctx, bg_buffer: Vec::with_capacity(2000),
atlas_canvas, glitch_buffer: Vec::with_capacity(100),
glass_buffer: Vec::with_capacity(50),
mouse_x: -1000.0, mouse_x: -1000.0,
mouse_y: -1000.0, mouse_y: -1000.0,
is_drawing: false, is_drawing: false,
is_vpn_on: false,
is_dark_mode: true,
is_mobile: false, is_mobile: false,
tick_count: 0, tick_count: 0,
emp_timer: 0.0, emp_timer: 0.0,
@@ -146,18 +110,14 @@ impl MatrixEngine {
tear_y: 0.0, tear_y: 0.0,
tear_timer: 0.0, tear_timer: 0.0,
}; };
engine.init_drops(); engine.init_drops();
engine.build_atlas(); engine
Ok(engine)
} }
fn init_drops(&mut self) { fn init_drops(&mut self) {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
self.fg_drops.clear(); self.fg_drops.clear();
self.bg_drops.clear(); self.bg_drops.clear();
// Передний план (крупные)
let fg_columns = self.width / self.font_size; let fg_columns = self.width / self.font_size;
for _ in 0..fg_columns { for _ in 0..fg_columns {
self.fg_drops.push(Drop { self.fg_drops.push(Drop {
@@ -166,9 +126,9 @@ impl MatrixEngine {
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
let density = if self.is_mobile { 1.2 } else { 0.6 };
let bg_columns = (self.width as f32 / (self.font_size as f32 * density)) as u32;
// Задний план (мелкие, больше колонок)
let bg_columns = (self.width as f32 / (self.font_size as f32 * 0.6)) as u32;
for _ in 0..bg_columns { for _ in 0..bg_columns {
self.bg_drops.push(Drop { self.bg_drops.push(Drop {
y: rng.gen_range(-20.0..self.height as f32), y: rng.gen_range(-20.0..self.height as f32),
@@ -178,41 +138,6 @@ impl MatrixEngine {
} }
} }
fn build_atlas(&self) {
let atlas_ctx = self
.atlas_canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
let (r, g, b) = self.get_theme_colors();
atlas_ctx.clear_rect(
0.0,
0.0,
self.atlas_canvas.width() as f64,
self.atlas_canvas.height() as f64,
);
atlas_ctx.set_font(&format!("bold {}px monospace", self.font_size));
atlas_ctx.set_text_baseline("top");
atlas_ctx.set_text_align("center");
let half_fs = self.font_size as f64 / 2.0;
atlas_ctx.set_fill_style_str(&format!("rgb({}, {}, {})", r, g, b));
for (i, c) in self.charset.iter().enumerate() {
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.set_fill_style_str("rgb(255, 255, 255)");
for (i, c) in self.charset.iter().enumerate() {
let x = (i as u32 * self.font_size) as f64 + half_fs;
let y = self.font_size as f64;
atlas_ctx.fill_text(&c.to_string(), x, y).unwrap_or(());
}
}
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;
@@ -226,46 +151,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) {
if self.is_vpn_on != is_vpn_on || self.is_dark_mode != is_dark_mode {
self.is_vpn_on = is_vpn_on;
self.is_dark_mode = is_dark_mode;
self.build_atlas();
}
}
pub fn set_mobile(&mut self, is_mobile: bool) { pub fn set_mobile(&mut self, is_mobile: bool) {
self.is_mobile = is_mobile; 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 { // Убираем if self.is_mobile { return; }
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;
let (r, g, b) = self.get_theme_colors();
let bg = self.get_bg_color_rgba();
for _ in 0..25 {
self.glitch_blocks
.push(GlitchBlock::new(x, y, r, g, b, &bg));
}
}
fn get_theme_colors(&self) -> (u8, u8, u8) { // На ПК генерим 25 блоков, на телефоне всего 8
match (self.is_vpn_on, self.is_dark_mode) { let blocks_count = if self.is_mobile { 8 } else { 25 };
(true, true) => (139, 61, 255),
(false, true) => (0, 229, 242),
(true, false) => (109, 40, 217),
(false, false) => (2, 132, 199),
}
}
fn get_bg_color_rgba(&self) -> String { for _ in 0..blocks_count {
if self.is_dark_mode { self.glitch_blocks.push(GlitchBlock::new(x, y));
"rgba(10, 10, 12, 1.0)".to_string()
} else {
"rgba(250, 250, 252, 1.0)".to_string()
} }
} }
@@ -273,25 +173,16 @@ 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();
self.ctx.set_global_alpha(1.0); self.fg_buffer.clear();
let bg_fill = if self.is_dark_mode { self.bg_buffer.clear();
"rgba(10, 10, 12, 0.2)" self.glitch_buffer.clear();
} else { self.glass_buffer.clear();
"rgba(250, 250, 252, 0.2)"
};
self.ctx.set_fill_style_str(bg_fill);
self.ctx
.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);
let (r, g, b) = self.get_theme_colors();
let bg_solid = self.get_bg_color_rgba();
if !self.is_mobile { if !self.is_mobile {
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);
self.glitch_blocks self.glitch_blocks.push(GlitchBlock::new(rx, ry));
.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);
@@ -306,47 +197,27 @@ impl MatrixEngine {
self.tear_timer -= 1.0; self.tear_timer -= 1.0;
} }
// --- 1. РИСУЕМ ЗАДНИЙ ПЛАН (Глубина) --- let bg_f = self.font_size as f32 * 0.6;
// Рисуем только на ПК, чтобы поберечь телефоны
if !self.is_mobile {
let bg_f = self.font_size as f64 * 0.6; // Уменьшенный размер
for i in 0..self.bg_drops.len() { for i in 0..self.bg_drops.len() {
let drop = &mut self.bg_drops[i]; let drop = &mut self.bg_drops[i];
let x = (i as f32) * (self.font_size as f32 * 0.6); let x = (i as f32) * bg_f;
let cell_y = drop.y.floor(); let cell_y = drop.y.floor();
let y = cell_y * (self.font_size as f32 * 0.6); let y = cell_y * bg_f;
let hash = (i as u32) let hash = (i as u32)
.wrapping_mul(12345) .wrapping_mul(12345)
.wrapping_add((cell_y as u32).wrapping_mul(67890)); .wrapping_add((cell_y as u32).wrapping_mul(67890));
let char_idx = (hash as usize) % self.charset.len(); let char_idx = (hash as usize) % self.charset_len;
// Задний слой очень тусклый self.bg_buffer
self.ctx.set_global_alpha(rng.gen_range(0.05..0.15)); .extend_from_slice(&[x, y, char_idx as f32, 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; drop.y += drop.speed;
if y > self.height as f32 && rng.gen_bool(0.005) { if y > self.height as f32 && rng.gen_bool(0.005) {
drop.y = rng.gen_range(-10.0..0.0); drop.y = rng.gen_range(-10.0..0.0);
} }
} }
}
// --- 2. РИСУЕМ ПЕРЕДНИЙ ПЛАН ---
for i in 0..self.fg_drops.len() { for i in 0..self.fg_drops.len() {
let drop = &mut self.fg_drops[i]; 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);
@@ -357,17 +228,16 @@ impl MatrixEngine {
let mut is_void = false; let mut is_void = false;
let mut is_emp_ring = false; let mut is_emp_ring = false;
if self.tear_timer > 0.0 && (y - self.tear_y).abs() < 60.0 { if !self.is_mobile && self.tear_timer > 0.0 && (y - self.tear_y).abs() < 60.0 {
x += rng.gen_range(-20.0..20.0); x += rng.gen_range(-20.0..20.0);
is_scrambled = true; is_scrambled = true;
} }
if self.emp_timer > 0.0 { if !self.is_mobile && 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 = (dx * dx + dy * dy).sqrt();
let emp_radius = (60.0 - self.emp_timer) * 15.0; let emp_radius = (60.0 - self.emp_timer) * 15.0;
if dist < emp_radius { if dist < emp_radius {
if dist > emp_radius - 20.0 { if dist > emp_radius - 20.0 {
is_emp_ring = true; is_emp_ring = true;
@@ -392,7 +262,7 @@ impl MatrixEngine {
.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_idx = (hash as usize) % self.charset.len(); let mut char_idx = (hash as usize) % self.charset_len;
let mut is_head = rng.gen_bool(0.15); let mut is_head = rng.gen_bool(0.15);
let mut target_alpha = if is_head { 1.0 } else { 0.6 }; let mut target_alpha = if is_head { 1.0 } else { 0.6 };
@@ -400,9 +270,9 @@ impl MatrixEngine {
if is_emp_ring { if is_emp_ring {
is_head = true; is_head = true;
target_alpha = 1.0; target_alpha = 1.0;
char_idx = rng.gen_range(0..self.charset.len()); 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_idx = 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); target_alpha = rng.gen_range(0.6..1.0);
is_head = rng.gen_bool(0.2); is_head = rng.gen_bool(0.2);
if drop.glitch_timer > 0.0 { if drop.glitch_timer > 0.0 {
@@ -410,24 +280,14 @@ impl MatrixEngine {
} }
} }
self.ctx.set_global_alpha(target_alpha as f64); if self.is_mobile {
let src_x = (char_idx as u32 * self.font_size) as f64; target_alpha = 0.8;
let src_y = if is_head { self.font_size as f64 } else { 0.0 }; } // Упрощаем альфу на мобилах
let f = self.font_size as f64;
self.ctx // СОХРАНЯЕМ В БУФЕР: [x, y, char_idx, atlas_row, alpha]
.draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh( let atlas_row = if is_head { 1.0 } else { 0.0 };
&self.atlas_canvas, self.fg_buffer
src_x, .extend_from_slice(&[x, y, char_idx as f32, atlas_row, target_alpha]);
src_y,
f,
f,
x as f64,
y as f64,
f,
f,
)
.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) {
@@ -436,42 +296,62 @@ impl MatrixEngine {
} }
} }
self.ctx.set_global_alpha(1.0);
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.glitch_buffer.extend_from_slice(&[
self.ctx.fill_rect( block.x,
block.x as f64, block.y,
block.y as f64, block.w,
block.w as f64, block.h,
block.h as f64, block.color_type,
); ]);
block.life -= 1.0; block.life -= 1.0;
} }
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_mobile && self.is_drawing && rng.gen_bool(0.3) {
self.glass_symbols.push(GlassSymbol::new( self.glass_symbols.push(GlassSymbol {
self.mouse_x, x: self.mouse_x + rng.gen_range(-15.0..15.0),
self.mouse_y, y: self.mouse_y + rng.gen_range(-15.0..15.0),
&self.charset, char_idx: rng.gen_range(0..self.charset_len),
self.is_vpn_on, life: 1.0,
self.is_dark_mode, max_life: rng.gen_range(30.0..80.0),
)); velocity_y: rng.gen_range(0.1..0.5),
});
} }
self.ctx
.set_font(&format!("bold {}px monospace", self.font_size));
for sym in self.glass_symbols.iter_mut() { for sym in self.glass_symbols.iter_mut() {
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.glass_buffer
self.ctx.set_fill_style_str(&sym.color); .extend_from_slice(&[sym.x, sym.y, sym.char_idx as f32, alpha]);
self.ctx
.fill_text(&sym.value.to_string(), sym.x as f64, sym.y as f64)
.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); }
// ПОКАЗЫВАЕМ ПАМЯТЬ ДЛЯ JAVASCRIPT
pub fn fg_ptr(&self) -> *const f32 {
self.fg_buffer.as_ptr()
}
pub fn fg_len(&self) -> usize {
self.fg_buffer.len()
}
pub fn bg_ptr(&self) -> *const f32 {
self.bg_buffer.as_ptr()
}
pub fn bg_len(&self) -> usize {
self.bg_buffer.len()
}
pub fn glitch_ptr(&self) -> *const f32 {
self.glitch_buffer.as_ptr()
}
pub fn glitch_len(&self) -> usize {
self.glitch_buffer.len()
}
pub fn glass_ptr(&self) -> *const f32 {
self.glass_buffer.as_ptr()
}
pub fn glass_len(&self) -> usize {
self.glass_buffer.len()
} }
} }
+49 -10
View File
@@ -11,28 +11,68 @@ struct CommentPayload {
message: String, message: String,
} }
// 1. Расширенный список КОРНЕЙ.
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| { static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(хуй|хуя|хуе|пизд|ебат|ебан|ебл|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш|спайс|cpam|scam|crypto|binance)").unwrap() Regex::new(r"(?i)(хуй|хуя|хуе|хул|пизд|ебат|ебан|ебл|ебуч|бляд|блят|шлюх|сука|сучк|жоп|пидор|пидар|педик|голубо|гондон|гандон|залуп|мудак|мудил|мраз|уеб|чмо|долбое|даун|чурк|нигер|нигга|хач|хохол|кацап|жид|порно|наркот|убить|насилие|соли|закладк|мефедрон|гашиш|спайс|heroin|meth|murder|gore|cpam|scam|crypto|binance|bitco)").unwrap()
}); });
// 2. БЕЛЫЙ СПИСОК (Исключения). Защищает нормальные слова, содержащие плохие корни.
const SAFE_WORDS: &[&str] = &[ const SAFE_WORDS: &[&str] = &[
// Защита корня "ебл/ебат" (самые частые ложные срабатывания)
"колебат", "колебат",
"оскорблят", "оскорблят",
"употреблят", "употреблят",
"расслаблят", "расслаблят",
"потреблят", "потреблят",
"страхуй", "истреблят",
"рубл",
"сабл",
"хлебат", "хлебат",
"греб",
"истреб",
"щебет", "щебет",
"мебел", "мебел",
"стебл",
"ослабл",
"гребл",
"хлебл",
"дебел",
"колебл",
// Защита корня "хуй"
"страхуй",
"подстрахуй",
// Защита корня "бля"
"рубл",
"сабл",
"челяб",
"влюбл",
"ограбл",
"дубл",
"гибл",
"корабл",
// Защита корня "жоп"
"джоп",
"джобс",
"прожора",
// Защита корня "пидар/пидор"
"скипидар",
"купидор",
"коридор",
// Защита корня "чмо"
"чмок",
"ячмен",
"черемух",
// Защита корня "мраз"
"мразер",
"замороз",
"мороз",
"мрамор",
// Защита корня "сука"
"рисука",
"присука",
"бамбука",
// Защита корня "жид"
"ожид",
"жидкос",
"жидк",
// Защита корня "даун"
"даунгрейд",
"адаун",
]; ];
// Заменяет легальные слова-исключения на плейсхолдер перед проверкой // Заменяет легальные слова-исключения на плейсхолдер перед проверкой
fn apply_allowlist(text: &str) -> String { fn apply_allowlist(text: &str) -> String {
let mut safe_text = text.to_string(); let mut safe_text = text.to_string();
@@ -41,7 +81,6 @@ fn apply_allowlist(text: &str) -> String {
} }
safe_text 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();
+61 -236
View File
@@ -12,15 +12,69 @@ export class MatrixEngine {
wasm.__wbg_matrixengine_free(ptr, 0); wasm.__wbg_matrixengine_free(ptr, 0);
} }
/** /**
* @param {HTMLCanvasElement} canvas * @returns {number}
*/
bg_len() {
const ret = wasm.matrixengine_bg_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
bg_ptr() {
const ret = wasm.matrixengine_bg_ptr(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
fg_len() {
const ret = wasm.matrixengine_fg_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
fg_ptr() {
const ret = wasm.matrixengine_fg_ptr(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
glass_len() {
const ret = wasm.matrixengine_glass_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
glass_ptr() {
const ret = wasm.matrixengine_glass_ptr(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
glitch_len() {
const ret = wasm.matrixengine_glitch_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @returns {number}
*/
glitch_ptr() {
const ret = wasm.matrixengine_glitch_ptr(this.__wbg_ptr);
return ret >>> 0;
}
/**
* @param {number} width
* @param {number} height
* @param {number} font_size * @param {number} font_size
*/ */
constructor(canvas, font_size) { constructor(width, height, font_size) {
const ret = wasm.matrixengine_new(canvas, font_size); const ret = wasm.matrixengine_new(width, height, font_size);
if (ret[2]) { this.__wbg_ptr = ret >>> 0;
throw takeFromExternrefTable0(ret[1]);
}
this.__wbg_ptr = ret[0] >>> 0;
MatrixEngineFinalization.register(this, this.__wbg_ptr, this); MatrixEngineFinalization.register(this, this.__wbg_ptr, this);
return this; return this;
} }
@@ -43,13 +97,6 @@ export class MatrixEngine {
set_mobile(is_mobile) { set_mobile(is_mobile) {
wasm.matrixengine_set_mobile(this.__wbg_ptr, is_mobile); wasm.matrixengine_set_mobile(this.__wbg_ptr, is_mobile);
} }
/**
* @param {boolean} is_vpn_on
* @param {boolean} is_dark_mode
*/
set_theme(is_vpn_on, is_dark_mode) {
wasm.matrixengine_set_theme(this.__wbg_ptr, is_vpn_on, is_dark_mode);
}
tick() { tick() {
wasm.matrixengine_tick(this.__wbg_ptr); wasm.matrixengine_tick(this.__wbg_ptr);
} }
@@ -72,13 +119,6 @@ if (Symbol.dispose) MatrixEngine.prototype[Symbol.dispose] = MatrixEngine.protot
function __wbg_get_imports() { function __wbg_get_imports() {
const import0 = { const import0 = {
__proto__: null, __proto__: null,
__wbg___wbindgen_debug_string_ab4b34d23d6778bd: function(arg0, arg1) {
const ret = debugString(arg1);
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
},
__wbg___wbindgen_is_function_3baa9db1a987f47d: function(arg0) { __wbg___wbindgen_is_function_3baa9db1a987f47d: function(arg0) {
const ret = typeof(arg0) === 'function'; const ret = typeof(arg0) === 'function';
return ret; return ret;
@@ -103,71 +143,13 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1, arg2); const ret = arg0.call(arg1, arg2);
return ret; return ret;
}, arguments); }, }, arguments); },
__wbg_clearRect_5fb1d6b44e6b6738: function(arg0, arg1, arg2, arg3, arg4) {
arg0.clearRect(arg1, arg2, arg3, arg4);
},
__wbg_createElement_bbd4c90086fe6f7b: function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.createElement(getStringFromWasm0(arg1, arg2));
return ret;
}, arguments); },
__wbg_crypto_38df2bab126b63dc: function(arg0) { __wbg_crypto_38df2bab126b63dc: function(arg0) {
const ret = arg0.crypto; const ret = arg0.crypto;
return ret; return ret;
}, },
__wbg_document_7a41071f2f439323: function(arg0) {
const ret = arg0.document;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_drawImage_2b01d8026be6b7ab: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
arg0.drawImage(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
}, arguments); },
__wbg_fillRect_992c5a4646ea7a7f: function(arg0, arg1, arg2, arg3, arg4) {
arg0.fillRect(arg1, arg2, arg3, arg4);
},
__wbg_fillText_dabb33ea287042e2: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
arg0.fillText(getStringFromWasm0(arg1, arg2), arg3, arg4);
}, arguments); },
__wbg_getContext_fc146f8ec021d074: function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.getContext(getStringFromWasm0(arg1, arg2));
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
}, arguments); },
__wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) {
arg0.getRandomValues(arg1); arg0.getRandomValues(arg1);
}, arguments); }, }, arguments); },
__wbg_height_528848d067cc2221: function(arg0) {
const ret = arg0.height;
return ret;
},
__wbg_instanceof_CanvasRenderingContext2d_24a3fe06e62b98d7: function(arg0) {
let result;
try {
result = arg0 instanceof CanvasRenderingContext2D;
} catch (_) {
result = false;
}
const ret = result;
return ret;
},
__wbg_instanceof_HtmlCanvasElement_ea4dfc3bb77c734b: function(arg0) {
let result;
try {
result = arg0 instanceof HTMLCanvasElement;
} catch (_) {
result = false;
}
const ret = result;
return ret;
},
__wbg_instanceof_Window_cc64c86c8ef9e02b: function(arg0) {
let result;
try {
result = arg0 instanceof Window;
} catch (_) {
result = false;
}
const ret = result;
return ret;
},
__wbg_length_9f1775224cf1d815: function(arg0) { __wbg_length_9f1775224cf1d815: function(arg0) {
const ret = arg0.length; const ret = arg0.length;
return ret; return ret;
@@ -198,27 +180,6 @@ function __wbg_get_imports() {
const ret = module.require; const ret = module.require;
return ret; return ret;
}, arguments); }, }, arguments); },
__wbg_set_fillStyle_e51447e54357dc46: function(arg0, arg1, arg2) {
arg0.fillStyle = getStringFromWasm0(arg1, arg2);
},
__wbg_set_font_295aa505e45244aa: function(arg0, arg1, arg2) {
arg0.font = getStringFromWasm0(arg1, arg2);
},
__wbg_set_globalAlpha_1660b0603161d11b: function(arg0, arg1) {
arg0.globalAlpha = arg1;
},
__wbg_set_height_be9b2b920bd68401: function(arg0, arg1) {
arg0.height = arg1 >>> 0;
},
__wbg_set_textAlign_e1a83482b00339b8: function(arg0, arg1, arg2) {
arg0.textAlign = getStringFromWasm0(arg1, arg2);
},
__wbg_set_textBaseline_8662e97190d0164a: function(arg0, arg1, arg2) {
arg0.textBaseline = getStringFromWasm0(arg1, arg2);
},
__wbg_set_width_5cda41d4d06a14dd: function(arg0, arg1) {
arg0.width = arg1 >>> 0;
},
__wbg_static_accessor_GLOBAL_8cfadc87a297ca02: function() { __wbg_static_accessor_GLOBAL_8cfadc87a297ca02: function() {
const ret = typeof global === 'undefined' ? null : global; const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -243,10 +204,6 @@ function __wbg_get_imports() {
const ret = arg0.versions; const ret = arg0.versions;
return ret; return ret;
}, },
__wbg_width_5adcb07d04d08bdf: function(arg0) {
const ret = arg0.width;
return ret;
},
__wbindgen_cast_0000000000000001: function(arg0, arg1) { __wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`.
const ret = getArrayU8FromWasm0(arg0, arg1); const ret = getArrayU8FromWasm0(arg0, arg1);
@@ -283,84 +240,11 @@ function addToExternrefTable0(obj) {
return idx; return idx;
} }
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches && builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
function getArrayU8FromWasm0(ptr, len) { function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0; ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
} }
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function getStringFromWasm0(ptr, len) { function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0; ptr = ptr >>> 0;
return decodeText(ptr, len); return decodeText(ptr, len);
@@ -387,49 +271,6 @@ function isLikeNone(x) {
return x === undefined || x === null; return x === undefined || x === null;
} }
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeFromExternrefTable0(idx) {
const value = wasm.__wbindgen_externrefs.get(idx);
wasm.__externref_table_dealloc(idx);
return value;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode(); cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072; const MAX_SAFARI_DECODE_BYTES = 2146435072;
@@ -444,26 +285,10 @@ function decodeText(ptr, len) {
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
} }
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
};
}
let WASM_VECTOR_LEN = 0;
let wasmModule, wasm; let wasmModule, wasm;
function __wbg_finalize_init(instance, module) { function __wbg_finalize_init(instance, module) {
wasm = instance.exports; wasm = instance.exports;
wasmModule = module; wasmModule = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null; cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start(); wasm.__wbindgen_start();
return wasm; return wasm;
Binary file not shown.
+225
View File
@@ -0,0 +1,225 @@
import init, { MatrixEngine } from "./matrix_engine.js";
let engine;
let wasm;
let ctx;
let atlasCanvas, atlasCtx;
let width, height;
let fontSize = 16;
let isMobile = false,
isVpnOn = false,
isDarkMode = true;
const chars =
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
function buildAtlas() {
// Создаем или пересоздаем атлас под текущий fontSize
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 2);
atlasCtx = atlasCanvas.getContext("2d", { alpha: false });
let r, g, b;
if (isVpnOn && isDarkMode) {
r = 139;
g = 61;
b = 255;
} else if (!isVpnOn && isDarkMode) {
r = 0;
g = 229;
b = 242;
} else if (isVpnOn && !isDarkMode) {
r = 109;
g = 40;
b = 217;
} else {
r = 2;
g = 132;
b = 199;
}
atlasCtx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
atlasCtx.fillRect(0, 0, atlasCanvas.width, atlasCanvas.height);
atlasCtx.font = `bold ${fontSize}px monospace`;
atlasCtx.textBaseline = "top";
atlasCtx.textAlign = "center";
const halfFs = fontSize / 2;
atlasCtx.fillStyle = `rgb(${r}, ${g}, ${b})`;
for (let i = 0; i < chars.length; i++)
atlasCtx.fillText(chars[i], i * fontSize + halfFs, 0);
atlasCtx.fillStyle = "rgb(255, 255, 255)";
for (let i = 0; i < chars.length; i++)
atlasCtx.fillText(chars[i], i * fontSize + halfFs, fontSize);
}
function getGlitchColor(type) {
let r, g, b;
if (isVpnOn && isDarkMode) {
r = 139;
g = 61;
b = 255;
} else if (!isVpnOn && isDarkMode) {
r = 0;
g = 229;
b = 242;
} else if (isVpnOn && !isDarkMode) {
r = 109;
g = 40;
b = 217;
} else {
r = 2;
g = 132;
b = 199;
}
if (type === 0) return isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
if (type === 1) return `rgba(${r},${g},${b},0.9)`;
if (type === 2) return `rgba(${255 - r},${255 - g},${255 - b},0.7)`;
if (type === 3) return "rgba(255,255,255,0.95)";
return "rgba(255,20,80,0.95)";
}
self.onmessage = async (e) => {
const { type, payload } = e.data;
if (type === "INIT") {
ctx = payload.canvas.getContext("2d", { alpha: false });
width = payload.width;
height = payload.height;
isMobile = payload.isMobile;
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
fontSize = payload.fontSize || 16;
wasm = await init("./matrix_engine_bg.wasm");
engine = new MatrixEngine(width, height, fontSize);
engine.set_mobile(isMobile);
buildAtlas();
requestAnimationFrame(render);
}
// ДОБАВЛЯЕМ ПРОВЕРКУ: if (!engine) return; во всех остальных случаях
else if (type === "RESIZE") {
if (!engine) return;
width = payload.width;
height = payload.height;
fontSize = payload.fontSize || 16;
ctx.canvas.width = width;
ctx.canvas.height = height;
engine.set_mobile(payload.isMobile);
engine.resize(width, height);
buildAtlas();
} else if (type === "THEME") {
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
buildAtlas();
if (ctx) {
ctx.globalAlpha = 1.0;
ctx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
ctx.fillRect(0, 0, width, height);
}
} else if (type === "MOUSE_MOVE") {
if (engine) engine.update_mouse(payload.x, payload.y);
} else if (type === "DRAW_START") {
if (engine) {
engine.set_drawing(true);
engine.trigger_glitch(payload.x, payload.y);
}
} else if (type === "DRAW_END") {
if (engine) engine.set_drawing(false);
}
};
function render() {
engine.tick();
// 1. Фон
ctx.globalAlpha = 1.0;
ctx.fillStyle = isDarkMode
? "rgba(10, 10, 12, 0.2)"
: "rgba(250, 250, 252, 0.2)";
ctx.fillRect(0, 0, width, height);
// 2. Читаем память Wasm и рисуем Задний план
const bgLen = engine.bg_len();
if (bgLen > 0) {
const bgData = new Float32Array(wasm.memory.buffer, engine.bg_ptr(), bgLen);
const bgF = fontSize * 0.6;
for (let i = 0; i < bgLen; i += 4) {
ctx.globalAlpha = bgData[i + 3];
ctx.drawImage(
atlasCanvas,
bgData[i + 2] * fontSize,
0,
fontSize,
fontSize,
bgData[i],
bgData[i + 1],
bgF,
bgF,
);
}
}
// 3. Читаем память Wasm и рисуем Передний план
const fgLen = engine.fg_len();
const fgData = new Float32Array(wasm.memory.buffer, engine.fg_ptr(), fgLen);
for (let i = 0; i < fgLen; i += 5) {
ctx.globalAlpha = fgData[i + 4];
ctx.drawImage(
atlasCanvas,
fgData[i + 2] * fontSize,
fgData[i + 3] * fontSize,
fontSize,
fontSize,
fgData[i],
fgData[i + 1],
fontSize,
fontSize,
);
}
// 4. Глитчи
const glLen = engine.glitch_len();
if (glLen > 0) {
ctx.globalAlpha = 1.0;
const glData = new Float32Array(
wasm.memory.buffer,
engine.glitch_ptr(),
glLen,
);
for (let i = 0; i < glLen; i += 5) {
ctx.fillStyle = getGlitchColor(glData[i + 4]);
ctx.fillRect(glData[i], glData[i + 1], glData[i + 2], glData[i + 3]);
}
}
// 5. След мыши
const gsLen = engine.glass_len();
if (gsLen > 0) {
const gsData = new Float32Array(
wasm.memory.buffer,
engine.glass_ptr(),
gsLen,
);
for (let i = 0; i < gsLen; i += 4) {
ctx.globalAlpha = gsData[i + 3];
ctx.drawImage(
atlasCanvas,
gsData[i + 2] * fontSize,
0,
fontSize,
fontSize,
gsData[i],
gsData[i + 1],
fontSize,
fontSize,
);
}
}
requestAnimationFrame(render);
}