537 lines
23 KiB
HTML
537 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Путеводитель — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="/css/ls.css" />
|
||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<style>
|
||
.sb-content { overflow: hidden !important; background: #09061c !important; padding: 0 !important; }
|
||
|
||
.gx-wrap {
|
||
position: relative; width: 100%; height: 100%;
|
||
overflow: hidden; user-select: none;
|
||
}
|
||
|
||
/* ── Canvas background ── */
|
||
#gx-canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; }
|
||
|
||
/* ── Nodes layer ── */
|
||
#gx-nodes { position: absolute; inset: 0; z-index: 2; }
|
||
|
||
/* ── Node ── */
|
||
.gx-node {
|
||
position: absolute;
|
||
transform: translate(-50%, -50%);
|
||
display: flex; flex-direction: column; align-items: center; gap: 7px;
|
||
cursor: pointer;
|
||
animation: gxNodeIn 0.55s ease both;
|
||
}
|
||
@keyframes gxNodeIn {
|
||
from { opacity: 0; transform: translate(-50%, -45%) scale(0.55); }
|
||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||
}
|
||
|
||
.gx-orb {
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at 38% 32%, rgba(var(--nc), 0.42), rgba(var(--nc), 0.07) 72%);
|
||
border: 1.5px solid rgba(var(--nc), 0.42);
|
||
box-shadow: 0 0 14px rgba(var(--nc), 0.16), inset 0 1px 0 rgba(255,255,255,0.13);
|
||
display: flex; align-items: center; justify-content: center;
|
||
width: 50px; height: 50px;
|
||
transition: transform 0.22s cubic-bezier(.34,1.56,.64,1), box-shadow 0.22s, border-color 0.22s;
|
||
position: relative;
|
||
}
|
||
.gx-orb svg { width: 20px; height: 20px; color: rgb(var(--nc)); stroke: rgb(var(--nc)); flex-shrink: 0; }
|
||
|
||
.gx-node:hover .gx-orb {
|
||
transform: scale(1.25);
|
||
box-shadow: 0 0 32px rgba(var(--nc), 0.6), 0 0 64px rgba(var(--nc), 0.18), inset 0 1px 0 rgba(255,255,255,0.2);
|
||
border-color: rgba(var(--nc), 0.85);
|
||
}
|
||
.gx-node.gx-main .gx-orb { width: 66px; height: 66px; }
|
||
.gx-node.gx-main .gx-orb svg { width: 26px; height: 26px; }
|
||
|
||
/* Pulse ring on main node */
|
||
.gx-node.gx-main .gx-orb::after {
|
||
content: '';
|
||
position: absolute; inset: -8px; border-radius: 50%;
|
||
border: 1.5px solid rgba(var(--nc), 0.22);
|
||
animation: gxPulse 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes gxPulse {
|
||
0%,100% { transform: scale(1); opacity: 0.6; }
|
||
50% { transform: scale(1.12); opacity: 0.15; }
|
||
}
|
||
|
||
.gx-label {
|
||
font-family: 'Manrope', sans-serif;
|
||
font-size: 0.68rem; font-weight: 700;
|
||
color: rgba(255,255,255,0.68);
|
||
text-shadow: 0 1px 8px rgba(0,0,0,0.7);
|
||
white-space: nowrap;
|
||
transition: color 0.18s;
|
||
}
|
||
.gx-node:hover .gx-label { color: rgba(255,255,255,0.95); }
|
||
|
||
/* Dim non-category nodes on filter */
|
||
.gx-node.gx-dim { opacity: 0.12; transition: opacity 0.3s; }
|
||
.gx-node.gx-connected .gx-orb {
|
||
box-shadow: 0 0 20px rgba(var(--nc), 0.35), inset 0 1px 0 rgba(255,255,255,0.13);
|
||
}
|
||
|
||
/* Category colors */
|
||
.gx-node[data-cat="study"] { --nc: 155,93,229; }
|
||
.gx-node[data-cat="practice"] { --nc: 6,214,224; }
|
||
.gx-node[data-cat="games"] { --nc: 241,91,181; }
|
||
.gx-node[data-cat="personal"] { --nc: 255,179,71; }
|
||
|
||
/* ── Tooltip ── */
|
||
.gx-tt {
|
||
position: fixed; z-index: 300;
|
||
background: rgba(10,6,24,0.95);
|
||
backdrop-filter: blur(24px) saturate(1.4);
|
||
border: 1px solid rgba(255,255,255,0.09);
|
||
border-radius: 18px;
|
||
padding: 15px 17px 17px;
|
||
width: 238px;
|
||
pointer-events: none;
|
||
opacity: 0; transform: translateY(8px) scale(0.97);
|
||
transition: opacity 0.18s, transform 0.18s;
|
||
box-shadow: 0 20px 56px rgba(0,0,0,0.55);
|
||
}
|
||
.gx-tt.vis { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
|
||
|
||
.gx-tt-badge {
|
||
display: inline-flex; align-items: center;
|
||
font-size: 0.59rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase;
|
||
padding: 2px 9px; border-radius: 99px; margin-bottom: 9px;
|
||
}
|
||
.gx-tt-title {
|
||
font-family: 'Unbounded', sans-serif;
|
||
font-size: 0.83rem; font-weight: 800; color: #fff; margin-bottom: 7px; line-height: 1.2;
|
||
}
|
||
.gx-tt-desc {
|
||
font-size: 0.75rem; color: rgba(255,255,255,0.5); line-height: 1.55; margin-bottom: 13px;
|
||
}
|
||
.gx-tt-btn {
|
||
display: inline-flex; align-items: center; gap: 7px;
|
||
font-size: 0.73rem; font-weight: 700;
|
||
padding: 7px 16px; border-radius: 99px;
|
||
text-decoration: none; transition: filter 0.18s;
|
||
}
|
||
.gx-tt-btn:hover { filter: brightness(1.15); }
|
||
.gx-tt-btn svg { width: 12px; height: 12px; }
|
||
|
||
/* ── Header ── */
|
||
.gx-hdr {
|
||
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
|
||
padding: 18px 28px;
|
||
background: linear-gradient(to bottom, rgba(9,6,28,0.88) 0%, transparent 100%);
|
||
display: flex; align-items: center; gap: 20px;
|
||
pointer-events: none;
|
||
}
|
||
.gx-hdr-logo {
|
||
font-family: 'Unbounded', sans-serif;
|
||
font-size: 0.95rem; font-weight: 800; color: #fff; line-height: 1;
|
||
margin-bottom: 2px;
|
||
}
|
||
.gx-hdr-sub { font-size: 0.62rem; color: rgba(255,255,255,0.38); letter-spacing: 0.08em; text-transform: uppercase; }
|
||
|
||
/* Beginner path hint */
|
||
.gx-hint {
|
||
position: absolute; top: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
|
||
background: rgba(255,255,255,0.05); backdrop-filter: blur(14px);
|
||
border: 1px solid rgba(255,255,255,0.1); border-radius: 99px;
|
||
padding: 7px 18px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-size: 0.69rem; color: rgba(255,255,255,0.5); white-space: nowrap;
|
||
pointer-events: none;
|
||
}
|
||
.gx-hint strong { color: rgba(255,255,255,0.82); font-weight: 700; }
|
||
.gx-hint-sep { color: rgba(255,255,255,0.22); margin: 0 2px; }
|
||
|
||
/* ── Category filters (bottom center) ── */
|
||
.gx-filters {
|
||
position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 10;
|
||
display: flex; align-items: center; gap: 5px;
|
||
background: rgba(255,255,255,0.05);
|
||
backdrop-filter: blur(16px);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 99px; padding: 5px 8px;
|
||
}
|
||
.gx-fil {
|
||
display: flex; align-items: center; gap: 6px;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.71rem; font-weight: 700;
|
||
color: rgba(255,255,255,0.45);
|
||
background: none; border: none; cursor: pointer;
|
||
padding: 5px 13px; border-radius: 99px;
|
||
transition: all 0.18s; white-space: nowrap;
|
||
}
|
||
.gx-fil:hover { color: rgba(255,255,255,0.75); background: rgba(255,255,255,0.08); }
|
||
.gx-fil.active { color: #fff; background: rgba(255,255,255,0.12); }
|
||
.gx-fil-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||
|
||
/* ── Module count (bottom right) ── */
|
||
.gx-count {
|
||
position: absolute; bottom: 26px; right: 28px; z-index: 10;
|
||
font-size: 0.61rem; color: rgba(255,255,255,0.2);
|
||
letter-spacing: 0.07em; text-transform: uppercase;
|
||
}
|
||
|
||
/* ── Mobile: simple grid fallback ── */
|
||
.gx-mobile { display: none; padding: 24px 16px 60px; }
|
||
.gx-mob-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 24px; }
|
||
.gx-mob-card {
|
||
display: flex; align-items: center; gap: 12px;
|
||
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 16px; padding: 14px 14px;
|
||
text-decoration: none; transition: background 0.18s;
|
||
}
|
||
.gx-mob-card:hover { background: rgba(255,255,255,0.1); }
|
||
.gx-mob-icon {
|
||
width: 40px; height: 40px; border-radius: 12px; flex-shrink: 0;
|
||
background: rgba(var(--nc), 0.15); border: 1px solid rgba(var(--nc), 0.3);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.gx-mob-icon svg { width: 18px; height: 18px; color: rgb(var(--nc)); stroke: rgb(var(--nc)); }
|
||
.gx-mob-name { font-size: 0.78rem; font-weight: 700; color: #fff; }
|
||
.gx-mob-cat { font-size: 0.62rem; color: rgba(255,255,255,0.4); margin-top: 1px; }
|
||
.gx-mob-sec-title {
|
||
font-family: 'Unbounded', sans-serif;
|
||
font-size: 0.68rem; font-weight: 800; color: rgba(255,255,255,0.4);
|
||
text-transform: uppercase; letter-spacing: 0.08em;
|
||
margin: 16px 0 8px; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.gx-mob-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||
|
||
@media (max-width: 768px) {
|
||
.gx-canvas-ui { display: none !important; }
|
||
#gx-canvas { display: none !important; }
|
||
#gx-nodes { display: none !important; }
|
||
.gx-mobile { display: block !important; }
|
||
.sb-content { overflow-y: auto !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
<div class="notif-drop" id="notif-drop"></div>
|
||
<div class="sb-content">
|
||
<div class="gx-wrap" id="gx-wrap">
|
||
|
||
<!-- Canvas background -->
|
||
<canvas id="gx-canvas"></canvas>
|
||
|
||
<!-- Nodes -->
|
||
<div id="gx-nodes"></div>
|
||
|
||
<!-- Tooltip -->
|
||
<div class="gx-tt" id="gx-tt">
|
||
<div class="gx-tt-badge" id="tt-badge"></div>
|
||
<div class="gx-tt-title" id="tt-title"></div>
|
||
<div class="gx-tt-desc" id="tt-desc"></div>
|
||
<a class="gx-tt-btn" id="tt-btn" href="#">
|
||
Открыть
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Header (canvas UI elements) -->
|
||
<div class="gx-canvas-ui">
|
||
<div class="gx-hdr">
|
||
<div>
|
||
<div class="gx-hdr-logo">LearnSpace</div>
|
||
<div class="gx-hdr-sub">Карта модулей</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Beginner hint -->
|
||
<div class="gx-hint">
|
||
<strong>Новичок?</strong>
|
||
<span class="gx-hint-sep">—</span>
|
||
Начни с <strong>Дашборда</strong>
|
||
<span class="gx-hint-sep">→</span>
|
||
<strong>Теории</strong>
|
||
<span class="gx-hint-sep">→</span>
|
||
<strong>Лаборатории</strong>
|
||
</div>
|
||
|
||
<!-- Category filters -->
|
||
<div class="gx-filters">
|
||
<button class="gx-fil active" data-cat="all">Все</button>
|
||
<button class="gx-fil" data-cat="study">
|
||
<span class="gx-fil-dot" style="background:#9B5DE5"></span>Учёба
|
||
</button>
|
||
<button class="gx-fil" data-cat="practice">
|
||
<span class="gx-fil-dot" style="background:#06D6E0"></span>Практика
|
||
</button>
|
||
<button class="gx-fil" data-cat="games">
|
||
<span class="gx-fil-dot" style="background:#F15BB5"></span>Игры
|
||
</button>
|
||
<button class="gx-fil" data-cat="personal">
|
||
<span class="gx-fil-dot" style="background:#FFB347"></span>Личное
|
||
</button>
|
||
</div>
|
||
|
||
<div class="gx-count" id="gx-count"></div>
|
||
</div>
|
||
|
||
<!-- Mobile fallback (hidden on desktop) -->
|
||
<div class="gx-mobile" id="gx-mobile"></div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/search.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
<script>
|
||
LS.initPage();
|
||
|
||
/* ══ Module data ══ */
|
||
const MODULES = [
|
||
/* ── Учёба ── */
|
||
{ id:'dashboard', href:'/dashboard', label:'Дашборд', desc:'Центр управления: задания, XP, уровни, прогресс и стрики', cat:'study', icon:'home', x:0.34, y:0.29 },
|
||
{ id:'theory', href:'/theory', label:'Теория', desc:'Уроки с текстом, формулами, медиа и встроенными мини-тестами', cat:'study', icon:'brain', x:0.17, y:0.38 },
|
||
{ id:'library', href:'/library', label:'Библиотека', desc:'Файлы и материалы от учителя: конспекты, презентации, документы', cat:'study', icon:'book-open', x:0.30, y:0.48 },
|
||
{ id:'knowledge', href:'/knowledge-map', label:'Карта знаний', desc:'Интерактивный граф связей между темами. Увидь всю картину', cat:'study', icon:'share-2', x:0.18, y:0.52 },
|
||
/* ── Практика ── */
|
||
{ id:'lab', href:'/lab', label:'Лаборатория', desc:'30+ интерактивных симуляций по физике, химии и биологии', cat:'practice', icon:'atom', x:0.64, y:0.25 },
|
||
{ id:'biochem', href:'/biochem', label:'Биохимия', desc:'Молекулы, реакции, метаболические пути и задачи-челленджи', cat:'practice', icon:'flask-conical', x:0.76, y:0.17 },
|
||
{ id:'classroom', href:'/classroom', label:'Онлайн-урок', desc:'Живой урок с учителем: интерактивная доска, чат и видеосвязь', cat:'practice', icon:'presentation', x:0.74, y:0.40 },
|
||
{ id:'board', href:'/board', label:'Доска', desc:'Свободная доска для рисования, заметок, схем и формул', cat:'practice', icon:'layout-dashboard', x:0.58, y:0.38 },
|
||
/* ── Игры ── */
|
||
{ id:'hangman', href:'/hangman', label:'Виселица', desc:'Угадывай слова по теме урока — учись играя', cat:'games', icon:'gamepad-2', x:0.20, y:0.67 },
|
||
{ id:'crossword', href:'/crossword', label:'Кроссворд', desc:'Тематические кроссворды по предметам. Проверь словарный запас', cat:'games', icon:'grid-3x3', x:0.35, y:0.74 },
|
||
{ id:'pet', href:'/pet', label:'Питомец', desc:'Виртуальный питомец растёт вместе с твоим прогрессом в учёбе', cat:'games', icon:'heart', x:0.13, y:0.77 },
|
||
{ id:'collection', href:'/collection', label:'Коллекция', desc:'Собирай карточки и предметы за достижения и активность', cat:'games', icon:'layers', x:0.46, y:0.78 },
|
||
{ id:'redbook', href:'/red-book', label:'Красная книга', desc:'Редкие виды животных и растений. Изучай биосферу Земли', cat:'games', icon:'leaf', x:0.27, y:0.83 },
|
||
/* ── Личное ── */
|
||
{ id:'profile', href:'/profile', label:'Профиль', desc:'Имя, аватар, статистика достижений, значки и история активности', cat:'personal', icon:'user', x:0.70, y:0.62 },
|
||
{ id:'history', href:'/lesson-history',label:'Архив уроков', desc:'Записи прошедших онлайн-уроков. Доски и материалы урока', cat:'personal', icon:'archive', x:0.82, y:0.54 },
|
||
];
|
||
|
||
const CONNECTIONS = [
|
||
['dashboard','theory'], ['dashboard','library'], ['dashboard','hangman'],
|
||
['theory','library'], ['theory','knowledge'], ['theory','lab'],
|
||
['lab','biochem'], ['lab','classroom'], ['classroom','board'],
|
||
['classroom','history'], ['hangman','crossword'], ['hangman','pet'],
|
||
['pet','collection'], ['collection','redbook'], ['profile','dashboard'],
|
||
['profile','history'], ['library','lab'], ['board','crossword'],
|
||
];
|
||
|
||
const CAT_INFO = {
|
||
study: { label:'Учёба', rgb:[155,93,229] },
|
||
practice: { label:'Практика', rgb:[6,214,224] },
|
||
games: { label:'Игры', rgb:[241,91,181] },
|
||
personal: { label:'Личное', rgb:[255,179,71] },
|
||
};
|
||
|
||
/* ══ Build HTML nodes ══ */
|
||
const nodesWrap = document.getElementById('gx-nodes');
|
||
const nodeMap = {};
|
||
|
||
MODULES.forEach((m, i) => {
|
||
const [r,g,b] = CAT_INFO[m.cat].rgb;
|
||
const el = document.createElement('div');
|
||
el.className = 'gx-node' + (m.id === 'dashboard' ? ' gx-main' : '');
|
||
el.dataset.id = m.id;
|
||
el.dataset.cat = m.cat;
|
||
el.style.cssText = `left:${m.x*100}%;top:${m.y*100}%;--nc:${r},${g},${b};animation-delay:${i*0.045}s`;
|
||
el.innerHTML = `
|
||
<div class="gx-orb"><i data-lucide="${m.icon}"></i></div>
|
||
<span class="gx-label">${m.label}</span>`;
|
||
el.addEventListener('mouseenter', () => showTip(m, el));
|
||
el.addEventListener('mouseleave', queueHideTip);
|
||
el.addEventListener('click', () => location.href = m.href);
|
||
nodesWrap.appendChild(el);
|
||
nodeMap[m.id] = el;
|
||
});
|
||
|
||
if (window.lucide) lucide.createIcons();
|
||
document.getElementById('gx-count').textContent = MODULES.length + ' модулей';
|
||
|
||
/* ══ Tooltip ══ */
|
||
const tt = document.getElementById('gx-tt');
|
||
const ttBadge = document.getElementById('tt-badge');
|
||
const ttTitle = document.getElementById('tt-title');
|
||
const ttDesc = document.getElementById('tt-desc');
|
||
const ttBtn = document.getElementById('tt-btn');
|
||
let hideTimer = null;
|
||
let hoveredId = null;
|
||
|
||
function showTip(m, nodeEl) {
|
||
clearTimeout(hideTimer);
|
||
hoveredId = m.id;
|
||
const [r,g,b] = CAT_INFO[m.cat].rgb;
|
||
ttBadge.textContent = CAT_INFO[m.cat].label;
|
||
ttBadge.style.cssText = `background:rgba(${r},${g},${b},0.14);color:rgb(${r},${g},${b});border:1px solid rgba(${r},${g},${b},0.3)`;
|
||
ttTitle.textContent = m.label;
|
||
ttDesc.textContent = m.desc;
|
||
ttBtn.href = m.href;
|
||
ttBtn.style.cssText = `background:rgba(${r},${g},${b},0.14);color:rgb(${r},${g},${b});border:1px solid rgba(${r},${g},${b},0.3)`;
|
||
|
||
// Position near node
|
||
const rect = nodeEl.getBoundingClientRect();
|
||
const cx = rect.left + rect.width / 2;
|
||
const cy = rect.top;
|
||
const W = window.innerWidth, H = window.innerHeight;
|
||
let left = cx + 24, top = cy - 16;
|
||
if (left + 250 > W - 8) left = cx - 258;
|
||
if (top + 200 > H - 8) top = H - 208;
|
||
if (top < 8) top = 8;
|
||
tt.style.left = left + 'px';
|
||
tt.style.top = top + 'px';
|
||
tt.classList.add('vis');
|
||
|
||
// Highlight connected nodes
|
||
highlightConnected(m.id);
|
||
}
|
||
|
||
function queueHideTip() {
|
||
hideTimer = setTimeout(() => { tt.classList.remove('vis'); hoveredId = null; highlightConnected(null); }, 180);
|
||
}
|
||
|
||
tt.addEventListener('mouseenter', () => clearTimeout(hideTimer));
|
||
tt.addEventListener('mouseleave', queueHideTip);
|
||
|
||
function highlightConnected(id) {
|
||
Object.values(nodeMap).forEach(el => {
|
||
el.classList.remove('gx-connected');
|
||
if (!id) return;
|
||
const nid = el.dataset.id;
|
||
const linked = CONNECTIONS.some(([a,b]) => (a===id && b===nid) || (b===id && a===nid));
|
||
if (linked) el.classList.add('gx-connected');
|
||
});
|
||
}
|
||
|
||
/* ══ Canvas ══ */
|
||
const canvas = document.getElementById('gx-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const wrap = document.getElementById('gx-wrap');
|
||
|
||
// Stars
|
||
const STARS = Array.from({length:220}, () => ({
|
||
x: Math.random(), y: Math.random(),
|
||
r: Math.random() * 1.2 + 0.25,
|
||
a: Math.random() * 0.55 + 0.2,
|
||
sp: Math.random() * 0.012 + 0.003,
|
||
ph: Math.random() * Math.PI * 2,
|
||
}));
|
||
|
||
// Nebula zones (x,y,r in 0-1 space)
|
||
const NEBULAS = [
|
||
{ x:0.22, y:0.40, r:0.26, c:[155,93,229], a:0.10 },
|
||
{ x:0.68, y:0.30, r:0.23, c:[6,214,224], a:0.08 },
|
||
{ x:0.27, y:0.74, r:0.22, c:[241,91,181], a:0.09 },
|
||
{ x:0.74, y:0.60, r:0.20, c:[255,179,71], a:0.07 },
|
||
];
|
||
|
||
let frame = 0;
|
||
|
||
function resize() {
|
||
canvas.width = wrap.clientWidth || window.innerWidth;
|
||
canvas.height = wrap.clientHeight || window.innerHeight;
|
||
}
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
function drawFrame() {
|
||
const W = canvas.width, H = canvas.height;
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// Deep space background
|
||
const bg = ctx.createRadialGradient(W*0.42, H*0.38, 0, W*0.5, H*0.5, W*0.82);
|
||
bg.addColorStop(0, '#1c0e40');
|
||
bg.addColorStop(0.45,'#110832');
|
||
bg.addColorStop(1, '#08051a');
|
||
ctx.fillStyle = bg;
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
// Nebula clouds
|
||
const t = frame * 0.007;
|
||
NEBULAS.forEach((n, i) => {
|
||
const pulse = 1 + Math.sin(t + i * 1.7) * 0.08;
|
||
const g = ctx.createRadialGradient(n.x*W, n.y*H, 0, n.x*W, n.y*H, n.r * W * pulse);
|
||
g.addColorStop(0, `rgba(${n.c},${n.a})`);
|
||
g.addColorStop(0.45,`rgba(${n.c},${n.a * 0.4})`);
|
||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
||
ctx.fillStyle = g;
|
||
ctx.fillRect(0, 0, W, H);
|
||
});
|
||
|
||
// Twinkling stars
|
||
STARS.forEach(s => {
|
||
const tw = Math.sin(frame * s.sp + s.ph) * 0.38 + 0.62;
|
||
ctx.globalAlpha = s.a * tw;
|
||
ctx.fillStyle = '#fff';
|
||
ctx.beginPath();
|
||
ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI*2);
|
||
ctx.fill();
|
||
});
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Connection lines
|
||
ctx.save();
|
||
ctx.setLineDash([3, 10]);
|
||
CONNECTIONS.forEach(([a, b]) => {
|
||
const ma = MODULES.find(m => m.id === a);
|
||
const mb = MODULES.find(m => m.id === b);
|
||
if (!ma || !mb) return;
|
||
const isHovered = hoveredId && (a === hoveredId || b === hoveredId);
|
||
ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.38)' : 'rgba(255,255,255,0.065)';
|
||
ctx.lineWidth = isHovered ? 1.4 : 0.7;
|
||
ctx.beginPath();
|
||
ctx.moveTo(ma.x * W, ma.y * H);
|
||
ctx.lineTo(mb.x * W, mb.y * H);
|
||
ctx.stroke();
|
||
});
|
||
ctx.restore();
|
||
|
||
frame++;
|
||
requestAnimationFrame(drawFrame);
|
||
}
|
||
drawFrame();
|
||
|
||
/* ══ Category filter ══ */
|
||
document.querySelectorAll('.gx-fil').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.gx-fil').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
const cat = btn.dataset.cat;
|
||
Object.values(nodeMap).forEach(el => {
|
||
el.classList.toggle('gx-dim', cat !== 'all' && el.dataset.cat !== cat);
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ══ Mobile fallback: card grid ══ */
|
||
const mobEl = document.getElementById('gx-mobile');
|
||
const byCat = {};
|
||
MODULES.forEach(m => { (byCat[m.cat] = byCat[m.cat]||[]).push(m); });
|
||
['study','practice','games','personal'].forEach(cat => {
|
||
const info = CAT_INFO[cat];
|
||
const [r,g,b] = info.rgb;
|
||
let html = `<div class="gx-mob-sec-title"><span class="gx-mob-dot" style="background:rgb(${r},${g},${b})"></span>${info.label}</div><div class="gx-mob-grid">`;
|
||
(byCat[cat]||[]).forEach(m => {
|
||
html += `<a href="${m.href}" class="gx-mob-card" data-cat="${cat}" style="--nc:${r},${g},${b}">
|
||
<div class="gx-mob-icon"><i data-lucide="${m.icon}"></i></div>
|
||
<div><div class="gx-mob-name">${m.label}</div><div class="gx-mob-cat">${info.label}</div></div>
|
||
</a>`;
|
||
});
|
||
html += '</div>';
|
||
mobEl.insertAdjacentHTML('beforeend', html);
|
||
});
|
||
if (window.lucide) lucide.createIcons();
|
||
</script>
|
||
</body>
|
||
</html>
|