Files
ApolloNvim2025/web/js/particles.js
T
2026-05-15 23:22:58 +02:00

359 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
}
});