refactor code

This commit is contained in:
kankys committed 2026-05-15 23:22:58 +02:00
1 parent 9a38077576
commit b13422d4af
13 files changed
+11934

No files matched your search

+647
View File
@@ -0,0 +1,647 @@
/**
* main.js — ApolloNvim Web Interaktivita
* Vanilla ES6+ JavaScript bez externích závislostí.
*/
"use strict";
// ════════════════════════════════════════════════════════════════════════════
// 8. LOADING SCREEN — fade-out při načtení stránky
// (registrujeme jako první, aby běžel co nejdříve)
// ════════════════════════════════════════════════════════════════════════════
function initLoadingScreen() {
const loader = document.getElementById("loading-screen");
if (!loader) return;
const hide = () => {
loader.classList.add("fade-out");
// Po dokončení animace element skryjeme úplně
loader.addEventListener(
"transitionend",
() => {
loader.style.display = "none";
},
{ once: true },
);
};
if (document.readyState === "complete") {
hide();
} else {
window.addEventListener("load", hide);
}
}
initLoadingScreen();
// ════════════════════════════════════════════════════════════════════════════
// Spuštění zbytku funkcí po načtení DOM
// ════════════════════════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => {
// ── Pořadí inicializace ──────────────────────────────────────────────────
initNavigation();
initHamburger();
initScrollAnimations();
initTabs();
initTypingEffects();
initCounters();
initCopyButtons();
initPluginFilters();
initTooltips();
initBackToTop();
});
// ════════════════════════════════════════════════════════════════════════════
// 1. NAVIGATION
// ════════════════════════════════════════════════════════════════════════════
function initNavigation() {
const navbar =
document.querySelector(".navbar") || document.querySelector("nav");
const hamburger =
document.querySelector(".hamburger") ||
document.querySelector(".menu-toggle");
const navMenu =
document.querySelector(".nav-menu") || document.querySelector(".nav-links");
const navLinks = document.querySelectorAll(".nav-links a, .nav-menu a");
// ── Třída "scrolled" při odrolování ──
if (navbar) {
const onScroll = () => {
navbar.classList.toggle("scrolled", window.scrollY > 50);
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll(); // Okamžitá inicializace
}
// ── Hamburger menu toggle ──
if (hamburger && navMenu) {
hamburger.addEventListener("click", () => {
const isOpen = navMenu.classList.toggle("open");
hamburger.classList.toggle("active", isOpen);
hamburger.setAttribute("aria-expanded", String(isOpen));
// Zabránit scrollování pozadí při otevřeném menu
document.body.classList.toggle("menu-open", isOpen);
});
}
// ── Smooth scroll pro anchor linky ──
document.querySelectorAll('a[href^="#"]').forEach((link) => {
link.addEventListener("click", (e) => {
const targetId = link.getAttribute("href");
if (targetId === "#") return;
const target = document.querySelector(targetId);
if (!target) return;
e.preventDefault();
const navHeight = navbar ? navbar.offsetHeight : 0;
const top =
target.getBoundingClientRect().top + window.scrollY - navHeight - 16;
window.scrollTo({ top, behavior: "smooth" });
// Zavření mobilního menu po kliknutí na odkaz
closeNavMenu(navMenu, hamburger);
});
});
// ── Zavření menu kliknutím mimo ──
document.addEventListener("click", (e) => {
if (!navMenu || !hamburger) return;
if (!navMenu.contains(e.target) && !hamburger.contains(e.target)) {
closeNavMenu(navMenu, hamburger);
}
});
// ── Aktivní stav nav linků dle URL ──
setActiveNavLink(navLinks);
}
/** Zavře mobilní navigation menu */
function closeNavMenu(navMenu, hamburger) {
if (!navMenu) return;
navMenu.classList.remove("open");
document.body.classList.remove("menu-open");
if (hamburger) {
hamburger.classList.remove("active");
hamburger.setAttribute("aria-expanded", "false");
}
}
/** Označí aktivní nav link dle aktuální URL */
function setActiveNavLink(links) {
const path = window.location.pathname;
links.forEach((link) => {
const href = link.getAttribute("href") || "";
// Přesná shoda nebo shoda části cesty (pro víceúrovňové URL)
const isActive =
href !== "#" &&
(path === href ||
path.endsWith(href) ||
(href !== "/" && path.includes(href)));
link.classList.toggle("active", isActive);
if (isActive) link.setAttribute("aria-current", "page");
});
}
// ════════════════════════════════════════════════════════════════════════════
// 2. SCROLL ANIMACE — Intersection Observer
// ════════════════════════════════════════════════════════════════════════════
function initScrollAnimations() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
// Odpojit observer po animaci (každý element se animuje jen jednou)
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.1,
rootMargin: "0px 0px -40px 0px", // spustí se dříve, než element dosáhne dna okna
},
);
document
.querySelectorAll(".animate-on-scroll")
.forEach((el) => observer.observe(el));
}
// ════════════════════════════════════════════════════════════════════════════
// 3. TAB SYSTEM — CZ / EN přepínač (a jakékoli další záložky)
// ════════════════════════════════════════════════════════════════════════════
function initTabs() {
// Podpora více nezávislých tab skupin na stránce
document.querySelectorAll("[data-tab-group]").forEach((group) => {
const buttons = group.querySelectorAll(".tab-btn");
const contents = group.querySelectorAll(".tab-content");
const storageKey = `apollo-tab-${group.dataset.tabGroup}`;
/** Přepne na záložku s daným id */
const switchTab = (targetId, saveToStorage = true) => {
buttons.forEach((btn) => {
const active = btn.dataset.tab === targetId;
btn.classList.toggle("active", active);
btn.setAttribute("aria-selected", String(active));
});
contents.forEach((content) => {
if (content.dataset.tabContent === targetId) {
content.classList.add("active");
content.classList.remove("hidden");
// Fade-in efekt
content.style.opacity = "0";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
content.style.opacity = "1";
});
});
} else {
content.classList.remove("active");
content.classList.add("hidden");
}
});
if (saveToStorage) {
try {
localStorage.setItem(storageKey, targetId);
} catch (_) {}
}
};
// Event listenery
buttons.forEach((btn) => {
btn.addEventListener("click", () => switchTab(btn.dataset.tab));
});
// Obnova uložené preference
let savedTab = null;
try {
savedTab = localStorage.getItem(storageKey);
} catch (_) {}
const firstBtn = buttons[0];
const initTab = savedTab || (firstBtn ? firstBtn.dataset.tab : null);
if (initTab) switchTab(initTab, false);
});
}
// ════════════════════════════════════════════════════════════════════════════
// 4. TERMINAL TYPING EFFECT
// ════════════════════════════════════════════════════════════════════════════
/**
* Animuje psaní textu do elementu znak po znaku.
* @param {HTMLElement} element - Cílový element
* @param {string} text - Text, který se "napíše"
* @param {number} speed - Prodleva mezi znaky v ms (default 60)
* @param {Function} callback - Volitelný callback po dokončení
*/
function typeWriter(element, text, speed = 60, callback = null) {
if (!element) return;
element.textContent = "";
element.style.borderRight = "2px solid currentColor"; // kurzor
let i = 0;
const tick = () => {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
setTimeout(tick, speed + Math.random() * 30); // mírná variabilita
} else {
// Blikající kurzor po dokončení
element.classList.add("typing-done");
if (typeof callback === "function") callback();
}
};
tick();
}
/** Aplikuje typing efekt na všechny elementy s atributem [data-typewriter] */
function initTypingEffects() {
const elements = document.querySelectorAll("[data-typewriter]");
if (!elements.length) return;
// Spustíme postupně s malým zpožděním pro každý element
elements.forEach((el, idx) => {
const text = el.dataset.typewriter || el.textContent;
const speed = parseInt(el.dataset.typeSpeed, 10) || 60;
const delay = parseInt(el.dataset.typeDelay, 10) || idx * 300;
el.textContent = ""; // Skryjeme původní text
setTimeout(() => typeWriter(el, text, speed), delay);
});
}
// ════════════════════════════════════════════════════════════════════════════
// 5. SMOOTH COUNTER ANIMATION
// ════════════════════════════════════════════════════════════════════════════
function initCounters() {
const counters = document.querySelectorAll("[data-count]");
if (!counters.length) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
observer.unobserve(entry.target);
animateCounter(entry.target);
});
},
{ threshold: 0.5 },
);
counters.forEach((el) => observer.observe(el));
}
/**
* Animuje čítač od 0 do cílové hodnoty.
* @param {HTMLElement} el - Element s atributem data-count
*/
function animateCounter(el) {
const target = parseInt(el.dataset.count, 10);
const duration = parseInt(el.dataset.countDuration, 10) || 1500; // ms
const suffix = el.dataset.countSuffix || "";
const start = performance.now();
const update = (now) => {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
// Ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(eased * target);
el.textContent = current.toLocaleString("cs-CZ") + suffix;
if (progress < 1) requestAnimationFrame(update);
else el.textContent = target.toLocaleString("cs-CZ") + suffix;
};
requestAnimationFrame(update);
}
// ════════════════════════════════════════════════════════════════════════════
// 6. COPY TO CLIPBOARD
// ════════════════════════════════════════════════════════════════════════════
function initCopyButtons() {
// Podpora pro explicitní tlačítka s třídou .copy-btn
document.querySelectorAll(".copy-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const targetSelector = btn.dataset.copyTarget;
const codeEl = targetSelector
? document.querySelector(targetSelector)
: btn.closest(".code-block, pre")?.querySelector("code") ||
btn.previousElementSibling;
if (!codeEl) return;
copyText(codeEl.textContent.trim(), btn);
});
});
// Automaticky přidá tlačítka ke všem <pre><code> blokům bez tlačítka
document.querySelectorAll("pre:not(.no-copy)").forEach((pre) => {
if (pre.querySelector(".copy-btn")) return; // Již má tlačítko
const btn = document.createElement("button");
btn.className = "copy-btn copy-btn--auto";
btn.textContent = "Kopírovat";
btn.setAttribute("aria-label", "Kopírovat kód");
pre.style.position = "relative";
pre.appendChild(btn);
btn.addEventListener("click", () => {
const code = pre.querySelector("code") || pre;
copyText(code.textContent.trim(), btn);
});
});
}
/**
* Zkopíruje text do schránky a ukáže vizuální feedback na tlačítku.
* @param {string} text - Text ke zkopírování
* @param {HTMLElement} btn - Tlačítko pro vizuální feedback
*/
function copyText(text, btn) {
navigator.clipboard
.writeText(text)
.then(() => {
const original = btn.textContent;
btn.textContent = "✓ Zkopírováno";
btn.classList.add("copied");
btn.disabled = true;
setTimeout(() => {
btn.textContent = original;
btn.classList.remove("copied");
btn.disabled = false;
}, 2000);
})
.catch(() => {
// Fallback pro starší prohlížeče
const ta = document.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;top:-9999px;left:-9999px";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
btn.textContent = "✓ Zkopírováno";
btn.classList.add("copied");
setTimeout(() => {
btn.textContent = "Kopírovat";
btn.classList.remove("copied");
}, 2000);
});
}
// ════════════════════════════════════════════════════════════════════════════
// 7. THEME PARTICLE COLORS (při hover na sekce)
// ════════════════════════════════════════════════════════════════════════════
function initParticleTheme() {
if (!window.particleSystem) return;
// Mapování sekcí na palety barev
const sectionThemes = {
"#plugins": ["#bd93f9", "#ff79c6", "#ffffff"],
"#themes": ["#ff79c6", "#ffb86c", "#ffffff"],
"#installation": ["#50fa7b", "#8be9fd", "#ffffff"],
"#tutorials": ["#f1fa8c", "#ffb86c", "#ffffff"],
};
Object.entries(sectionThemes).forEach(([selector, colors]) => {
const section = document.querySelector(selector);
if (!section) return;
section.addEventListener("mouseenter", () => {
window.particleSystem.config.colors = colors;
});
section.addEventListener("mouseleave", () => {
// Obnovení výchozích barev
window.particleSystem.config.colors = [
"#ffffff",
"#8be9fd",
"#bd93f9",
"#ff79c6",
];
});
});
}
// Spustíme až po inicializaci particle systému (je v DOMContentLoaded v particles.js)
document.addEventListener("DOMContentLoaded", () => {
// Malé zpoždění pro jistotu, že particleSystem je vytvořen
setTimeout(initParticleTheme, 100);
});
// ════════════════════════════════════════════════════════════════════════════
// 9. TOOLTIP SYSTEM
// ════════════════════════════════════════════════════════════════════════════
function initTooltips() {
let tooltip = document.getElementById("tooltip-global");
// Vytvoření globálního tooltip elementu pokud neexistuje
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.id = "tooltip-global";
tooltip.className = "tooltip";
tooltip.setAttribute("role", "tooltip");
tooltip.setAttribute("aria-hidden", "true");
document.body.appendChild(tooltip);
}
let hideTimer = null;
/** Zobrazí tooltip */
const show = (text, anchorEl) => {
clearTimeout(hideTimer);
tooltip.textContent = text;
tooltip.style.display = "block";
tooltip.removeAttribute("aria-hidden");
// Pozicování nad elementem
const rect = anchorEl.getBoundingClientRect();
const tRect = tooltip.getBoundingClientRect();
let left = rect.left + rect.width / 2 - tRect.width / 2 + window.scrollX;
let top = rect.top - tRect.height - 8 + window.scrollY;
// Zamezit přetečení přes pravý okraj
if (left + tRect.width > window.innerWidth - 8) {
left = window.innerWidth - tRect.width - 8;
}
if (left < 8) left = 8;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
tooltip.classList.add("visible");
};
/** Skryje tooltip */
const hide = () => {
hideTimer = setTimeout(() => {
tooltip.classList.remove("visible");
tooltip.setAttribute("aria-hidden", "true");
}, 150);
};
// Event delegation — posloucháme na document
document.addEventListener("mouseover", (e) => {
const target = e.target.closest("[data-tooltip]");
if (target) show(target.dataset.tooltip, target);
});
document.addEventListener("mouseout", (e) => {
if (e.target.closest("[data-tooltip]")) hide();
});
// Klávesnicová přístupnost
document.addEventListener("focusin", (e) => {
const target = e.target.closest("[data-tooltip]");
if (target) show(target.dataset.tooltip, target);
});
document.addEventListener("focusout", (e) => {
if (e.target.closest("[data-tooltip]")) hide();
});
}
// ════════════════════════════════════════════════════════════════════════════
// 10. BACK TO TOP BUTTON
// ════════════════════════════════════════════════════════════════════════════
function initBackToTop() {
let btn = document.getElementById("back-to-top");
// Vytvoření tlačítka pokud neexistuje v HTML
if (!btn) {
btn = document.createElement("button");
btn.id = "back-to-top";
btn.className = "back-to-top";
btn.innerHTML = "&#8679;"; // ⇧
btn.setAttribute("aria-label", "Zpět nahoru");
btn.setAttribute("title", "Zpět nahoru");
document.body.appendChild(btn);
}
// Zobrazení/skrytí dle scroll pozice
const SHOW_THRESHOLD = 300;
const toggleVisibility = () => {
const visible = window.scrollY > SHOW_THRESHOLD;
btn.classList.toggle("visible", visible);
btn.setAttribute("aria-hidden", String(!visible));
};
window.addEventListener("scroll", toggleVisibility, { passive: true });
toggleVisibility();
// Scroll nahoru po kliknutí
btn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
}
// ════════════════════════════════════════════════════════════════════════════
// UTILITY — veřejné API pro použití z HTML atributů nebo jiných skriptů
// ════════════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════════════
// 11. HAMBURGER MENU (pro HTML strukturu s .navbar__hamburger a .navbar__mobile)
// ════════════════════════════════════════════════════════════════════════════
function initHamburger() {
const hamburger = document.getElementById("hamburger");
const mobileMenu = document.getElementById("mobile-menu");
if (!hamburger || !mobileMenu) return;
hamburger.addEventListener("click", () => {
const isHidden = mobileMenu.hasAttribute("hidden");
if (isHidden) {
mobileMenu.removeAttribute("hidden");
hamburger.setAttribute("aria-expanded", "true");
hamburger.classList.add("open");
} else {
mobileMenu.setAttribute("hidden", "");
hamburger.setAttribute("aria-expanded", "false");
hamburger.classList.remove("open");
}
});
// Zavřít při kliknutí mimo menu
document.addEventListener("click", (e) => {
if (
!mobileMenu.hasAttribute("hidden") &&
!mobileMenu.contains(e.target) &&
!hamburger.contains(e.target)
) {
mobileMenu.setAttribute("hidden", "");
hamburger.setAttribute("aria-expanded", "false");
hamburger.classList.remove("open");
}
});
// Zavřít při kliknutí na odkaz v menu
mobileMenu.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => {
mobileMenu.setAttribute("hidden", "");
hamburger.setAttribute("aria-expanded", "false");
hamburger.classList.remove("open");
});
});
}
// ════════════════════════════════════════════════════════════════════════════
// 12. PLUGIN CATEGORY FILTER (pro pluginy.html)
// ════════════════════════════════════════════════════════════════════════════
function initPluginFilters() {
const filterContainer = document.getElementById("plugin-filters");
const pluginsGrid = document.getElementById("plugins-grid");
if (!filterContainer || !pluginsGrid) return;
const buttons = filterContainer.querySelectorAll(".filter-btn");
const cards = pluginsGrid.querySelectorAll(".plugin-card");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
// Aktualizace aktivního tlačítka
buttons.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const filter = btn.dataset.filter;
// Zobrazit / skrýt karty s animací
cards.forEach((card) => {
const category = card.dataset.category || "";
const match = filter === "all" || category === filter;
if (match) {
card.classList.remove("hidden");
// Krátké zpoždění pro plynulý efekt
requestAnimationFrame(() => {
card.style.opacity = "1";
card.style.transform = "";
});
} else {
card.classList.add("hidden");
}
});
});
});
}
window.ApolloNvim = {
typeWriter,
copyText,
};
+358
View File
@@ -0,0 +1,358 @@
/**
* particles.js — ApolloNvim Vesmírný Particle Systém
* Canvas-based animace hvězd s parallaxem, blikáním, spojeními a padajícími hvězdami.
*/
class ParticleSystem {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
// Konfigurace částic
this.config = {
count: 200,
minSize: 0.5,
maxSize: 2.5,
colors: ['#ffffff', '#8be9fd', '#bd93f9', '#ff79c6'],
connectionDist: 100, // px — maximální vzdálenost pro spojovací linky
driftSpeed: 0.25, // max px/frame náhodný drift
parallaxStrength: 0.04, // síla parallax efektu myši
blinkChance: 0.35, // podíl hvězd, které blikají
shootingInterval:[5000, 8000], // ms min/max mezi padajícími hvězdami
};
// Stav myši
this.mouse = { x: 0, y: 0, active: false };
// Pole částic a padající hvězdy
this.particles = [];
this.shootingStars = [];
this._animFrameId = null;
this._shootingTimer = null;
this.init();
}
// ─── Inicializace ─────────────────────────────────────────────────────────
init() {
this.resize();
this.createParticles();
this._bindEvents();
this._scheduleShootingStar();
this.animate();
}
/** Nastaví rozměry canvasu dle okna */
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
/** Vytvoří pole hvězdičkových částic */
createParticles() {
this.particles = [];
const { count, minSize, maxSize, colors, blinkChance, driftSpeed } = this.config;
for (let i = 0; i < count; i++) {
const blinks = Math.random() < blinkChance;
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
baseX: 0, // bude nastaveno níže
baseY: 0,
size: minSize + Math.random() * (maxSize - minSize),
color: colors[Math.floor(Math.random() * colors.length)],
// drift – každá hvězda má svůj vektor
vx: (Math.random() - 0.5) * driftSpeed,
vy: (Math.random() - 0.5) * driftSpeed,
// blikání
blinks,
opacity: blinks ? 0.3 + Math.random() * 0.7 : 0.6 + Math.random() * 0.4,
blinkSpeed: blinks ? 0.005 + Math.random() * 0.015 : 0,
blinkDir: Math.random() < 0.5 ? 1 : -1,
// parallax offset (akumulovaný)
offsetX: 0,
offsetY: 0,
});
// baseX/baseY = počáteční pozice (pro parallax výpočet)
this.particles[i].baseX = this.particles[i].x;
this.particles[i].baseY = this.particles[i].y;
}
}
// ─── Hlavní animační smyčka ───────────────────────────────────────────────
animate() {
this._animFrameId = requestAnimationFrame(() => this.animate());
const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Výpočet parallax posunu středu obrazovky
const cx = this.canvas.width / 2;
const cy = this.canvas.height / 2;
const mouseOffsetX = this.mouse.active
? (this.mouse.x - cx) * this.config.parallaxStrength
: 0;
const mouseOffsetY = this.mouse.active
? (this.mouse.y - cy) * this.config.parallaxStrength
: 0;
// ── Aktualizace a kreslení hvězd ──
for (const p of this.particles) {
// Drift
p.baseX += p.vx;
p.baseY += p.vy;
// Wrap-around přes okraje
if (p.baseX < -10) p.baseX = this.canvas.width + 10;
if (p.baseX > this.canvas.width + 10) p.baseX = -10;
if (p.baseY < -10) p.baseY = this.canvas.height + 10;
if (p.baseY > this.canvas.height + 10) p.baseY = -10;
// Parallax offset (každá hvězda reaguje trochu jinak dle velikosti)
const parallaxFactor = p.size / this.config.maxSize;
p.x = p.baseX + mouseOffsetX * parallaxFactor;
p.y = p.baseY + mouseOffsetY * parallaxFactor;
// Blikání opacity
if (p.blinks) {
p.opacity += p.blinkSpeed * p.blinkDir;
if (p.opacity >= 1.0) { p.opacity = 1.0; p.blinkDir = -1; }
if (p.opacity <= 0.15) { p.opacity = 0.15; p.blinkDir = 1; }
}
// Kreslení hvězdy jako malého hvězdičkového tvaru
this._drawStar(p);
}
// ── Spojovací linky ──
this.drawConnections();
// ── Padající hvězdy ──
this._updateShootingStars();
}
/** Nakreslí hvězdu (5-cípý tvar pro větší, kroužek pro malé) */
_drawStar(p) {
const ctx = this.ctx;
ctx.save();
ctx.globalAlpha = p.opacity;
ctx.fillStyle = p.color;
if (p.size >= 1.5) {
// Pěticípá hvězdička pro větší částice
this._drawStarShape(ctx, p.x, p.y, p.size * 2, p.size, 5);
} else {
// Jednoduchý kroužek pro malé hvězdičky
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
// Jemná záře kolem hvězdy
if (p.size > 1.2) {
const grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 4);
grd.addColorStop(0, this._hexAlpha(p.color, 0.3 * p.opacity));
grd.addColorStop(1, this._hexAlpha(p.color, 0));
ctx.fillStyle = grd;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
/** Nakreslí pěticípou hvězdičku */
_drawStarShape(ctx, cx, cy, outerR, innerR, points) {
const step = Math.PI / points;
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = i * step - Math.PI / 2;
const x = cx + Math.cos(angle) * r;
const y = cy + Math.sin(angle) * r;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
}
// ─── Spojovací linky ─────────────────────────────────────────────────────
drawConnections() {
const ctx = this.ctx;
const dist = this.config.connectionDist;
const ps = this.particles;
for (let i = 0; i < ps.length; i++) {
for (let j = i + 1; j < ps.length; j++) {
const dx = ps[i].x - ps[j].x;
const dy = ps[i].y - ps[j].y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < dist) {
const alpha = (1 - d / dist) * 0.25; // max 25% opacity
ctx.save();
ctx.globalAlpha = alpha;
ctx.strokeStyle = '#8be9fd';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(ps[i].x, ps[i].y);
ctx.lineTo(ps[j].x, ps[j].y);
ctx.stroke();
ctx.restore();
}
}
}
}
// ─── Padající hvězdy ─────────────────────────────────────────────────────
/** Naplánuje další padající hvězdu s náhodným zpožděním */
_scheduleShootingStar() {
const [min, max] = this.config.shootingInterval;
const delay = min + Math.random() * (max - min);
this._shootingTimer = setTimeout(() => {
this.createShootingStar();
this._scheduleShootingStar(); // naplánuje další
}, delay);
}
/** Vytvoří novou padající hvězdu */
createShootingStar() {
const angle = (15 + Math.random() * 25) * (Math.PI / 180); // 15°–40°
const speed = 6 + Math.random() * 8;
const length = 80 + Math.random() * 120;
this.shootingStars.push({
x: Math.random() * this.canvas.width * 0.8,
y: Math.random() * this.canvas.height * 0.3,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
length,
opacity: 1,
trail: [], // pole minulých pozic pro ocas
maxTrail: Math.floor(length / speed),
done: false,
});
}
/** Aktualizuje a kreslí všechny padající hvězdy */
_updateShootingStars() {
const ctx = this.ctx;
this.shootingStars = this.shootingStars.filter(s => !s.done);
for (const s of this.shootingStars) {
// Uloží pozici do ocasu
s.trail.push({ x: s.x, y: s.y });
if (s.trail.length > s.maxTrail) s.trail.shift();
// Pohyb
s.x += s.vx;
s.y += s.vy;
s.opacity -= 0.012;
if (s.opacity <= 0 || s.x > this.canvas.width + 50 || s.y > this.canvas.height + 50) {
s.done = true;
continue;
}
// Kreslení ocasu (gradient linka)
if (s.trail.length > 1) {
const grad = ctx.createLinearGradient(
s.trail[0].x, s.trail[0].y,
s.x, s.y
);
grad.addColorStop(0, `rgba(255,255,255,0)`);
grad.addColorStop(1, `rgba(255,255,255,${s.opacity})`);
ctx.save();
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5;
ctx.shadowColor = '#ffffff';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.moveTo(s.trail[0].x, s.trail[0].y);
for (const pt of s.trail) ctx.lineTo(pt.x, pt.y);
ctx.lineTo(s.x, s.y);
ctx.stroke();
ctx.restore();
}
// Jasná tečka na špičce
ctx.save();
ctx.globalAlpha = s.opacity;
ctx.fillStyle = '#ffffff';
ctx.shadowColor = '#8be9fd';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(s.x, s.y, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// ─── Pomocné metody ───────────────────────────────────────────────────────
/** Převede HEX barvu na rgba string s danou průhledností */
_hexAlpha(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
/** Naváže posluchače událostí */
_bindEvents() {
// Resize
window.addEventListener('resize', () => {
this.resize();
// Po resize přepočítá baseX/baseY aby hvězdy zůstaly proporcionálně
const scaleX = this.canvas.width / (this.canvas.width || 1);
const scaleY = this.canvas.height / (this.canvas.height || 1);
for (const p of this.particles) {
p.baseX = Math.random() * this.canvas.width;
p.baseY = Math.random() * this.canvas.height;
}
});
// Mouse parallax
window.addEventListener('mousemove', (e) => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
this.mouse.active = true;
});
// Dotyk (mobile)
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0) {
this.mouse.x = e.touches[0].clientX;
this.mouse.y = e.touches[0].clientY;
this.mouse.active = true;
}
}, { passive: true });
window.addEventListener('mouseleave', () => {
this.mouse.active = false;
});
}
/** Zastaví animaci a časovače */
destroy() {
if (this._animFrameId) cancelAnimationFrame(this._animFrameId);
if (this._shootingTimer) clearTimeout(this._shootingTimer);
}
}
// ─── Auto-init při načtení DOM ─────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('particles-canvas')) {
window.particleSystem = new ParticleSystem('particles-canvas');
}
});