使用canvas实现曲线中点上升动画,并给曲线面积填充颜色

129 阅读2分钟

需求

  • 实现曲线像小山堆一样慢慢隆起:平时使用的echarts中曲线图的动画都是从左至右沿着x轴方向运动,而本次项目中需要每个数据,从0开始慢慢递增至其数据大小,也就是要沿着y轴进行运动。

灵感来源

  • 之前看过同学用canvas绘制的贪食蛇的游戏,贪食蛇的移动效果,原理就是沿着需要移动的方向不断的清空重绘,每次重绘,位置向前移动一格,那么在不断的重复过程中,在用户看来,贪食蛇就在移动了,而移动的速度也就跟重绘的时间间隔相关。
  • 同理可得,我需要让曲线动起来,那么我就可以不断地清空重绘,每次都把曲线的顶点升高一点,这样就可以实现曲线慢慢隆起的效果了。

实现步骤

  1. 使用贝塞尔曲线绘制出需要的曲线形状
  2. 给曲线面积填充颜色
  3. 使用定时器,修改贝塞尔曲线中心控制点的高度

主要代码

  • 绘制曲线及颜色填充
// 绘制曲线及颜色填充
drawCurve(params) {
      const rateW = this.eleW / 768;

      // 算沙丘中点和两边的一共四个控制点
      const centerPointX =
        params.centerPointX || params.startPosX + this.halfCurveWidth;
      const centerPointY = params.centerPointY || params.partHeightest;
      const control1X = params.control1X || centerPointX - 55 * rateW;
      const control1Y = params.control1Y || params.startPosY;
      const control2X = params.control2X || centerPointX - 55 * rateW;
      const control2Y = params.control2Y || centerPointY;
      const control3X = params.control3X || centerPointX + 55 * rateW;
      const control3Y = params.control3Y || centerPointY;
      const control4X = params.control4X || centerPointX + 55 * rateW;
      const control4Y = params.control4Y || params.startPosY;

      this.context.beginPath();
      this.context.moveTo(params.startPosX, params.startPosY);
      this.context.bezierCurveTo(
        control1X,
        control1Y,
        control2X,
        control2Y,
        centerPointX,
        centerPointY
      );
      this.context.moveTo(centerPointX, centerPointY);
      this.context.bezierCurveTo(
        control3X,
        control3Y,
        control4X,
        control4Y,
        params.endPosX,
        params.endPosY
      );
      // 设置线的粗细和颜色
      params.isLast
        ? this.context.setLineDash([])
        : this.context.setLineDash([2]);
      this.context.strokeStyle = params.isUp
        ? this.chartData.lineStyle.curve.color.up
        : this.chartData.lineStyle.curve.color.down;
      this.context.lineWidth = this.chartData.lineStyle.curve.width;
      this.context.stroke();
      this.context.closePath();
      // 颜色填充
      this.context.beginPath();
      this.context.moveTo(params.startPosX, params.startPosY);
      this.context.bezierCurveTo(
        control1X,
        control1Y,
        control2X,
        control2Y,
        centerPointX,
        centerPointY
      );
      this.context.lineTo(
        centerPointX,
        params.isUp ? params.heightY || this.heightY : params.lowY || this.lowY
      );
      this.context.moveTo(centerPointX, centerPointY);
      this.context.bezierCurveTo(
        control3X,
        control3Y,
        control4X,
        control4Y,
        params.endPosX,
        params.endPosY
      );
      this.context.lineTo(
        centerPointX,
        params.isUp ? params.heightY || this.heightY : params.lowY || this.lowY
      );
      if (params.isLast) {
        const grad = this.context.createLinearGradient(
          centerPointX,
          centerPointY,
          centerPointX,
          params.isUp
            ? params.heightY || this.heightY
            : params.lowY || this.lowY
        ); // 创建一个渐变色线性对象
        // 设置渐变颜色
        const color = params.isUp
          ? this.chartData.fillStyle.gradient.up
          : this.chartData.fillStyle.gradient.down;
        color.forEach((item, index) => {
          const stop = item.stop + 0.2 > 1 ? 1 : item.stop + 0.2;
          grad.addColorStop(
            params.legend && index !== 0 ? stop : item.stop,
            item.color
          );
        });
        this.context.fillStyle = grad; // 设置fillStyle为当前的渐变对象
      } else {
        this.context.fillStyle = params.isUp
          ? this.chartData.fillStyle.pureColor.up
          : this.chartData.fillStyle.pureColor.down;
      }
      this.context.fill();
    }
  • 使用定时器实现动画
// 曲线文字动画
    animateAll() {
      let countNum = 0;
      this.itemList = this.setShort(this.itemList);
      this.timer = setInterval(() => {
        this.context.clearRect(0, 0, this.eleW, this.eleH);
        this.drawAll(this.wordList, this.lineList, this.curveList);
        this.itemList.map(item => {
          item.curveList.map((item1, index) => {
            if (item1.isUp) {
              if (
                item1.partHeightest &&
                item1.partHeightest <= item1.partShortest - countNum
              ) {
                this.drawCurve({
                  ...item1,
                  partHeightest: item1.partShortest - countNum
                });
                if (index === item.curveList.length - 1) {
                  this.drawChangeWord(item1, countNum);
                }
                if (item1.partHeightest === item1.partShortest - countNum) {
                  this.curveList.push(item1);
                  item1.wordList.map(word => {
                    this.wordList.push(word);
                  });
                }
              }
            } else {
              if (
                item1.partHeightest &&
                item1.partHeightest >= item1.partShortest + countNum
              ) {
                this.drawCurve({
                  ...item1,
                  partHeightest: item1.partShortest + countNum
                });
                if (index === item.curveList.length - 1) {
                  this.drawChangeWord(item1, countNum);
                }
                if (item1.partHeightest === item1.partShortest + countNum) {
                  this.curveList.push(item1);
                  item1.wordList.map(word => {
                    this.wordList.push(word);
                  });
                }
              }
            }
          });
        });
        countNum = countNum + 1;
        const flag = this.itemList.every(item => {
          const flag = item.curveList.every(item1 => {
            return (
              (item1.isUp &&
                item1.partHeightest > item1.partShortest - countNum) ||
              (!item1.isUp &&
                item1.partHeightest < item1.partShortest + countNum) ||
              !item1.partHeightest
            );
          });
          return flag;
        });
        if (flag) {
          clearInterval(this.timer);
          this.context.clearRect(0, 0, this.eleW, this.eleH);
          this.drawAll(this.wordList, this.lineList, this.curveList);
        }
      }, 50);
    }

demo展示

好像不能上传视频,所以就截取了两张过程图。

image.png

image.png

总结

  • 这个动画的实现,难点在于三次贝塞尔曲线的运用,可参考相关文章进行学习,如blog.csdn.net/fe_watermel…
  • 颜色的填充,重点在于空间闭合,所以使用两段贝塞尔曲线拼成一段完整曲线时,可以在中间画一条竖线路径分割,最后会自动闭合空间,如下图所示:

image.png