mirror of
https://codeberg.org/Kankys/ApolloNvim2025.git
synced 2026-11-12 00:42:00 +01:00
359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
/**
|
||
* 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');
|
||
}
|
||
});
|