feat: 创建粒子效果组件
- 创建 Canvas 粒子动画背景 - 支持自定义粒子数量 - 粒子之间自动连线 - 响应式窗口大小调整 - 科技蓝色粒子效果
This commit is contained in:
@@ -0,0 +1,110 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
size: number;
|
||||||
|
opacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParticleBackgroundProps {
|
||||||
|
particleCount?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParticleBackground({
|
||||||
|
particleCount = 50,
|
||||||
|
className = ''
|
||||||
|
}: ParticleBackgroundProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
let particles: Particle[] = [];
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createParticles = () => {
|
||||||
|
particles = [];
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
particles.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: Math.random() * canvas.height,
|
||||||
|
vx: (Math.random() - 0.5) * 0.5,
|
||||||
|
vy: (Math.random() - 0.5) * 0.5,
|
||||||
|
size: Math.random() * 2 + 1,
|
||||||
|
opacity: Math.random() * 0.5 + 0.2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawParticles = () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
particles.forEach((particle, i) => {
|
||||||
|
particle.x += particle.vx;
|
||||||
|
particle.y += particle.vy;
|
||||||
|
|
||||||
|
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
||||||
|
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(0, 217, 255, ${particle.opacity})`;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
particles.forEach((otherParticle, j) => {
|
||||||
|
if (i === j) return;
|
||||||
|
const dx = particle.x - otherParticle.x;
|
||||||
|
const dy = particle.y - otherParticle.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 150) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(particle.x, particle.y);
|
||||||
|
ctx.lineTo(otherParticle.x, otherParticle.y);
|
||||||
|
ctx.strokeStyle = `rgba(0, 217, 255, ${0.1 * (1 - distance / 150)})`;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(drawParticles);
|
||||||
|
};
|
||||||
|
|
||||||
|
resizeCanvas();
|
||||||
|
createParticles();
|
||||||
|
drawParticles();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
resizeCanvas();
|
||||||
|
createParticles();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
};
|
||||||
|
}, [particleCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||||
|
style={{ opacity: 0.3 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user