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

news2024/9/25 19:20:08

在这里插入图片描述

🚀 用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;
  };

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1909926.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux-多线程

线程的概念 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程线程在进程内部运行,本质是在进程地址空间内运行在Linux系统中,在CPU眼中…

IO之反序列化漏洞

hutool之XmlUtil反序列化漏洞 同样存在漏洞的方法还有IoUtil.readObject方法,存在反序列化漏洞,这些方法的漏洞在JDK中本身就存在,而且JDK的做法是要求用户自行检查内容,作为工具类,这块没法解决。hutool在新版本中把这…

800 元打造家庭版 SOC 安全运营中心

今天,我们开始一系列新的文章,将从独特而全面的角度探索网络安全世界,结合安全双方:红队和蓝队。 这种方法通常称为“紫队”,集成了进攻和防御技术,以提供对威胁和安全解决方案的全面了解。 在本系列的第一篇文章中,我们将指导您完成以 100 欧元约800元左右的预算创建…

电竞玩家的云端盛宴!四大云电脑平台:ToDesk、顺网云、青椒云、极云普惠云实测大比拼

本文目录 一、云电脑概念及市场需求二、云电竞性能测试2.1 ToDesk云电脑2.2 顺网云2.3 青椒云2.4 极云普惠云电脑 三、四大云电脑平台综合配置对比3.1 CPU处理器3.2 GPU显卡3.3 内存 四、总结 一、云电脑概念及市场需求 在数字化时代的推动下,云计算技术日益成熟&a…

【VUE基础】VUE3第九节—Pinia使用

Pinia使用 Pinia简介安装Pinia存储数据和读取数据State读取数据重置 state修改state值storeToRefs监听state Getter读取数据 Action Pinia简介 Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 安装Pinia yarn add pinia # 或者使用 npm npm inst…

24年,计算机仍然是最热门的专业?!

大家好,我是程序员鱼皮。最近很多高考完的朋友开始进入了填志愿选专业的时期。出于好奇,我也在网上了解了一下今年的热门专业和就业情况,结果并没有出乎我的意料,对于很多省份,计算机科学与技术依然是最热门的专业&…

视频汇聚平台EasyCVR设备录像回看请求播放时间和实际时间对不上,是何原因?

安防监控EasyCVR视频汇聚平台可提供多协议(RTSP/RTMP/国标GB28181/GAT1400/海康Ehome/大华/海康/宇视等SDK)的设备接入、音视频采集、视频转码、处理、分发等服务,系统具备实时监控、云端录像、回看、告警、平台级联以及多视频流格式分发等视…

spring的bean注册

bean注册 第三方jar包的类想添加到ioc中,加不了Component该怎么办呢。 可以使用Bean和Import引入jar包,可以使用maven安装到本地仓库。 修改bean的名字:Bean("aaa")使用ioc的已经存在的bean对象,如Country:p…

HTML实现图片查看与隐藏

你好呀,我是小邹。 在网页设计中,提供一个直观且用户友好的图片查看功能是提升用户体验的重要一环。本文将详细介绍如何使用HTML、CSS和JavaScript来实现图片的查看与隐藏功能。通过本教程,你将学会如何让页面上的图片在点击时放大显示&…

产线级MES系统在装配行业的具体应用

在装配行业中,产线级MES系统能够帮助企业优化生产流程,提高产品质量,增强生产效率。以下是产线级MES系统在装配行业中的一些具体应用场景。 了解慧都产线级MES系统>> 产线级MES系统应用场景 1. 生产过程监控与管理 MES系统可以实时…

基石Apollo国际化整合及配置上线规范治理

背景 随着公司站点的增多和国际化业务的发展,Apollo配置平台也呈现出多国家、多环境部署、各环境配置孤立操作、配置上下线无规范流程的情况。基于当前的情况,主要的痛点问题如下: 国际化apollo未与主营apollo统一, 导致国际化Apollo生产配…

react umi把将file文件数据转成二进制流数据格式

后端要求文件上传传递二进制文件??? 参考 umi-request上传FormData类型问题_umi-request formdata-CSDN博客 import request from /utils/request; // 批量下发(此接口使用from表单接收) export async function issuance_audit_create(param…

第33讲:K8S集群StorageClass使用Ceph CSI供应商与Cephfs文件系统集成

文章目录 1.Ceph CSI供应商简介2.创建Cephfs文件系统为StorageCLass提供底层存储端2.1.创建Cephfs文件系统2.2.在Cephfs文件系统中为Storageclass创建子目录2.3.在Cephfs文件系统中创建一个子卷 3.在K8S集群中部署Cephfs-CSI供应商客户端3.1.下载Cephfs-CSI客户端的资源编排文件…

计算机网络——网络层(概念及IP地址)

网络层概念 网络层向上层提供的两种服务 在计算机网络领域,网络层应该向运输层提供怎样的服务(“面向连接”还是“无连接”)曾引起了长期的争论。 争论焦点的实质就是:在计算机通信中,可靠交付应当由谁来负责?是网络还是端系统&#xff1f…

rfid资产管理系统解决方案 rfid固定资产管理系统建设方案

在现代化的仓库储备中,仅仅完成对货物进出的简单批次处理已经不再足够,对库内货品的种类、数量、生产属性、垛位等信息的清晰记录变得至关重要。然而,传统的资产管理方式如条形码在长期使用中逐渐暴露出不耐脏、数据存储量小、读取间隔短、不…

js吸顶导航

吸顶导航 当我们浏览页面篇幅较大,浏览过半的时候想回到导航位置,只能通过往回滚动或通过”回到顶部”重新滚动到导航位置,这样的操作显得繁琐与不便。于是便有了吸顶式导航的交互方式,吸顶条导航最大的好处是将最常用或者设计者最…

WAIC | 穿越千年!华院计算AIGC技术实现刘徽、祖冲之和毕达哥拉斯跨时空对话

祖冲之利用刘徽的割圆术,将圆周率π的近似计算精确到小数点后七位,这不仅是数学史上的一项重要突破,也是对无理数逼近问题的早期探索。在现代人工智能中同样能观察到数值逼近的思想,例如模型的训练通常依赖随机优化算法等数值方法…

2.贪心算法.基础

2.贪心算法.基础 基础知识题目1.分发饼干2.摆动序列3.最大子序和4.买股票的最佳时机24.2.买股票的最佳时机5.跳跃游戏5.1.跳跃游戏26.K次取反后最大化的数组和7.加油站8.分发糖果 基础知识 什么是贪心? 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。 贪…

Studying-代码随想录训练营day33| 动态规划理论基础、509.斐波那契函数、70.爬楼梯、746.使用最小花费爬楼梯

第33天,动态规划开始,新的算法💪(ง •_•)ง,编程语言:C 目录 动态规划理论基础 动态规划的解题步骤 动态规划包含的问题 动态规划如何debug 509.斐波那契函数 70.爬楼梯 746.使用最小花费爬楼梯 总结 动态…

MSPM0G3507——时钟配置(与32关系)

先将32端时钟配置分为1,2,3如图 1是PSC左边未经分频的时钟源(HZ) 2是经过PSC分频的时钟信号(HZ) 3是最终的输出信号(HZ) 3输出的是一个定时器周期的HZ,可以转换成时间 …