Улучшение взаимодействия и добавление веб-приложения
This commit is contained in:
373
web_app/static/css/style.css
Normal file
373
web_app/static/css/style.css
Normal file
@@ -0,0 +1,373 @@
|
||||
:root {
|
||||
--bg-color: #050510;
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-highlight: rgba(255, 255, 255, 0.15);
|
||||
--primary: #6366f1;
|
||||
--primary-glow: rgba(99, 102, 241, 0.5);
|
||||
--text-main: #ffffff;
|
||||
--text-muted: #94a3b8;
|
||||
--radius: 16px;
|
||||
--font-main: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-main);
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#stars-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
width: 95vw;
|
||||
height: 90vh;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
border-right: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: var(--primary);
|
||||
filter: drop-shadow(0 0 8px var(--primary-glow));
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
}
|
||||
|
||||
.user-mini {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.big-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #a855f7, #6366f1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.big-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.input-glass {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-main);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--glass-bg);
|
||||
}
|
||||
|
||||
.view-container {
|
||||
padding: 32px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-info .value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sub-details {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 4px 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px var(--primary-glow);
|
||||
}
|
||||
|
||||
.btn-primary.full-width {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Shop */
|
||||
.plans-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.plan-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.plan-features {
|
||||
list-style: none;
|
||||
margin-bottom: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plan-features li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.view-container>* {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex-direction: row;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
display: none;
|
||||
/* Mobile nav needs a toggle, simplifying for now */
|
||||
}
|
||||
|
||||
.user-mini {
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
130
web_app/static/index.html
Normal file
130
web_app/static/index.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Marzban Bot Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="stars-container"></div>
|
||||
|
||||
<div class="app-container">
|
||||
<aside class="sidebar glass">
|
||||
<div class="logo">
|
||||
<i data-lucide="rocket" class="logo-icon"></i>
|
||||
<span>CometBot</span>
|
||||
</div>
|
||||
<nav>
|
||||
<button class="nav-item active" data-page="dashboard" onclick="router('dashboard')">
|
||||
<i data-lucide="layout-dashboard"></i> Dashboard
|
||||
</button>
|
||||
<button class="nav-item" data-page="shop" onclick="router('shop')">
|
||||
<i data-lucide="shopping-bag"></i> Shop
|
||||
</button>
|
||||
<button class="nav-item" data-page="profile" onclick="router('profile')">
|
||||
<i data-lucide="user"></i> Profile
|
||||
</button>
|
||||
</nav>
|
||||
<div class="user-mini">
|
||||
<div class="avatar" id="sidebar-avatar">U</div>
|
||||
<div class="info">
|
||||
<span class="name" id="sidebar-name">User</span>
|
||||
<span class="status">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<header class="glass">
|
||||
<h1 id="page-title">Dashboard</h1>
|
||||
<div class="actions">
|
||||
<button class="icon-btn"><i data-lucide="bell"></i></button>
|
||||
<button class="icon-btn theme-toggle"><i data-lucide="sun"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="app-view" class="view-container">
|
||||
<!-- Dynamic Content Loaded Here -->
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Templates for Views -->
|
||||
<template id="view-dashboard">
|
||||
<div class="stats-grid">
|
||||
<div class="card glass stat-card">
|
||||
<div class="icon-box <color>"><i data-lucide="activity"></i></div>
|
||||
<div class="stat-info">
|
||||
<h3>Status</h3>
|
||||
<p class="value" id="dash-status">Active</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card glass stat-card">
|
||||
<div class="icon-box"><i data-lucide="calendar"></i></div>
|
||||
<div class="stat-info">
|
||||
<h3>Days Left</h3>
|
||||
<p class="value" id="dash-days">0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card glass stat-card">
|
||||
<div class="icon-box"><i data-lucide="wifi"></i></div>
|
||||
<div class="stat-info">
|
||||
<h3>Data Usage</h3>
|
||||
<p class="value" id="dash-data">0 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">
|
||||
<h2>Your Subscription</h2>
|
||||
</div>
|
||||
<div class="card glass sub-details">
|
||||
<div class="detail-row">
|
||||
<span>Plan</span>
|
||||
<strong id="sub-plan-name">Free Tier</strong>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>Expires</span>
|
||||
<strong id="sub-expire-date">-</strong>
|
||||
</div>
|
||||
<button class="btn-primary full-width" onclick="router('shop')">Upgrade Plan</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="view-shop">
|
||||
<div class="plans-grid" id="plans-container">
|
||||
<!-- Plans will be injected here -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="view-profile">
|
||||
<div class="card glass profile-card">
|
||||
<div class="profile-header">
|
||||
<div class="big-avatar">U</div>
|
||||
<h2>User Profile</h2>
|
||||
<p>Telegram ID: <span id="profile-tg-id">123456</span></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="profile-username" readonly value="@username" class="glass-input">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="js/background.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
183
web_app/static/js/app.js
Normal file
183
web_app/static/js/app.js
Normal file
@@ -0,0 +1,183 @@
|
||||
// Navigation Router
|
||||
function router(pageName) {
|
||||
const viewContainer = document.getElementById('app-view');
|
||||
const title = document.getElementById('page-title');
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
|
||||
// Update Nav
|
||||
navItems.forEach(item => {
|
||||
if (item.dataset.page === pageName) item.classList.add('active');
|
||||
else item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Set Title
|
||||
title.textContent = pageName.charAt(0).toUpperCase() + pageName.slice(1);
|
||||
|
||||
// Load View
|
||||
const template = document.getElementById(`view-${pageName}`);
|
||||
if (template) {
|
||||
viewContainer.innerHTML = '';
|
||||
viewContainer.appendChild(template.content.cloneNode(true));
|
||||
|
||||
// Initialize view specific logic
|
||||
if (pageName === 'dashboard') loadDashboard();
|
||||
if (pageName === 'shop') loadShop();
|
||||
if (pageName === 'profile') loadProfile();
|
||||
|
||||
// Re-init generic UI stuff like icons if new ones added
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// Data Fetching
|
||||
const API_BASE = '/api';
|
||||
|
||||
// Telegram Integration
|
||||
let tgUser = null;
|
||||
if (window.Telegram && window.Telegram.WebApp) {
|
||||
const tg = window.Telegram.WebApp;
|
||||
tg.ready();
|
||||
tgUser = tg.initDataUnsafe?.user;
|
||||
|
||||
// Theme sync
|
||||
if (tg.colorScheme === 'dark') document.body.classList.add('dark');
|
||||
|
||||
// Expand
|
||||
tg.expand();
|
||||
}
|
||||
|
||||
// Fallback for browser testing
|
||||
if (!tgUser) {
|
||||
console.warn("No Telegram user detected, using mock user");
|
||||
tgUser = { id: 123456789, first_name: 'Test', username: 'testuser' };
|
||||
}
|
||||
|
||||
// Update UI with User Info
|
||||
const sidebarName = document.getElementById('sidebar-name');
|
||||
const sidebarAvatar = document.getElementById('sidebar-avatar');
|
||||
if (sidebarName) sidebarName.textContent = tgUser.first_name || tgUser.username;
|
||||
if (sidebarAvatar) sidebarAvatar.textContent = (tgUser.first_name || 'U')[0].toUpperCase();
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/user/${tgUser.id}`);
|
||||
if (!res.ok) throw new Error("Failed to fetch user");
|
||||
const data = await res.json();
|
||||
|
||||
const statusEl = document.getElementById('dash-status');
|
||||
const daysEl = document.getElementById('dash-days');
|
||||
const dataEl = document.getElementById('dash-data');
|
||||
const planEl = document.getElementById('sub-plan-name');
|
||||
const expireEl = document.getElementById('sub-expire-date');
|
||||
|
||||
if (statusEl) statusEl.textContent = data.status;
|
||||
if (daysEl) daysEl.textContent = data.days_left;
|
||||
if (dataEl) dataEl.textContent = `${data.data_usage || 0} GB`;
|
||||
if (planEl) planEl.textContent = data.plan;
|
||||
if (expireEl) expireEl.textContent = data.expire_date;
|
||||
|
||||
// Colorize status
|
||||
if (data.status === 'Active') {
|
||||
document.querySelector('.stat-info .value').style.color = '#4ade80';
|
||||
} else {
|
||||
document.querySelector('.stat-info .value').style.color = '#f87171';
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Show error state?
|
||||
}
|
||||
}
|
||||
|
||||
async function loadShop() {
|
||||
const container = document.getElementById('plans-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Loading plans...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/plans`);
|
||||
if (!res.ok) throw new Error("Failed to fetch plans");
|
||||
const plans = await res.json();
|
||||
|
||||
container.innerHTML = '';
|
||||
plans.forEach(plan => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card glass plan-card';
|
||||
|
||||
// Features list generation
|
||||
const features = [
|
||||
`${plan.data_limit} GB Data`,
|
||||
`${plan.days} Days`,
|
||||
'High Speed'
|
||||
];
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plan-name">${plan.name}</div>
|
||||
<div class="plan-price">${plan.price} XTR</div>
|
||||
<ul class="plan-features">
|
||||
${features.map(f => `<li>${f}</li>`).join('')}
|
||||
</ul>
|
||||
<button class="btn-primary" onclick="buyPlan('${plan.id}')">Buy for ${plan.price}</button>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
} catch (e) {
|
||||
container.innerHTML = 'Error loading plans.';
|
||||
}
|
||||
}
|
||||
|
||||
async function buyPlan(planId) {
|
||||
if (!window.Telegram || !window.Telegram.WebApp) {
|
||||
alert("Payment only works inside Telegram!");
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.activeElement;
|
||||
const originalText = btn.innerText;
|
||||
btn.innerText = 'Creating Invoice...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/create-invoice`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: tgUser.id,
|
||||
plan_id: planId
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.invoice_link) {
|
||||
window.Telegram.WebApp.openInvoice(data.invoice_link, (status) => {
|
||||
if (status === 'paid') {
|
||||
window.Telegram.WebApp.showAlert('Payment Successful! Subscription activated.');
|
||||
router('dashboard');
|
||||
} else if (status === 'cancelled') {
|
||||
// User cancelled
|
||||
} else {
|
||||
window.Telegram.WebApp.showAlert('Payment failed or pending.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.Telegram.WebApp.showAlert('Error creating invoice: ' + data.error);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
window.Telegram.WebApp.showAlert('Network error');
|
||||
console.error(e);
|
||||
} finally {
|
||||
btn.innerText = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
document.getElementById('profile-tg-id').textContent = tgUser.id;
|
||||
document.getElementById('profile-username').value = '@' + (tgUser.username || 'unknown');
|
||||
}
|
||||
|
||||
// Init
|
||||
router('dashboard');
|
||||
144
web_app/static/js/background.js
Normal file
144
web_app/static/js/background.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const container = document.getElementById('stars-container');
|
||||
|
||||
// Create Canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
container.appendChild(canvas);
|
||||
|
||||
let width, height;
|
||||
let stars = [];
|
||||
let comets = [];
|
||||
|
||||
function resize() {
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
initStars();
|
||||
}
|
||||
|
||||
class Star {
|
||||
constructor() {
|
||||
this.reset();
|
||||
this.y = Math.random() * height; // Initial random y
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height;
|
||||
this.z = Math.random() * 2 + 0.5; // Depth/Size/Speed
|
||||
this.baseSize = Math.random() * 1.5;
|
||||
this.alpha = Math.random() * 0.5 + 0.1;
|
||||
this.twinkle = Math.random() * 0.05;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.alpha += this.twinkle;
|
||||
if (this.alpha > 0.8 || this.alpha < 0.1) this.twinkle = -this.twinkle;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${this.alpha})`;
|
||||
ctx.beginPath();
|
||||
const size = this.baseSize * (this.z / 2);
|
||||
ctx.arc(this.x, this.y, size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
class Comet {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height * 0.5;
|
||||
this.len = Math.random() * 80 + 20;
|
||||
this.speed = Math.random() * 5 + 2;
|
||||
this.angle = Math.PI / 4 + (Math.random() - 0.5) * 0.2; // 45 degrees
|
||||
this.active = false;
|
||||
this.wait = Math.random() * 200 + 50;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.active) {
|
||||
this.wait--;
|
||||
if (this.wait <= 0) {
|
||||
this.active = true;
|
||||
this.x = Math.random() * width - 200; // Start off screen slightly
|
||||
this.y = Math.random() * height * 0.5;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.x += Math.cos(this.angle) * this.speed;
|
||||
this.y += Math.sin(this.angle) * this.speed;
|
||||
|
||||
if (this.x > width + 100 || this.y > height + 100) {
|
||||
this.active = false;
|
||||
this.reset();
|
||||
this.wait = Math.random() * 500 + 100; // Wait longer before next
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.active) return;
|
||||
|
||||
// Gradient tail
|
||||
const grad = ctx.createLinearGradient(
|
||||
this.x, this.y,
|
||||
this.x - Math.cos(this.angle) * this.len,
|
||||
this.y - Math.sin(this.angle) * this.len
|
||||
);
|
||||
grad.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
|
||||
grad.addColorStop(1, 'rgba(99, 102, 241, 0)'); // Fade to purple blue
|
||||
|
||||
ctx.strokeStyle = grad;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.x, this.y);
|
||||
ctx.lineTo(
|
||||
this.x - Math.cos(this.angle) * this.len,
|
||||
this.y - Math.sin(this.angle) * this.len
|
||||
);
|
||||
ctx.stroke();
|
||||
|
||||
// Head
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Glow
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = '#6366f1';
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function initStars() {
|
||||
stars = [];
|
||||
comets = [];
|
||||
for (let i = 0; i < 150; i++) stars.push(new Star());
|
||||
for (let i = 0; i < 3; i++) comets.push(new Comet());
|
||||
}
|
||||
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Background gradient for depth
|
||||
// ctx.fillStyle = 'rgba(5, 5, 16, 0.2)';
|
||||
// ctx.fillRect(0,0,width,height);
|
||||
|
||||
stars.forEach(s => { s.update(); s.draw(); });
|
||||
comets.forEach(c => { c.update(); c.draw(); });
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
animate();
|
||||
Reference in New Issue
Block a user