Files
loyalyweb/src/components/ProblemParticles.tsx
2026-06-29 18:07:43 +05:30

197 lines
5.9 KiB
TypeScript

'use client';
import { useEffect, useRef } from 'react';
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
alpha: number;
type: 'lost' | 'converted' | 'regular';
pulseSpeed: number;
pulseTime: number;
}
export default function ProblemParticles() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let animationFrameId = 0;
let cssWidth = canvas.offsetWidth;
let cssHeight = canvas.offsetHeight;
const particles: Particle[] = [];
const storeNode = { x: cssWidth / 2, y: cssHeight / 2, radius: 18 };
const setCanvasSize = () => {
const dpr = Math.max(1, window.devicePixelRatio || 1);
cssWidth = canvas.offsetWidth;
cssHeight = canvas.offsetHeight;
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
// Map drawing coordinates to CSS pixels
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
storeNode.x = cssWidth / 2;
storeNode.y = cssHeight / 2;
};
const createParticle = (isInitial = false): Particle => {
const typeRand = Math.random();
const type = typeRand < 0.2 ? 'lost' : typeRand < 0.45 ? 'converted' : 'regular';
let x: number, y: number;
if (isInitial) {
x = Math.random() * cssWidth;
y = Math.random() * cssHeight;
} else {
// Spawn from edges
if (Math.random() < 0.5) {
x = Math.random() < 0.5 ? 0 : cssWidth;
y = Math.random() * cssHeight;
} else {
x = Math.random() * cssWidth;
y = Math.random() < 0.5 ? 0 : cssHeight;
}
}
// Vector pointing towards the store node (center)
const angle = Math.atan2(storeNode.y - y, storeNode.x - x);
const speed = 0.4 + Math.random() * 0.8;
return {
x,
y,
vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 0.2,
vy: Math.sin(angle) * speed + (Math.random() - 0.5) * 0.2,
radius: 2 + Math.random() * 4,
alpha: 0.1 + Math.random() * 0.7,
type,
pulseSpeed: 0.05 + Math.random() * 0.05,
pulseTime: Math.random() * Math.PI,
};
};
// Initialize
setCanvasSize();
for (let i = 0; i < 45; i++) particles.push(createParticle(true));
const handleResize = () => {
if (!canvas) return;
setCanvasSize();
};
window.addEventListener('resize', handleResize);
const draw = () => {
ctx.clearRect(0, 0, cssWidth, cssHeight);
// Background glow
const bgGlow = ctx.createRadialGradient(
storeNode.x,
storeNode.y,
10,
storeNode.x,
storeNode.y,
cssWidth * 0.4
);
bgGlow.addColorStop(0, 'rgba(0, 102, 255, 0.08)');
bgGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = bgGlow;
ctx.fillRect(0, 0, cssWidth, cssHeight);
// Store hub
ctx.shadowBlur = 30;
ctx.shadowColor = '#0066ff';
ctx.fillStyle = 'rgba(0, 102, 255, 0.9)';
ctx.beginPath();
ctx.arc(storeNode.x, storeNode.y, storeNode.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(storeNode.x, storeNode.y, storeNode.radius - 6, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
// Particles
particles.forEach((p, idx) => {
p.x += p.vx;
p.y += p.vy;
p.pulseTime += p.pulseSpeed;
let alpha = p.alpha;
let radius = p.radius;
if (p.type === 'converted') {
alpha = Math.max(0.4, p.alpha + Math.sin(p.pulseTime) * 0.3);
radius = p.radius + Math.sin(p.pulseTime) * 1.5;
} else if (p.type === 'lost') {
const dist = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
if (dist < 150) p.alpha -= 0.008;
}
// Connections between particles
for (let j = idx + 1; j < particles.length; j++) {
const p2 = particles[j];
const dist = Math.hypot(p2.x - p.x, p2.y - p.y);
if (dist < 80 && p.alpha > 0 && p2.alpha > 0) {
const lineAlpha = (1 - dist / 80) * 0.18 * Math.min(alpha, p2.alpha);
ctx.strokeStyle = `rgba(0, 102, 255, ${lineAlpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
}
// Connection to store hub
const distToStore = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
if (distToStore < 120 && p.alpha > 0) {
const lineAlpha = (1 - distToStore / 120) * 0.25 * p.alpha;
ctx.strokeStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${lineAlpha})` : `rgba(0, 102, 255, ${lineAlpha})`;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(storeNode.x, storeNode.y);
ctx.stroke();
}
// Respawn check
if (distToStore < storeNode.radius || p.x < 0 || p.x > cssWidth || p.y < 0 || p.y > cssHeight || p.alpha <= 0) {
particles[idx] = createParticle();
} else {
ctx.fillStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${alpha})` : p.type === 'lost' ? `rgba(255, 68, 68, ${alpha})` : `rgba(255, 255, 255, ${alpha})`;
if (p.type === 'converted') {
ctx.shadowBlur = 10;
ctx.shadowColor = '#00dcff';
}
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
});
animationFrameId = requestAnimationFrame(draw);
};
draw();
return () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return <canvas ref={canvasRef} className="w-full h-full absolute inset-0" />;
}