StarryBackground.vue 5.38 KB
<template>
  <div class="canvas-box" :style="containerStyle">
    <canvas ref="canvasRef">你的浏览器不支持canvas</canvas>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';

const props = defineProps({
  // 背景颜色
  bgColor: {
    type: String,
    default: 'white'
  },
  // 背景图片 URL
  bgImage: {
    type: String,
    default: ''
  },
  // 星星数量
  starCount: {
    type: Number,
    default: 500
  },
  // 星星颜色 (RGBA 前缀,例如 '255,255,255')
  starColor: {
    type: String,
    default: '255,255,255'
  },
  // 星星移动速度系数 (值越大移动越快)
  starSpeed: {
    type: Number,
    default: 0.15
  },
  // 流星速度 - 水平分量 (负值向左,正值向右)
  meteorVx: {
    type: Number,
    default: -2
  },
  // 流星速度 - 垂直分量 (正值向下)
  meteorVy: {
    type: Number,
    default: 4
  },
  // 流星拖尾长度系数 (值越大尾巴越长)
  meteorTrail: {
    type: Number,
    default: 20
  }
});

const containerStyle = computed(() => {
  const style = {
    backgroundColor: props.bgColor
  };
  if (props.bgImage) {
    style.backgroundImage = `url(${props.bgImage})`;
    style.backgroundSize = 'cover';
    style.backgroundPosition = 'center';
    style.backgroundRepeat = 'no-repeat';
  }
  return style;
});

const canvasRef = ref(null);
let context = null;
let width = 0;
let height = 0;
let stars = [];
let animationFrameId = null;
let meteorTimeoutId = null;
let meteorIndex = -1; // 当前流星的索引

// 初始化星星
const initStars = () => {
  stars = [];
  for (let i = 0; i < props.starCount; i++) {
    stars.push({
      x: Math.round(Math.random() * width),
      y: Math.round(Math.random() * height),
      r: Math.random() * 3,
      ra: Math.random() * 0.01,
      alpha: Math.random(),
      vx: Math.random() * props.starSpeed - props.starSpeed / 2,
      vy: Math.random() * props.starSpeed - props.starSpeed / 2
    });
  }
};

// 渲染函数
const render = () => {
  if (!context) return;

  // 使用 clearRect 代替 fillRect 覆盖,如果需要透明背景或自定义背景色由 CSS 控制
  // 但为了实现星星的拖尾效果或者覆盖上一帧,这里使用 clearRect 清除整个画布
  context.clearRect(0, 0, width, height);

  for (let i = 0; i < props.starCount; i++) {
    const star = stars[i];

    // 流星逻辑
    if (i === meteorIndex) {
      star.vx = props.meteorVx;
      star.vy = props.meteorVy;
      context.beginPath();
      context.strokeStyle = `rgba(${props.starColor}, ${star.alpha})`;
      context.lineWidth = star.r;
      context.moveTo(star.x, star.y);
      context.lineTo(star.x + star.vx * props.meteorTrail, star.y + star.vy * props.meteorTrail);
      context.stroke();
      context.closePath();
    }

    // 星星闪烁与移动逻辑
    star.alpha += star.ra;
    if (star.alpha <= 0) {
      star.alpha = 0;
      star.ra = -star.ra;
      star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
      star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
    } else if (star.alpha > 1) {
      star.alpha = 1;
      star.ra = -star.ra;
    }

    star.x += star.vx;
    // 边界检查
    if (star.x >= width) {
      star.x = 0;
    } else if (star.x < 0) {
      star.x = width;
      star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
      star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
    }

    star.y += star.vy;
    if (star.y >= height) {
      star.y = 0;
      star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
      star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
    } else if (star.y < 0) {
      star.y = height;
    }

    // 绘制星星
    context.beginPath();
    const bg = context.createRadialGradient(star.x, star.y, 0, star.x, star.y, star.r);
    bg.addColorStop(0, `rgba(${props.starColor}, ${star.alpha})`);
    bg.addColorStop(1, `rgba(${props.starColor}, 0)`);
    context.fillStyle = bg;
    context.arc(star.x, star.y, star.r, 0, Math.PI * 2, true);
    context.fill();
    context.closePath();
  }

  animationFrameId = requestAnimationFrame(render);
};

// 流星触发逻辑
const triggerMeteor = () => {
  const time = Math.round(Math.random() * 3000 + 33);
  meteorTimeoutId = setTimeout(() => {
    meteorIndex = Math.ceil(Math.random() * stars.length);
    triggerMeteor();
  }, time);
};

// 窗口大小调整处理
const handleResize = () => {
  if (canvasRef.value) {
    width = window.innerWidth;
    height = window.innerHeight;
    canvasRef.value.width = width;
    canvasRef.value.height = height;
    // 大小改变时可能需要重置星星位置,或者保留。这里选择不重置,只更新边界。
  }
};

onMounted(() => {
  if (canvasRef.value) {
    width = window.innerWidth;
    height = window.innerHeight;
    canvasRef.value.width = width;
    canvasRef.value.height = height;
    context = canvasRef.value.getContext('2d');

    initStars();
    render();
    triggerMeteor();

    window.addEventListener('resize', handleResize);
  }
});

onUnmounted(() => {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }
  if (meteorTimeoutId) {
    clearTimeout(meteorTimeoutId);
  }
  window.removeEventListener('resize', handleResize);
});
</script>

<style scoped>
.canvas-box {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  overflow: hidden;
}
</style>