hookehuyr

feat(背景效果): 添加星空背景组件并集成到测试页面

新增可配置的星空背景组件,支持自定义星星数量、颜色、速度等参数
在测试页面中集成该组件作为背景效果
......@@ -37,6 +37,7 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SharePoster: typeof import('./components/ui/SharePoster.vue')['default']
StarryBackground: typeof import('./components/effects/StarryBackground.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default']
TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default']
......
<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>
<!--
* @Date: 2025-12-18 00:22:07
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-22 14:01:23
* @FilePath: /mlaj/src/views/test.vue
* @Description: 文件描述
-->
<template>
<button @click="setVolume">设置音量</button>
<audio ref="audioRef" src="https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3"></audio>
<!-- 使用封装的星空背景组件 -->
<StarryBackground bgImage="https://cdn.ipadbiz.cn/mlaj/images/test-bgg03.jpg" />
</template>
<script setup>
import { ref } from 'vue';
import StarryBackground from '@/components/effects/StarryBackground.vue';
const audioRef = ref(null);
......@@ -36,3 +47,13 @@ const setVolume = async () => {
}
};
</script>
<style scoped>
button {
position: relative;
z-index: 10;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
......