feat(ui): add autonomous self-moving particle drift and mobile touch interactivity

This commit is contained in:
katR 2026-09-12 18:23:57 +05:30
parent 4d63d43b5a
commit 2a642bbfa4

View file

@ -84,7 +84,7 @@
}
}
// Interactive Particle Background
// Autonomous & Interactive Particle Background
function initInteractiveParticles() {
const canvas = document.getElementById('particles');
if (!canvas) return;
@ -98,61 +98,80 @@
height = canvas.height = window.innerHeight;
});
// Mouse tracking for interactivity
const mouse = {
// Mouse & Touch tracking for interactivity
const pointer = {
x: null,
y: null,
radius: 140
radius: 150
};
window.addEventListener('mousemove', e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
pointer.x = e.clientX;
pointer.y = e.clientY;
});
window.addEventListener('mouseleave', () => {
mouse.x = null;
mouse.y = null;
pointer.x = null;
pointer.y = null;
});
// Click to push particles slightly
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;
});
// Click to push particles outward
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 < 180 && dist > 0) {
const force = (180 - dist) / 180;
p.vx += (dx / dist) * force * 3;
p.vy += (dy / dist) * force * 3;
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(50, Math.floor(width / 26));
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,
vx: (Math.random() - 0.5) * 0.45,
vy: (Math.random() - 0.5) * 0.45,
radius: Math.random() * 1.5 + 1
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() {
function animate(time) {
ctx.clearRect(0, 0, width, height);
// Connect particles to each other
// 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 dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 115) {
ctx.strokeStyle = `rgba(255, 255, 255, ${0.06 * (1 - dist / 115)})`;
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);
@ -162,47 +181,54 @@
}
}
// Update & draw particles + connect to mouse
// Update & draw each particle
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
// Continuous organic autonomous motion: base velocity + subtle harmonic wave drift
const waveX = Math.sin(time * 0.0012 + p.phase) * 0.22;
const waveY = Math.cos(time * 0.0015 + p.phase) * 0.22;
// Dampen velocity back to normal
p.vx *= 0.985;
p.vy *= 0.985;
p.x += p.baseVx + p.disturbVx + waveX;
p.y += p.baseVy + p.disturbVy + waveY;
// Boundary bounce
if (p.x < 0) { p.x = 0; p.vx *= -1; }
if (p.x > width) { p.x = width; p.vx *= -1; }
if (p.y < 0) { p.y = 0; p.vy *= -1; }
if (p.y > height) { p.y = height; p.vy *= -1; }
// Dampen interactive disturbance back to zero (leaves autonomous base speed intact)
p.disturbVx *= 0.94;
p.disturbVy *= 0.94;
// Interaction with mouse cursor
if (mouse.x !== null && mouse.y !== null) {
const mdx = mouse.x - p.x;
const mdy = mouse.y - p.y;
const mDist = Math.sqrt(mdx * mdx + mdy * mdy);
// Seamless boundary wrap with padding so particles never bunch at edges
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 (mDist < mouse.radius) {
// Connect line to mouse
ctx.strokeStyle = `rgba(77, 184, 255, ${0.18 * (1 - mDist / mouse.radius)})`;
// Interaction with mouse/touch pointer
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);
// Connect line to pointer
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(mouse.x, mouse.y);
ctx.lineTo(pointer.x, pointer.y);
ctx.stroke();
// Repel gently if very close
if (mDist < 70 && mDist > 0) {
const repel = (70 - mDist) / 70;
p.x -= (mdx / mDist) * repel * 1.5;
p.y -= (mdy / mDist) * repel * 1.5;
// Repel gently if close
if (mDist < 75 && mDist > 0) {
const repel = (75 - mDist) / 75;
p.disturbVx -= (mdx / mDist) * repel * 1.6;
p.disturbVy -= (mdy / mDist) * repel * 1.6;
}
}
}
// Draw particle dot
ctx.fillStyle = 'rgba(255, 255, 255, 0.28)';
// Breathing particle glow
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();
@ -211,7 +237,7 @@
requestAnimationFrame(animate);
}
animate();
requestAnimationFrame(animate);
}
// Safe HTML helper