canvas 贝塞尔曲线绘制动态流动线

发布时间 2023-09-01 15:11:40作者: 风紧了

效果如下:

无意看到类似上图效果,就想着自己复现下,也熟悉下canvas方法。为了方便计算,我把每个tab列表等分10份,每个192px,渐变色长度为192 X 2;曲线是通过三次贝赛尔曲线绘制的,曲线运动是通过这个drawCurvePath方法,根据曲线的占比绘制曲线,具体代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    .tab {
      width: 1920px;
      height: 76px;
      display: flex;
      font-size: 30px;
      text-align: center;
      align-items: center;
      position: absolute;
    }

    .tab span {
      display: inline-block;
      width: 192px;
    }
  </style>

  <body>
    <div>
      <div>
        <div class="tab">
          <span attr_index="1">tab1</span>
          <span attr_index="2">tab2</span>
          <span attr_index="3">tab3</span>
          <span attr_index="4">tab4</span>
        </div>
        <canvas width="1920" height="100" id="canvas"></canvas>
      </div>
    </div>
  </body>
  <script>
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    const radius = 25; // 圆角大小
    const height = 50; //高度
    let current = 4; // 圆角所在索引
    let process = 0.3; // 高亮线进度

    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      let currentPre = (current - 1) / 10;
      let startX = 0; // 起点x坐标
      let startY = 60; // 起点y坐标
      let startX1 = 192 * (current - 1); //第一个圆角x起点
      let startY1 = 60; //第一个圆角y起点
      let startX2 = startX1 + radius; // 第一个圆角y终点
      let startY2 = startY1 - radius; // 第一个圆角y终点
      let arcX1 = startX2; // 第一个圆角x圆心
      let arcY1 = startY1; // 第一个圆角y圆心
      let startX3 = startX2 + height - 2 * radius; // 第二个圆角x起点
      let startY3 = startY2 - (height - 2 * radius); // 第二个圆角y起点
      let startX4 = startX3 + radius; // 第二个圆角x终点
      let startY4 = startY3 - radius; // 第二个圆角y终点
      let arcX2 = startX3; // 第二个圆角x圆心
      let arcY2 = startY4; // 第二个圆角y圆心
      let startX5 = startX4 + 192 - height * 2; // 第三个圆角x起点
      let startY5 = startY4; // 第三个圆角y起点
      let startX6 = startX5 + radius; // 第三个圆角x终点
      let startY6 = startY5 + radius; // 第三个圆角y终点
      let arcX3 = startX6; // 第三个圆角x圆心
      let arcY3 = startY5; // 第三个圆角y圆心
      let startX7 = startX6 + height - 2 * radius; // 第四个圆角x起点
      let startY7 = startY6 + height - 2 * radius; // 第四个圆角y起点
      let startX8 = startX7 + radius; // 第四个圆角x终点
      let startY8 = startY7 + radius; // 第四个圆角y终点
      let arcX4 = startX7; // 第四个圆角x圆心
      let arcY4 = startY8; // 第四个圆角y圆心
      let endX = startX8 + 192 * (10 - current); // 终点x坐标
      let endY = startY8; // 终点y坐标
      // 绘制默认线
      ctx.strokeStyle = "green"; // 设置默认线颜色为绿色
      ctx.beginPath();
      ctx.moveTo(startX, startY);
      ctx.lineTo(startX1, startY1);
      ctx.bezierCurveTo(arcX1, arcY1, arcX2, arcY2, startX4, startY4);
      ctx.lineTo(startX5, startY5);
      ctx.bezierCurveTo(arcX3, arcY3, arcX4, arcY4, startX8, startY8);
      ctx.lineTo(endX, endY);
      ctx.stroke();
      ctx.closePath();
      ctx.save();
      //绘制高亮线

      let gradient = ctx.createLinearGradient(
        192 * smoothstep(process - 0.2) * 10,
        100,
        192 * process * 10,
        100
      );
      gradient.addColorStop(0, "#ffeb3b3b");
      gradient.addColorStop(0.5, "#ffc107b3");
      gradient.addColorStop(1, "#ffeb3b");
      ctx.strokeStyle = gradient; //设置渐变色
      ctx.beginPath();
      ctx.shadowColor = "red";
      ctx.shadowBlur = 3;
      ctx.moveTo(
        startX +
          (startX1 - startX) *
            smoothstep(((process - 0.2) * 10) / (current - 1)),
        startY +
          (startY1 - startY) *
            smoothstep(((process - 0.2) * 10) / (current - 1))
      );
      ctx.lineTo(
        startX +
          (startX1 - startX) * smoothstep((process * 10) / (current - 1)),
        startY + (startY1 - startY) * smoothstep((process * 10) / (current - 1))
      );

      if (process > currentPre && process < currentPre + 0.2) {
        let t = smoothstep((process - currentPre) / 0.026);
        drawCurvePath(
          ctx,
          startX1,
          arcX1,
          arcX2,
          startX4,
          startY1,
          arcY1,
          arcY2,
          startY4,
          t
        );
      }
      if (process > currentPre + 0.026 && process < currentPre + 0.226) {
        if (process > currentPre + 0.2 && process < currentPre + 0.226) {
          ctx.moveTo(startX4, startY4);
          let t = smoothstep((currentPre + 0.226 - process) / 0.026);

          drawCurvePath(
            ctx,
            startX4,
            arcX2,
            arcX1,
            startX1,
            startY4,
            arcY2,
            arcY1,
            startY1,
            t
          );
        }
        ctx.moveTo(startX4, startY4);
        ctx.lineTo(
          startX4 +
            (startX5 - startX4) *
              smoothstep((process - (currentPre + 0.026)) / 0.048),
          startY5
        );
      }
      if (process > currentPre + 0.074 && process < currentPre + 0.274) {
        let t = smoothstep((process - (currentPre + 0.074)) / 0.026);
        ctx.moveTo(startX5, startY5);
        drawCurvePath(
          ctx,
          startX5,
          arcX3,
          arcX4,
          startX8,
          startY5,
          arcY3,
          arcY4,
          startY8,
          t
        );
      }

      if (process >= currentPre + 0.226 && process < currentPre + 0.274) {
        ctx.moveTo(startX5, startY5);
        ctx.lineTo(
          startX4 +
            (startX5 - startX4) *
              smoothstep(1 - (currentPre + 0.274 - process) / 0.048),
          startY5
        );
      }
      if (process > currentPre + 0.274 && process < 0.9 - currentPre) {
        ctx.moveTo(startX8, startY8);
        let t = smoothstep((0.9 - currentPre - process) / 0.026);

        drawCurvePath(
          ctx,
          startX8,
          arcX4,
          arcX3,
          startX5,
          startY8,
          arcY4,
          arcY3,
          startY5,
          t
        );
      }
      if (process >= currentPre + 0.1) {
        ctx.moveTo(startX8, startY8);
        ctx.moveTo(
          startX8 +
            (endX - startX8) *
              smoothstep(
                ((process - (currentPre + 0.3)) * 10) / (10 - current)
              ),
          startY8 +
            (endY - startY8) *
              smoothstep(((process - (currentPre + 0.3)) * 10) / (10 - current))
        );
        ctx.lineTo(
          startX8 +
            (endX - startX8) *
              smoothstep(
                ((process - (currentPre + 0.1)) * 10) / (10 - current)
              ),
          startY8 +
            (endY - startY8) *
              smoothstep(((process - (currentPre + 0.1)) * 10) / (10 - current))
        );
      }

      ctx.stroke();
      ctx.restore();
      if (process >= 1) {
        process = 0;
      } else {
        process += 0.004;
      }
      requestAnimationFrame(draw);
    }
    draw();

    let tab = document.getElementsByClassName("tab")[0];

    tab.addEventListener("click", function (event) {
      let index = event.target.getAttribute("attr_index");
      current = index;
    });

    function getBezierCurveXY(t, px0, px1, px2, px3, py0, py1, py2, py3) {
      //三次贝塞尔曲线方程式,根据t参数,返回对应的x,y值
      //let x2 =(1-t)^3 * P0 + 3t(1-t)^2 * P1 + 3t^2(1-t) * P2 + t^3 * P3

      let x =
        px0 * Math.pow(1 - t, 3) +
        px1 * 3 * t * Math.pow(1 - t, 2) +
        px2 * 3 * Math.pow(t, 2) * (1 - t) +
        px3 * Math.pow(t, 3);

      let y =
        py0 * Math.pow(1 - t, 3) +
        py1 * 3 * t * Math.pow(1 - t, 2) +
        py2 * 3 * Math.pow(t, 2) * (1 - t) +
        py3 * Math.pow(t, 3);
      let points = {
        x,
        y,
      };
      return points;
    }
    //返回一个0-1之间的值
    function smoothstep(sum, max = 1, min = 0) {
      if (sum < min) {
        sum = min;
      } else if (sum > max) {
        sum = max;
      }
      return sum;
    }

    // 绘制整个贝塞尔曲线的百分比
    function drawCurvePath(ctx, px0, px1, px2, px3, py0, py1, py2, py3, t) {
      var p01 = [px1 - px0, py1 - py0]; // 向量 p0 -> p1
      var p12 = [px2 - px1, py2 - py1]; // 向量 p1 -> p2
      var p23 = [px3 - px2, py3 - py2]; // 向量 p2 -> p3

      var q0 = [px0 + p01[0] * t, py0 + p01[1] * t];
      var q1 = [px1 + p12[0] * t, py1 + p12[1] * t];
      var q2 = [px2 + p23[0] * t, py2 + p23[1] * t];

      var q01 = [q1[0] - q0[0], q1[1] - q0[1]]; // 向量 q0 -> q1
      var q12 = [q2[0] - q1[0], q2[1] - q1[1]]; // 向量 q1 -> q2

      var r0 = [q0[0] + q01[0] * t, q0[1] + q01[1] * t];
      var r1 = [q1[0] + q12[0] * t, q1[1] + q12[1] * t];

      var r01 = [r1[0] - r0[0], r1[1] - r0[1]]; // 向量 r0 -> r1

      var b = [r0[0] + r01[0] * t, r0[1] + r01[1] * t];

      // ctx.moveTo(px0, py0);
      ctx.bezierCurveTo(q0[0], q0[1], r0[0], r0[1], b[0], b[1]);
    }
  </script>
</html>