feat(ui): add interactive constellation particle background from katdrop

This commit is contained in:
katR 2026-09-25 17:14:18 +05:30
parent 277b0fd301
commit 707d78646d
3 changed files with 162 additions and 2 deletions

View file

@ -465,8 +465,158 @@ function initEvents() {
});
}
/**
* Autonomous & Interactive Particle Background (from katdrop)
*/
function initInteractiveParticles() {
const canvas = document.getElementById('particles');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let width = (canvas.width = window.innerWidth);
let height = (canvas.height = window.innerHeight);
window.addEventListener('resize', () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
});
const pointer = {
x: null,
y: null,
radius: 150
};
window.addEventListener('mousemove', (e) => {
pointer.x = e.clientX;
pointer.y = e.clientY;
});
window.addEventListener('mouseleave', () => {
pointer.x = null;
pointer.y = null;
});
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0) {
pointer.x = e.touches[0].clientX;
pointer.y = e.touches[0].clientY;
}
}, { passive: true });
window.addEventListener('touchend', () => {
pointer.x = null;
pointer.y = null;
});
window.addEventListener('click', (e) => {
for (const p of particles) {
const dx = p.x - e.clientX;
const dy = p.y - e.clientY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 220 && dist > 0) {
const force = (220 - dist) / 220;
p.disturbVx += (dx / dist) * force * 4;
p.disturbVy += (dy / dist) * force * 4;
}
}
});
const particles = [];
const count = Math.min(80, Math.max(35, Math.floor(width / 20)));
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 0.35 + Math.random() * 0.45;
particles.push({
x: Math.random() * width,
y: Math.random() * height,
baseVx: Math.cos(angle) * speed,
baseVy: Math.sin(angle) * speed,
disturbVx: 0,
disturbVy: 0,
radius: Math.random() * 1.5 + 1,
phase: Math.random() * Math.PI * 2,
twinkleSpeed: 0.0018 + Math.random() * 0.0025
});
}
function animate(time) {
ctx.clearRect(0, 0, width, height);
// Connect particles to each other (constellation network)
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distSq = dx * dx + dy * dy;
if (distSq < 15625) { // 125px
const dist = Math.sqrt(distSq);
ctx.strokeStyle = `rgba(255, 255, 255, ${0.075 * (1 - dist / 125)})`;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
// Update & draw each particle
for (const p of particles) {
const waveX = Math.sin(time * 0.0012 + p.phase) * 0.22;
const waveY = Math.cos(time * 0.0015 + p.phase) * 0.22;
p.x += p.baseVx + p.disturbVx + waveX;
p.y += p.baseVy + p.disturbVy + waveY;
p.disturbVx *= 0.94;
p.disturbVy *= 0.94;
const pad = 15;
if (p.x < -pad) p.x = width + pad;
if (p.x > width + pad) p.x = -pad;
if (p.y < -pad) p.y = height + pad;
if (p.y > height + pad) p.y = -pad;
if (pointer.x !== null && pointer.y !== null) {
const mdx = pointer.x - p.x;
const mdy = pointer.y - p.y;
const mDistSq = mdx * mdx + mdy * mdy;
if (mDistSq < pointer.radius * pointer.radius) {
const mDist = Math.sqrt(mDistSq);
ctx.strokeStyle = `rgba(255, 255, 255, ${0.2 * (1 - mDist / pointer.radius)})`;
ctx.lineWidth = 0.9;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(pointer.x, pointer.y);
ctx.stroke();
if (mDist < 75 && mDist > 0) {
const repel = (75 - mDist) / 75;
p.disturbVx -= (mdx / mDist) * repel * 1.6;
p.disturbVy -= (mdy / mDist) * repel * 1.6;
}
}
}
const alpha = 0.25 + Math.sin(time * p.twinkleSpeed + p.phase) * 0.14;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha.toFixed(3)})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
// Initialization
document.addEventListener('DOMContentLoaded', () => {
initInteractiveParticles();
initEvents();
loadStats();
loadProxies();

View file

@ -11,7 +11,7 @@
<link rel="stylesheet" href="style.css?v=2.0.0">
</head>
<body>
<div class="ambient-glow"></div>
<canvas id="particles"></canvas>
<div class="app-container">
<!-- Header -->
@ -169,6 +169,6 @@
<!-- Toast Notification Container -->
<div class="toast-container" id="toast-container"></div>
<script src="app.js?v=1.0.1"></script>
<script src="app.js?v=2.1.0"></script>
</body>
</html>

View file

@ -48,6 +48,16 @@ body {
letter-spacing: -0.01em;
}
#particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
/* App Container */
.app-container {
max-width: 1200px;