前端用Canvas绘制一个高可配置的圆形进度条

效果图

在这里插入图片描述

🚀 用Canvas绘制一个高可配置的圆形进度条

  • 效果图
  • 问题分析与拆解
  • 第一步,初始化一些默认参数,处理canvas模糊问题
  • 第二步,定义绘制函数
        • 1. 定义绘制主函数入口,该函数汇集了我们定义的其它绘制方法
        • 2. 定义绘制圆环函数
        • 3. 定义绘制小圆球函数
        • 4. 定义绘制进度百分比文字函数
        • 5.绘制标题
  • 第三步,制作动画

问题分析与拆解

  1. 首先背景渐变圆是静态,需要先把这个圆绘制出来,他是具有背景色,且没有动画;
  2. 外侧深橘色的也是一个圆,只不过它的背景色为透明色,并且是会进行动画的;
  3. 绘制小球,小球是需要跟随深橘色圆一起做动画的;
  4. 绘制圆中心的进度数字,且数字也是带有动画的;
  5. 绘制圆形进度条的标题;
  6. 需要先把静态的东西绘制出来,最后考虑动画;
  7. 动画使用requestAnimationFrame对canva进行擦除绘制,就这样不断的擦除重新绘制就会产生动画。

我这里使用React组件来呈现,该组件接受props如下,且这些props都存在默认值。当然也可以完全脱离React,只不过需要把这些参数定义在绘制类中。

export interface CircularProgressBarProps {
  /**
   * 进度条粗细
   */
  lineWidth: number;
  /**
   * 当前进度条粗细
   */
  outsideLineWidth: number;
  /**
   * #ffdfb3
   */
  color: string;
  /**
   * 圆形进度条外颜色
   * #FFB54D
   *
   */
  outsideColor: string;
  /**
   * 圆形进度条内颜色(渐变)
   */
  insideColor:
    | {
        /**
         * #fff | white
         */
        inner: string;
        /**
         * rgba(255, 247, 230, 0.3)
         */
        middle: string;
        /**
         * rgba(255, 230, 188, 0.6)
         */
        out: string;
      }
    | string;
  /**
   * 百分比%
   * 单位%
   * 60
   */
  percent: number | string;

  /**
   * 圆内数值,为空时,取percent
   */
  insideValue?: number | string;

  /**
   * 显示百分号
   */
  showPercentSign: boolean;
  /**
   * 动画速度
   * 0.01
   */
  stepSpeed: number;
  /**
   * 百分比数值样式
   * 500 28px PingFangSC-Regular, PingFang SC
   */
  percentageFont: string;
  /**
   * 百分比数值填充颜色
   * #1A2233
   */
  percentageFillStyle: string;
  /**
   * 是否显示小圆圈
   */
  isDrawSmallCircle: boolean;
  /**
   * 小圆圈半径
   */
  smallCircleR: number;
  /**
   * 小圆圈边框
   */
  smallCircleLineWidth: number;
  /**
   * 小圆圈填充颜色
   * #fff
   */
  smallCircleFillStyle: string;
  /**
   * 是否显示文本
   */
  isDrawText: boolean;
  /**
   * 文本字体样式
   * 14px Microsoft YaHei
   */
  textFont: string;
  /**
   * 字体颜色
   * #999
   */
  textFillStyle: string;
  /**
   * 文本内容
   */
  textContent: string;
}

第一步,初始化一些默认参数,处理canvas模糊问题

定义一个类,需要做一些初始化工作。

  1. 该类的构造函数接受canvas元素和绘制进度条需要的一些参数。
  2. 进度条存在一些默认配置,比如圆的横纵向坐标、圆的半径、绘制一整个圆需要360度。
  3. canvas和svg不一样,canvas是位图,在dpr高的屏幕下会模糊的,所以需要解决这个问题。即:原始尺寸 = css尺寸 * dpr。只要保证该等式成立,canvas就是清晰的,当然这个公式也适用于图片,一样的原理。
class CanvasChart {
  ctx: CanvasRenderingContext2D;
  width: number;
  height: number;
  circleDefaultConfig: CircleConfig;
  config: CircularProgressBarProps;

  constructor(ctx: HTMLCanvasElement, config: CircularProgressBarProps) {
    const dpr = window.devicePixelRatio;
    const { smallCircleR, smallCircleLineWidth } = config;
    const { width: cssWidth, height: cssHeight } = ctx.getBoundingClientRect();
    
    this.ctx = ctx.getContext("2d") as CanvasRenderingContext2D;
    ctx.style.width = `${cssWidth}px`;
    ctx.style.height = `${cssHeight}px`;
    ctx.width = Math.round(dpr * cssWidth);
    ctx.height = Math.round(dpr * cssHeight);
    this.ctx.scale(dpr, dpr);

    this.width = cssWidth;
    this.height = cssHeight;
    this.config = config;

    // 圆形进度条默认配置
    this.circleDefaultConfig = {
      x: this.width / 2,
      y: this.height / 2,
      radius:
        this.width > this.height
          ? this.height / 2 - smallCircleR - smallCircleLineWidth
          : this.width / 2 - smallCircleR - smallCircleLineWidth,
      startAngle: 0,
      endAngle: 360,
      speed: 0,
    };
  }
}

这里看下如何解决canvas在高dpr下模糊问题:若样式尺寸为500,宽高都为500
dpr为1; 样式尺寸为500,原始尺寸为500
dpr为2; 样式尺寸为500,原始尺寸为1000
当 dpr为1时,canvas尺寸不会变化,所以矩形的位置为 (100, 100, 100, 100)
当 dpr为2时,canvas画布会放大2倍,也就是 (1000, 1000),矩形的位置为(100, 100, 100, 100)
但是canva尺寸会适应样式尺寸,所以会缩小2倍。使用横坐标也就是 1个css像素等于2个canvas像素
所以会看到矩形会绘制在 css像素为(50, 50)的位置,且大小也变成了50。为了使得无论dpr为多少时,我们看到的效果都是一样的,所以需要缩放canvas为dpr
比如放大2倍 1个css像素就等于1个canvas像素
或者每次定义位置的时候 使用坐标乘以dpr也可以实现一样的效果

第二步,定义绘制函数

1. 定义绘制主函数入口,该函数汇集了我们定义的其它绘制方法

绘制入口,用来调用绘制函数,绘制前需要清除画布,通过重新绘制来达到动画效果。然后根据条件值来决定是否渲染其它元素。
因为深橘色圆环、小圆球、百分比文字是具有动画的,所以需要根据percent数值动态生成弧度值来绘制深橘色进度条(即 _endAngle = _startAngle + (percent / 100) * holeCicle)和小圆球,根据百分比来绘制百分比文字。

  // 绘制圆形进度条
  drawCircularProgressBar = (percent: number | string) => {
    const { width, height, ctx } = this;
    const {
      outsideColor,
      percentageFont,
      percentageFillStyle,
      isDrawSmallCircle,
      isDrawText,
      showPercentSign,
      textFont,
      textFillStyle,
      textContent,
      outsideLineWidth,
      insideValue = percent,
    } = this.config;
    ctx.clearRect(0, 0, width, height);
    
    // 背景的圆环
    this.drawCircle(this.config);
    // 有色的圆环
    const holeCicle = 2 * Math.PI;
    // 处理渐变色
    // const gnt1 = ctx.createLinearGradient(radius * 2, radius * 2, 0, 0);
    // gnt1.addColorStop(0, '#FF8941');
    // gnt1.addColorStop(0.3, '#FF8935');
    // gnt1.addColorStop(1, '#FFC255');
    
    // 从-90度的地方开始画,把起始点改成数学里的12点方向
    const _startAngle = -0.5 * Math.PI;
    let _endAngle = -0.5 * Math.PI;
    if (typeof percent === "number") {
      _endAngle = _startAngle + (percent / 100) * holeCicle;
    }

    this.drawCircle(
      {
        ...this.config,
        lineWidth: outsideLineWidth,
        insideColor: "transparent",
        color: outsideColor,
      },
      _startAngle,
      _endAngle
    );

    // 绘制小圆球
    isDrawSmallCircle && this.drawSmallCircle(this.config, percent);
    // 绘制百分比
    this.drawPercentage({
      percentageFont,
      percentageFillStyle,
      insideValue,
      showPercentSign,
      percent,
    });
    // 绘制文字
    isDrawText &&
      this.drawText({
        textFont,
        textFillStyle,
        textContent,
      });
  };
2. 定义绘制圆环函数

绘制一个圆,使用ctx.arc,需要圆弧的坐标、半径、起始弧度和结束弧度、填充色(支持渐变和普通色彩)、描边色。
需要通过此函数来绘制两个圆弧。一个是静态的填充色是渐变的圆;另外一个动态的圆弧,用来根据弧度的变化来生成动画,且填充色为透明色。

// 绘制圆曲线
  drawCircle = (
    config: CircularProgressBarProps,
    _startAngle?: number,
    _endAngle?: number
  ) => {
    const { ctx } = this;
    const { x, y, radius, startAngle, endAngle } = this.circleDefaultConfig;
    const { lineWidth, color, insideColor } = config;
    const startRadian = (_startAngle ??= startAngle);
    const endRadian = (_endAngle ??= endAngle);
    let fillStyle;
    if (typeof insideColor === "string") {
      fillStyle = insideColor;
    } else {
      const grd = ctx.createRadialGradient(x, y, 5, x, y, radius);
      const { inner, middle, out } = insideColor;
      grd.addColorStop(0, inner);
      grd.addColorStop(0.5, middle);
      grd.addColorStop(1, out);
      fillStyle = grd;
    }
    ctx.beginPath();
    ctx.arc(x, y, radius, startRadian, endRadian, false);
    ctx.fillStyle = fillStyle;
    ctx.fill();
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = color;
    ctx.lineCap = "round";
    ctx.stroke();
    ctx.closePath();
  };
3. 定义绘制小圆球函数

绘制小圆球,小圆球是具有动画的,唯一是需要注意的就是这个小圆圈是在外层圆上面的,所以小圆球的坐标位置是动态计算的。我在代码中输出了坐标的计算公式。

如果仔细阅读代码的话,我想你看到了angle - 90。那么这里为什么减去90?
Canvas 中,角度是从圆的右侧(即 3 点钟方向)开始,逆时针方向为正。
角度起始点:
在数学上,标准的极坐标系中,角度是从 x 轴的正方向(即右侧)开始计算的。
在 Canvas 中,角度也是从 x 轴的正方向开始,逆时针方向为正。
圆的绘制起始位置:
在许多情况下,尤其是在绘制进度条等图形时,我们希望从圆的顶部(即 12 点钟方向)开始绘制。
圆的顶部对应的角度是 -90 度(或 270 度),因为它在 x 轴的正方向逆时针旋转了 90 度。
调整角度:
为了使绘制的起点从顶部开始,需要将计算的角度减去 90 度。
例如,如果我们计算出一个角度 angle,这个角度是从 x 轴的正方向开始的,为了使其从顶部开始,我们需要减去 90 度,即 angle - 90

  // 绘制小圆球
  drawSmallCircle = (config: CircularProgressBarProps, percent: number) => {
    const { ctx, startAngle, endAngle } = this;
    const { x, y, radius } = this.circleDefaultConfig;
    // 圆弧的角度
    const angle = Number(percent / 100) * 360;
    // 圆心坐标:(x0, y0)
    // 半径:r
    // 弧度:a  =>  圆弧计算公式:(角度 * Math.PI) / 180
    // 则圆上任一点为:(x1, y1)
    // x1 = x0 + r * cos(a)
    // y1 = y0 + r * sin(a)
    const { smallCircleR, smallCircleLineWidth, smallCircleFillStyle } = config;
    const x1 = x + radius * Math.cos(((angle - 90) * Math.PI) / 180);
    const y1 = y + radius * Math.sin(((angle - 90) * Math.PI) / 180);
    ctx.beginPath();
    ctx.arc(x1, y1, smallCircleR, startAngle, endAngle);
    ctx.lineWidth = smallCircleLineWidth;
    ctx.fillStyle = smallCircleFillStyle;
    ctx.fill();
    ctx.stroke();
    ctx.closePath();
  };
4. 定义绘制进度百分比文字函数

绘制文字,需要注意文字位于圆的正中央,Canvas提供了计算文字尺寸的API,且通过画布的宽高,可以轻松的计算出文字的坐标位置。
绘制百分号,我这里绘制的百分号大小为文字大小的一半,这样显示效果更美观。然后就是计算调整百分号的位置了。

  // 绘制百分比
  drawPercentage = ({
    percentageFont,
    percentageFillStyle,
    insideValue,
    showPercentSign,
    percent,
  }: {
    percentageFont: string;
    percentageFillStyle: string;
    insideValue: number | string;
    showPercentSign: boolean;
    percent: string | number;
  }) => {
    const { ctx, width, height } = this;
    ctx.font = percentageFont;
    ctx.fillStyle = percentageFillStyle;
    const ratioStr = `${(parseFloat(`${percent}`)).toFixed(0)}`;
    const text = ctx.measureText(ratioStr);
    ctx.fillText(
      ratioStr,
      width / 2 - text.width / 2,
      height / 2 + (text.width * Number(showPercentSign)) / ratioStr.length / 2
    );
    if (showPercentSign) {
      const reg = /(\d)+(px)/;
      const persentFont = percentageFont.replace(reg, (a) => {
        const fontSize = a.split("").slice(0, -2);
        return `${(Number(fontSize.join("")) * 0.5).toFixed(0)}px`;
      });
      ctx.font = persentFont;
      ctx.fillStyle = percentageFillStyle;
      const percentStr = "%";
      const percentText = ctx.measureText(percentStr);
      ctx.fillText(
        percentStr,
        width / 2 + text.width / 4 + (percentText.width * 2) / 3,
        height / 2 + text.width / ratioStr.length / 2 - 2
      );
    }
  };
5.绘制标题
  // 绘制文字
  drawText = ({
    textFont,
    textFillStyle,
    textContent,
  }: {
    textFont: string;
    textFillStyle: string;
    textContent: string;
  }) => {
    const { ctx, width, height } = this;
    const measureText = ctx.measureText(textContent);
    ctx.font = textFont;
    ctx.fillStyle = textFillStyle;
    ctx.fillText(textContent, width / 2 - measureText.width / 2, height * 0.75);
  };

第三步,制作动画

这也是最后一步,动画需要从0 到 percent通过requestAnimationFrame来实现,还需要定义一个步长,该步长可以控制动画的执行速度。

const makeAnimation = (config: CircularProgressBarProps) => {
    const { percent } = config;
    const id = window.requestAnimationFrame(() => {
      this.makeAnimation(config);
    });
    this.drawCircularProgressBar(this.speed);
    if (this.speed >= +percent) {
      this.drawCircularProgressBar(percent);
      window.cancelAnimationFrame(id);
      this.speed = 0;
      return;
    }
    this.speed += this.stepSpeed;
  };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值