使用css和canvas实现鼠标长按环形进度条

需求

鼠标长按圆形按钮,实现环形进度条效果。
一开始做了css的效果,但是css的圆环看起来不圆,所以就用canvas,但是canvas也有缺点,首先是锯齿严重,其次是渲染不方便,需要调用生成N个实例。还是得根据具体使用场景来选择方案了。

效果

css效果
在这里插入图片描述
js效果
在这里插入图片描述
vue版本效果
在这里插入图片描述

CSS代码案例

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>环形进度条</title>
    <style>
      .wrapper {
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
      }
      .wrapper .center {
        width: 46px;
        height: 46px;
        border-radius: 50%;
        font-size: 30px;
        text-align: center;
        line-height: 40px;
        color: #fff;
        background: #615fe9;
        cursor: pointer;
        position: relative;
        z-index: 10;
      }
      .wrapper .center:active + .ring-progress {
        display: block;
      }
      /* 
      * 环形进度条 *
      */
      .ring-progress {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        width: 56px;
        height: 56px;
        border-radius: 50%;
        background: #fff;
        margin: auto;
        z-index: 9;
        display: none;
      }

      .ring-progress > i {
        position: absolute;
        top: 0;
        left: 0px;
        width: 28px;
        height: 56px;
        background-color: #fff;
        border-radius: 28px 0 0 28px;
        z-index: 9;
        transform-origin: 100% 50%;
        transform: rotateZ(0deg);
        animation: a3 2s linear;

        /*动画只执行一次*/
        animation-iteration-count: 1;

        /*让动画停留在最后一帧 */
        animation-fill-mode: forwards;
      }

      .ring-progress::after,
      .ring-progress::before {
        content: "";
        position: absolute;
        top: 0;
        width: 28px;
        height: 56px;
        background-color: #ff7070;
        z-index: 8;
      }

      .ring-progress::after {
        left: 28px;
        border-radius: 0 28px 28px 0;
        transform-origin: 0 50%;
        transform: rotateZ(-180deg) scale(0.96);

        animation: a1 1s linear;

        /*动画只执行一次*/
        animation-iteration-count: 1;

        /*让动画停留在最后一帧 */
        animation-fill-mode: forwards;
      }

      .ring-progress::before {
        left: 0;
        border-radius: 28px 0 0 28px;
        transform-origin: 100% 50%;
        transform: rotateZ(0deg) scale(0.96);
        animation: a2 2s linear;

        /*动画只执行一次*/
        animation-iteration-count: 1;

        /*让动画停留在最后一帧 */
        animation-fill-mode: forwards;
        /* display: none; */
      }

      @keyframes a1 {
        0% {
          transform: rotateZ(-180deg) scale(0.96);
        }

        100% {
          transform: rotateZ(0deg) scale(0.96);
        }
      }

      @keyframes a2 {
        0% {
          transform: rotateZ(0) scale(0.96);
        }

        100% {
          transform: rotateZ(360deg) scale(0.96);
        }
      }

      @keyframes a3 {
        0%,
        50% {
          opacity: 1;
        }

        51%,
        100% {
          opacity: 0;
        }
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <div class="center"></div>
      <div class="ring-progress"><i></i></div>
    </div>
  </body>
</html>

canvas代码案例

由于文字部分自定义需求比较大,就不写死了,有需要可自行定义,可参考 drawTextCB回调

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>环形进度条</title>
    <style>
      .wrapper {
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
      }

      .container {
        margin-top: 30px;
        width: 100px;
        height: 100px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <div class="container" id="progress"></div>
    </div>

    <div class="wrapper">
      <button onclick="setProgress(100)">100</button>
      <button onclick="setProgress(50)">50</button>
      <button onclick="setProgress(25)">25</button>
      <button onclick="setProgress(0)">0</button>
    </div>

    <div class="wrapper">
      <div class="container" id="progress2"></div>
    </div>

    <script>
      /**
       * 环形进度条
       * 功能分析:
       *  1、该功能为鼠标按下后出现一个环形进度条,默认进度条起始位置为-90度
       *  2、该功能一共有四个层级,从下往上依次为背景层、环形进度条层、中心圆层、居中文字层
       * @param {string} containerId - 容器id
       * @param {string} bgColor - 背景图层颜色
       * @param {number} lineWidth - 进度条宽度
       * @param {string} r1Color - 进度条颜色
       * @param {string} r2Color - 中心圆颜色
       * @param {number} defaultValue - 默认进度位置 (0-100)
       * @param {bealoon} animation - 是否开启动画效果
       * @param {number} animationTime - 动画时间长度
       * @param {function} drawTextCB - 绘制文字的回调
       **/

      var ops = {
        containerId: "",
        bgColor: "",
        lineWidth: "",
        r1Color: "",
        r2Color: "",
        defaultValue: "",
        animation: "",
        animationTime: "",
        drawTextCB: "",
      };

      var CircleProgress = function (ops) {
        this.containerEl = document.getElementById(ops.containerId);
        this.containerEl.innerHTML = `<canvas ></canvas>`;
        this.canvasEl = this.containerEl.querySelector("canvas");
        this.ctx = this.canvasEl.getContext("2d");

        // canvasEl已在此处被重新计算
        this.dprInfo = this.getDprInfo();

        // 画布真实宽高
        this.ctxW = parseFloat(this.canvasEl.style.width);
        this.ctxH = parseFloat(this.canvasEl.style.height);

        // 是否开启动画
        this.animation = ops.animation || true;
        // 整体动画时长
        this.animationTime = ops.animationTime || 2000;
        // 每个百分比的时长
        this.stepTime = this.animationTime / 100;

        // 当前进度值
        this.currentValue = 0;
        // 最终结果
        this.finalValue = ops.defaultValue || 0;

        // 整体背景色
        this.bgColor = ops.bgColor || "#ccc";
        // 进度条宽度
        this.lineWidth = ops.lineWidth || 5;
        // 进度条颜色
        this.r1Color = ops.r1Color || "#FF7070";
        // 中心圆颜色
        this.r2Color = ops.r2Color || "#615fe9";

        // 提前存储中心圆的路径
        this.centerPath = new Path2D();
        this.centerPath.arc(
          this.ctxW / 2,
          this.ctxH / 2,
          this.ctxW / 2 - this.lineWidth,
          0,
          2 * Math.PI
        );

        // 文字回调函数
        this.drawTextCB = ops.drawTextCB || function () {};

        // 定时器
        this.timer = null;

        // 加减类型(true为加,false为减)
        this.increase = true;

        this.init();
      };

      CircleProgress.prototype = {
        constructor: CircleProgress,
        // 初始化
        init: function () {
          this.setDraw();
        },
        // 绘制
        setDraw: function () {
          this.increase = this.finalValue > this.currentValue ? true : false;
          if (this.animation) {
            this.setAnimationDraw();
          } else {
            this.currentValue = this.finalValue;
            this.drawFrame();
          }
        },
        // 动画进度条
        setAnimationDraw: function () {
          var that = this;
          this.drawFrame();
          this.timer = setTimeout(function () {
            if (that.increase) {
              if (that.currentValue < that.finalValue) {
                that.currentValue++;
                that.setAnimationDraw();
              }
            } else {
              if (that.currentValue > that.finalValue) {
                that.currentValue--;
                that.setAnimationDraw();
              }
            }
            this.timer && clearTimeout(this.timer);
          }, this.stepTime);
        },
        /**
         * 设置value
         * @params {number} value
         */
        setValue: function (value) {
          this.timer && clearTimeout(this.timer);
          this.finalValue = value;
          this.setDraw();
        },
        // 绘制帧
        drawFrame: function () {
          this.ctx.clearRect(0, 0, this.ctxW, this.ctxH);
          this.drawbg();
          this.drawCenter();
          this.drawProgress();
          this.drawText();
        },
        // 绘制背景环
        drawbg: function () {
          this.drawCircle({
            x: this.ctxW / 2,
            y: this.ctxH / 2,
            r: this.ctxW / 2 - this.lineWidth / 2,
            sAngle: Math.PI * (1 / 180) * -90,
            eAngle: Math.PI * (1 / 180) * 270,
            color: this.bgColor,
          });
        },
        // 绘制中心圆
        drawCenter: function () {
          this.drawPathCircle({
            path: this.centerPath,
            color: this.r2Color,
          });
        },
        // 绘制进度环
        drawProgress: function (angle) {
          var angle = (this.currentValue / 100) * 360;
          this.drawCircle({
            x: this.ctxW / 2,
            y: this.ctxH / 2,
            r: this.ctxW / 2 - this.lineWidth / 2,
            sAngle: Math.PI * (1 / 180) * -90,
            eAngle: Math.PI * (1 / 180) * (angle - 90),
            color: this.r1Color,
          });
        },
        /**
         * 绘制圆环
         */
        drawCircle: function ({ x, y, r, sAngle, eAngle, color }) {
          this.ctx.beginPath();
          this.ctx.lineWidth = this.lineWidth;
          this.ctx.strokeStyle = color;
          this.ctx.arc(x, y, r, sAngle, eAngle);
          this.ctx.stroke();
        },
        /**
         * 根据 Path2D 绘制实心圆
         */
        drawPathCircle: function ({ path, color }) {
          this.ctx.beginPath();
          this.ctx.fillStyle = color;
          this.ctx.fill(path);
        },
        // 文本绘制
        drawText: function () {
          this.drawTextCB && this.drawTextCB();
        },
        // 解决像素锯齿问题
        getDprInfo: function () {
          // 解决像素锯齿问题
          var cInfo = this.containerEl.getBoundingClientRect();
          var dpr = window.devicePixelRatio || 1;

          if (dpr) {
            this.canvasEl.style.width = cInfo.width + "px";
            this.canvasEl.style.height = cInfo.height + "px";
            this.canvasEl.width = cInfo.width * dpr;
            this.canvasEl.height = cInfo.height * dpr;
            this.ctx.scale(dpr, dpr);
          }

          return { cInfo, dpr };
        },
      };

      /**
       * 创建一个进度条按钮类
       **/
      var CircleProgressBtn = function () {
        CircleProgress.call(this, arguments[0]);

        // 鼠标按下的时间
        this.downTime = 0;

        // 是否开启状态
        this.isOpen = false;

        this.init();
        this.addDownHandle();
      };

      // 继承prototype
      (function () {
        var Super = function () {};
        Super.prototype = CircleProgress.prototype;
        CircleProgressBtn.prototype = new Super();
        CircleProgressBtn.prototype.constructor = CircleProgressBtn;
      })();

      // 开始
      CircleProgressBtn.prototype.start = function () {
        this.step();
      };

      // 结束
      CircleProgressBtn.prototype.end = function () {
        this.step();
      };

      // 清除绘制信息
      CircleProgressBtn.prototype.clear = function () {
        clearTimeout(this.timer);
        this.currentValue = this.isOpen ? 100 : 0;
        this.drawFrame();
      };

      // 添加长按事件
      CircleProgressBtn.prototype.addDownHandle = function () {
        var that = this;
        this.canvasEl.addEventListener("mousedown", function (e) {
          var ctxInfo = that.canvasEl.getBoundingClientRect();
          that.downTime = new Date().getTime();
          if (
            that.ctx.isPointInPath(
              that.centerPath,
              (e.clientX - ctxInfo.left) * that.dprInfo.dpr,
              (e.clientY - ctxInfo.top) * that.dprInfo.dpr
            )
          ) {
            that.isOpen ? that.end() : that.start();
          }
        });

        document.addEventListener("mouseup", function () {
          var t = new Date().getTime() - that.downTime;
          if (t < that.animationTime) {
            that.clear();
          }
        });
      };

      // 步进器
      CircleProgressBtn.prototype.step = function () {
        var that = this;
        if (this.timer) {
          clearTimeout(that.timer);
        }
        this.timer = setTimeout(function () {
          if (that.flag) {
            return;
          }
          // 如果是开启状态,长按进度条是减少;否则长按是累加
          if (that.isOpen) {
            if (that.currentValue > 0) {
              that.currentValue--;
              that.step();
            } else {
              that.isOpen = false;
            }
          } else {
            if (that.currentValue < 100) {
              that.currentValue++;
              that.step();
            } else {
              that.isOpen = true;
            }
          }
          that.drawFrame();
        }, this.stepTime);
      };

      // 实例1
      var p = new CircleProgress({
        containerId: "progress",
        lineWidth: 20,
        defaultValue: 16,
        drawTextCB: function () {
          // 文字1
          this.ctx.font = "20px Verdana";
          this.ctx.textBaseline = "middle";
          this.ctx.textAlign = "center";
          this.ctx.fillStyle = "#fff";
          this.ctx.fillText(
            this.currentValue,
            this.ctxW / 2 - 4,
            this.ctxH / 2
          );
          // 文字2
          this.ctx.font = "10px Verdana";
          this.ctx.fillText("%", this.ctxW / 2 + 20, this.ctxH / 2);
        },
      });

      function setProgress(n) {
        p.setValue(n);
      }

      // 实例2
      var p2 = new CircleProgressBtn({
        containerId: "progress2",
        lineWidth: 20,
        drawTextCB: function () {
          // 文字1
          this.ctx.font = "20px Verdana";
          this.ctx.textBaseline = "middle";
          this.ctx.textAlign = "center";
          this.ctx.fillStyle = this.isOpen ? "#fff" : "#ccc";
          this.ctx.fillText(
            this.isOpen ? "NO" : "OFF",
            this.ctxW / 2,
            this.ctxH / 2
          );
        },
      });
    </script>
  </body>
</html>

vue版本

 <!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>环形进度条</title>
    <style>
      .wrapper {
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
      }

      .container {
        margin-top: 30px;
        width: 100px;
        height: 100px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="wrapper">
        <div class="container">
          <circle-progress
            :default-value="25"
            :progress-value="myValue"
            :line-width="16"
            :btn-mode="true"
            v-on:draw-text="handleDrawText"
            v-on:start-callback="handleStartCB"
            v-on:end-callback="handleEndCB"
          ></circle-progress>
        </div>
      </div>
      <div class="wrapper"><button @click="handlePlus">+1</button></div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
      // 注册组件
      Vue.component("circle-progress", {
        props: {
          // 名称
          info: {
            type: Object,
            default: null,
          },
          // 进度值
          progressValue: {
            type: [Number, String],
            default: 0,
          },
          // 动画
          animation: {
            type: Boolean,
            default: true,
          },
          // 动画时间
          animationTime: {
            type: Number,
            default: 2000,
          },
          // 默认值
          defaultValue: {
            type: Number,
            default: 0,
          },
          // 背景色
          bgColor: {
            type: String,
            default: "#ccc",
          },
          // 进度条颜色
          r1Color: {
            type: String,
            default: "#FF7070",
          },
          // 中心圆颜色
          r2Color: {
            type: String,
            default: "#615fe9",
          },
          // 进度条粗细
          lineWidth: {
            type: Number,
            default: 5,
          },
          // 按钮模式
          btnMode: {
            type: Boolean,
            default: false,
          },
        },
        data: function () {
          return {
            ctx: null,
            uid: this.guid(),
            // dpr信息
            dprInfo: null,
            // 画布真实宽高
            ctxW: 0,
            ctxH: 0,
            // 每个百分比的时长
            stepTime: this.animationTime / 100,
            // 当前进度值
            currentValue: 0,
            // 最终结果
            finalValue: this.defaultValue,
            // 提前存储中心圆的路径
            centerPath: new Path2D(),
            // 定时器
            timer: null,
            // 进度条类型(true为加,false为减)
            increase: true,
            // 鼠标按下的时间
            downTime: 0,
            // 是否开启状态
            isOpen: false,
          };
        },
        template: `
            <div :id='uid' ref='containerRef' style='width: 100%;height: 100%;'>
                <canvas ref='canvasRef' v-on='{ mousedown: handleMouseDown}'></canvas>
            </div>
            `,
        watch: {
          progressValue: function (newValue) {
            console.log(newValue);
            var val = Math.abs(parseInt(newValue));
            if (val >= 0 && val <= 100) {
              this.finalValue = val;
              this.setDraw();
            }
          },
        },
        mounted() {
          try {
            this.init();
          } catch (err) {
            console.log(err);
          }
        },
        methods: {
          // 初始化
          init: function () {
            this.ctx = this.$refs.canvasRef.getContext("2d");
            this.dprInfo = this.getDprInfo();
            this.ctx.scale(this.dprInfo.dpr, this.dprInfo.dpr);
            this.ctxW = parseFloat(this.$refs.canvasRef.style.width);
            this.ctxH = parseFloat(this.$refs.canvasRef.style.height);
            this.centerPath.arc(
              this.ctxW / 2,
              this.ctxH / 2,
              this.ctxW / 2 - this.lineWidth,
              0,
              2 * Math.PI
            );
            this.setDraw();
            document.addEventListener("mouseup", this.handleMouseUp);
          },
          // 绘制
          setDraw: function () {
            this.increase = this.finalValue > this.currentValue ? true : false;
            if (this.animation) {
              this.setAnimationDraw();
            } else {
              this.currentValue = this.finalValue;
              this.drawFrame();
            }
          },
          // 动画进度条
          setAnimationDraw: function () {
            var that = this;
            this.drawFrame();
            this.timer = setTimeout(function () {
              if (that.increase) {
                if (that.currentValue < that.finalValue) {
                  that.currentValue++;
                  that.setAnimationDraw();
                }
              } else {
                if (that.currentValue > that.finalValue) {
                  that.currentValue--;
                  that.setAnimationDraw();
                }
              }
            }, this.stepTime);
          },
          /**
           * 设置value
           * @params {number} value
           */
          setValue: function (value) {
            // 及时清除定时器,防止定时器叠加
            this.timer && clearTimeout(this.timer);
            this.finalValue = value;
            this.setDraw();
          },
          // 绘制帧
          drawFrame: function () {
            this.ctx.clearRect(0, 0, this.ctxW, this.ctxH);
            this.drawbg();
            this.drawCenter();
            this.drawProgress();
            this.drawText();
          },
          // 绘制背景环
          drawbg: function () {
            this.drawCircle({
              x: this.ctxW / 2,
              y: this.ctxH / 2,
              r: this.ctxW / 2 - this.lineWidth / 2,
              sAngle: Math.PI * (1 / 180) * -90,
              eAngle: Math.PI * (1 / 180) * 270,
              color: this.bgColor,
            });
          },
          // 绘制中心圆
          drawCenter: function () {
            this.drawPathCircle({
              path: this.centerPath,
              color: this.r2Color,
            });
          },
          // 绘制进度环
          drawProgress: function (angle) {
            var angle = (this.currentValue / 100) * 360;
            this.drawCircle({
              x: this.ctxW / 2,
              y: this.ctxH / 2,
              r: this.ctxW / 2 - this.lineWidth / 2,
              sAngle: Math.PI * (1 / 180) * -90,
              eAngle: Math.PI * (1 / 180) * (angle - 90),
              color: this.r1Color,
            });
          },
          /**
           * 绘制圆环
           */
          drawCircle: function ({ x, y, r, sAngle, eAngle, color }) {
            // console.log(x, y, r, sAngle, eAngle, color);
            this.ctx.beginPath();
            this.ctx.lineWidth = this.lineWidth;
            this.ctx.strokeStyle = color;
            this.ctx.arc(x, y, r, sAngle, eAngle);
            this.ctx.stroke();
          },
          /**
           * 根据 Path2D 绘制实心圆
           */
          drawPathCircle: function ({ path, color }) {
            this.ctx.beginPath();
            this.ctx.fillStyle = color;
            this.ctx.fill(path);
          },
          // 文本绘制
          drawText: function () {
            // this.drawTextCB && this.drawTextCB(this.ctx);
            this.$emit("draw-text", this);
          },
          // 解决像素锯齿问题
          getDprInfo: function () {
            // 解决像素锯齿问题
            var cInfo = this.$refs.containerRef.getBoundingClientRect();
            var dpr = window.devicePixelRatio || 1;

            if (dpr) {
              this.$refs.canvasRef.style.width = cInfo.width + "px";
              this.$refs.canvasRef.style.height = cInfo.height + "px";
              this.$refs.canvasRef.width = cInfo.width * dpr;
              this.$refs.canvasRef.height = cInfo.height * dpr;
            }

            return { cInfo, dpr };
          },
          // 生成uid
          guid: function () {
            return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
              /[xy]/g,
              function (c) {
                var r = (Math.random() * 16) | 0,
                  v = c == "x" ? r : (r & 0x3) | 0x8;
                return v.toString(16);
              }
            );
          },
          // 清空帧
          clear: function () {
            clearTimeout(this.timer);
            this.currentValue = this.isOpen ? 100 : 0;
            this.drawFrame();
          },
          // 步进函数
          step: function () {
            var that = this;
            if (this.timer) {
              clearTimeout(that.timer);
            }
            this.timer = setTimeout(function () {
              if (that.flag) {
                return;
              }
              // 如果是开启状态,长按进度条是减少;否则长按是累加
              if (that.isOpen) {
                if (that.currentValue > 0) {
                  that.currentValue--;
                  that.step();
                } else {
                  that.isOpen = false;
                  that.$emit("end-callback", that.info);
                }
              } else {
                if (that.currentValue < 100) {
                  that.currentValue++;
                  that.step();
                } else {
                  that.isOpen = true;
                  that.$emit("start-callback", that.info);
                }
              }
              that.drawFrame();
            }, this.stepTime);
          },
          // 长按事件
          handleMouseDown: function (e) {
            if (this.btnMode) {
              var ctxInfo = this.$refs.canvasRef.getBoundingClientRect();
              this.downTime = new Date().getTime();
              // 判断长按范围
              if (
                this.ctx.isPointInPath(
                  this.centerPath,
                  (e.clientX - ctxInfo.left) * this.dprInfo.dpr,
                  (e.clientY - ctxInfo.top) * this.dprInfo.dpr
                )
              ) {
                this.step();
              }
            }
          },
          handleMouseUp: function () {
            var t = new Date().getTime() - this.downTime;
            if (t < this.animationTime) {
              this.clear();
            }
          },
        },
      });

      var app = new Vue({
        el: "#app",
        data: function () {
          return {
            myValue: 0,
          };
        },
        methods: {
          handlePlus: function () {
            this.myValue++;
          },
          // 绘制文字
          handleDrawText(_this) {
            // 文字1
            _this.ctx.font = "20px Verdana";
            _this.ctx.textBaseline = "middle";
            _this.ctx.textAlign = "center";
            _this.ctx.fillStyle = "#fff";
            _this.ctx.fillText(
              _this.currentValue,
              _this.ctxW / 2 - 4,
              _this.ctxH / 2
            );
            // 文字2
            _this.ctx.font = "10px Verdana";
            _this.ctx.fillText("%", _this.ctxW / 2 + 20, _this.ctxH / 2);
          },
          // 启动完成回调
          handleStartCB: function (info) {
            console.log(info);
          },
          // 关闭完成回调
          handleEndCB: function (info) {
            console.log(info);
          },
        },
      });
    </script>
  </body>
</html>

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值