CSS锥形渐变实现环形进度条

10月份因为疫情原因、又开启了居家办公模式,空闲之余,与其选择“躺平”,不如去做一些有意义的事情,内心的想法驱使着我去做些什么,但是又没有合适的素材,直到接手了最近的一个可视化项目,一个图表勾起了我无限的好奇心,本着对技术死磕到底的想法,于是开启了我的探索之旅。具体的原型效果如下:

关于此类进度条的实现方式,在我之前的章节(SVG绘制圆环进度条)中也有涉及,本章则另辟蹊跷,从另一个维度简单介绍一下CSS锥形渐变(conic-gradient)在可视化图表中的应用场景。本章依旧采用vue+原生css的形式进行案例展示、在了解本章节之前,需要对vue框架、css变量、css属性conic-gradient有一定程度的认识。案例实现效果如下:

实现思路:首先从原型图入手,我们可以将效果图进行拆分,背景圆环+进度圆环+进度条开始处小圆点(和边框一样大小、模拟圆角效果)+进度尾部圆点+进度尾部小眼睛+进度条中心内容。因此我们只需要将以上几个小功能点实现即可

1.背景圆环:div添加背景颜色+圆角

2.进度圆环:使用css属性conic-gradient进行进度控制

3.进度条开始处小圆点:使用伪元素(::before或::after)或div均可,定位解决

4.进度条尾部圆点:相当于在一个指针上添加一个小球,然后将指针根据数值旋转一定的角度

5.进度尾部小眼睛:使用指针头部小球元素的伪元素进行定位

6.进度条中心内容:可根据需要,使用插槽的形式解决

首先看一下前两个图表的具体实现细节:

<!-- demo1.vue -->
<template>
  <div class="chart-box" :style="styObj">
    <!-- 进度条部分 -->
    <div class="outer-box">
      <div class="inner-box">
        <div class="pointer-box"></div>
      </div>
    </div>
    <!-- 插槽内容 -->
    <div class="slot-content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    rate: {
      type: Number,
      default: 0,
    },
    config: {
      type: Object,
      default: () => {
        return {};
      },
    },
  },
  computed: {
    styObj() {
      let rate = 0;
      if (this.rate <= 0) {
        rate = 0;
      } else if (this.rate >= 1) {
        rate = 1;
      } else {
        rate = this.rate;
      }
      let endPos = `${rate * 100}%`;
      let obj = Object.assign({}, this.defaultConfig, this.config);
      let rotate = `rotate(${360 * rate}deg)`;
      let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
      let showEyes = obj.showEyes ? 1 : 0;
      return {
        "--background-image": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
        "--border-width": obj.borderWidth,
        "--dot-width": obj.circleSize,
        "--pointer-rotate": rotate,
        "--background-color": obj.borderBackground,
        "--center-gap-bg": obj.centerCircleBg,
        "--circle-color": obj.circleColor,
        "--clockwise-wise": chartRotate,
        "--show-eyes": showEyes,
        "--eyes-size": obj.eyesSize,
        "--start-color": obj.startColor,
      };
    },
  },
  data() {
    return {
      /* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
      defaultConfig: {
        borderWidth: "8px", // 描边宽度
        borderBackground: "#eee", // 描边背景颜色
        circleSize: "16px", // 结尾处圆点直径
        circleColor: "#2ec4a7", // 结尾处圆点颜色
        startColor: "#d5f4ee", // 进度条起始颜色
        endColor: "#2ec4a7", // 进度条结束颜色
        centerCircleBg: "#fff", // 中间空心圆背景
        clockwise: true, // 是否顺时针
        showEyes: false, // 是否显示结尾处小眼睛
        eyesSize: "8px", // 结尾处小眼睛大小
      },
    };
  },
};
</script>
<style scoped>
.chart-box {
  position: relative;
  width: 100%;
  height: 100%;
}
/* 核心代码、控制进度条样式及进度 */
.outer-box {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  box-sizing: border-box;
  background-color: var(--background-color);
  background-image: var(--background-image);
  padding: var(--border-width);
  transform: var(--clockwise-wise);
}
/* 开始处增加一个圆形端点, 模拟圆角效果 */
.outer-box::after {
  content: "";
  width: var(--border-width);
  height: var(--border-width);
  border-radius: 50%;
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  background: var(--start-color);
}
/* 中间添加一个和背景色一样的圆圈 */
.inner-box {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--center-gap-bg);
}
/* 指示针 */
.pointer-box {
  position: absolute;
  left: 50%;
  top: calc(0px - var(--border-width) / 2);
  bottom: calc(0px - var(--border-width) / 2);
  z-index: 1;
  transform-origin: center center;
  transform: var(--pointer-rotate);
}
/* 指示针的头部添加一个小圆点 */
.pointer-box::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--dot-width);
  height: var(--dot-width);
  border-radius: 50%;
  background: var(--circle-color);
  transform: translate(-50%, -50%);
}
/* 进度条结尾处添加一个小眼睛,背景白色 */
.pointer-box::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--eyes-size);
  height: var(--eyes-size);
  border-radius: 50%;
  background: #fff;
  transform: translate(-50%, -50%);
  z-index: 1;
  opacity: var(--show-eyes);
}
/* 插槽内容样式 */
.slot-content {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

分析:从代码中不难看出、进度圆环中间的空白地方(class类名为“inner-box”)使用了一个背景为白色的元素进行遮盖,这点需要根据具体场景进行微调,在纯色背景下并无大碍,但是在有背景图的场景下,显示效果就有点差强人意了。因此此处需要做一下优化,扒一扒css手册,刚好有一个属性可以解决这个问题,那就是mask属性了,优化后代码如下,实现效果见第三、四个图表

<!-- demo2 -->
<template>
  <div class="chart-box" :style="styObj">
    <!-- 进度条部分 -->
    <div class="process-wrapper">
      <div class="process-box"></div>
      <div class="pointer-box"></div>
    </div>
    <!-- 插槽内容 -->
    <div class="slot-content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    rate: {
      type: Number,
      default: 0,
    },
    config: {
      type: Object,
      default: () => {
        return {};
      },
    },
  },
  computed: {
    styObj() {
      let rate = 0;
      if (this.rate <= 0) {
        rate = 0;
      } else if (this.rate >= 1) {
        rate = 1;
      } else {
        rate = this.rate;
      }
      let endPos = `${rate * 100}%`;
      let obj = Object.assign({}, this.defaultConfig, this.config);
      let rotate = `rotate(${360 * rate}deg)`;
      let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
      let showEyes = obj.showEyes ? 1 : 0;
      return {
        "--background-image": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
        "--border-width": obj.borderWidth,
        "--dot-width": obj.circleSize,
        "--pointer-rotate": rotate,
        "--background-color": obj.borderBackground,
        "--circle-color": obj.circleColor,
        "--clockwise-wise": chartRotate,
        "--show-eyes": showEyes,
        "--eyes-size": obj.eyesSize,
        "--start-color": obj.startColor,
      };
    },
  },
  data() {
    return {
      /* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
      defaultConfig: {
        borderWidth: "8px", // 描边宽度
        borderBackground: "#eee", // 描边背景颜色
        circleSize: "16px", // 结尾处圆点直径
        circleColor: "#2ec4a7", // 结尾处圆点颜色
        startColor: "#d5f4ee", // 进度条起始颜色
        endColor: "#2ec4a7", // 进度条结束颜色
        clockwise: true, // 是否顺时针
        showEyes: false, // 是否显示结尾处小眼睛
        eyesSize: "8px", // 结尾处小眼睛大小
      },
    };
  },
};
</script>
<style scoped>
.chart-box {
  position: relative;
  width: 100%;
  height: 100%;
}
/* 将图表和插槽内容分开,便于控制进度条顺时针亦或是逆时针 */
.process-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  transform: var(--clockwise-wise);
}
/* 开始处增加一个圆形端点,模拟圆角效果 */
.process-wrapper::after {
  content: "";
  width: var(--border-width);
  height: var(--border-width);
  border-radius: 50%;
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  background: var(--start-color);
}
/* 核心代码、控制进度条样式及进度 */
.process-box {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  box-sizing: border-box;
  background-color: var(--background-color);
  background-image: var(--background-image);
  -webkit-mask: radial-gradient(
    closest-side at center center,
    transparent calc(100% - var(--border-width)),
    #fff calc(100% - var(--border-width))
  );
  mask: radial-gradient(
    closest-side at center center,
    transparent calc(100% - var(--border-width)),
    #fff calc(100% - var(--border-width))
  );
}
/* 指示针 */
.pointer-box {
  position: absolute;
  left: 50%;
  top: calc(0px + var(--border-width) / 2);
  bottom: calc(0px + var(--border-width) / 2);
  z-index: 1;
  transform: var(--pointer-rotate);
}
/* 指示针的头部(进度条结尾处)添加一个小圆点 */
.pointer-box::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--dot-width);
  height: var(--dot-width);
  border-radius: 50%;
  background: var(--circle-color);
  transform: translate(-50%, -50%);
}
/* 进度条结尾处添加一个小眼睛,背景白色 */
.pointer-box::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--eyes-size);
  height: var(--eyes-size);
  border-radius: 50%;
  background: #fff;
  transform: translate(-50%, -50%);
  z-index: 1;
  opacity: var(--show-eyes);
}
/* 插槽内容样式 */
.slot-content {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

至此、已完成了进度条的优化改造、不过还存在一个小小的瑕疵、使用mask后,进度条内部交合区域稍微也有一点锯齿感,这个暂时还没有找到优化措施,不过并无大碍。

为了实现更加丰富的展现形式、我们可以在进度条上添加分割线实现花纹效果、这个其实也不麻烦、只需要再添加一层锥形渐变即可解决,具体实现如下:

<!-- demo3 -->
<template>
  <div class="chart-box" :style="styObj">
    <div class="process-box">
      <div class="center-mask"></div>
    </div>
    <!-- 插槽内容 -->
    <div class="slot-content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    rate: {
      type: Number,
      default: 0,
    },
    config: {
      type: Object,
      default: () => {
        return {};
      },
    },
  },
  computed: {
    styObj() {
      let rate = 0;
      if (this.rate <= 0) {
        rate = 0;
      } else if (this.rate >= 1) {
        rate = 1;
      } else {
        rate = this.rate;
      }
      let endPos = `${rate * 100}%`;
      let obj = Object.assign({}, this.defaultConfig, this.config);
      let rotate = `rotate(${360 * rate}deg)`;
      let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
      let bgInfo = [];
      let gap = 100 / obj.gapNum;
      for (let i = 0; i < obj.gapNum; i++) {
        bgInfo.push(`#fff ${i * gap}%`);
        bgInfo.push(`#fff ${i * gap + obj.lineWidth}%`);
        bgInfo.push(`transparent ${i * gap + obj.lineWidth}%`);
        bgInfo.push(`transparent ${(i + 1) * gap}%`);
      }
      return {
        "--background-image": `conic-gradient(${bgInfo.join(",")})`,
        "--background-image1": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
        "--border-width": obj.borderWidth,
        "--background-color": obj.borderBackground,
        "--center-gap-bg": obj.centerCircleBg,
        "--clockwise-wise": chartRotate,
      };
    },
  },
  data() {
    return {
      /* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
      defaultConfig: {
        borderWidth: "8px", // 描边宽度
        borderBackground: "#eee", // 描边背景颜色
        startColor: "#d5f4ee", // 进度条起始颜色
        endColor: "#2ec4a7", // 进度条结束颜色
        centerCircleBg: "#fff", // 中间空心圆背景
        clockwise: true, // 是否顺时针
        gapNum: 10, // 分割段数
        lineWidth: 2, // 间隔线宽度,百分比
      },
    };
  },
};
</script>
<style scoped>
.chart-box {
  position: relative;
  width: 100%;
  height: 100%;
}
.process-box {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  padding: var(--border-width);
  box-sizing: border-box;
  background-color: var(--background-color);
  background-image: var(--background-image), var(--background-image1);
  transform: var(--clockwise-wise);
}
.center-mask {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--center-gap-bg);
}
/* 插槽内容样式 */
.slot-content {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

最后,在汇总页面中依次将三个组件引入,增加不同的个性化参数,即可实现封面的展示效果,贴一下汇总页面代码

<template>
  <div class="page-box">
    <div class="main-box">
      <!-- 第一种实现方式,中间的镂空部分采用背景色(和页面背景一致)的形式 -->
      <div class="module">
        <conic-gradient :rate="0.8888">
          <span class="slot-font1">88.88%</span>
        </conic-gradient>
      </div>
      <div class="module">
        <conic-gradient :rate="0.8888" :config="config">
          <div class="slot-bg">
            <span class="slot-font2">88.88%</span>
          </div>
        </conic-gradient>
      </div>
      <!-- 第二种实现方式,中间镂空部分采用遮罩(mask)的方式实现 -->
      <div class="module">
        <conic-mask
          :rate="0.6666"
          :config="{ showEyes: true, eyesSize: '6px', circleSize: '8px' }"
        >
          <span class="slot-font1">66.66%</span>
        </conic-mask>
      </div>
      <div class="module">
        <conic-mask :rate="0.6666" :config="config">
          <div class="slot-bg">
            <span class="slot-font2">66.66%</span>
          </div>
        </conic-mask>
      </div>
    </div>
    <hr />
    <!-- 锥形渐变实现花纹进度条 -->
    <div class="main-box">
      <div class="module">
        <conic-process :rate="0.6666">
          <span class="slot-font1">66.66%</span>
        </conic-process>
      </div>
      <div class="module">
        <conic-process
          :rate="0.8888"
          :config="{
            startColor: '#e45739',
            endColor: '#e45739',
            borderBackground: '#fbedea',
            gapNum: 20,
            lineWidth: 1,
            clockwise: false
          }"
        >
          <div class="slot-bg">
            <span class="slot-font2">88.88%</span>
          </div>
        </conic-process>
      </div>
      <div class="module">
        <conic-process :rate="0.8888" :config="{ gapNum: 1, lineWidth: 0 }">
          <span class="slot-font1">88.88%</span>
        </conic-process>
      </div>
      <div class="module">
        <conic-process
          :rate="0.8888"
          :config="{
            startColor: '#e45739',
            endColor: '#e45739',
            borderBackground: '#fbedea',
            gapNum: 1,
            lineWidth: 0,
            clockwise: false,
          }"
        >
          <div class="slot-bg">
            <span class="slot-font2">88.88%</span>
          </div>
        </conic-process>
      </div>
    </div>
  </div>
</template>
<script>
import ConicGradient from "./demo1";
import ConicMask from "./demo2";
import ConicProcess from "./demo3";
export default {
  components: {
    ConicGradient,
    ConicMask,
    ConicProcess,
  },
  data() {
    return {
      config: {
        borderWidth: "8px",
        circleSize: "16px",
        circleColor: "#e45739",
        borderColor: "#d5f4ee",
        startColor: "#eead99",
        endColor: "#e45739",
        borderBackground: "#fbedea",
        centerCircleBg: "#fff",
        clockwise: false,
        showEyes: true,
      },
    };
  },
};
</script>
<style scoped>
.page-box {
  width: 100%;
  height: 100%;
  overflow: auto;
}
.main-box {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
}
.module {
  width: 200px;
  height: 200px;
  box-sizing: border-box;
  padding: 20px;
}
.slot-bg {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 75%;
  height: 75%;
  border-radius: 50%;
  background: #fbedea;
}
.slot-font1 {
  color: #009d84;
  font-size: 20px;
  font-weight: bold;
}
.slot-font2 {
  color: #e45638;
  font-size: 20px;
  font-weight: bold;
}
</style>

好了,本章节的内容就到这里,如小伙伴有疑问,可评论区留言、随时交流。

猜你喜欢

转载自blog.csdn.net/qq_40289557/article/details/127550274