放烟花啦!

Published on
/
/趣玩前端
<!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%,
          /* Bleu nuit foncé en haut */ #1b3b5f 20%,
          /* Bleu nuit moyen */ #2a4973 40%,
          /* Bleu plus clair avec une touche de violet */ #1b3b5f 60%,
          /* Retour au bleu moyen */ #0a1f3b 100%
            /* Fondu vers le bleu foncé en bas */
        );
      }
      #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() {
          // Launch point
          this.x = Math.random() * this.canvasWidth;
          this.y = this.canvasHeight;

          // Vibrant, random color
          this.color = `hsl(${Math.random() * 360}, 100%, 60%)`;

          // Randomized launch trajectory
          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;

          // Draw launch trail
          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();

          // Check for explosion
          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];

            // Mouvement de particule
            particle.x += particle.dx;
            particle.y += particle.dy;

            // Ajouter le point actuel à la traînée
            particle.trail.push({ x: particle.x, y: particle.y });
            if (particle.trail.length > particle.maxTrailLength) {
              particle.trail.shift();
            }

            // Gravité et friction
            particle.dy += 0.15;
            particle.dx *= 0.99;

            // Dessiner la traînée
            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();
            }

            // Dessiner la particule
            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();

            // Diminution de l'alpha
            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();
          }

          // Reset if all particles are gone
          if (this.exploded && this.particles.length === 0) {
            this.reset();
          }
        }

        // Fonction utilitaire pour convertir HSL en RGB
        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'; // Couleur par défaut si la conversion échoue
        }

        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() {
          // Effet de fondu plus subtil
          this.ctx.fillStyle = 'rgba(7, 7, 48, 0.15)';
          this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

          // Update and draw fireworks
          this.fireworks.forEach((firework) => firework.update());

          requestAnimationFrame(() => this.animate());
        }
      }

      // Initialize on page load
      new FireworksDisplay();
    </script>
  </body>
</html>