StarryBackground.vue
9.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
<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: 'https://cdn.ipadbiz.cn/mlaj/recall/img/bg01@2x.png'
},
/** 星星数量 */
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; // 当前流星的索引
/**
* @description 创建星星纹理(放射效果)
* @returns {HTMLCanvasElement} 包含星星纹理的 canvas 元素
*/
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;
};
/**
* @description 初始化星星数组
*/
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
});
}
};
/**
* @description 渲染动画帧
*/
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);
};
/**
* @description 触发流星动画
*/
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>