Fireworks.vue
7.01 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
<template>
<canvas ref="canvasRef" class="fireworks-canvas"></canvas>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const canvasRef = ref<HTMLCanvasElement | null>(null);
let animationId: number | null = null;
let particles: Particle[] = [];
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
color: string;
life: number;
decay: number;
size: number;
trail: Array<{ x: number; y: number; alpha: number }>;
gravity: number; // 重力加速度
airResistance: number; // 空气阻力系数
startX: number; // 起始位置(用于贝塞尔曲线)
startY: number;
}
const colors = [
"#ff0000", // 红
"#ff8800", // 橙
"#ffff00", // 黄
"#00ff00", // 绿
"#00ffff", // 青
"#0088ff", // 蓝
"#8800ff", // 紫
"#ff00ff", // 粉
"#ff0088", // 玫红
"#ffaa00", // 金橙
"#88ff00", // 黄绿
"#00ff88", // 青绿
];
const initCanvas = () => {
if (!canvasRef.value) return;
const canvas = canvasRef.value;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
const createBurst = (centerX: number, centerY: number) => {
if (!canvasRef.value) return;
const particleCount = 80 + Math.floor(Math.random() * 40);
const baseSpeed = 4 + Math.random() * 5;
for (let i = 0; i < particleCount; i++) {
const angle =
(Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
const speed = baseSpeed + Math.random() * 4;
const colorIndex = Math.floor(Math.random() * colors.length);
const color = colors[colorIndex] || "#ff0000";
// 更小的粒子尺寸
const size = 1 + Math.random() * 1.5;
const life = 0.9 + Math.random() * 0.1;
const decay = 0.01 + Math.random() * 0.01;
// 重力加速度(向下为正)
const gravity = 0.15 + Math.random() * 0.1;
// 空气阻力系数(0.98-0.995之间)
const airResistance = 0.98 + Math.random() * 0.015;
particles.push({
x: centerX,
y: centerY,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color,
life,
decay,
size,
trail: [],
gravity,
airResistance,
startX: centerX,
startY: centerY,
});
}
};
const update = () => {
if (!canvasRef.value) return;
const canvas = canvasRef.value;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 完全透明清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新粒子
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
if (!particle) {
particles.splice(i, 1);
continue;
}
// 应用重力效果(向下加速)
particle.vy += particle.gravity;
// 应用空气阻力(衰减速度)
particle.vx *= particle.airResistance;
particle.vy *= particle.airResistance;
// 更新位置
particle.x += particle.vx;
particle.y += particle.vy;
// 保存当前位置到拖尾(在位置更新后)
particle.trail.push({
x: particle.x,
y: particle.y,
alpha: particle.life,
});
// 限制拖尾长度(用于贝塞尔曲线绘制)
if (particle.trail.length > 15) {
particle.trail.shift();
}
// 更新生命周期
particle.life -= particle.decay;
if (particle.life <= 0) {
particles.splice(i, 1);
} else {
// 使用贝塞尔曲线绘制轨迹
if (particle.trail.length >= 2) {
ctx.strokeStyle = particle.color;
ctx.lineCap = "round";
ctx.lineJoin = "round";
// 绘制贝塞尔曲线轨迹
for (let j = 0; j < particle.trail.length - 1; j++) {
const point = particle.trail[j];
const nextPoint = particle.trail[j + 1];
if (!point || !nextPoint) continue;
// 计算控制点(用于创建平滑的曲线)
const controlX = point.x + (nextPoint.x - point.x) * 0.5;
const controlY = point.y + (nextPoint.y - point.y) * 0.5;
// 根据拖尾位置计算透明度(越远越淡)
const progress = j / (particle.trail.length - 1);
const alpha = point.alpha * (1 - progress * 0.6) * 0.7;
ctx.globalAlpha = alpha;
ctx.lineWidth = particle.size * 0.6 + (1 - progress) * 0.4;
// 使用二次贝塞尔曲线绘制
ctx.beginPath();
ctx.moveTo(point.x, point.y);
ctx.quadraticCurveTo(controlX, controlY, nextPoint.x, nextPoint.y);
ctx.stroke();
}
}
// 绘制主粒子(更小的光晕效果)
const gradient = ctx.createRadialGradient(
particle.x,
particle.y,
0,
particle.x,
particle.y,
particle.size * 1.5,
);
gradient.addColorStop(0, particle.color);
gradient.addColorStop(0.6, particle.color + "60");
gradient.addColorStop(1, particle.color + "00");
ctx.globalAlpha = particle.life;
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
ctx.fill();
// 绘制核心亮点(更小的核心)
ctx.globalAlpha = particle.life;
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size * 0.6, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
animationId = requestAnimationFrame(update);
};
const start = () => {
if (!canvasRef.value) return;
initCanvas();
particles = [];
const canvas = canvasRef.value;
// 创建初始随机爆发点
const initialBursts = 8 + Math.floor(Math.random() * 5);
for (let i = 0; i < initialBursts; i++) {
const delay = i * 100 + Math.random() * 50;
setTimeout(() => {
// 完全随机位置,覆盖整个屏幕
const randomX = Math.random() * canvas.width;
const randomY = Math.random() * canvas.height;
createBurst(randomX, randomY);
}, delay);
}
// 持续创建额外的随机爆发,覆盖整个屏幕
const burstInterval = setInterval(() => {
if (particles.length < 500) {
// 在整个屏幕范围内完全随机分布
const randomX = Math.random() * canvas.width;
const randomY = Math.random() * canvas.height;
createBurst(randomX, randomY);
}
}, 250);
// 开始动画
update();
// 3秒后停止创建新爆发
setTimeout(() => {
clearInterval(burstInterval);
}, 3000);
};
const stop = () => {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
particles = [];
if (canvasRef.value) {
const ctx = canvasRef.value.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
}
}
};
defineExpose({
start,
stop,
});
onMounted(() => {
window.addEventListener("resize", initCanvas);
});
onUnmounted(() => {
stop();
window.removeEventListener("resize", initCanvas);
});
</script>
<style scoped>
.fireworks-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9998;
pointer-events: none;
background: transparent;
}
</style>