<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>放烟花啦!</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css"
/>
<style>
body,
html {
margin: 0;
height: 100%;
overflow: hidden;
background: linear-gradient(
to bottom,
#0a1f3b 0%,
#1b3b5f 20%,
#2a4973 40%,
#1b3b5f 60%,
#0a1f3b 100%
);
}
#fireworks-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.4) 100%
);
}
.title {
position: absolute;
top: 50px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: clamp(2rem, 5vw, 4rem);
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5em;
font-weight: 200;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8),
0 0 20px rgba(255, 255, 255, 0.6), 0 0 30px rgba(255, 255, 255, 0.4),
0 0 40px rgba(255, 182, 255, 0.3);
opacity: 0.92;
z-index: 1000;
animation: glow 2s ease-in-out infinite alternate;
padding-left: 0.5em;
}
.title span {
display: block;
font-family: 'Quicksand', sans-serif;
font-size: 0.4em;
letter-spacing: 0.2em;
text-transform: lowercase;
margin-top: 1em;
font-weight: 300;
opacity: 0.8;
}
@keyframes glow {
from {
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8),
0 0 20px rgba(255, 255, 255, 0.6), 0 0 30px rgba(255, 255, 255, 0.4),
0 0 40px rgba(255, 182, 255, 0.3);
}
to {
text-shadow: 0 0 20px rgba(255, 255, 255, 0.9),
0 0 30px rgba(255, 255, 255, 0.7), 0 0 40px rgba(255, 255, 255, 0.5),
0 0 50px rgba(255, 182, 255, 0.4), 0 0 60px rgba(255, 182, 255, 0.3);
}
}
@media (max-width: 768px) {
.title {
letter-spacing: 0.3em;
padding-left: 0.3em;
}
.title span {
font-size: 0.5em;
letter-spacing: 0.15em;
}
}
</style>
</head>
<body>
<canvas id="fireworks-canvas"></canvas>
<script>
class Firework {
constructor(ctx, canvasWidth, canvasHeight) {
this.ctx = ctx;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.reset();
}
reset() {
this.x = Math.random() * this.canvasWidth;
this.y = this.canvasHeight;
this.color = `hsl(${Math.random() * 360}, 100%, 60%)`;
this.dx = (Math.random() - 0.5) * 3;
this.dy = -(Math.random() * 10 + 10);
this.exploded = false;
this.particles = [];
this.age = 0;
}
launch() {
this.x += this.dx;
this.y += this.dy;
this.ctx.beginPath();
this.ctx.moveTo(this.x - this.dx, this.y - this.dy);
this.ctx.lineTo(this.x, this.y);
this.ctx.strokeStyle = this.color;
this.ctx.stroke();
if (this.y <= this.canvasHeight * Math.random() * 0.5) {
this.explode();
}
}
explode() {
const particleCount = Math.floor(Math.random() * 50 + 50);
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 5 + 2;
this.particles.push({
x: this.x,
y: this.y,
dx: Math.cos(angle) * speed,
dy: Math.sin(angle) * speed,
size: Math.random() * 3 + 1,
alpha: 1,
color: this.color,
trail: [{ x: this.x, y: this.y }],
maxTrailLength: 10,
});
}
this.exploded = true;
}
updateParticles() {
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
particle.x += particle.dx;
particle.y += particle.dy;
particle.trail.push({ x: particle.x, y: particle.y });
if (particle.trail.length > particle.maxTrailLength) {
particle.trail.shift();
}
particle.dy += 0.15;
particle.dx *= 0.99;
if (particle.trail.length > 1) {
this.ctx.beginPath();
this.ctx.moveTo(particle.trail[0].x, particle.trail[0].y);
for (let j = 1; j < particle.trail.length; j++) {
this.ctx.lineTo(particle.trail[j].x, particle.trail[j].y);
}
this.ctx.strokeStyle = `rgba(${this.getRGB(particle.color)}, ${
particle.alpha
})`;
this.ctx.lineWidth = particle.size / 2;
this.ctx.stroke();
}
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${this.getRGB(particle.color)}, ${
particle.alpha
})`;
this.ctx.fill();
particle.alpha -= 0.01;
if (
particle.alpha <= 0 ||
particle.x < 0 ||
particle.x > this.canvasWidth ||
particle.y > this.canvasHeight
) {
this.particles.splice(i, 1);
}
}
}
update() {
this.age++;
if (!this.exploded) {
this.launch();
} else {
this.updateParticles();
}
if (this.exploded && this.particles.length === 0) {
this.reset();
}
}
getRGB(hslColor) {
const match = hslColor.match(
/hsl\((\d+\.?\d*),\s*(\d+)%,\s*(\d+)%\)/
);
if (match) {
const [_, h, s, l] = match;
const rgb = this.hslToRgb(parseFloat(h), parseInt(s), parseInt(l));
return rgb.join(',');
}
return '255,255,255';
}
hslToRgb(h, s, l) {
s /= 100;
l /= 100;
const k = (n) => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = (n) =>
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return [
Math.round(255 * f(0)),
Math.round(255 * f(8)),
Math.round(255 * f(4)),
];
}
}
class FireworksDisplay {
constructor() {
this.canvas = document.getElementById('fireworks-canvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
this.fireworks = [];
for (let i = 0; i < 8; i++) {
this.fireworks.push(
new Firework(this.ctx, this.canvas.width, this.canvas.height)
);
}
window.addEventListener('resize', () => this.resize());
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
animate() {
this.ctx.fillStyle = 'rgba(7, 7, 48, 0.15)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.fireworks.forEach((firework) => firework.update());
requestAnimationFrame(() => this.animate());
}
}
new FireworksDisplay();
</script>
</body>
</html>