Fireworks.vue 7.01 KB
<template>
  <canvas ref="canvasRef" class="fireworks-canvas"></canvas>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const canvasRef = ref<HTMLCanvasElement | null>(null);
let animationId: number | null = null;
let particles: Particle[] = [];

interface Particle {
  x: number;
  y: number;
  vx: number;
  vy: number;
  color: string;
  life: number;
  decay: number;
  size: number;
  trail: Array<{ x: number; y: number; alpha: number }>;
  gravity: number; // 重力加速度
  airResistance: number; // 空气阻力系数
  startX: number; // 起始位置(用于贝塞尔曲线)
  startY: number;
}

const colors = [
  "#ff0000", // 红
  "#ff8800", // 橙
  "#ffff00", // 黄
  "#00ff00", // 绿
  "#00ffff", // 青
  "#0088ff", // 蓝
  "#8800ff", // 紫
  "#ff00ff", // 粉
  "#ff0088", // 玫红
  "#ffaa00", // 金橙
  "#88ff00", // 黄绿
  "#00ff88", // 青绿
];

const initCanvas = () => {
  if (!canvasRef.value) return;
  const canvas = canvasRef.value;
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
};

const createBurst = (centerX: number, centerY: number) => {
  if (!canvasRef.value) return;
  const particleCount = 80 + Math.floor(Math.random() * 40);
  const baseSpeed = 4 + Math.random() * 5;

  for (let i = 0; i < particleCount; i++) {
    const angle =
      (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
    const speed = baseSpeed + Math.random() * 4;
    const colorIndex = Math.floor(Math.random() * colors.length);
    const color = colors[colorIndex] || "#ff0000";
    // 更小的粒子尺寸
    const size = 1 + Math.random() * 1.5;
    const life = 0.9 + Math.random() * 0.1;
    const decay = 0.01 + Math.random() * 0.01;
    // 重力加速度(向下为正)
    const gravity = 0.15 + Math.random() * 0.1;
    // 空气阻力系数(0.98-0.995之间)
    const airResistance = 0.98 + Math.random() * 0.015;

    particles.push({
      x: centerX,
      y: centerY,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      color,
      life,
      decay,
      size,
      trail: [],
      gravity,
      airResistance,
      startX: centerX,
      startY: centerY,
    });
  }
};

const update = () => {
  if (!canvasRef.value) return;
  const canvas = canvasRef.value;
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  // 完全透明清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 更新粒子
  for (let i = particles.length - 1; i >= 0; i--) {
    const particle = particles[i];
    if (!particle) {
      particles.splice(i, 1);
      continue;
    }

    // 应用重力效果(向下加速)
    particle.vy += particle.gravity;

    // 应用空气阻力(衰减速度)
    particle.vx *= particle.airResistance;
    particle.vy *= particle.airResistance;

    // 更新位置
    particle.x += particle.vx;
    particle.y += particle.vy;

    // 保存当前位置到拖尾(在位置更新后)
    particle.trail.push({
      x: particle.x,
      y: particle.y,
      alpha: particle.life,
    });

    // 限制拖尾长度(用于贝塞尔曲线绘制)
    if (particle.trail.length > 15) {
      particle.trail.shift();
    }

    // 更新生命周期
    particle.life -= particle.decay;

    if (particle.life <= 0) {
      particles.splice(i, 1);
    } else {
      // 使用贝塞尔曲线绘制轨迹
      if (particle.trail.length >= 2) {
        ctx.strokeStyle = particle.color;
        ctx.lineCap = "round";
        ctx.lineJoin = "round";

        // 绘制贝塞尔曲线轨迹
        for (let j = 0; j < particle.trail.length - 1; j++) {
          const point = particle.trail[j];
          const nextPoint = particle.trail[j + 1];
          if (!point || !nextPoint) continue;

          // 计算控制点(用于创建平滑的曲线)
          const controlX = point.x + (nextPoint.x - point.x) * 0.5;
          const controlY = point.y + (nextPoint.y - point.y) * 0.5;

          // 根据拖尾位置计算透明度(越远越淡)
          const progress = j / (particle.trail.length - 1);
          const alpha = point.alpha * (1 - progress * 0.6) * 0.7;

          ctx.globalAlpha = alpha;
          ctx.lineWidth = particle.size * 0.6 + (1 - progress) * 0.4;

          // 使用二次贝塞尔曲线绘制
          ctx.beginPath();
          ctx.moveTo(point.x, point.y);
          ctx.quadraticCurveTo(controlX, controlY, nextPoint.x, nextPoint.y);
          ctx.stroke();
        }
      }

      // 绘制主粒子(更小的光晕效果)
      const gradient = ctx.createRadialGradient(
        particle.x,
        particle.y,
        0,
        particle.x,
        particle.y,
        particle.size * 1.5,
      );
      gradient.addColorStop(0, particle.color);
      gradient.addColorStop(0.6, particle.color + "60");
      gradient.addColorStop(1, particle.color + "00");

      ctx.globalAlpha = particle.life;
      ctx.fillStyle = gradient;
      ctx.beginPath();
      ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
      ctx.fill();

      // 绘制核心亮点(更小的核心)
      ctx.globalAlpha = particle.life;
      ctx.fillStyle = particle.color;
      ctx.beginPath();
      ctx.arc(particle.x, particle.y, particle.size * 0.6, 0, Math.PI * 2);
      ctx.fill();
    }
  }
  ctx.globalAlpha = 1;

  animationId = requestAnimationFrame(update);
};

const start = () => {
  if (!canvasRef.value) return;
  initCanvas();
  particles = [];

  const canvas = canvasRef.value;

  // 创建初始随机爆发点
  const initialBursts = 8 + Math.floor(Math.random() * 5);
  for (let i = 0; i < initialBursts; i++) {
    const delay = i * 100 + Math.random() * 50;
    setTimeout(() => {
      // 完全随机位置,覆盖整个屏幕
      const randomX = Math.random() * canvas.width;
      const randomY = Math.random() * canvas.height;
      createBurst(randomX, randomY);
    }, delay);
  }

  // 持续创建额外的随机爆发,覆盖整个屏幕
  const burstInterval = setInterval(() => {
    if (particles.length < 500) {
      // 在整个屏幕范围内完全随机分布
      const randomX = Math.random() * canvas.width;
      const randomY = Math.random() * canvas.height;
      createBurst(randomX, randomY);
    }
  }, 250);

  // 开始动画
  update();

  // 3秒后停止创建新爆发
  setTimeout(() => {
    clearInterval(burstInterval);
  }, 3000);
};

const stop = () => {
  if (animationId) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }
  particles = [];
  if (canvasRef.value) {
    const ctx = canvasRef.value.getContext("2d");
    if (ctx) {
      ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
    }
  }
};

defineExpose({
  start,
  stop,
});

onMounted(() => {
  window.addEventListener("resize", initCanvas);
});

onUnmounted(() => {
  stop();
  window.removeEventListener("resize", initCanvas);
});
</script>

<style scoped>
.fireworks-canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 9998;
  pointer-events: none;
  background: transparent;
}
</style>