feat(背景效果): 添加星空背景组件并集成到测试页面
新增可配置的星空背景组件,支持自定义星星数量、颜色、速度等参数 在测试页面中集成该组件作为背景效果
Showing
3 changed files
with
239 additions
and
0 deletions
| ... | @@ -37,6 +37,7 @@ declare module 'vue' { | ... | @@ -37,6 +37,7 @@ declare module 'vue' { |
| 37 | RouterView: typeof import('vue-router')['RouterView'] | 37 | RouterView: typeof import('vue-router')['RouterView'] |
| 38 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 38 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| 39 | SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] | 39 | SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] |
| 40 | + StarryBackground: typeof import('./components/effects/StarryBackground.vue')['default'] | ||
| 40 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 41 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 41 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] | 42 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] |
| 42 | TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default'] | 43 | TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default'] | ... | ... |
src/components/effects/StarryBackground.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="canvas-box" :style="containerStyle"> | ||
| 3 | + <canvas ref="canvasRef">你的浏览器不支持canvas</canvas> | ||
| 4 | + </div> | ||
| 5 | +</template> | ||
| 6 | + | ||
| 7 | +<script setup> | ||
| 8 | +import { ref, onMounted, onUnmounted, computed } from 'vue'; | ||
| 9 | + | ||
| 10 | +const props = defineProps({ | ||
| 11 | + // 背景颜色 | ||
| 12 | + bgColor: { | ||
| 13 | + type: String, | ||
| 14 | + default: 'white' | ||
| 15 | + }, | ||
| 16 | + // 背景图片 URL | ||
| 17 | + bgImage: { | ||
| 18 | + type: String, | ||
| 19 | + default: '' | ||
| 20 | + }, | ||
| 21 | + // 星星数量 | ||
| 22 | + starCount: { | ||
| 23 | + type: Number, | ||
| 24 | + default: 500 | ||
| 25 | + }, | ||
| 26 | + // 星星颜色 (RGBA 前缀,例如 '255,255,255') | ||
| 27 | + starColor: { | ||
| 28 | + type: String, | ||
| 29 | + default: '255,255,255' | ||
| 30 | + }, | ||
| 31 | + // 星星移动速度系数 (值越大移动越快) | ||
| 32 | + starSpeed: { | ||
| 33 | + type: Number, | ||
| 34 | + default: 0.15 | ||
| 35 | + }, | ||
| 36 | + // 流星速度 - 水平分量 (负值向左,正值向右) | ||
| 37 | + meteorVx: { | ||
| 38 | + type: Number, | ||
| 39 | + default: -2 | ||
| 40 | + }, | ||
| 41 | + // 流星速度 - 垂直分量 (正值向下) | ||
| 42 | + meteorVy: { | ||
| 43 | + type: Number, | ||
| 44 | + default: 4 | ||
| 45 | + }, | ||
| 46 | + // 流星拖尾长度系数 (值越大尾巴越长) | ||
| 47 | + meteorTrail: { | ||
| 48 | + type: Number, | ||
| 49 | + default: 20 | ||
| 50 | + } | ||
| 51 | +}); | ||
| 52 | + | ||
| 53 | +const containerStyle = computed(() => { | ||
| 54 | + const style = { | ||
| 55 | + backgroundColor: props.bgColor | ||
| 56 | + }; | ||
| 57 | + if (props.bgImage) { | ||
| 58 | + style.backgroundImage = `url(${props.bgImage})`; | ||
| 59 | + style.backgroundSize = 'cover'; | ||
| 60 | + style.backgroundPosition = 'center'; | ||
| 61 | + style.backgroundRepeat = 'no-repeat'; | ||
| 62 | + } | ||
| 63 | + return style; | ||
| 64 | +}); | ||
| 65 | + | ||
| 66 | +const canvasRef = ref(null); | ||
| 67 | +let context = null; | ||
| 68 | +let width = 0; | ||
| 69 | +let height = 0; | ||
| 70 | +let stars = []; | ||
| 71 | +let animationFrameId = null; | ||
| 72 | +let meteorTimeoutId = null; | ||
| 73 | +let meteorIndex = -1; // 当前流星的索引 | ||
| 74 | + | ||
| 75 | +// 初始化星星 | ||
| 76 | +const initStars = () => { | ||
| 77 | + stars = []; | ||
| 78 | + for (let i = 0; i < props.starCount; i++) { | ||
| 79 | + stars.push({ | ||
| 80 | + x: Math.round(Math.random() * width), | ||
| 81 | + y: Math.round(Math.random() * height), | ||
| 82 | + r: Math.random() * 3, | ||
| 83 | + ra: Math.random() * 0.01, | ||
| 84 | + alpha: Math.random(), | ||
| 85 | + vx: Math.random() * props.starSpeed - props.starSpeed / 2, | ||
| 86 | + vy: Math.random() * props.starSpeed - props.starSpeed / 2 | ||
| 87 | + }); | ||
| 88 | + } | ||
| 89 | +}; | ||
| 90 | + | ||
| 91 | +// 渲染函数 | ||
| 92 | +const render = () => { | ||
| 93 | + if (!context) return; | ||
| 94 | + | ||
| 95 | + // 使用 clearRect 代替 fillRect 覆盖,如果需要透明背景或自定义背景色由 CSS 控制 | ||
| 96 | + // 但为了实现星星的拖尾效果或者覆盖上一帧,这里使用 clearRect 清除整个画布 | ||
| 97 | + context.clearRect(0, 0, width, height); | ||
| 98 | + | ||
| 99 | + for (let i = 0; i < props.starCount; i++) { | ||
| 100 | + const star = stars[i]; | ||
| 101 | + | ||
| 102 | + // 流星逻辑 | ||
| 103 | + if (i === meteorIndex) { | ||
| 104 | + star.vx = props.meteorVx; | ||
| 105 | + star.vy = props.meteorVy; | ||
| 106 | + context.beginPath(); | ||
| 107 | + context.strokeStyle = `rgba(${props.starColor}, ${star.alpha})`; | ||
| 108 | + context.lineWidth = star.r; | ||
| 109 | + context.moveTo(star.x, star.y); | ||
| 110 | + context.lineTo(star.x + star.vx * props.meteorTrail, star.y + star.vy * props.meteorTrail); | ||
| 111 | + context.stroke(); | ||
| 112 | + context.closePath(); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + // 星星闪烁与移动逻辑 | ||
| 116 | + star.alpha += star.ra; | ||
| 117 | + if (star.alpha <= 0) { | ||
| 118 | + star.alpha = 0; | ||
| 119 | + star.ra = -star.ra; | ||
| 120 | + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 121 | + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 122 | + } else if (star.alpha > 1) { | ||
| 123 | + star.alpha = 1; | ||
| 124 | + star.ra = -star.ra; | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + star.x += star.vx; | ||
| 128 | + // 边界检查 | ||
| 129 | + if (star.x >= width) { | ||
| 130 | + star.x = 0; | ||
| 131 | + } else if (star.x < 0) { | ||
| 132 | + star.x = width; | ||
| 133 | + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 134 | + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + star.y += star.vy; | ||
| 138 | + if (star.y >= height) { | ||
| 139 | + star.y = 0; | ||
| 140 | + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 141 | + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2; | ||
| 142 | + } else if (star.y < 0) { | ||
| 143 | + star.y = height; | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // 绘制星星 | ||
| 147 | + context.beginPath(); | ||
| 148 | + const bg = context.createRadialGradient(star.x, star.y, 0, star.x, star.y, star.r); | ||
| 149 | + bg.addColorStop(0, `rgba(${props.starColor}, ${star.alpha})`); | ||
| 150 | + bg.addColorStop(1, `rgba(${props.starColor}, 0)`); | ||
| 151 | + context.fillStyle = bg; | ||
| 152 | + context.arc(star.x, star.y, star.r, 0, Math.PI * 2, true); | ||
| 153 | + context.fill(); | ||
| 154 | + context.closePath(); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + animationFrameId = requestAnimationFrame(render); | ||
| 158 | +}; | ||
| 159 | + | ||
| 160 | +// 流星触发逻辑 | ||
| 161 | +const triggerMeteor = () => { | ||
| 162 | + const time = Math.round(Math.random() * 3000 + 33); | ||
| 163 | + meteorTimeoutId = setTimeout(() => { | ||
| 164 | + meteorIndex = Math.ceil(Math.random() * stars.length); | ||
| 165 | + triggerMeteor(); | ||
| 166 | + }, time); | ||
| 167 | +}; | ||
| 168 | + | ||
| 169 | +// 窗口大小调整处理 | ||
| 170 | +const handleResize = () => { | ||
| 171 | + if (canvasRef.value) { | ||
| 172 | + width = window.innerWidth; | ||
| 173 | + height = window.innerHeight; | ||
| 174 | + canvasRef.value.width = width; | ||
| 175 | + canvasRef.value.height = height; | ||
| 176 | + // 大小改变时可能需要重置星星位置,或者保留。这里选择不重置,只更新边界。 | ||
| 177 | + } | ||
| 178 | +}; | ||
| 179 | + | ||
| 180 | +onMounted(() => { | ||
| 181 | + if (canvasRef.value) { | ||
| 182 | + width = window.innerWidth; | ||
| 183 | + height = window.innerHeight; | ||
| 184 | + canvasRef.value.width = width; | ||
| 185 | + canvasRef.value.height = height; | ||
| 186 | + context = canvasRef.value.getContext('2d'); | ||
| 187 | + | ||
| 188 | + initStars(); | ||
| 189 | + render(); | ||
| 190 | + triggerMeteor(); | ||
| 191 | + | ||
| 192 | + window.addEventListener('resize', handleResize); | ||
| 193 | + } | ||
| 194 | +}); | ||
| 195 | + | ||
| 196 | +onUnmounted(() => { | ||
| 197 | + if (animationFrameId) { | ||
| 198 | + cancelAnimationFrame(animationFrameId); | ||
| 199 | + } | ||
| 200 | + if (meteorTimeoutId) { | ||
| 201 | + clearTimeout(meteorTimeoutId); | ||
| 202 | + } | ||
| 203 | + window.removeEventListener('resize', handleResize); | ||
| 204 | +}); | ||
| 205 | +</script> | ||
| 206 | + | ||
| 207 | +<style scoped> | ||
| 208 | +.canvas-box { | ||
| 209 | + position: fixed; | ||
| 210 | + left: 0; | ||
| 211 | + top: 0; | ||
| 212 | + width: 100vw; | ||
| 213 | + height: 100vh; | ||
| 214 | + z-index: -1; | ||
| 215 | + overflow: hidden; | ||
| 216 | +} | ||
| 217 | +</style> |
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-12-18 00:22:07 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-12-22 14:01:23 | ||
| 5 | + * @FilePath: /mlaj/src/views/test.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 1 | <template> | 8 | <template> |
| 2 | <button @click="setVolume">设置音量</button> | 9 | <button @click="setVolume">设置音量</button> |
| 3 | <audio ref="audioRef" src="https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3"></audio> | 10 | <audio ref="audioRef" src="https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3"></audio> |
| 11 | + | ||
| 12 | + <!-- 使用封装的星空背景组件 --> | ||
| 13 | + <StarryBackground bgImage="https://cdn.ipadbiz.cn/mlaj/images/test-bgg03.jpg" /> | ||
| 4 | </template> | 14 | </template> |
| 5 | 15 | ||
| 6 | <script setup> | 16 | <script setup> |
| 7 | import { ref } from 'vue'; | 17 | import { ref } from 'vue'; |
| 18 | +import StarryBackground from '@/components/effects/StarryBackground.vue'; | ||
| 8 | 19 | ||
| 9 | const audioRef = ref(null); | 20 | const audioRef = ref(null); |
| 10 | 21 | ||
| ... | @@ -36,3 +47,13 @@ const setVolume = async () => { | ... | @@ -36,3 +47,13 @@ const setVolume = async () => { |
| 36 | } | 47 | } |
| 37 | }; | 48 | }; |
| 38 | </script> | 49 | </script> |
| 50 | + | ||
| 51 | +<style scoped> | ||
| 52 | +button { | ||
| 53 | + position: relative; | ||
| 54 | + z-index: 10; | ||
| 55 | + padding: 10px 20px; | ||
| 56 | + font-size: 16px; | ||
| 57 | + cursor: pointer; | ||
| 58 | +} | ||
| 59 | +</style> | ... | ... |
-
Please register or login to post a comment