feat(ui): add autonomous self-moving particle drift and mobile touch interactivity
This commit is contained in:
parent
4d63d43b5a
commit
2a642bbfa4
1 changed files with 76 additions and 50 deletions
126
public/js/app.js
126
public/js/app.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue