optimizations and fixes

This commit is contained in:
2026-04-24 13:49:04 +07:00
parent 615a3d17b8
commit 811bfb99b7
9 changed files with 251 additions and 337 deletions
+6 -8
View File
@@ -30,17 +30,15 @@ jobs:
script: | script: |
cd /root/netrunner-frontend cd /root/netrunner-frontend
# 1. Штатно тушим текущий проект # Принудительно включаем BuildKit для использования кэшей
docker compose down || true export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# 2. ЖЕСТКАЯ ЗАЧИСТКА: Убиваем контейнеры по именам, docker compose down || true
# даже если они зависли от старых деплоев или других папок.
# || true гарантирует, что пайплайн не упадет, если контейнеров уже нет.
docker rm -f netrunner-landing netrunner-cms || true docker rm -f netrunner-landing netrunner-cms || true
# 3. Поднимаем всё заново с пересборкой # Поднимаем контейнеры
docker compose up -d --build docker compose up -d --build
# 4. Чистим мусор: удаляем dangling образы (без тегов), # Чистим старые образы, но НЕ ТРОГАЕМ build cache
# чтобы сервер не задохнулся от нехватки места на диске.
docker image prune -f docker image prune -f
+11 -14
View File
@@ -1,26 +1,28 @@
# syntax=docker/dockerfile:1
# --- Stage 0: Сборка Rust/Wasm --- # --- Stage 0: Сборка Rust/Wasm ---
FROM rust:1.80-slim AS wasm-builder FROM rust:1.80-slim AS wasm-builder
# Устанавливаем зависимости для сборки (wasm-pack нужен openssl)
RUN apt-get update && apt-get install -y pkg-config libssl-dev curl && \ RUN apt-get update && apt-get install -y pkg-config libssl-dev curl && \
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
WORKDIR /build WORKDIR /build
# Копируем папку с движком
COPY ./matrix-engine ./matrix-engine COPY ./matrix-engine ./matrix-engine
# Собираем Wasm
WORKDIR /build/matrix-engine WORKDIR /build/matrix-engine
# RELEASE сборка для максимальной скорости # КЭШИРУЕМ: Реестр Cargo и папку target. При повторном деплое компиляция будет мгновенной.
RUN wasm-pack build --target web --release RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/build/matrix-engine/target \
wasm-pack build --target web --release
# --- Stage 1: Установка зависимостей Node.js --- # --- Stage 1: Установка зависимостей Node.js ---
FROM node:20-alpine AS deps FROM node:20-alpine AS deps
WORKDIR /app WORKDIR /app
RUN npm install -g pnpm RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml* ./ COPY package.json pnpm-lock.yaml* ./
# Устанавливаем только нужное # КЭШИРУЕМ: Хранилище pnpm
RUN pnpm install --frozen-lockfile RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# --- Stage 2: Сборка Next.js --- # --- Stage 2: Сборка Next.js ---
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
@@ -29,13 +31,11 @@ RUN npm install -g pnpm
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# ВАЖНО: Забираем свежесобранный Wasm и JS-обертку из Stage 0 # Копируем Wasm ДО билда Next.js
# Копируем их прямо в public, чтобы Next.js подхватил их при билде
COPY --from=wasm-builder /build/matrix-engine/pkg/matrix_engine_bg.wasm ./public/wasm-matrix/ COPY --from=wasm-builder /build/matrix-engine/pkg/matrix_engine_bg.wasm ./public/wasm-matrix/
COPY --from=wasm-builder /build/matrix-engine/pkg/matrix_engine.js ./public/wasm-matrix/ COPY --from=wasm-builder /build/matrix-engine/pkg/matrix_engine.js ./public/wasm-matrix/
# Билдим проект (Next.js теперь видит правильные Wasm файлы) ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED 1
RUN pnpm run build RUN pnpm run build
# --- Stage 3: Продакшен сервер --- # --- Stage 3: Продакшен сервер ---
@@ -43,17 +43,14 @@ FROM node:20-alpine AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Создаем пользователя, чтобы не запускать от root (безопасность)
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
USER nextjs USER nextjs
# Копируем необходимое
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
EXPOSE 3000 EXPOSE 3000
# Используем pnpm, раз ты его выбрал
CMD ["npm", "start"] CMD ["npm", "start"]
+67 -57
View File
@@ -13,69 +13,77 @@ 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 workerRef = useRef<Worker | null>(null); const workerRef = useRef<Worker | null>(null);
const dimensionsRef = useRef({ width: 0, height: 0 });
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
// Используем Ref для отслеживания того, был ли уже передан контроль.
// Это критично, так как React в StrictMode или при смене языка может дергать эффект дважды.
const hasTransferred = useRef(false); const hasTransferred = useRef(false);
// --- БЕЗОПАСНОЕ ЧТЕНИЕ ТЕМЫ NEXT.JS ---
const { resolvedTheme, theme } = useTheme(); const { resolvedTheme, theme } = useTheme();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
return () => setMounted(false);
}, []); }, []);
// Вычисляем актуальную тему
const isDarkMode = mounted const isDarkMode = mounted
? resolvedTheme === "dark" || theme === "dark" ? resolvedTheme === "dark" || theme === "dark"
: true; : true;
const checkIsMobile = () => { const checkIsMobile = () => {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
return ( return window.innerWidth <= 768 || "ontouchstart" in window;
window.innerWidth <= 768 ||
"ontouchstart" in window ||
navigator.maxTouchPoints > 0
);
}; };
useEffect(() => { useEffect(() => {
if (typeof window === "undefined" || !("OffscreenCanvas" in window)) return; if (
!mounted ||
typeof window === "undefined" ||
!("OffscreenCanvas" in window)
)
return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
// ГЛАВНЫЙ ФИКС: Если контроль уже передан, ничего не делаем с этим canvas
if (!canvas || !container || hasTransferred.current) return; if (!canvas || !container || hasTransferred.current) return;
const isMobile = checkIsMobile();
const pixelScale = isMobile
? 1.0
: Math.min(window.devicePixelRatio || 1, 1.5);
const initialWidth = container.clientWidth * pixelScale;
const initialHeight = container.clientHeight * pixelScale;
dimensionsRef.current = { width: initialWidth, height: initialHeight };
try { try {
const isMobile = checkIsMobile(); // 1. Устанавливаем размер ТОЛЬКО ОДИН РАЗ до вызова transfer
// Используем 1.0 для четкости на мобилках, как ты и хотел canvas.width = initialWidth;
const pixelScale = isMobile canvas.height = initialHeight;
? 1.0
: Math.min(window.devicePixelRatio || 1, 1.5);
const width = container.clientWidth * pixelScale;
const height = container.clientHeight * pixelScale;
canvas.width = width;
canvas.height = height;
const worker = new Worker("/wasm-matrix/matrix_worker.js", { const worker = new Worker("/wasm-matrix/matrix_worker.js", {
type: "module", type: "module",
}); });
workerRef.current = worker; workerRef.current = worker;
// 2. Передаем контроль
const offscreen = canvas.transferControlToOffscreen(); const offscreen = canvas.transferControlToOffscreen();
hasTransferred.current = true; hasTransferred.current = true; // Блокируем любые изменения canvas.width навсегда для этого инстанса
worker.postMessage( worker.postMessage(
{ {
type: "INIT", type: "INIT",
payload: { payload: {
canvas: offscreen, canvas: offscreen,
width, width: initialWidth,
height, height: initialHeight,
isMobile, isMobile,
isVpnOn: isSecure, isVpnOn: isSecure,
isDarkMode, // Здесь передается начальная тема isDarkMode,
fontSize: isMobile ? 10 : 16, fontSize: isMobile ? 10 : 16,
}, },
}, },
@@ -87,14 +95,18 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const handleResize = () => { const handleResize = () => {
if (!container || !workerRef.current) return; if (!container || !workerRef.current) return;
const isMob = checkIsMobile(); const isMob = checkIsMobile();
// Держим scale консистентным с INIT (1.0 для мобилок)
const scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5); const scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5);
const w = container.clientWidth * scale;
const h = container.clientHeight * scale;
dimensionsRef.current = { width: w, height: h }; // Обновляем тут
workerRef.current.postMessage({ workerRef.current.postMessage({
type: "RESIZE", type: "RESIZE",
payload: { payload: {
width: container.clientWidth * scale, width: w,
height: container.clientHeight * scale, height: h,
isMobile: isMob, isMobile: isMob,
fontSize: isMob ? 10 : 16, fontSize: isMob ? 10 : 16,
}, },
@@ -102,29 +114,32 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
worker.terminate(); if (workerRef.current) {
workerRef.current.terminate();
workerRef.current = null;
}
// Сбрасываем флаг, так как DOM-элемент canvas будет удален и заменен новым
hasTransferred.current = false;
}; };
} catch (e) { } catch (e) {
console.error("Matrix Worker Bridge Failed:", e); console.error("Matrix Worker Bridge Failed:", e);
} }
}, []); // Пустой массив, так как INIT делается один раз }, [mounted]); // Зависим только от mounted
// --- ДОБАВЛЕНО: ОБНОВЛЕНИЕ ТЕМЫ И VPN --- // Синхронизация темы
useEffect(() => { useEffect(() => {
if (workerRef.current && mounted) { if (workerRef.current && mounted) {
workerRef.current.postMessage({ workerRef.current.postMessage({
type: "THEME", type: "THEME",
payload: { payload: { isVpnOn: isSecure, isDarkMode },
isVpnOn: isSecure,
isDarkMode: isDarkMode,
},
}); });
} }
}, [isSecure, isDarkMode, mounted]); }, [isSecure, isDarkMode, mounted]);
// ФИКС ОШИБКИ clientX (Твой код был верным, оставляем с микро-правкой) // Обработка событий (clientX и т.д.)
useEffect(() => { useEffect(() => {
const handleEvents = (e: any) => { const handleEvents = (e: any) => {
if (!workerRef.current || !canvasRef.current) return; if (!workerRef.current || !canvasRef.current) return;
@@ -153,52 +168,47 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
} }
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
// Используем фактическую ширину буфера канваса для правильных координат в Wasm const scaleX = dimensionsRef.current.width / rect.width;
const scaleX = canvasRef.current.width / rect.width; const scaleY = dimensionsRef.current.height / rect.height;
const scaleY = canvasRef.current.height / rect.height;
const x = (clientX - rect.left) * scaleX; workerRef.current.postMessage({
const y = (clientY - rect.top) * scaleY; type,
payload: {
workerRef.current.postMessage({ type, payload: { x, y } }); x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY,
},
});
}; };
const options = { passive: true };
window.addEventListener("pointerdown", handleEvents);
window.addEventListener("pointermove", handleEvents); window.addEventListener("pointermove", handleEvents);
window.addEventListener("pointerdown", handleEvents);
window.addEventListener("pointerup", handleEvents); window.addEventListener("pointerup", handleEvents);
window.addEventListener("pointercancel", handleEvents);
window.addEventListener("touchstart", handleEvents, options);
window.addEventListener("touchmove", handleEvents, options);
window.addEventListener("touchend", handleEvents, options);
return () => { return () => {
window.removeEventListener("pointerdown", handleEvents);
window.removeEventListener("pointermove", handleEvents); window.removeEventListener("pointermove", handleEvents);
window.removeEventListener("pointerdown", handleEvents);
window.removeEventListener("pointerup", handleEvents); window.removeEventListener("pointerup", handleEvents);
window.removeEventListener("pointercancel", handleEvents);
window.removeEventListener("touchstart", handleEvents);
window.removeEventListener("touchmove", handleEvents);
window.removeEventListener("touchend", handleEvents);
}; };
}, []); }, [mounted]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="absolute inset-0 overflow-hidden z-0 bg-background" className="absolute inset-0 overflow-hidden z-0 bg-background"
> >
{/* КЛЮЧЕВОЙ МОМЕНТ: Добавляем key={resolvedTheme + mounted}.
Это заставит React полностью удалять старый DOM-узел <canvas> и создавать новый.
Это единственный надежный способ сбросить состояние OffscreenControl в браузере.
*/}
<canvas <canvas
key="matrix-canvas"
ref={canvasRef} ref={canvasRef}
className={`w-full h-full block transition-opacity duration-1000 ${isLoaded ? "opacity-100" : "opacity-0"}`} className={`w-full h-full block transition-opacity duration-1000 ${isLoaded ? "opacity-100" : "opacity-0"}`}
/> />
<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={{
background: ` 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))`,
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%", backgroundSize: "100% 4px, 3px 100%",
}} }}
/> />
+1
View File
@@ -78,6 +78,7 @@ export function Header({
href={`/${lang}/download`} href={`/${lang}/download`}
variant="primary" variant="primary"
className="hidden sm:block scale-75 md:scale-90 origin-right" className="hidden sm:block scale-75 md:scale-90 origin-right"
onClick={() => setIsMenuOpen(false)}
> >
{dict.cta} {dict.cta}
</CyberButton> </CyberButton>
+1 -1
View File
@@ -251,7 +251,7 @@ export function Hero({
<section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden select-none"> <section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden select-none">
{/* --- 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} key={lang} />
<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>
+26 -70
View File
@@ -30,108 +30,64 @@ export function CyberButton({
whileHover={!disabled ? { scale: 1.02 } : {}} whileHover={!disabled ? { scale: 1.02 } : {}}
whileTap={!disabled ? { scale: 0.98 } : {}} whileTap={!disabled ? { scale: 0.98 } : {}}
className={cn( className={cn(
"relative z-10 flex items-center justify-center px-8 py-4 overflow-hidden transition-all duration-300 border rounded-xl font-mono", "relative z-10 flex items-center justify-center px-6 py-3 overflow-hidden transition-all duration-300 border rounded-xl font-mono", // Уменьшил padding
// --- ACTIVE STATE ---
!disabled && [ !disabled && [
isPrimary isPrimary
? "bg-[#8B3DFF] border-[#8B3DFF] text-white shadow-lg shadow-purple-500/20 dark:bg-[radial-gradient(circle_at_center,_rgba(139,61,255,0.4)_0%,_rgba(139,61,255,0.1)_100%)] dark:border-[#8B3DFF]/50 dark:shadow-[0_0_25px_rgba(139,61,255,0.3)] dark:after:absolute dark:after:inset-0 dark:after:shadow-[inset_0_0_15px_rgba(139,61,255,0.2)]" ? "bg-[#8B3DFF] border-[#8B3DFF] text-white shadow-lg shadow-purple-500/20 dark:bg-[radial-gradient(circle_at_center,_rgba(139,61,255,0.4)_0%,_rgba(139,61,255,0.1)_100%)] dark:border-[#8B3DFF]/50 dark:shadow-[0_0_25px_rgba(139,61,255,0.3)]"
: "text-[#00E5F2] border-[#00E5F2]/40 backdrop-blur-md bg-slate-900/5 border-[#00E5F2]/50 shadow-sm dark:bg-[#00E5F2]/5 dark:border-[#00E5F2]/30 dark:shadow-[0_0_15px_rgba(0,229,242,0.1)] hover:border-[#00E5F2] hover:shadow-[0_0_20px_rgba(0,229,242,0.3)]", : "text-[#00E5F2] border-[#00E5F2]/40 backdrop-blur-md bg-slate-900/5 border-[#00E5F2]/50 dark:bg-[#00E5F2]/5",
], ],
disabled && "cursor-not-allowed opacity-50 border-dashed",
// --- DISABLED STATE (Исправленный) ---
disabled && [
"cursor-not-allowed border-dashed shadow-none",
// Светлая тема: делаем кнопку матовой и "холодной"
"bg-slate-100 border-slate-300 text-slate-400",
// Темная тема: делаем кнопку глубокой и "выключенной"
"dark:bg-slate-900/40 dark:border-slate-800 dark:text-slate-600",
],
className, className,
)} )}
> >
{/* CRT Сетка (только когда активна) */} {/* Энергетический блик */}
{!isPrimary && !disabled && (
<div className="absolute inset-0 opacity-[0.03] dark:opacity-10 pointer-events-none bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,229,242,0.1)_50%),linear-gradient(90deg,rgba(255,0,0,0.02),rgba(0,255,0,0.01),rgba(0,0,255,0.02))] bg-[length:100%_4px,3px_100%]" />
)}
{/* Энергетический блик (только когда активна) */}
{!disabled && ( {!disabled && (
<div <div className="absolute inset-0 w-full h-full -translate-x-full group-hover:animate-scan pointer-events-none bg-gradient-to-r from-transparent via-white/10 to-transparent" />
className={cn(
"absolute inset-0 w-full h-full -translate-x-full group-hover:animate-scan pointer-events-none",
isPrimary
? "bg-gradient-to-r from-transparent via-white/20 to-transparent"
: "bg-gradient-to-r from-transparent via-[#00E5F2]/20 to-transparent",
)}
/>
)} )}
{/* Угловые элементы (⌜ ⌟) - прячем или приглушаем при disabled */} {/* Улучшенные скобки-маркеры */}
<div <div className="flex items-center gap-3">
className={cn(
"absolute inset-0 pointer-events-none transition-opacity",
disabled ? "opacity-20" : "opacity-60",
)}
>
<span <span
className={cn( className={cn(
"absolute top-1 left-1 text-[10px] font-mono", "text-[10px] font-bold transition-transform group-hover:-translate-x-0.5",
isPrimary ? "text-white" : "text-[#00E5F2]", isPrimary ? "text-white/50" : "text-secondary/50",
)} )}
> >
&lt;/
</span> </span>
<span className="relative z-20 font-black uppercase tracking-wider text-xs flex items-center gap-2">
{children}
</span>
<span <span
className={cn( className={cn(
"absolute bottom-1 right-1 text-[10px] font-mono", "text-[10px] font-bold transition-transform group-hover:translate-x-0.5",
isPrimary ? "text-white" : "text-[#00E5F2]", isPrimary ? "text-white/50" : "text-secondary/50",
)} )}
> >
/&gt;
</span> </span>
</div> </div>
{/* Анимированная полоска заряда (только для Secondary и когда активна) */}
{!isPrimary && !disabled && (
<motion.div
initial={{ height: 0 }}
whileHover={{ height: "70%" }}
className="absolute left-0 w-[2px] bg-[#00E5F2] shadow-[0_0_10px_#00E5F2] transition-all"
/>
)}
{/* Текст */}
<span
className={cn(
"relative z-20 font-black uppercase tracking-[0.2em] text-sm flex items-center gap-2",
!isPrimary &&
!disabled &&
"drop-shadow-[0_0_5px_rgba(0,229,242,0.5)]",
disabled && "drop-shadow-none",
)}
>
<span className="opacity-50">[</span>
{children}
<span className="opacity-50">]</span>
</span>
</motion.div> </motion.div>
); );
// Фоновое облако свечения (только когда активна)
const glowEffect = !disabled && ( const glowEffect = !disabled && (
<div <div
className={cn( className={cn(
"absolute inset-0 blur-2xl opacity-0 group-hover:opacity-40 transition-opacity duration-500 -z-10", "absolute inset-0 blur-xl opacity-0 group-hover:opacity-30 transition-opacity duration-500 -z-10",
isPrimary ? "bg-[#8B3DFF]" : "bg-[#00E5F2]/40", isPrimary ? "bg-[#8B3DFF]" : "bg-[#00E5F2]",
)} )}
/> />
); );
if (href && !disabled) { if (href && !disabled) {
return ( return (
<Link href={href} className="group relative inline-block"> <Link
href={href}
onClick={(e) => onClick?.(e as any)}
className="group relative inline-block"
>
{content} {content}
{glowEffect} {glowEffect}
</Link> </Link>
-108
View File
@@ -24,45 +24,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "matrix-engine" name = "matrix-engine"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"getrandom",
"js-sys",
"rand",
"wasm-bindgen", "wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
@@ -71,15 +38,6 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -98,36 +56,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -151,12 +79,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.118" version = "0.2.118"
@@ -201,33 +123,3 @@ checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "web-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
-11
View File
@@ -8,15 +8,4 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"CanvasRenderingContext2d",
"HtmlCanvasElement",
"Window",
"Performance",
"Document",
"Element"
] }
rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
+139 -68
View File
@@ -1,6 +1,49 @@
use rand::Rng;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
// 1. УЛЬТРА-БЫСТРЫЙ ГЕНЕРАТОР СЛУЧАЙНЫХ ЧИСЕЛ (Xorshift32)
// Работает в сотни раз быстрее стандартного rand в Wasm, так как не дергает Web API.
struct FastRng {
state: u32,
}
impl FastRng {
fn new(seed: u32) -> Self {
Self {
state: if seed == 0 { 1 } else { seed },
}
}
#[inline(always)]
fn next_u32(&mut self) -> u32 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.state = x;
x
}
#[inline(always)]
fn gen_f32(&mut self) -> f32 {
(self.next_u32() >> 8) as f32 / 16777216.0
}
#[inline(always)]
fn gen_range(&mut self, min: f32, max: f32) -> f32 {
min + self.gen_f32() * (max - min)
}
#[inline(always)]
fn gen_bool(&mut self, chance: f32) -> bool {
self.gen_f32() < chance
}
#[inline(always)]
fn gen_usize(&mut self, max: usize) -> usize {
(self.next_u32() as usize) % max
}
}
struct Drop { struct Drop {
y: f32, y: f32,
speed: f32, speed: f32,
@@ -17,13 +60,11 @@ struct GlitchBlock {
} }
impl GlitchBlock { impl GlitchBlock {
fn new(x: f32, y: f32) -> Self { fn new(x: f32, y: f32, rng: &mut FastRng) -> Self {
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_usize(100);
let choice = rng.gen_range(0..100);
// 0=bg, 1=theme_dense, 2=theme_invert, 3=white, 4=red
let color_type = match choice { let color_type = match choice {
0..=15 => 0.0, 0..=15 => 0.0,
16..=50 => 1.0, 16..=50 => 1.0,
@@ -33,11 +74,11 @@ impl GlitchBlock {
}; };
Self { Self {
x: x - w / 2.0 + rng.gen_range(-120.0..120.0), x: x - w / 2.0 + rng.gen_range(-120.0, 120.0),
y: y - h / 2.0 + rng.gen_range(-120.0..120.0), y: y - h / 2.0 + rng.gen_range(-120.0, 120.0),
w, w,
h, h,
life: rng.gen_range(5.0..25.0), life: rng.gen_range(5.0, 25.0),
color_type, color_type,
} }
} }
@@ -58,12 +99,12 @@ pub struct MatrixEngine {
height: u32, height: u32,
font_size: u32, font_size: u32,
charset_len: usize, charset_len: usize,
rng: FastRng, // Храним наш быстрый генератор
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>,
// БУФЕРЫ ДЛЯ ПЕРЕДАЧИ В JS
fg_buffer: Vec<f32>, fg_buffer: Vec<f32>,
bg_buffer: Vec<f32>, bg_buffer: Vec<f32>,
glitch_buffer: Vec<f32>, glitch_buffer: Vec<f32>,
@@ -86,20 +127,21 @@ impl MatrixEngine {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine { pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let charset_len = 84; // Длина нашего алфавита
let mut engine = MatrixEngine { let mut engine = MatrixEngine {
width, width,
height, height,
font_size, font_size,
charset_len, charset_len: 84,
rng: FastRng::new(1337), // Сид для старта
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(),
fg_buffer: Vec::with_capacity(2000), fg_buffer: Vec::new(),
bg_buffer: Vec::with_capacity(2000), bg_buffer: Vec::new(),
glitch_buffer: Vec::with_capacity(100), glitch_buffer: Vec::new(),
glass_buffer: Vec::with_capacity(50), glass_buffer: Vec::new(),
mouse_x: -1000.0, mouse_x: -1000.0,
mouse_y: -1000.0, mouse_y: -1000.0,
is_drawing: false, is_drawing: false,
@@ -116,27 +158,32 @@ impl MatrixEngine {
} }
fn init_drops(&mut self) { fn init_drops(&mut self) {
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 {
y: rng.gen_range(-20.0..self.height as f32), y: self.rng.gen_range(-20.0, self.height as f32),
speed: rng.gen_range(0.15..0.45), speed: self.rng.gen_range(0.15, 0.45),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
let density = if self.is_mobile { 1.2 } else { 0.6 }; 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 * density)) 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: self.rng.gen_range(-20.0, self.height as f32),
speed: rng.gen_range(0.05..0.15), speed: self.rng.gen_range(0.05, 0.15),
glitch_timer: 0.0, glitch_timer: 0.0,
}); });
} }
// Преаллоцируем буферы под точный размер, чтобы не менять их длину каждый кадр
self.fg_buffer.resize(self.fg_drops.len() * 5, 0.0);
self.bg_buffer.resize(self.bg_drops.len() * 4, 0.0);
} }
pub fn resize(&mut self, width: u32, height: u32) { pub fn resize(&mut self, width: u32, height: u32) {
@@ -149,45 +196,43 @@ impl MatrixEngine {
self.mouse_x = x; self.mouse_x = x;
self.mouse_y = y; self.mouse_y = y;
} }
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_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 { 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;
// На ПК генерим 25 блоков, на телефоне всего 8
let blocks_count = if self.is_mobile { 8 } else { 25 }; let blocks_count = if self.is_mobile { 8 } else { 25 };
for _ in 0..blocks_count { for _ in 0..blocks_count {
self.glitch_blocks.push(GlitchBlock::new(x, y)); let block = GlitchBlock::new(x, y, &mut self.rng);
self.glitch_blocks.push(block);
} }
} }
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.tick_count = self.tick_count.wrapping_add(1); self.tick_count = self.tick_count.wrapping_add(1);
let mut rng = rand::thread_rng();
self.fg_buffer.clear();
self.bg_buffer.clear();
self.glitch_buffer.clear(); self.glitch_buffer.clear();
self.glass_buffer.clear(); self.glass_buffer.clear();
if !self.is_mobile { if !self.is_mobile {
if rng.gen_bool(0.02) { if self.rng.gen_bool(0.02) {
let rx = rng.gen_range(0.0..self.width as f32); let rx = self.rng.gen_range(0.0, self.width as f32);
let ry = rng.gen_range(0.0..self.height as f32); let ry = self.rng.gen_range(0.0, self.height as f32);
self.glitch_blocks.push(GlitchBlock::new(rx, ry)); let block = GlitchBlock::new(rx, ry, &mut self.rng);
self.glitch_blocks.push(block);
} }
if rng.gen_bool(0.005) { if self.rng.gen_bool(0.005) {
self.tear_y = rng.gen_range(0.0..self.height as f32); self.tear_y = self.rng.gen_range(0.0, self.height as f32);
self.tear_timer = rng.gen_range(5.0..15.0); self.tear_timer = self.rng.gen_range(5.0, 15.0);
} }
} }
@@ -200,29 +245,48 @@ impl MatrixEngine {
let bg_f = self.font_size as f32 * 0.6; let bg_f = self.font_size as f32 * 0.6;
// ПРЯМАЯ ЗАПИСЬ В БУФЕР (БЕЗ EXTEND)
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) * bg_f; let x = (i as f32) * bg_f;
let cell_y = drop.y.floor(); let cell_y = drop.y as i32 as f32; // Оптимизация: каст вместо floor()
let y = cell_y * bg_f; 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 let base_idx = i * 4;
.extend_from_slice(&[x, y, char_idx as f32, rng.gen_range(0.05..0.15)]); self.bg_buffer[base_idx] = x;
self.bg_buffer[base_idx + 1] = y;
self.bg_buffer[base_idx + 2] = char_idx as f32;
self.bg_buffer[base_idx + 3] = self.rng.gen_range(0.05, 0.15);
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 && self.rng.gen_bool(0.005) {
drop.y = rng.gen_range(-10.0..0.0); drop.y = self.rng.gen_range(-10.0, 0.0);
} }
} }
// РАСЧЕТЫ КВАДРАТОВ ДЛЯ EMP (Отказ от медленного sqrt)
let emp_rad = (60.0 - self.emp_timer) * 15.0;
let emp_rad_sq = emp_rad * emp_rad;
let emp_r20_sq = if emp_rad > 20.0 {
(emp_rad - 20.0) * (emp_rad - 20.0)
} else {
0.0
};
let emp_r80_sq = if emp_rad > 80.0 {
(emp_rad - 80.0) * (emp_rad - 80.0)
} else {
0.0
};
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);
let cell_y = drop.y.floor(); let cell_y = drop.y as i32 as f32;
let y = cell_y * (self.font_size as f32); let y = cell_y * (self.font_size as f32);
let mut is_scrambled = false; let mut is_scrambled = false;
@@ -230,19 +294,19 @@ impl MatrixEngine {
let mut is_emp_ring = false; let mut is_emp_ring = false;
if !self.is_mobile && 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 += self.rng.gen_range(-20.0, 20.0);
is_scrambled = true; is_scrambled = true;
} }
if !self.is_mobile && 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_sq = dx * dx + dy * dy; // Быстрая дистанция
let emp_radius = (60.0 - self.emp_timer) * 15.0;
if dist < emp_radius { if dist_sq < emp_rad_sq {
if dist > emp_radius - 20.0 { if dist_sq > emp_r20_sq {
is_emp_ring = true; is_emp_ring = true;
} else if dist > emp_radius - 80.0 { } else if dist_sq > emp_r80_sq {
is_scrambled = true; is_scrambled = true;
} else { } else {
is_void = true; is_void = true;
@@ -252,9 +316,12 @@ impl MatrixEngine {
if is_void { if is_void {
drop.y += drop.speed; drop.y += drop.speed;
let base_idx = i * 5;
self.fg_buffer[base_idx + 4] = 0.0; // Скрываем каплю (alpha 0)
continue; continue;
} }
if rng.gen_bool(0.001) {
if self.rng.gen_bool(0.001) {
drop.glitch_timer = 5.0; drop.glitch_timer = 5.0;
} }
@@ -265,17 +332,17 @@ impl MatrixEngine {
.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 = self.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 };
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 = self.rng.gen_usize(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 = self.rng.gen_usize(self.charset_len);
target_alpha = rng.gen_range(0.6..1.0); target_alpha = self.rng.gen_range(0.6, 1.0);
is_head = rng.gen_bool(0.2); is_head = self.rng.gen_bool(0.2);
if drop.glitch_timer > 0.0 { if drop.glitch_timer > 0.0 {
drop.glitch_timer -= 1.0; drop.glitch_timer -= 1.0;
} }
@@ -283,17 +350,22 @@ impl MatrixEngine {
if self.is_mobile { if self.is_mobile {
target_alpha = 0.8; target_alpha = 0.8;
} // Упрощаем альфу на мобилах }
// СОХРАНЯЕМ В БУФЕР: [x, y, char_idx, atlas_row, alpha]
let atlas_row = if is_head { 1.0 } else { 0.0 }; let atlas_row = if is_head { 1.0 } else { 0.0 };
self.fg_buffer
.extend_from_slice(&[x, y, char_idx as f32, atlas_row, target_alpha]); // ПРЯМАЯ ЗАПИСЬ В БУФЕР
let base_idx = i * 5;
self.fg_buffer[base_idx] = x;
self.fg_buffer[base_idx + 1] = y;
self.fg_buffer[base_idx + 2] = char_idx as f32;
self.fg_buffer[base_idx + 3] = atlas_row;
self.fg_buffer[base_idx + 4] = target_alpha;
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 && self.rng.gen_bool(0.01) {
drop.y = rng.gen_range(-10.0..0.0); drop.y = self.rng.gen_range(-10.0, 0.0);
drop.speed = rng.gen_range(0.15..0.45); drop.speed = self.rng.gen_range(0.15, 0.45);
} }
} }
@@ -309,14 +381,14 @@ 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_mobile && self.is_drawing && self.rng.gen_bool(0.3) {
self.glass_symbols.push(GlassSymbol { self.glass_symbols.push(GlassSymbol {
x: self.mouse_x + rng.gen_range(-15.0..15.0), x: self.mouse_x + self.rng.gen_range(-15.0, 15.0),
y: self.mouse_y + rng.gen_range(-15.0..15.0), y: self.mouse_y + self.rng.gen_range(-15.0, 15.0),
char_idx: rng.gen_range(0..self.charset_len), char_idx: self.rng.gen_usize(self.charset_len),
life: 1.0, life: 1.0,
max_life: rng.gen_range(30.0..80.0), max_life: self.rng.gen_range(30.0, 80.0),
velocity_y: rng.gen_range(0.1..0.5), velocity_y: self.rng.gen_range(0.1, 0.5),
}); });
} }
@@ -330,7 +402,6 @@ impl MatrixEngine {
self.glass_symbols.retain(|s| s.life > 0.0); self.glass_symbols.retain(|s| s.life > 0.0);
} }
// ПОКАЗЫВАЕМ ПАМЯТЬ ДЛЯ JAVASCRIPT
pub fn fg_ptr(&self) -> *const f32 { pub fn fg_ptr(&self) -> *const f32 {
self.fg_buffer.as_ptr() self.fg_buffer.as_ptr()
} }