生日祝福:粒子烟花+蛋糕+音乐

113 阅读3分钟

乔乔生日快乐

<html>

<head>
  <meta charset="UTF-8" />
  <title>乔乔生日快乐</title>
  <style type="text/css">
    body,
    html {
      margin: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #000000;
    }

    canvas {
      position: absolute;
      top: 0;
      left: 0;
    }

    #text-canvas {
      z-index: 1;
    }

    #fireworks-canvas {
      z-index: 2;
    }

    #cake-canvas {
      z-index: 3;
    }

    #audio-control {
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 100;
      background: rgba(255, 255, 255, 0.2);
      border-radius: 50%;
      width: 40px;
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      color: white;
      font-size: 20px;
    }
  </style>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<body>
  <canvas id="text-canvas"></canvas>
  <canvas id="fireworks-canvas"></canvas>
  <canvas id="cake-canvas"></canvas>

  <script type="text/javascript">
    // 生日快乐歌的音符序列
    const birthdaySong = [
      { note: 'G4', duration: 0.5 }, { note: 'G4', duration: 0.5 },
      { note: 'A4', duration: 1 }, { note: 'G4', duration: 1 },
      { note: 'C5', duration: 1 }, { note: 'B4', duration: 2 },
      
      { note: 'G4', duration: 0.5 }, { note: 'G4', duration: 0.5 },
      { note: 'A4', duration: 1 }, { note: 'G4', duration: 1 },
      { note: 'D5', duration: 1 }, { note: 'C5', duration: 2 },
      
      { note: 'G4', duration: 0.5 }, { note: 'G4', duration: 0.5 },
      { note: 'G5', duration: 1 }, { note: 'E5', duration: 1 },
      { note: 'C5', duration: 1 }, { note: 'B4', duration: 1 },
      { note: 'A4', duration: 2 },
      
      { note: 'F5', duration: 0.5 }, { note: 'F5', duration: 0.5 },
      { note: 'E5', duration: 1 }, { note: 'C5', duration: 1 },
      { note: 'D5', duration: 1 }, { note: 'C5', duration: 2 }
    ];

    // 音符频率映射
    const noteFrequencies = {
      'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23,
      'G4': 392.00, 'A4': 440.00, 'B4': 493.88, 'C5': 523.25,
      'D5': 587.33, 'E5': 659.25, 'F5': 698.46, 'G5': 783.99
    };

    // 创建音频上下文
    let audioContext;
    let playPrompt = document.getElementById('playPrompt');

    // 播放音符函数
    function playNote(frequency, duration) {
      return new Promise((resolve) => {
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();
        
        oscillator.type = 'sine';
        oscillator.frequency.value = frequency;
        
        // 设置音量包络
        gainNode.gain.setValueAtTime(0, audioContext.currentTime);
        gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.1);
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration * 0.9);
        
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);
        
        oscillator.start();
        oscillator.stop(audioContext.currentTime + duration);
        
        oscillator.onended = () => resolve();
      });
    }

    // 播放整首歌
    async function playSong() {
      const tempo = 100; // 每分钟100拍
      const beatDuration = 60 / tempo;
      
      while (true) { // 无限循环播放
        for (const {note, duration} of birthdaySong) {
          const frequency = noteFrequencies[note];
          await playNote(frequency, duration * beatDuration);
        }
        await new Promise(resolve => setTimeout(resolve, 2000)); // 歌曲间隔2秒
      }
    }

    // 尝试自动播放
    function tryAutoPlay() {
      try {
        audioContext = new (window.AudioContext || window.webkitAudioContext)();
        playSong().catch(e => {
          console.log('自动播放被阻止:', e);
          showPlayPrompt();
        });
      } catch (e) {
        console.log('创建音频上下文失败:', e);
        showPlayPrompt();
      }
    }

    // 显示播放提示
    function showPlayPrompt() {
      playPrompt.style.display = 'block';
      document.body.addEventListener('click', function startOnClick() {
        audioContext.resume().then(() => {
          playSong();
          playPrompt.style.display = 'none';
        });
        document.body.removeEventListener('click', startOnClick);
      });
    }

    // 页面加载后尝试自动播放
    window.addEventListener('load', tryAutoPlay);
    // 文字画布
    const textCanvas = document.getElementById("text-canvas");
    const textCtx = textCanvas.getContext("2d");

    // 烟花画布
    const fireworksCanvas = document.getElementById("fireworks-canvas");

    // 蛋糕画布
    const cakeCanvas = document.getElementById("cake-canvas");
    const cakeCtx = cakeCanvas.getContext("2d");

    // 初始化画布大小
    function resize() {
      textCanvas.width = fireworksCanvas.width = cakeCanvas.width = window.innerWidth;
      textCanvas.height = fireworksCanvas.height = cakeCanvas.height = window.innerHeight;
    }
    window.onresize = resize;
    resize();

    // 清空画布
    function clearCanvas(ctx) {
      ctx.fillStyle = "#000";
      ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    }

    // 烟花系统
    const Fireworks = function () {
      const self = this;
      const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

      self.fireworkSettings = {
        autoFireDelay: 400,
        particleCount: 250,
        fireworkSpeed: 5,
        hueRange: 360,
        autoFireEnabled: true
      };

      self.init = function () {
        self.ctx = fireworksCanvas.getContext("2d");
        self.ctx.lineCap = "round";
        self.ctx.lineJoin = "round";

        self.particles = [];
        self.fireworks = [];
        self.currentHue = rand(0, 360);
        self.lineWidth = 1;

        self.bindEvents();
        self.loop();

        if (self.fireworkSettings.autoFireEnabled) {
          self.autoFire();
        }
      };

      self.autoFire = function () {
        setInterval(() => {
          self.currentHue = rand(0, self.fireworkSettings.hueRange);
          self.createFireworks(
            rand(100, window.innerWidth - 100),
            window.innerHeight,
            rand(100, window.innerWidth - 100),
            rand(50, window.innerHeight * 0.7)
          );
        }, self.fireworkSettings.autoFireDelay);
      };

      self.createFireworks = function (startX, startY, targetX, targetY) {
        const newFirework = {
          x: startX,
          y: startY,
          startX,
          startY,
          targetX,
          targetY,
          speed: 3 + Math.random() * self.fireworkSettings.fireworkSpeed,
          angle: Math.atan2(targetY - startY, targetX - startX),
          hue: self.currentHue,
          brightness: rand(60, 90),
          alpha: 0.7 + Math.random() * 0.3
        };
        self.fireworks.push(newFirework);
      };

      self.createParticles = function (x, y, hue) {
        for (let i = 0; i < self.fireworkSettings.particleCount; i++) {
          const angle = Math.random() * Math.PI * 2;
          const speed = 2 + Math.random() * 6;

          self.particles.push({
            x,
            y,
            vx: Math.cos(angle) * speed,
            vy: Math.sin(angle) * speed,
            hue: hue + rand(-30, 30),
            brightness: rand(60, 90),
            alpha: 0.6 + Math.random() * 0.4,
            decay: 0.01 + Math.random() * 0.02,
            size: 1 + Math.random() * 3
          });
        }
      };

      self.updateFireworks = function () {
        for (let i = self.fireworks.length - 1; i >= 0; i--) {
          const f = self.fireworks[i];
          const dx = f.targetX - f.x;
          const dy = f.targetY - f.y;
          const dist = Math.sqrt(dx * dx + dy * dy);

          if (dist < 10) {
            self.createParticles(f.x, f.y, f.hue);
            self.fireworks.splice(i, 1);
          } else {
            f.x += Math.cos(f.angle) * f.speed;
            f.y += Math.sin(f.angle) * f.speed;
          }
        }
      };

      self.updateParticles = function () {
        for (let i = self.particles.length - 1; i >= 0; i--) {
          const p = self.particles[i];

          p.x += p.vx;
          p.y += p.vy;
          p.vy += 0.05;
          p.alpha -= p.decay;

          if (p.alpha <= 0) {
            self.particles.splice(i, 1);
          }
        }
      };

      self.drawFireworks = function () {
        self.ctx.globalCompositeOperation = "lighter";

        for (const f of self.fireworks) {
          self.ctx.beginPath();
          self.ctx.moveTo(f.startX, f.startY);
          self.ctx.lineTo(f.x, f.y);
          self.ctx.strokeStyle = `hsla(${f.hue}, 100%, ${f.brightness}%, ${f.alpha})`;
          self.ctx.lineWidth = self.lineWidth;
          self.ctx.stroke();
        }
      };

      self.drawParticles = function () {
        self.ctx.globalCompositeOperation = "lighter";

        for (const p of self.particles) {
          self.ctx.beginPath();
          self.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
          self.ctx.fillStyle = `hsla(${p.hue}, 100%, ${p.brightness}%, ${p.alpha})`;
          self.ctx.fill();
        }
      };

      self.loop = function () {
        requestAnimationFrame(self.loop);

        self.ctx.globalCompositeOperation = "destination-out";
        self.ctx.fillStyle = "rgba(0, 0, 0, 0.15)";
        self.ctx.fillRect(0, 0, fireworksCanvas.width, fireworksCanvas.height);

        self.updateFireworks();
        self.updateParticles();
        self.drawFireworks();
        self.drawParticles();
      };

      self.bindEvents = function () {
        $(fireworksCanvas).on("click", function (e) {
          for (let i = 0; i < 3; i++) {
            setTimeout(() => {
              self.currentHue = rand(0, self.fireworkSettings.hueRange);
              self.createFireworks(
                window.innerWidth / 2,
                window.innerHeight,
                e.pageX - fireworksCanvas.offsetLeft + rand(-50, 50),
                e.pageY - fireworksCanvas.offsetTop + rand(-50, 50)
              );
            }, i * 100);
          }
        });
      };

      self.init();
    };

    // 简约蛋糕设计
    const Cake = {
      init() {
        this.cakeWidth = Math.min(350, window.innerWidth * 0.7);
        this.cakeHeight = this.cakeWidth * 0.4;
        this.x = (window.innerWidth - this.cakeWidth) / 2;
        this.y = window.innerHeight - this.cakeHeight - 80;

        // 简约蜡烛设计
        this.candles = [];
        const candleCount = 5;
        const spacing = this.cakeWidth / (candleCount + 1);

        for (let i = 0; i < candleCount; i++) {
          this.candles.push({
            x: this.x + spacing * (i + 1),
            y: this.y - 10,
            height: 60,
            flameSize: 10,
            hue: 50 + i * 20, // 暖色调渐变
            flicker: Math.random() * Math.PI * 2
          });
        }

        this.drawCake();
      },

      drawCake() {
        cakeCtx.clearRect(0, 0, cakeCanvas.width, cakeCanvas.height);

        // 简约蛋糕主体 - 双层设计
        cakeCtx.fillStyle = "#f8f1e5"; // 象牙白
        cakeCtx.beginPath();
        cakeCtx.roundRect(this.x, this.y, this.cakeWidth, this.cakeHeight, 15);
        cakeCtx.fill();

        // 上层蛋糕
        cakeCtx.fillStyle = "#fff8f0"; // 更浅的象牙白
        cakeCtx.beginPath();
        cakeCtx.roundRect(
          this.x + 10,
          this.y - 30,
          this.cakeWidth - 20,
          this.cakeHeight,
          10
        );
        cakeCtx.fill();

        // 简约装饰线条
        cakeCtx.strokeStyle = "#f2d3ac";
        cakeCtx.lineWidth = 3;
        cakeCtx.beginPath();
        cakeCtx.moveTo(this.x + 20, this.y + 10);
        cakeCtx.lineTo(this.x + this.cakeWidth - 20, this.y + 10);
        cakeCtx.stroke();

        cakeCtx.beginPath();
        cakeCtx.moveTo(this.x + 20, this.y + this.cakeHeight - 15);
        cakeCtx.lineTo(this.x + this.cakeWidth - 20, this.y + this.cakeHeight - 15);
        cakeCtx.stroke();

        // 绘制简约蜡烛
        this.drawCandles();

        requestAnimationFrame(() => this.drawCake());
      },

      drawCandles() {
        const now = Date.now();

        this.candles.forEach(candle => {
          // 简约蜡烛柱
          cakeCtx.fillStyle = `hsl(${candle.hue}, 80%, 85%)`;
          cakeCtx.fillRect(candle.x - 6, candle.y - candle.height, 12, candle.height);

          // 蜡烛火焰动画
          candle.flicker += 0.1;
          const flameVariation = Math.sin(candle.flicker) * 0.3 + 0.7;

          // 简约火焰效果
          const gradient = cakeCtx.createRadialGradient(
            candle.x, candle.y - candle.height,
            0,
            candle.x, candle.y - candle.height,
            candle.flameSize * flameVariation
          );
          gradient.addColorStop(0, `hsla(50, 100%, 90%, 0.9)`);
          gradient.addColorStop(1, `hsla(20, 100%, 50%, 0)`);

          cakeCtx.fillStyle = gradient;
          cakeCtx.beginPath();
          cakeCtx.arc(
            candle.x,
            candle.y - candle.height,
            candle.flameSize * flameVariation,
            0, Math.PI * 2
          );
          cakeCtx.fill();

          // 简约光晕效果
          cakeCtx.globalCompositeOperation = "lighter";
          cakeCtx.fillStyle = `hsla(40, 100%, 70%, ${0.2 * flameVariation})`;
          cakeCtx.beginPath();
          cakeCtx.arc(
            candle.x,
            candle.y - candle.height,
            candle.flameSize * 1.5 * flameVariation,
            0, Math.PI * 2
          );
          cakeCtx.fill();
          cakeCtx.globalCompositeOperation = "source-over";
        });
      }
    };

    // 闪烁文字系统
    class TextParticles {
      constructor() {
        this.particles = [];
        this.colors = ["#f8d3b8", "#f2a6a2", "#d4e6f4", "#f2e8c4", "#efd282"];
        this.textAlpha = 1;
        this.fadeDirection = -1;
      }

      createFromText(text) {
        clearCanvas(textCtx);
        this.particles = [];

        // 绘制文字
        textCtx.fillStyle = `rgba(255,255,255,${this.textAlpha})`;
        textCtx.font = "bold 120px 'Microsoft YaHei', sans-serif";

        const texts = text.split("\n");
        const lineHeight = 150;
        const startY = (textCanvas.height - (texts.length * lineHeight)) / 2;

        texts.forEach((line, i) => {
          const x = (textCanvas.width - textCtx.measureText(line).width) / 2;
          const y = startY + (i * lineHeight);
          textCtx.fillText(line, x, y);
        });

        // 生成粒子
        const imageData = textCtx.getImageData(0, 0, textCanvas.width, textCanvas.height).data;

        for (let i = 0; i < imageData.length; i += 16) {
          if (imageData[i] > 128 && Math.random() > 0.7) {
            const x = (i / 4) % textCanvas.width;
            const y = Math.floor((i / 4) / textCanvas.width);

            this.particles.push({
              x,
              y,
              targetX: x,
              targetY: y,
              color: this.colors[Math.floor(Math.random() * this.colors.length)],
              size: 1 + Math.random() * 2,
              alpha: 0.8 + Math.random() * 0.2
            });
          }
        }

        clearCanvas(textCtx);
      }

      updateFade() {
        this.textAlpha += this.fadeDirection * 0.02;
        if (this.textAlpha <= 0.6 || this.textAlpha >= 1) {
          this.fadeDirection *= -1;
        }
      }

      draw() {
        this.updateFade();
        textCtx.globalCompositeOperation = "lighter";
        // 绘制粒子
        this.particles.forEach(p => {
          textCtx.beginPath();
          textCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
          textCtx.fillStyle = p.color.replace(")", "," + p.alpha + ")").replace("rgb", "rgba");
          textCtx.fill();
        });
      }
    }

    // 初始化系统
    const fireworks = new Fireworks();
    const textParticles = new TextParticles();
    Cake.init();

    // 创建文字粒子
    textParticles.createFromText("乔乔\n生日快乐!");

    // 动画循环
    function animate() {
      requestAnimationFrame(animate);
      textParticles.draw();
    }
    animate();
  </script>
</body>

</html>

微信图片_20250623105220.png