optimizations and fixes
This commit is contained in:
@@ -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
@@ -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"]
|
||||||
@@ -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%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
⌜
|
</
|
||||||
</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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
⌟
|
/>
|
||||||
</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>
|
||||||
|
|||||||
Generated
-108
@@ -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",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user