Canvas 写的酷炫动画代码分析

霓虹灯线形成的自发六边形,随机性生成火花

在看这篇文章时,里面有个动画的示例(如上图),然后感觉有点很酷炫,就打算了解一下怎么写的。( 先上代码示例链接)

前景提要

需要先确保你还记得三角函数的知识。
对 Canvas 的 API 有点了解,且稍微了解其中的 globalCompositeOperation API (不了解的话,可以看下这个文章,基本可以有点感觉)。

代码分析

尽我所能,我尽量在代码里关键地方都增加了注释。(可能有些描述表达不够好,请见谅~)

   var w = (c.width = window.innerWidth),
      h = (c.height = window.innerHeight),
      ctx = c.getContext("2d"),
      //一些配置项
      opts = {
        len: 20, //线长
        count: 50, //线总数
        baseTime: 10, //线停留基础时间
        addedTime: 10, //线额外停留时间
        dieChance: 0.05, //线重置的概率
        spawnChance: 1, //线生成的概率
        sparkChance: 0.1, //火花生成的概率
        sparkDist: 10, //火花距离线的距离
        sparkSize: 2, //火花大小

        color: "hsl(hue,100%,light%)", //hsl() 函数使用色相、饱和度、亮度来定义颜色。
        baseLight: 50, //基础的颜色亮度
        addedLight: 10, // [50-10,50+10]
        shadowToTimePropMult: 6, //阴影的模糊级别
        baseLightInputMultiplier: 0.01, //基础亮度
        addedLightInputMultiplier: 0.02, //额外亮度

        cx: w / 2,
        cy: h / 2,
        repaintAlpha: 0.04,
        hueChange: 0.1,
      },
      tick = 0, //控制颜色色相
      lines = [],
      dieX = w / 2 / opts.len,
      dieY = h / 2 / opts.len,
      baseRad = (Math.PI * 2) / 6;

    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, w, h);

    function loop() {
      //浏览器下次重绘前调用该方法
      window.requestAnimationFrame(loop);
      //循环过程中更改生成的霓虹灯颜色色相
      ++tick;

      /*  目标图像 = 已经放置在画布上的绘图。
  源图像 = 打算放置到画布上的绘图。 */
      ctx.globalCompositeOperation = "source-over"; //目标图像上显示源图像
      ctx.shadowBlur = 0;
      ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
      ctx.fillRect(0, 0, w, h);
      ctx.globalCompositeOperation = "lighter"; //显示源图像 + 目标图像(重叠图形的颜色是通过颜色值相加来确定)

      //保持生成的霓虹灯线共有 count 个
      if (lines.length < opts.count && Math.random() < opts.spawnChance)
        lines.push(new Line());

      lines.map(function (line) {
        line.step();
      });
    }
    function Line() {
      //生成霓虹灯线时进行初始化
      this.reset();
    }
    //初始化,重置
    Line.prototype.reset = function () {
      this.x = 0;
      this.y = 0;
      this.addedX = 0;
      this.addedY = 0;

      this.rad = 0;
      //亮度
      this.lightInputMultiplier =
        opts.baseLightInputMultiplier +
        opts.addedLightInputMultiplier * Math.random();

      this.color = opts.color.replace("hue", tick * opts.hueChange);
      this.cumulativeTime = 0; //累计的时间

      this.beginPhase();
    };
    //霓虹灯线每一步的开始前规划阶段
    Line.prototype.beginPhase = function () {
      this.x += this.addedX;
      this.y += this.addedY;

      this.time = 0;
      //霓虹灯线每一步的停留时间
      this.targetTime = (opts.baseTime + opts.addedTime * Math.random()) | 0;
      //随机六边形路线方向
      this.rad += baseRad * (Math.random() < 0.5 ? 1 : -1);
      this.addedX = Math.cos(this.rad);
      this.addedY = Math.sin(this.rad);
      //霓虹灯线消失重置的条件
      if (
        Math.random() < opts.dieChance ||
        this.x > dieX ||
        this.x < -dieX ||
        this.y > dieY ||
        this.y < -dieY
      )
        this.reset();
    };
    //行走一步
    Line.prototype.step = function () {
      ++this.time;
      ++this.cumulativeTime;
      //超过行走时间,规划下一步
      if (this.time >= this.targetTime) this.beginPhase();

      var prop = this.time / this.targetTime,
        wave = Math.sin((prop * Math.PI) / 2), //sin90°=1
        x = this.addedX * wave, //cos(R)=b/c
        y = this.addedY * wave; //sin(R)=a/c

      ctx.shadowBlur = prop * opts.shadowToTimePropMult; //阴影的模糊级别
      //模糊和填充的颜色
      ctx.fillStyle = ctx.shadowColor = this.color.replace(
        "light",
        opts.baseLight +
          opts.addedLight *
            Math.sin(this.cumulativeTime * this.lightInputMultiplier)
      );

      //绘制霓虹灯线
      ctx.fillRect(
        opts.cx + (this.x + x) * opts.len,
        opts.cy + (this.y + y) * opts.len,
        2,
        2
      );
      //随机生成火花
      if (Math.random() < opts.sparkChance)
        ctx.fillRect(
          opts.cx +
            (this.x + x) * opts.len +
            Math.random() * opts.sparkDist * (Math.random() < 0.5 ? 1 : -1) -
            opts.sparkSize / 2,
          opts.cy +
            (this.y + y) * opts.len +
            Math.random() * opts.sparkDist * (Math.random() < 0.5 ? 1 : -1) -
            opts.sparkSize / 2,
          opts.sparkSize,
          opts.sparkSize
        );
    };
    loop();

    //监听浏览器窗口调整,重置
    window.addEventListener("resize", function () {
      w = c.width = window.innerWidth;
      h = c.height = window.innerHeight;
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, w, h);

      opts.cx = w / 2;
      opts.cy = h / 2;

      dieX = w / 2 / opts.len;
      dieY = h / 2 / opts.len;
    });

简单来描述下,上面的主要代码:

  • 每一次浏览器重绘前都调用 loop() 函数。
  • 在 loop() 函数里,保持共有 count 个实例化的 Line 。
  • 在实例化时,调用 reset() 函数进行一些属性的初始化。
  • 在初始化完成后,调用 beginPhase() 函数进行下一步绘制的路线规划。(其中霓虹灯线触发重置条件时,调用 reset() 函数,进行属性的数值化)
  • 回到 loop() 函数,遍历每一个示例 Line ,调用 step() 函数,进行绘制霓虹灯线和线周围的火花。(其中超过每一步规定的停留时间后,调用 beginPhase() 函数,规划下一步。)
问题

这个动画,让我一开始感觉到厉害的地方是,霓虹灯线行走的尾部,有个渐渐的变暗淡的过程。所以,让人感觉这个动画,就很酷炫。
而这个是怎么做的呢?我上面描述刻意没有讲到。可以看下代码,思考下,思路感觉挺微妙的。(我是重新看了下代码才明白的)

答案

关键点就在于 loop() 函数里的这两行代码。

   ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
   ctx.fillRect(0, 0, w, h);

通过每一次的层层叠加上有一定透明度的黑色,从而达到了后面尾巴逐渐消灭的效果。(如果你开始一眼就发现了,打扰了,献丑了)

最后

酷炫的 Canvas 从来没有写过,也没接触过。这次试着分析这个酷炫动画代码,算是对如何用 Canvas 画动画有了点感觉了吧。
另外,虽然看懂了代码,但似乎不是知道 Canvas 怎么画动画就能写出这个效果的,总感觉里面似乎蕴涵了一些数学功底~

你可能感兴趣的:(Canvas 写的酷炫动画代码分析)