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

<script>
import { wxInfo } from '@/utils/tools';

const { isMobile, isWeiXin } = wxInfo();
const isWxMobile = isMobile && isWeiXin;
</script>

<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: isWxMobile ? 500 : 500
  },
  // 星星颜色 (RGBA 前缀,例如 '255,255,255')
  starColor: {
    type: String,
    default: '255,255,255'
  },
  // 星星移动速度系数 (值越大移动越快)
  starSpeed: {
    type: Number,
    default: isWxMobile ? 0.3 : 0.1
  },
  // 流星速度 - 水平分量 (负值向左,正值向右)
  meteorVx: {
    type: Number,
    default: isWxMobile ? -10 : -2
  },
  // 流星速度 - 垂直分量 (正值向下)
  meteorVy: {
    type: Number,
    default: isWxMobile ? 10 : 2
  },
  // 流星拖尾长度系数 (值越大尾巴越长)
  meteorTrail: {
    type: Number,
    default: isWxMobile ? 4 : 10
  }
});

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 starTexture = null; // 预渲染的星星纹理
let animationFrameId = null;
let meteorTimeoutId = null;
let meteorIndex = -1; // 当前流星的索引

// 创建星星纹理(放射效果)
const createStarTexture = () => {
  const canvas = document.createElement('canvas');
  canvas.width = 32;
  canvas.height = 32;
  const ctx = canvas.getContext('2d');
  const center = 16;
  const radius = 16;

  // 1. 核心发光 (径向渐变)
  const gradient = ctx.createRadialGradient(center, center, 0, center, center, radius);
  gradient.addColorStop(0, `rgba(${props.starColor}, 1)`);
  gradient.addColorStop(0.2, `rgba(${props.starColor}, 0.8)`);
  gradient.addColorStop(0.5, `rgba(${props.starColor}, 0.1)`);
  gradient.addColorStop(1, `rgba(${props.starColor}, 0)`);
  ctx.fillStyle = gradient;
  ctx.beginPath();
  ctx.arc(center, center, radius, 0, Math.PI * 2);
  ctx.fill();

  // 2. 十字星芒 (模拟放射效果)
  ctx.strokeStyle = `rgba(${props.starColor}, 0.9)`;
  ctx.lineWidth = 1; // 细线条
  ctx.beginPath();
  // 横向光芒
  ctx.moveTo(center - radius, center);
  ctx.lineTo(center + radius, center);
  // 纵向光芒
  ctx.moveTo(center, center - radius);
  ctx.lineTo(center, center + radius);
  ctx.stroke();

  return canvas;
};

// 初始化星星
const initStars = () => {
  stars = [];
  starTexture = createStarTexture(); // 生成纹理

  for (let i = 0; i < props.starCount; i++) {
    stars.push({
      x: Math.round(Math.random() * width),
      y: Math.round(Math.random() * height),
      // 增加星星大小的随机性:范围 [2, 12] (因为使用纹理绘制,这里的r代表缩放基准)
      // 较小的星星居多,较大的星星较少
      r: Math.random() < 0.9 ? Math.random() * 2 + 1 : Math.random() * 6 + 4,
      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) {
      // 检查流星是否超出边界,如果是则重置
      if (star.x < -100 || star.y > height + 100) {
        meteorIndex = -1; // 结束当前流星
      } else {
        star.vx = props.meteorVx;
        star.vy = props.meteorVy;
        context.beginPath();

        // 创建线性渐变来实现淡入淡出和拖尾效果
        const meteorHeadX = star.x + star.vx;
        const meteorHeadY = star.y + star.vy;
        const meteorTailX = star.x + star.vx * props.meteorTrail;
        const meteorTailY = star.y + star.vy * props.meteorTrail;

        const gradient = context.createLinearGradient(meteorHeadX, meteorHeadY, meteorTailX, meteorTailY);
        gradient.addColorStop(0, `rgba(${props.starColor}, 1)`); // 头部最亮
        gradient.addColorStop(0.4, `rgba(${props.starColor}, 0.5)`); // 中间半透明
        gradient.addColorStop(1, `rgba(${props.starColor}, 0)`); // 尾部完全透明

        context.strokeStyle = gradient;
        context.lineWidth = 1; // 细一点
        context.moveTo(meteorHeadX, meteorHeadY);
        context.lineTo(meteorTailX, meteorTailY);
        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;
    }

    // 绘制星星
    // 叠加闪烁效果:使用正弦波在基础 alpha 上叠加快速变化
    const twinkle = Math.abs(Math.sin(Date.now() * 0.006 + i));
    const finalAlpha = Math.min(1, Math.max(0, star.alpha * (0.5 + 0.5 * twinkle)));

    if (starTexture) {
      // 使用预渲染的纹理绘制
      context.globalAlpha = finalAlpha;
      // star.r 作为大小的一半
      context.drawImage(starTexture, star.x - star.r, star.y - star.r, star.r * 2, star.r * 2);
      context.globalAlpha = 1.0; // 恢复
    } else {
      // 降级处理
      context.beginPath();
      const bg = context.createRadialGradient(star.x, star.y, 0, star.x, star.y, star.r);
      bg.addColorStop(0, `rgba(${props.starColor}, ${finalAlpha})`);
      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);

    // 如果选中了星星,重置它的位置到右上方,以便从右往左飞
    if (stars[meteorIndex]) {
        stars[meteorIndex].x = width + Math.random() * 200; // 屏幕右侧外
        stars[meteorIndex].y = Math.random() * height * 0.5; // 屏幕上半部分
    }

    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>