Canvas如何做个心电图动画

2,528 阅读3分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

前言

爱情所爱的是无限,所畏惧的是界限。

——克尔凯郭尔

介绍

本期讲的是绘制一个心电图动画,谁说码农不懂浪漫,只是表达方式特殊而已。

15dc8c71a2cee83f37e897d5347c0bc2.gif

大致就是以上这样,我会分为基础结构,心的波动,生成心形,来讲解这个案例,准备好做完分享给谁了么,那就出发吧~

出发

1.基础结构

<style>
    * {
        padding: 0;
        margin: 0;
    }

    * {
        padding: 0;
        margin: 0;
    }

    html,
    body {
        width: 100%;
        height: 100vh;
        position: relative;
        background-color: #000;
    }
    #canvas {
        width: 100%;
        height: 100%;
    }
</style>

<body>
    <canvas id="canvas"></canvas>
    <script type="module" src="./app.js"></script>
</body>

基础的html和css不做赘述,就是做个全屏黑布。

/*app.js*/
class Application {
  constructor() {
    this.canvas = null;      // 画布
    this.ctx = null;         // 环境
    this.w = 0;              // 画布宽
    this.h = 0;              // 画布高
    this.speed = 5;          // 绘制速度
    this.lineData = [];      // 绘制数据
    this.maxHeight = 50;     // 波动幅度
    this.active = 0;         // 激活状态
    this.heartData = [];     // 心形数据
    this.heartR = 7;         // 心形半径
    this.dt = 0;             // 周期值
    this.x = 0;              // 当前x轴坐标
    this.y = 0;              // 当前y轴坐标
    this.startX = 0;         // 绘制心形起始x轴坐标
    this.startY = 0;         // 绘制心形起始y轴坐标
    this.lineColor = "rgba(218,40,0,1)";        // 线段颜色
    this.shadowColor = "rgba(255,255,255,.5)";  // 投影色
    this.centerY = 0;        // y轴固定点
    this.init();
  }
  init() {
    // 初始化
    this.canvas = document.getElementById("canvas");
    this.ctx = this.canvas.getContext("2d");
    window.addEventListener("resize", this.reset.bind(this));
    this.render();
  }
  reset() {
    // 屏幕变化
    this.w = this.canvas.width = this.ctx.width = window.innerWidth;
    this.h = this.canvas.height = this.ctx.height = window.innerHeight;
    this.centerY = this.h / 2 + this.heartR*Math.PI*2;
    this.y = this.centerY;
    this.clear();
  }
  clear(){
      // 清空
  }
  render() {
    // 主渲染
    this.reset();
    this.step();
  }
  getHeart() {
      // 获得心型
  }
  drawTopLine() {
      // 绘制白线
  }
  drawLine() {
      // 绘制红线
  }
  step() {
    // 重绘
    requestAnimationFrame(this.step.bind(this));
    if (this.dt % 2 == 0) {
      this.drawLine();
      this.drawTopLine();
    }
    if (this.x > this.w + this.speed) {
      this.clear()
    }
    this.dt++
  }
}

window.onload = new Application();

看完结构,先说说,我们的期望吧:

  1. 我们先要绘制一条白色心跳线,他是随机产生的,有峰谷值,每绘制一段后其坐标会存储到lineData里面。
  2. 再绘制一条红色心跳线,他不是随机产生的,而是根据lineData里面的数据而绘制,这样就做到了白线先绘制,紧跟着红色线再绘制的效果,这就是心电图的基础动画了。
  3. 心形我们要做个完美心形(之前七夕有个绘制心形的栗子),拿到心形数据,在白线绘制周期内找个地方塞入执行,就大功告成了。
  4. 绘制完心跳后,我们做清空,把他还原回初始状态,再从头重复动画。

2.心的波动

drawTopLine() {
    // 白线
    const { ctx, w, h, x, y, shadowColor, maxHeight, lineData, speed, active,centerY } = this;
    lineData.unshift({ x, y })
    let x1 = x + Math.random() * speed + speed;
    let y1 = centerY;
    if (x1 > w * 0.05 && x1 < w * 0.95) {
        if (Math.random() > 0.8 && active == 0) {
            y1 += Math.random() * maxHeight * 2 - maxHeight
        }
    }
    ctx.lineWidth = 3;
    ctx.strokeStyle = "rgba(255,255,255,.5)";
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.shadowBlur = 20;
    ctx.shadowColor = shadowColor;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x1, y1);
    ctx.stroke();
    ctx.closePath();
    this.x = x1;
    this.y = y1;
}
drawLine() {
    // 红线
    const { ctx,shadowColor, lineColor, maxHeight, lineData } = this;
    if (lineData.length < 2) return;
    ctx.lineWidth = 3;
    ctx.strokeStyle = lineColor;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.shadowBlur = 20;
    ctx.shadowColor = shadowColor;
    ctx.beginPath();
    ctx.moveTo(lineData[1].x, lineData[1].y);
    ctx.lineTo(lineData[0].x, lineData[0].y);
    ctx.stroke();
    ctx.closePath();
}

绘制线段我们会每隔一个周期在x轴方向增加一个值,y轴固定,但再某个随机时刻y轴出现加一个随机变量,使其变化,但下次绘制y轴依然以固定值为基础绘制。每次绘制保存绘制记录,给红线使用。值得注意的是,因为绘制一个线段要有两个坐标点,所以红线要在绘制记录两条以上,再开始绘制。

微信截图_20210812093918.png

到这里我们基础的心电图已经做好了,接下来要绘制心形了。

3.生成心形

getHeart() {
    let t = Math.PI + 0.5;
    let maxt = 2 * Math.PI - 1;
    let vt = this.speed/100;
    let x = 0;
    let y = 0;
    let r = this.heartR;
    for (let i = 0; i < Math.ceil(maxt / vt); i++) {
        x = 16 * Math.pow(Math.sin(t), 3);
        y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
        t += vt;
        x *= r;
        y = -y * r - r * Math.PI * 4;
        if (y < 0) {
            this.heartData.push({ x, y });
        }
    }
}

我们先把心形数据不是一个变化值所以先计算并保存起来方便后面绘制。

drawTopLine() {
    // 白线
    // ...
    if (x1 > w * 0.05 && x1 < w * 0.95) {
      if (Math.random() > 0.8 && active == 0) {
        y1 += Math.random() * maxHeight * 2 - maxHeight
      }
      if (x1 > w * 0.25 && this.active == 0) {
        this.active = 1;
      }
      if (x1 > w * 0.38 && this.active == 1) {
        this.active = 2;
        this.startX = x1 + speed * 3;
        this.startY = centerY;
      }
      if (this.heartData.length > 0 && this.active == 2) {
        let _pos = this.heartData.shift();
        x1 = this.startX + _pos.x;
        y1 = this.startY + _pos.y;
        if (y1 > this.startY) {
          y1 = this.startY;
        }
      }
      if (x1 > 0.55 * w && this.heartData.length == 0 && this.active == 2) {
        this.active = 0;
      }
    }
    // ...
  }

我们绘制肯定要在白线当中,控制他的什么时候绘制,绘制多少变回来,属于你自己需求逻辑,这里也不过多赘述。唯一要讲的是要保存起始点,以这个坐标为基础来绘制心形,而不是变化值。绘制前后期望都要来段横线,这样峰谷与心形不会靠的太近。

微信截图_20210830094246.png

4.重新绘制

clear(){
    this.heartData.length = this.lineData.length = 0;
    this.active = 0;
    this.x = 0;
    this.getHeart();
    this.ctx.clearRect(0, 0, this.w, this.h);
}

我们最后要在屏幕变化或者完全绘制完毕后,重新绘制一遍。所以我们要清空画布,清空数据,状态,横坐标归0。这样就可以反复生成了。


这里我们就讲完了,很容易吧,在线演示

拓展&延伸

我们做的其实并不完美,还有好多需要处理,比如大量判断逻辑存在与白线内并不好,如果出现剧本这样逻辑过多会爆炸的,而且一些边界限定也没有做。当然如果你想扩展更多效果的话处理这些还是很有必要的。你可以不光画心形,可以生成点阵文字连接起来,好多的创意都可以在这种心电图上实现。


希望我们走时不像来时那样空落落。