生日快乐烟花

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>生日快乐烟花</title>
    <style>
      canvas {
        position: absolute;
        top: 0;
        left: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="c"></canvas>

    <script>
      var w = (c.width = window.innerWidth),
        h = (c.height = window.innerHeight),
        ctx = c.getContext('2d'),
        hw = w / 2, // half-width
        hh = h / 2,
        opts = {
          strings: ['HAPPY', 'BIRTHDAY!'],
          charSize: 30,
          charSpacing: 35,
          lineHeight: 40,

          cx: w / 2,
          cy: h / 2,

          fireworkPrevPoints: 10,
          fireworkBaseLineWidth: 5,
          fireworkAddedLineWidth: 8,
          fireworkSpawnTime: 200,
          fireworkBaseReachTime: 30,
          fireworkAddedReachTime: 30,
          fireworkCircleBaseSize: 20,
          fireworkCircleAddedSize: 10,
          fireworkCircleBaseTime: 30,
          fireworkCircleAddedTime: 30,
          fireworkCircleFadeBaseTime: 10,
          fireworkCircleFadeAddedTime: 5,
          fireworkBaseShards: 5,
          fireworkAddedShards: 5,
          fireworkShardPrevPoints: 3,
          fireworkShardBaseVel: 4,
          fireworkShardAddedVel: 2,
          fireworkShardBaseSize: 3,
          fireworkShardAddedSize: 3,
          gravity: 0.1,
          upFlow: -0.1,
          letterContemplatingWaitTime: 360,
          balloonSpawnTime: 20,
          balloonBaseInflateTime: 10,
          balloonAddedInflateTime: 10,
          balloonBaseSize: 20,
          balloonAddedSize: 20,
          balloonBaseVel: 0.4,
          balloonAddedVel: 0.4,
          balloonBaseRadian: -(Math.PI / 2 - 0.5),
          balloonAddedRadian: -1,
        },
        calc = {
          totalWidth:
            opts.charSpacing *
            Math.max(opts.strings[0].length, opts.strings[1].length),
        },
        Tau = Math.PI * 2,
        TauQuarter = Tau / 4,
        letters = [];

      ctx.font = opts.charSize + 'px Verdana';

      function Letter(char, x, y) {
        this.char = char;
        this.x = x;
        this.y = y;

        this.dx = -ctx.measureText(char).width / 2;
        this.dy = +opts.charSize / 2;

        this.fireworkDy = this.y - hh;

        var hue = (x / calc.totalWidth) * 360;

        this.color = 'hsl(hue,80%,50%)'.replace('hue', hue);
        this.lightAlphaColor = 'hsla(hue,80%,light%,alp)'.replace('hue', hue);
        this.lightColor = 'hsl(hue,80%,light%)'.replace('hue', hue);
        this.alphaColor = 'hsla(hue,80%,50%,alp)'.replace('hue', hue);

        this.reset();
      }
      Letter.prototype.reset = function () {
        this.phase = 'firework';
        this.tick = 0;
        this.spawned = false;
        this.spawningTime = (opts.fireworkSpawnTime * Math.random()) | 0;
        this.reachTime =
          (opts.fireworkBaseReachTime +
            opts.fireworkAddedReachTime * Math.random()) |
          0;
        this.lineWidth =
          opts.fireworkBaseLineWidth +
          opts.fireworkAddedLineWidth * Math.random();
        this.prevPoints = [[0, hh, 0]];
      };
      Letter.prototype.step = function () {
        if (this.phase === 'firework') {
          if (!this.spawned) {
            ++this.tick;
            if (this.tick >= this.spawningTime) {
              this.tick = 0;
              this.spawned = true;
            }
          } else {
            ++this.tick;

            var linearProportion = this.tick / this.reachTime,
              armonicProportion = Math.sin(linearProportion * TauQuarter),
              x = linearProportion * this.x,
              y = hh + armonicProportion * this.fireworkDy;

            if (this.prevPoints.length > opts.fireworkPrevPoints)
              this.prevPoints.shift();

            this.prevPoints.push([x, y, linearProportion * this.lineWidth]);

            var lineWidthProportion = 1 / (this.prevPoints.length - 1);

            for (var i = 1; i < this.prevPoints.length; ++i) {
              var point = this.prevPoints[i],
                point2 = this.prevPoints[i - 1];

              ctx.strokeStyle = this.alphaColor.replace(
                'alp',
                i / this.prevPoints.length
              );
              ctx.lineWidth = point[2] * lineWidthProportion * i;
              ctx.beginPath();
              ctx.moveTo(point[0], point[1]);
              ctx.lineTo(point2[0], point2[1]);
              ctx.stroke();
            }

            if (this.tick >= this.reachTime) {
              this.phase = 'contemplate';

              this.circleFinalSize =
                opts.fireworkCircleBaseSize +
                opts.fireworkCircleAddedSize * Math.random();
              this.circleCompleteTime =
                (opts.fireworkCircleBaseTime +
                  opts.fireworkCircleAddedTime * Math.random()) |
                0;
              this.circleCreating = true;
              this.circleFading = false;

              this.circleFadeTime =
                (opts.fireworkCircleFadeBaseTime +
                  opts.fireworkCircleFadeAddedTime * Math.random()) |
                0;
              this.tick = 0;
              this.tick2 = 0;

              this.shards = [];

              var shardCount =
                  (opts.fireworkBaseShards +
                    opts.fireworkAddedShards * Math.random()) |
                  0,
                angle = Tau / shardCount,
                cos = Math.cos(angle),
                sin = Math.sin(angle),
                x = 1,
                y = 0;

              for (var i = 0; i < shardCount; ++i) {
                var x1 = x;
                x = x * cos - y * sin;
                y = y * cos + x1 * sin;

                this.shards.push(
                  new Shard(this.x, this.y, x, y, this.alphaColor)
                );
              }
            }
          }
        } else if (this.phase === 'contemplate') {
          ++this.tick;

          if (this.circleCreating) {
            ++this.tick2;
            var proportion = this.tick2 / this.circleCompleteTime,
              armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;

            ctx.beginPath();
            ctx.fillStyle = this.lightAlphaColor
              .replace('light', 50 + 50 * proportion)
              .replace('alp', proportion);
            ctx.beginPath();
            ctx.arc(this.x, this.y, armonic * this.circleFinalSize, 0, Tau);
            ctx.fill();

            if (this.tick2 > this.circleCompleteTime) {
              this.tick2 = 0;
              this.circleCreating = false;
              this.circleFading = true;
            }
          } else if (this.circleFading) {
            ctx.fillStyle = this.lightColor.replace('light', 70);
            ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

            ++this.tick2;
            var proportion = this.tick2 / this.circleFadeTime,
              armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;

            ctx.beginPath();
            ctx.fillStyle = this.lightAlphaColor
              .replace('light', 100)
              .replace('alp', 1 - armonic);
            ctx.arc(this.x, this.y, this.circleFinalSize, 0, Tau);
            ctx.fill();

            if (this.tick2 >= this.circleFadeTime) this.circleFading = false;
          } else {
            ctx.fillStyle = this.lightColor.replace('light', 70);
            ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
          }

          for (var i = 0; i < this.shards.length; ++i) {
            this.shards[i].step();

            if (!this.shards[i].alive) {
              this.shards.splice(i, 1);
              --i;
            }
          }

          if (this.tick > opts.letterContemplatingWaitTime) {
            this.phase = 'balloon';

            this.tick = 0;
            this.spawning = true;
            this.spawnTime = (opts.balloonSpawnTime * Math.random()) | 0;
            this.inflating = false;
            this.inflateTime =
              (opts.balloonBaseInflateTime +
                opts.balloonAddedInflateTime * Math.random()) |
              0;
            this.size =
              (opts.balloonBaseSize + opts.balloonAddedSize * Math.random()) |
              0;

            var rad =
                opts.balloonBaseRadian +
                opts.balloonAddedRadian * Math.random(),
              vel = opts.balloonBaseVel + opts.balloonAddedVel * Math.random();

            this.vx = Math.cos(rad) * vel;
            this.vy = Math.sin(rad) * vel;
          }
        } else if (this.phase === 'balloon') {
          ctx.strokeStyle = this.lightColor.replace('light', 80);

          if (this.spawning) {
            ++this.tick;
            ctx.fillStyle = this.lightColor.replace('light', 70);
            ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

            if (this.tick >= this.spawnTime) {
              this.tick = 0;
              this.spawning = false;
              this.inflating = true;
            }
          } else if (this.inflating) {
            ++this.tick;

            var proportion = this.tick / this.inflateTime,
              x = (this.cx = this.x),
              y = (this.cy = this.y - this.size * proportion);

            ctx.fillStyle = this.alphaColor.replace('alp', proportion);
            ctx.beginPath();
            generateBalloonPath(x, y, this.size * proportion);
            ctx.fill();

            ctx.beginPath();
            ctx.moveTo(x, y);
            ctx.lineTo(x, this.y);
            ctx.stroke();

            ctx.fillStyle = this.lightColor.replace('light', 70);
            ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

            if (this.tick >= this.inflateTime) {
              this.tick = 0;
              this.inflating = false;
            }
          } else {
            this.cx += this.vx;
            this.cy += this.vy += opts.upFlow;

            ctx.fillStyle = this.color;
            ctx.beginPath();
            generateBalloonPath(this.cx, this.cy, this.size);
            ctx.fill();

            ctx.beginPath();
            ctx.moveTo(this.cx, this.cy);
            ctx.lineTo(this.cx, this.cy + this.size);
            ctx.stroke();

            ctx.fillStyle = this.lightColor.replace('light', 70);
            ctx.fillText(
              this.char,
              this.cx + this.dx,
              this.cy + this.dy + this.size
            );

            if (this.cy + this.size < -hh || this.cx < -hw || this.cy > hw)
              this.phase = 'done';
          }
        }
      };
      function Shard(x, y, vx, vy, color) {
        var vel =
          opts.fireworkShardBaseVel +
          opts.fireworkShardAddedVel * Math.random();

        this.vx = vx * vel;
        this.vy = vy * vel;

        this.x = x;
        this.y = y;

        this.prevPoints = [[x, y]];
        this.color = color;

        this.alive = true;

        this.size =
          opts.fireworkShardBaseSize +
          opts.fireworkShardAddedSize * Math.random();
      }
      Shard.prototype.step = function () {
        this.x += this.vx;
        this.y += this.vy += opts.gravity;

        if (this.prevPoints.length > opts.fireworkShardPrevPoints)
          this.prevPoints.shift();

        this.prevPoints.push([this.x, this.y]);

        var lineWidthProportion = this.size / this.prevPoints.length;

        for (var k = 0; k < this.prevPoints.length - 1; ++k) {
          var point = this.prevPoints[k],
            point2 = this.prevPoints[k + 1];

          ctx.strokeStyle = this.color.replace(
            'alp',
            k / this.prevPoints.length
          );
          ctx.lineWidth = k * lineWidthProportion;
          ctx.beginPath();
          ctx.moveTo(point[0], point[1]);
          ctx.lineTo(point2[0], point2[1]);
          ctx.stroke();
        }

        if (this.prevPoints[0][1] > hh) this.alive = false;
      };
      function generateBalloonPath(x, y, size) {
        ctx.moveTo(x, y);
        ctx.bezierCurveTo(
          x - size / 2,
          y - size / 2,
          x - size / 4,
          y - size,
          x,
          y - size
        );
        ctx.bezierCurveTo(
          x + size / 4,
          y - size,
          x + size / 2,
          y - size / 2,
          x,
          y
        );
      }

      function anim() {
        window.requestAnimationFrame(anim);

        ctx.fillStyle = '#111';
        ctx.fillRect(0, 0, w, h);

        ctx.translate(hw, hh);

        var done = true;
        for (var l = 0; l < letters.length; ++l) {
          letters[l].step();
          if (letters[l].phase !== 'done') done = false;
        }

        ctx.translate(-hw, -hh);

        if (done) for (var l = 0; l < letters.length; ++l) letters[l].reset();
      }

      for (var i = 0; i < opts.strings.length; ++i) {
        for (var j = 0; j < opts.strings[i].length; ++j) {
          letters.push(
            new Letter(
              opts.strings[i][j],
              j * opts.charSpacing +
                opts.charSpacing / 2 -
                (opts.strings[i].length * opts.charSize) / 2,
              i * opts.lineHeight +
                opts.lineHeight / 2 -
                (opts.strings.length * opts.lineHeight) / 2
            )
          );
        }
      }

      anim();

      window.addEventListener('resize', function () {
        w = c.width = window.innerWidth;
        h = c.height = window.innerHeight;

        hw = w / 2;
        hh = h / 2;

        ctx.font = opts.charSize + 'px Verdana';
      });
    </script>
  </body>
</html>