Compare commits

..

No commits in common. "633b0f0d052ef962b26a623a8f6890e682f66d94" and "f7153248c580073adce450aa7be6fb730a2d490a" have entirely different histories.

3 changed files with 109 additions and 445 deletions

View file

@ -18,33 +18,6 @@ import config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = Router() router = Router()
async def safe_edit_or_reply(event, text: str, reply_markup=None, parse_mode='HTML'):
"""
Безопасное редактирование сообщения.
Если сообщение содержит медиа (фото и т.д.), мы удаляем его и отправляем новое.
"""
if isinstance(event, CallbackQuery):
await event.answer()
message = event.message
if (hasattr(message, 'photo') and message.photo) or \
(hasattr(message, 'video') and message.video) or \
(hasattr(message, 'document') and message.document) or \
(hasattr(message, 'content_type') and message.content_type != 'text'):
try:
await message.delete()
except Exception as e:
logger.warning(f"Не удалось удалить сообщение с медиа: {e}")
await message.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
else:
try:
await message.edit_text(text, reply_markup=reply_markup, parse_mode=parse_mode)
except Exception as e:
logger.warning(f"Ошибка edit_text: {e}. Отправляем новое сообщение.")
await message.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
elif isinstance(event, Message):
await event.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
@router.message(Command('cancel'), StateFilter('*')) @router.message(Command('cancel'), StateFilter('*'))
@router.message(F.text.lower().in_(['отмена', 'отменить']), StateFilter('*')) @router.message(F.text.lower().in_(['отмена', 'отменить']), StateFilter('*'))
async def global_cancel(message: Message, state: FSMContext): async def global_cancel(message: Message, state: FSMContext):
@ -101,8 +74,7 @@ async def cb_main_menu(callback: CallbackQuery):
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True) ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
await safe_edit_or_reply( await callback.message.edit_text(
callback,
"Главное меню:", "Главное меню:",
reply_markup=get_main_menu(verified=user.verified, is_ig=is_ig, is_admin=(user.is_admin or user_id == config.ADMIN_USER_ID), is_superadmin=(user_id == config.ADMIN_USER_ID)), reply_markup=get_main_menu(verified=user.verified, is_ig=is_ig, is_admin=(user.is_admin or user_id == config.ADMIN_USER_ID), is_superadmin=(user_id == config.ADMIN_USER_ID)),
parse_mode='HTML' parse_mode='HTML'
@ -113,7 +85,11 @@ async def cb_main_menu(callback: CallbackQuery):
async def cb_phones(event): async def cb_phones(event):
text = '📞 <b>ЭКСТРЕННЫЕ ТЕЛЕФОНЫ</b>\n\nВыберите категорию ниже:' text = '📞 <b>ЭКСТРЕННЫЕ ТЕЛЕФОНЫ</b>\n\nВыберите категорию ниже:'
kb = get_phones_menu() kb = get_phones_menu()
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML') if isinstance(event, CallbackQuery):
await event.answer()
await event.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
else:
await event.answer(text, reply_markup=kb, parse_mode='HTML')
@router.message(F.text & ~F.text.startswith('/')) @router.message(F.text & ~F.text.startswith('/'))
async def chat_with_ai(message: Message): async def chat_with_ai(message: Message):
@ -159,19 +135,9 @@ async def cb_phone_category(callback: CallbackQuery):
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
stmt = select(Service).where(Service.category == cat_code).order_by(Service.name) stmt = select(Service).where(Service.category == cat_code).order_by(Service.name)
services = list((await session.execute(stmt)).scalars().all()) services = list((await session.execute(stmt)).scalars().all())
user_stmt = select(User).where(User.user_id == callback.from_user.id)
user = (await session.execute(user_stmt)).scalar_one_or_none()
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == callback.from_user.id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
is_admin = user.is_admin or callback.from_user.id == config.ADMIN_USER_ID if user else False
is_superadmin = callback.from_user.id == config.ADMIN_USER_ID
verified = user.verified if user else False
menu_kb = get_main_menu(verified=verified, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin)
if not services: if not services:
return await safe_edit_or_reply(callback, "👷 Данные для этой категории еще не заполнены.", reply_markup=menu_kb) return await callback.message.edit_text("👷 Данные для этой категории еще не заполнены.", reply_markup=get_main_menu())
text = f"📞 <b>СПИСОК ТЕЛЕФОНОВ: {cat_code.upper()}</b>\n\n" text = f"📞 <b>СПИСОК ТЕЛЕФОНОВ: {cat_code.upper()}</b>\n\n"
for s in services: for s in services:
@ -184,9 +150,11 @@ async def cb_phone_category(callback: CallbackQuery):
if photo_path and os.path.exists(photo_path): if photo_path and os.path.exists(photo_path):
await callback.message.delete() await callback.message.delete()
await callback.message.answer_photo(FSInputFile(photo_path), caption=text, reply_markup=menu_kb, parse_mode='HTML') await callback.message.answer_photo(FSInputFile(photo_path), caption=text, reply_markup=get_main_menu(), parse_mode='HTML')
else: else:
await safe_edit_or_reply(callback, text, reply_markup=menu_kb, parse_mode='HTML') await callback.message.edit_text(text, reply_markup=get_main_menu(), parse_mode='HTML')
await callback.answer()
@router.message(Command('rules')) @router.message(Command('rules'))
@router.callback_query(F.data == 'rules') @router.callback_query(F.data == 'rules')
@ -195,7 +163,11 @@ async def cb_rules(event):
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu")) kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML') if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(text, reply_markup=kb.as_markup(), parse_mode='HTML')
@router.message(Command('about')) @router.message(Command('about'))
@router.callback_query(F.data == 'about_house') @router.callback_query(F.data == 'about_house')
@ -208,7 +180,11 @@ async def cb_about(event):
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu")) kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML') if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(text, reply_markup=kb.as_markup(), parse_mode='HTML')
@router.message(Command('profile')) @router.message(Command('profile'))
@router.callback_query(F.data == 'my_profile') @router.callback_query(F.data == 'my_profile')
@ -231,7 +207,11 @@ async def cb_profile(event):
kb = get_profile_keyboard(user.meter_reminders_enabled if user else True) kb = get_profile_keyboard(user.meter_reminders_enabled if user else True)
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML') if isinstance(event, Message):
await event.answer(text, reply_markup=kb, parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
@router.callback_query(F.data == "toggle_meter_reminders") @router.callback_query(F.data == "toggle_meter_reminders")
async def cb_toggle_meter_reminders(callback: CallbackQuery): async def cb_toggle_meter_reminders(callback: CallbackQuery):
@ -264,11 +244,12 @@ async def cb_toggle_meter_reminders(callback: CallbackQuery):
f"Рейтинг: {user.rating} 🏆") f"Рейтинг: {user.rating} 🏆")
kb = get_profile_keyboard(new_val) kb = get_profile_keyboard(new_val)
await safe_edit_or_reply(callback, text, reply_markup=kb, parse_mode='HTML') await callback.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
@router.callback_query(F.data == "edit_location") @router.callback_query(F.data == "edit_location")
async def edit_location_start(callback: CallbackQuery): async def edit_location_start(callback: CallbackQuery):
await safe_edit_or_reply(callback, "Введите ваш подъезд и этаж через пробел\n<i>Например: '3 5' (3 подъезд, 5 этаж)</i>") await callback.message.edit_text("Введите ваш подъезд и этаж через пробел\n<i>Например: '3 5' (3 подъезд, 5 этаж)</i>", parse_mode='HTML')
await callback.answer()
@router.message(F.text.regexp(r"^\d+\s+\d+$")) @router.message(F.text.regexp(r"^\d+\s+\d+$"))
async def process_location(message: Message): async def process_location(message: Message):
@ -301,4 +282,8 @@ async def cb_help(event):
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu")) kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML') if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(text, reply_markup=kb.as_markup(), parse_mode='HTML')

View file

@ -1,327 +1,74 @@
/* GLASSMORPHISM DARK UI FOR DOMOVOY WEB PANEL */ /* MATRIX STYLE FOR LKM37 ADMIN PANEL */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root { :root {
--bg-main: #06060c; --matrix-green: #22c55e;
--bg-gradient: radial-gradient(circle at 50% 0%, #150f30 0%, #06060c 80%); --matrix-green-glow: #10b981;
--panel-bg: rgba(18, 18, 29, 0.45); --matrix-black: #050505;
--panel-bg-hover: rgba(26, 26, 42, 0.6); --matrix-gray: #1a1a1a;
--panel-border: rgba(255, 255, 255, 0.08); --matrix-cyan: #06b6d4;
--panel-border-hover: rgba(255, 255, 255, 0.15); --matrix-red: #ef4444;
--color-primary: #8a2be2;
--color-primary-glow: rgba(138, 43, 226, 0.35);
--color-success: #00ffcc;
--color-success-glow: rgba(0, 255, 204, 0.25);
--color-warning: #ffb300;
--color-warning-glow: rgba(255, 179, 0, 0.25);
--color-danger: #ff2d55;
--color-danger-glow: rgba(255, 45, 85, 0.25);
--color-info: #00bfff;
--color-info-glow: rgba(0, 191, 255, 0.25);
--color-text-main: #f5f5f7;
--color-text-muted: #8e8e93;
--color-text-dark: #3a3a3c;
--font-main: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--shadow-neon: 0 8px 32px 0 rgba(0, 0, 0, 0.4), 0 0 16px 0 var(--color-primary-glow);
--transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
} }
body { body {
background-color: var(--bg-main) !important; background-color: var(--matrix-black);
background-image: var(--bg-gradient) !important; color: var(--matrix-green);
background-attachment: fixed !important; font-family: 'Courier New', Courier, monospace;
color: var(--color-text-main) !important;
font-family: var(--font-main) !important;
min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
line-height: 1.5;
} }
/* Сайдбар */
.sidebar { .sidebar {
background: rgba(10, 10, 18, 0.3) !important; background: var(--matrix-black);
backdrop-filter: blur(20px) !important; border-right: 1px solid var(--matrix-green);
-webkit-backdrop-filter: blur(20px) !important; box-shadow: 2px 0 15px rgba(34, 197, 94, 0.2);
border-right: 1px solid var(--panel-border) !important;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3) !important;
} }
.sidebar .nav-link { .sidebar .nav-link {
color: var(--color-text-muted) !important; color: var(--matrix-green);
border-left: 3px solid transparent !important; border-left: 2px solid transparent;
transition: var(--transition-smooth) !important; transition: 0.3s;
padding: 0.75rem 1.5rem !important; text-transform: uppercase;
font-weight: 500 !important; font-size: 0.9rem;
text-transform: uppercase !important; letter-spacing: 1px;
font-family: var(--font-main) !important;
letter-spacing: 0.5px !important;
} }
.sidebar .nav-link:hover { .sidebar .nav-link:hover, .sidebar .nav-link.active {
color: var(--color-text-main) !important; background-color: rgba(34, 197, 94, 0.1);
background: rgba(255, 255, 255, 0.03) !important; color: white;
border-left-color: rgba(255, 255, 255, 0.2) !important; border-left-color: var(--matrix-green);
text-shadow: 0 0 10px var(--matrix-green);
} }
.sidebar .nav-link.active {
color: var(--color-success) !important;
background: rgba(0, 255, 204, 0.05) !important;
border-left-color: var(--color-success) !important;
text-shadow: 0 0 10px var(--color-success-glow) !important;
}
.sidebar .nav-link i, .sidebar .nav-link bi {
margin-right: 0.5rem !important;
font-size: 1.1rem !important;
}
/* Карточки */
.card { .card {
background: var(--panel-bg) !important; background-color: var(--matrix-gray);
backdrop-filter: blur(12px) !important; border: 1px solid var(--matrix-green);
-webkit-backdrop-filter: blur(12px) !important; color: var(--matrix-green);
border: 1px solid var(--panel-border) !important;
border-radius: 16px !important;
transition: var(--transition-smooth) !important;
color: var(--color-text-main) !important;
overflow: hidden !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
}
.card:hover {
background: var(--panel-bg-hover) !important;
border-color: var(--panel-border-hover) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4) !important;
transform: translateY(-2px) !important;
} }
.card-header { .card-header {
background: rgba(255, 255, 255, 0.02) !important; background-color: rgba(34, 197, 94, 0.1);
border-bottom: 1px solid var(--panel-border) !important; border-bottom: 1px solid var(--matrix-green);
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
padding: 1rem 1.25rem !important;
color: var(--color-text-main) !important;
} }
.card-body { .btn-matrix {
padding: 1.25rem !important; background-color: transparent;
border: 1px solid var(--matrix-green);
color: var(--matrix-green);
text-transform: uppercase;
} }
/* Переопределение старых рамок и фонов */ .btn-matrix:hover {
.border-success, .border-warning, .border-cyan, .border-danger { background-color: var(--matrix-green);
border-color: var(--panel-border) !important; color: black;
box-shadow: 0 0 20px var(--matrix-green);
} }
.card.border-success:hover, .card.border-warning:hover, .card.border-cyan:hover { /* Эффект падающих символов на фоне (легкий) */
border-color: var(--panel-border-hover) !important; .matrix-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.05;
pointer-events: none;
} }
.card.border-danger {
border-color: rgba(255, 45, 85, 0.3) !important;
}
.card.border-danger:hover {
border-color: var(--color-danger) !important;
box-shadow: 0 8px 30px rgba(255, 45, 85, 0.15) !important;
}
/* Таблицы */
.table {
color: var(--color-text-main) !important;
background-color: transparent !important;
}
.table > :not(caption) > * > * {
background-color: transparent !important;
color: var(--color-text-main) !important;
border-bottom: 1px solid var(--panel-border) !important;
}
.table > thead > tr > th {
color: var(--color-text-muted) !important;
font-weight: 600 !important;
text-transform: uppercase !important;
font-size: 0.8rem !important;
letter-spacing: 0.5px !important;
border-bottom: 2px solid var(--panel-border) !important;
}
.table-hover tbody tr:hover td {
background-color: rgba(255, 255, 255, 0.02) !important;
color: var(--color-text-main) !important;
}
/* Кнопки */
.btn {
border-radius: 8px !important;
font-weight: 500 !important;
transition: var(--transition-smooth) !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
}
.btn-matrix, .btn-outline-success {
background-color: transparent !important;
border: 1px solid var(--color-success) !important;
color: var(--color-success) !important;
}
.btn-matrix:hover, .btn-outline-success:hover, .btn-outline-success:active, .btn-outline-success:focus {
background-color: var(--color-success) !important;
color: #000 !important;
border-color: var(--color-success) !important;
box-shadow: 0 0 15px var(--color-success-glow) !important;
}
.btn-outline-primary {
border-color: var(--color-primary) !important;
color: var(--color-text-main) !important;
}
.btn-outline-primary:hover {
background-color: var(--color-primary) !important;
color: var(--color-text-main) !important;
box-shadow: 0 0 15px var(--color-primary-glow) !important;
}
.btn-outline-danger {
border-color: var(--color-danger) !important;
color: var(--color-danger) !important;
background-color: transparent !important;
}
.btn-outline-danger:hover {
background-color: var(--color-danger) !important;
color: #fff !important;
box-shadow: 0 0 15px var(--color-danger-glow) !important;
}
.btn-outline-warning {
border-color: var(--color-warning) !important;
color: var(--color-warning) !important;
background-color: transparent !important;
}
.btn-outline-warning:hover {
background-color: var(--color-warning) !important;
color: #000 !important;
box-shadow: 0 0 15px var(--color-warning-glow) !important;
}
.btn-outline-info {
border-color: var(--color-info) !important;
color: var(--color-info) !important;
background-color: transparent !important;
}
.btn-outline-info:hover {
background-color: var(--color-info) !important;
color: #000 !important;
box-shadow: 0 0 15px var(--color-info-glow) !important;
}
/* Формы */
.form-control, .form-select {
background-color: rgba(255, 255, 255, 0.03) !important;
border: 1px solid var(--panel-border) !important;
color: var(--color-text-main) !important;
border-radius: 8px !important;
transition: var(--transition-smooth) !important;
}
.form-control:focus, .form-select:focus {
background-color: rgba(255, 255, 255, 0.05) !important;
border-color: var(--color-primary) !important;
color: var(--color-text-main) !important;
box-shadow: 0 0 8px var(--color-primary-glow) !important;
}
.form-control::placeholder {
color: var(--color-text-muted) !important;
opacity: 0.6 !important;
}
.bg-black {
background-color: rgba(10, 10, 15, 0.5) !important;
border: 1px solid var(--panel-border) !important;
}
/* Ссылки */
a {
color: var(--color-success);
text-decoration: none;
transition: var(--transition-smooth);
}
a:hover {
color: #fff;
text-shadow: 0 0 8px var(--color-success-glow);
}
/* Баджи */
.badge {
border-radius: 6px !important;
font-weight: 500 !important;
padding: 0.4em 0.6em !important;
}
.bg-success {
background-color: rgba(0, 255, 204, 0.15) !important;
color: var(--color-success) !important;
border: 1px solid var(--color-success) !important;
}
.bg-danger {
background-color: rgba(255, 45, 85, 0.15) !important;
color: var(--color-danger) !important;
border: 1px solid var(--color-danger) !important;
}
.bg-warning {
background-color: rgba(255, 179, 0, 0.15) !important;
color: var(--color-warning) !important;
border: 1px solid var(--color-warning) !important;
}
.bg-info {
background-color: rgba(0, 191, 255, 0.15) !important;
color: var(--color-info) !important;
border: 1px solid var(--color-info) !important;
}
/* Утилиты и кастомный контент */
hr {
border-color: var(--panel-border) !important;
opacity: 0.5 !important;
}
pre, code {
font-family: var(--font-mono) !important;
}
/* Кастомный Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(10, 10, 15, 0.5);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}

View file

@ -21,10 +21,10 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<!-- Sidebar (ВОССТАНОВЛЕННОЕ ПОЛНОЕ МЕНЮ) --> <!-- Sidebar (ВОССТАНОВЛЕННОЕ ПОЛНОЕ МЕНЮ) -->
<div class="col-md-2 sidebar p-0"> <div class="col-md-2 sidebar p-0 border-end border-success">
<div class="p-3 text-center border-bottom" style="border-color: var(--panel-border) !important;"> <div class="p-3 text-center border-bottom border-success">
<h4 class="mb-0" style="color: var(--color-success); text-shadow: 0 0 15px var(--color-success-glow); font-weight: 700; letter-spacing: 1px;">LKM37</h4> <h4 class="mb-0" style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">LKM37</h4>
<small class="text-success opacity-50" style="font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.5px;">CORE SYSTEM v4.5</small> <small class="text-success opacity-50">CORE SYSTEM v4.0</small>
</div> </div>
<nav class="nav flex-column mt-3"> <nav class="nav flex-column mt-3">
<a class="nav-link {% block nav_dashboard %}{% endblock %}" href="/"> <a class="nav-link {% block nav_dashboard %}{% endblock %}" href="/">
@ -94,98 +94,30 @@
<script> <script>
const canvas = document.getElementById('matrix-canvas'); const canvas = document.getElementById('matrix-canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
let width = canvas.width = window.innerWidth; canvas.height = window.innerHeight;
let height = canvas.height = window.innerHeight; const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const fontSize = 16;
const particles = []; const columns = canvas.width / fontSize;
const properties = { const drops = Array(Math.floor(columns)).fill(1);
bgColor: '#06060c',
particleColor: 'rgba(138, 43, 226, 0.25)', function draw() {
lineColor: 'rgba(0, 255, 204, 0.05)', ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
particleRadius: 2.5, ctx.fillRect(0, 0, canvas.width, canvas.height);
particleCount: 70, ctx.fillStyle = "#22c55e";
maxVelocity: 0.4, ctx.font = fontSize + "px monospace";
lineLength: 140 for (let i = 0; i < drops.length; i++) {
}; const text = letters[Math.floor(Math.random() * letters.length)];
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
window.addEventListener('resize', function() { if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) drops[i] = 0;
width = canvas.width = window.innerWidth; drops[i]++;
height = canvas.height = window.innerHeight; }
}
setInterval(draw, 33);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}); });
class Particle {
constructor() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.velocityPosition = {
x: (Math.random() - 0.5) * properties.maxVelocity,
y: (Math.random() - 0.5) * properties.maxVelocity
}
}
position() {
if (this.x + this.velocityPosition.x > width || this.x + this.velocityPosition.x < 0) {
this.velocityPosition.x = -this.velocityPosition.x;
}
if (this.y + this.velocityPosition.y > height || this.y + this.velocityPosition.y < 0) {
this.velocityPosition.y = -this.velocityPosition.y;
}
this.x += this.velocityPosition.x;
this.y += this.velocityPosition.y;
}
reDraw() {
ctx.beginPath();
ctx.arc(this.x, this.y, properties.particleRadius, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = properties.particleColor;
ctx.fill();
}
}
function drawLines() {
let x1, y1, x2, y2, length, opacity;
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
x1 = particles[i].x;
y1 = particles[i].y;
x2 = particles[j].x;
y2 = particles[j].y;
length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
if (length < properties.lineLength) {
opacity = 1 - (length / properties.lineLength);
ctx.strokeStyle = `rgba(0, 255, 204, ${opacity * 0.06})`;
ctx.lineWidth = '0.5';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.stroke();
}
}
}
}
function init() {
for (let i = 0; i < properties.particleCount; i++) {
particles.push(new Particle());
}
loop();
}
function loop() {
ctx.fillStyle = properties.bgColor;
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < particles.length; i++) {
particles[i].position();
particles[i].reDraw();
}
drawLines();
requestAnimationFrame(loop);
}
init();
</script> </script>
{% block extra_script %}{% endblock %} {% block extra_script %}{% endblock %}
</body> </body>