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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【跨境分享】中国商家如何卷到国外?电商独立站和电商平台的优势对比

为什么要选择独立站而不是电商平台 对于跨境电商经营者而言,采取多平台、多站点的运营策略是至关重要的战略布局。这一做法不仅有助于分散风险,避免将所有投资集中于单一市场,从而降低“所有鸡蛋置于同一篮子”的隐患,而且有利于拓…

最近换工作的一些启示,清华学姐篇

最近更新频率慢下来了,一部分原因是沉迷运动不能自拔,还有一部分原因是业余分出来很大的精力来拓展个人的边界,希望在工作之外取得一些成绩,写作上耽误了不少,很难做到日更。 所以整体上今年更新频率较低,但…

揭秘机器学习如何改变广告营销游戏规则

揭秘机器学习如何改变广告营销游戏规则 一、前言1.1 大数据时代的到来1.2 广告营销面临的挑战1.3 机器学习为广告营销带来的机遇 二、机器学习在广告营销中的应用2.1 了解消费者2.1.1 数据收集和分析2.1.2 行为模型的建立2.1.3 消费者画像的制作 2.2 定位广告投放人群2.2.1 人群…

vscode取消未使用变量的提示(爆红)

目前项目正在使用ts(TypeScript),可以在 tsconfig.json 文件中调整编译选项 在你的项目中找到并打开 tsconfig.json 文件,将noUnusedLocals和noUnusedParameters设置为false,关闭vscode重新打开项目即可 {"comp…

ISO 20000认证:驱动企业IT服务管理变革的利器

在信息技术驱动商业发展的今天,企业对高效、可靠和安全的IT服务需求日益增长。ISO 20000作为国际公认的IT服务管理标准,能够帮助企业在竞争激烈的市场环境中脱颖而出,实现IT服务管理的全面提升。本文将深入探讨ISO 20000认证如何帮助企业优化…

机器学习中的可解释性

「AI秘籍」系列课程: 人工智能应用数学基础 人工智能Python基础 人工智能基础核心知识 人工智能BI核心知识 人工智能CV核心知识 为什么我们需要了解模型如何进行预测 我们是否应该始终信任表现良好的模型?模型可能会拒绝你的抵押贷款申请或诊断你患…

基础跟张宇,强化用36讲还是高数辅导讲义?

基础跟的张宇老师,强化阶段跟谁要看基础学的怎么样! 因为张宇老师今年课程大改版,和以往的课程一点也不一样! 具体变动是: 张宇老师把往年强化阶段的知识前移,也就是说现在的基础阶段要讲的内容是以往基…

一文读懂:LLM大模型RAG

RAG 检索增强生成(Retrieval Augmented Generation),简称 RAG,已经成为当前最火热的LLM应用方案。经历今年年初那一波大模型潮,想必大家对大模型的能力有了一定的了解,但是当我们将大模型应用于实际业务场…

分布式一致性算法:Raft学习

分布式一致性算法:Raft学习 1 什么是分布式系统? 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。这些节点可能位于不同的物理位置,但它们协同工作以提供一个统一的计算平台或服务。分布式系统…

springmvc重定向和返回json,如何同时实现?

🏆本文收录于「Bug调优」专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&…

怎样把图片转成pdf文件,简鹿格式工厂轻松批量搞定

信息的存储和分享方式变得越来越多样化,而PDF文件以其跨平台兼容性和内容完整性,成为了许多用户首选的文档格式。无论是在学术研究、商务办公,还是个人创作中,将图片转换为PDF文件的需求日益凸显。 想象一下,当你需要整…

高性能Python网络框架实现网络应用详解

概要 Python作为一种广泛使用的编程语言,其简洁易读的语法和强大的生态系统,使得它在Web开发领域占据重要位置。高性能的网络框架是构建高效网络应用的关键因素之一。本文将介绍几个高性能的Python网络框架,详细描述它们的特点、使用场景及具体示例代码,帮助高效实现网络应…

veriloga要怎么在candence中编译和加密?

从编译器的角度来说,我在ADS中可能就是直接用notepad编写,然后生成检查,它会有提示成功或报错的信息。但是对比下来,我发现candence的编译器更加方便编写va,所以把运行成功的步骤记录下来,防止遗忘。 首先&#xff0c…

2024.7.9.小组汇报postman分享会

文章目录 一、前言(一)界面导航说明(二)发送第一个请求 二、基本功能(一)常见类型的接口请求(常见的接口有如下四种类型:1.查询参数的接口请求2.表单类型的接口请求3.上传文件的表单请求4.JSON …

关于MySQL mvcc

innodb mvcc mvcc 多版本并发控制 在RR isolution 情况下 trx在启动的时候就拍了个快照。这个快照是基于整个数据库的。 其实这个快照并不是说拷贝整个数据库。并不是说要拷贝出这100个G的数据。 innodb里面每个trx有一个唯一的trxID 叫做trx id .在trx 开始的时候向innodb系…

java基础01—根据源码分析128陷阱以及如何避免128陷阱

源码分析128陷阱 如上图所示,int类型数据超过127依旧能正常比较,但Integer类型就无法正确比较了 /*** Cache to support the object identity semantics of autoboxing for values between* -128 and 127 (inclusive) as required by JLS.** The cache …

C++内存的一些知识点

一、内存分区 在C中,内存主要分为以下几个区域: 代码区:存放函数体的二进制代码。 全局/静态存储区:存放全局变量和静态变量,这些变量在程序的整个运行期间都存在。常量存储区:存放常量,这些值…

新加坡工作和生活指北:教育篇

文章首发于公众号:Keegan小钢 新加坡的基础教育在东南亚处于领先地位,这点基本是人尽皆知,但很多人对其教育体系只是一知半解,今日我们就来深入了解一下。 新加坡的学校主要分为三大类:政府学校、国际学校、私立学校。…

谷歌上新!最强开源模型Gemma 2,27B媲美LLaMA3 70B,挑战3140亿Grok-1

文章目录 LMSYS Chatbot Arena:开源模型性能第一Gemma为什么这么强?架构创新对AI安全性的提升 A领域竞争激烈,GPT-4o 和 Claude 3.5 Sonnet 持续发力,谷歌迅速跟进。 谷歌为应对AI竞争所采取的策略:依靠 Gemini 闭源模…

STM32F446RE实现多通道ADC转换功能实现(DMA)

目录 概述 1 软硬件介绍 1.1 软件版本 1.2 ADC引脚介绍 2 STM32Cube配置项目 2.1 配置基本参数 2.2 ADC通道配置 2.3 DMA通道配置 3 项目代码介绍 3.1 自生成代码 3.2 ADC-DMA初始化 3.3 测试函数 3.4 ADC1、ADC2、ADC3轮询采集数据存贮格式 4 测试 源代码下载地…