ParticleBackground.vue 3.9 KB
<template>
  <canvas ref="canvasRef" class="absolute inset-0 w-full h-full"></canvas>
</template>

<script lang="ts" setup>
const props = defineProps<{
  particleCount?: number;
  particleColor?: string;
  lineColor?: string;
  particleSize?: number;
  lineDistance?: number;
  speed?: number;
}>();

const canvasRef = ref<HTMLCanvasElement | null>(null);
const { isDark } = useDarkMode();

const config = {
  particleCount: props.particleCount || 80,
  particleColor: props.particleColor || "#5961f9",
  lineColor: props.lineColor || "#5961f9",
  particleSize: props.particleSize || 2,
  lineDistance: props.lineDistance || 120,
  speed: props.speed || 0.5,
};

interface Particle {
  x: number;
  y: number;
  vx: number;
  vy: number;
  size: number;
}

let particles: Particle[] = [];
let animationId: number | null = null;
let ctx: CanvasRenderingContext2D | null = null;
let canvas: HTMLCanvasElement | null = null;

const initParticles = () => {
  particles = [];
  if (!canvas) return;

  for (let i = 0; i < config.particleCount; i++) {
    particles.push({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      vx: (Math.random() - 0.5) * config.speed,
      vy: (Math.random() - 0.5) * config.speed,
      size: Math.random() * config.particleSize + 1,
    });
  }
};

const drawParticles = () => {
  if (!ctx || !canvas) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const particleAlpha = isDark.value ? 0.6 : 0.8;
  const lineAlpha = isDark.value ? 0.15 : 0.2;

  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 = `${config.particleColor}${Math.round(particleAlpha * 255)
      .toString(16)
      .padStart(2, "0")}`;
    ctx!.fill();

    for (let j = i + 1; j < particles.length; j++) {
      const dx = particles[j].x - particle.x;
      const dy = particles[j].y - particle.y;
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (distance < config.lineDistance) {
        const opacity = (1 - distance / config.lineDistance) * lineAlpha;
        ctx!.beginPath();
        ctx!.moveTo(particle.x, particle.y);
        ctx!.lineTo(particles[j].x, particles[j].y);
        ctx!.strokeStyle = `${config.lineColor}${Math.round(opacity * 255)
          .toString(16)
          .padStart(2, "0")}`;
        ctx!.lineWidth = 1;
        ctx!.stroke();
      }
    }
  });

  animationId = requestAnimationFrame(drawParticles);
};

const handleResize = () => {
  if (!canvas || !ctx) return;

  const parent = canvas.parentElement;
  if (parent) {
    canvas.width = parent.offsetWidth;
    canvas.height = parent.offsetHeight;
  }

  initParticles();
};

const handleMouse = (e: MouseEvent) => {
  if (!canvas) return;

  const rect = canvas.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  const mouseY = e.clientY - rect.top;

  particles.forEach((particle) => {
    const dx = mouseX - particle.x;
    const dy = mouseY - particle.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance < 100) {
      const force = (100 - distance) / 100;
      particle.vx -= (dx / distance) * force * 0.5;
      particle.vy -= (dy / distance) * force * 0.5;
    }
  });
};

onMounted(() => {
  canvas = canvasRef.value;
  if (!canvas) return;

  ctx = canvas.getContext("2d");
  if (!ctx) return;

  handleResize();
  drawParticles();

  window.addEventListener("resize", handleResize);
  canvas.addEventListener("mousemove", handleMouse);
});

onUnmounted(() => {
  if (animationId) {
    cancelAnimationFrame(animationId);
  }
  window.removeEventListener("resize", handleResize);
  if (canvas) {
    canvas.removeEventListener("mousemove", handleMouse);
  }
});

watch(isDark, () => {
  drawParticles();
});
</script>