pokedex-web / frontend /src /components /ParticlesBg.js
gpimentel's picture
feat: implement anomaly detection ensemble, feedback system, and upgraded model configuration
d56d352
Raw
History Blame Contribute Delete
4.47 kB
'use client';
import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
const ParticlesBg = forwardRef((props, ref) => {
const canvasRef = useRef(null);
const targetColorRef = useRef({ r: 56, g: 189, b: 248 });
const currentColorRef = useRef({ r: 56, g: 189, b: 248 });
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 56, g: 189, b: 248 };
};
useImperativeHandle(ref, () => ({
setAccentColor: (hex) => {
if (hex) {
targetColorRef.current = hexToRgb(hex);
} else {
targetColorRef.current = { r: 56, g: 189, b: 248 };
}
}
}));
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
let animationFrameId;
let particles = [];
let connectionDist = 120;
const BASELINE_AREA = 1920 * 1080;
const BASELINE_PARTICLES = 70;
const BASELINE_DIAGONAL = Math.sqrt(1920 * 1920 + 1080 * 1080);
const BASELINE_CONN_DIST = 120;
const MIN_PARTICLES = 12;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const area = canvas.width * canvas.height;
const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2);
const areaRatio = area / BASELINE_AREA;
const targetCount = Math.max(MIN_PARTICLES, Math.round(BASELINE_PARTICLES * areaRatio));
connectionDist = Math.round(BASELINE_CONN_DIST * (diagonal / BASELINE_DIAGONAL));
if (particles.length > targetCount) {
particles.length = targetCount;
} else {
while (particles.length < targetCount) {
particles.push(new Particle());
}
}
};
window.addEventListener('resize', resize);
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.8;
this.vy = (Math.random() - 0.5) * 0.8;
this.radius = Math.random() * 2 + 1;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw(ctx, color) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${Math.floor(color.r)}, ${Math.floor(color.g)}, ${Math.floor(color.b)}, 0.5)`;
ctx.fill();
}
}
resize();
const animate = () => {
currentColorRef.current.r += (targetColorRef.current.r - currentColorRef.current.r) * 0.05;
currentColorRef.current.g += (targetColorRef.current.g - currentColorRef.current.g) * 0.05;
currentColorRef.current.b += (targetColorRef.current.b - currentColorRef.current.b) * 0.05;
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.update();
p.draw(ctx, currentColorRef.current);
});
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 < connectionDist) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(${Math.floor(currentColorRef.current.r)}, ${Math.floor(currentColorRef.current.g)}, ${Math.floor(currentColorRef.current.b)}, ${(1 - dist / connectionDist) * 0.4})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
animationFrameId = requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none'
}}
/>
);
});
ParticlesBg.displayName = 'ParticlesBg';
export default ParticlesBg;