用canvas实现一个头像上的间断式的能量条

2,919 阅读4分钟

今天遇到一个很有意思的面试题,面试官给我一道题目,要我实现之前它们公司之前写的一个组件。

e416716a-0b87-4411-ba39-7ec328968391.webp 首先我介绍下这道题,首先我是先想到用flex布局来写头像分布,因为grid布局不能实现头像最后一排不能居中的效果。

然后这道题的重点来了,我一开始以为它头像上的边框是死的,是张贴图。然后我去问面试官,他说是一个能量条,能根据投票的数量进行改变。我脑袋有点懵,问ai也没结果,生成的非常垃圾,然后就开始思考怎么才能实现。首先想到的是echats,但没有找到合适的,我就开始想echats是用canvas写的,我就想用canvas写下,在bilibili上看了下canvas的使用方法,于是就想到了这道题的解法。这是我的成果。

image.png 我就不做过多的讲解关于canvas的使用方法,我只在我的演示代码注释中讲每条代码的作用,和使用方法。不会的话,可以去看看bilibili,然后做个笔记,然后就印象深刻了。

代码讲解

这里是初步实现的代码,写出了大概的轮廓方便理解。完整代码在最后面。

具体的代码讲解就写在注释中了。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <canvas id="canvas" width="600" height="600" backgroud></canvas>
  <script>
    function animate() {
      var canvas = document.getElementById('canvas');//找到canvas
      var ctx = canvas.getContext('2d');//读取canvas的上下文,进行修改,就能实现对canvas的绘画

      ctx.translate(canvas.width / 2, canvas.height / 2);//这个是将canvas的坐标轴移到中间
      ctx.rotate(-Math.PI / 2);//这个是将坐标轴反向转动90度

      ctx.strokeStyle = 'rgb(144, 211, 205)';//设置画笔的颜色
      ctx.lineWidth = 20;  // 这里是设置画笔的宽度,也就是能量条的宽度
      ctx.lineCap = "butt"; //这里设置画笔结束的位置是圆的直的还是弯的

      for (let i = 0; i < 17; i++) {//这里17表示要绘制17段线,到时候这里循环的次数会传过来在我后面的成品中。
        ctx.beginPath();//这里开始绘制路径
        // 绘制小段圆弧 (角度改为弧度制)
        ctx.arc(0, 0, 100, -Math.PI / 34, Math.PI / 34, false);//前两个位置是圆心,第三个是半径,第四个是开始角度,第五个是结束角度,第六个是是否逆时针
        ctx.stroke();//这个表填充绘画的轨迹
        // 旋转到下一个位置
        ctx.rotate(Math.PI / 16);//这里坐标轴顺时针移动一定角度,如果想要格子更多就设的更小,上面画线的角度也要调小
        ctx.closePath()//结束绘制
      }
    }
    animate();
  </script>
</body>

</html>

image.png

成品代码

最后的成品我是用vue写的,没有特别去封装,毕竟只是面试题。

<template>
  <div class="grid-container">
    <div class="member-card" v-for="(member, index) in members" :key="index">
      <canvas :id="'canvas-' + index" width="150" height="150"></canvas>
      <div class="circle">
        <img :src="member.avatar" alt="avatar" class="avatar" />
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';


const members = [
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 10 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 2 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 18 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 31 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
  { name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
];

onMounted(() => {
  members.forEach((member, index) => {
    drawEnergyBar(index, member.numbers); // 使用member.numbers作为参数
  });
});

function drawEnergyBar(index, count) {
  const canvas = document.getElementById(`canvas-${index}`);
  const ctx = canvas.getContext('2d');

  // 重置画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 绘制设置
  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate(-Math.PI / 2);

  ctx.strokeStyle = 'rgb(144, 211, 205)';
  ctx.lineWidth = 60;
  ctx.lineCap = "butt";

  // 根据传入的count值绘制线段
  for (let i = 0; i < count; i++) {
    ctx.beginPath();
    ctx.arc(0, 0, 44, -Math.PI / 36, Math.PI / 36, false);
    ctx.stroke();
    ctx.rotate(Math.PI / 16);
  }
}
</script>

<style scoped>
/* 修改canvas样式 */
canvas {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 1;
  /* 作为背景层 */
}

.member-card {
  position: relative;
  width: 150px;
  height: 150px;
  /* 添加固定高度 */
  display: flex;
  justify-content: center;
  align-items: center;
  transition: transform 0.3s ease;
  border: rgb(144, 211, 205) solid 2px;
  border-radius: 50%;
  background-color: black;
  overflow: hidden;
}

.circle {
  position: relative;
  border: 2px solid black;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  z-index: 2;
  /* 确保在画布上方 */
  margin: 0;
  /* 移除外边距 */
}

.grid-container {
  height: 100%;
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 30px;
  padding: 30px;
  max-width: calc(150px * 6 + 30px * 5);
  margin: 0 auto;
  background: url(https://pic.nximg.cn/file/20230303/33857552_140701783106_2.jpg);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-attachment: fixed;
}

.member-card {
  position: relative;
  width: 150px;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: transform 0.3s ease;
  border: rgb(144, 211, 205) solid 2px;
  border-radius: 50%;
  background-color: black;
}


.circle {
  position: relative;
  border: 2px solid black;
  margin: 20px 20px;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s ease;
}
</style>

结语

虽然这道题有点难,但好处是我对canvas的理解加深了,canvas绝对是前端的一个非常有用的东西,值得掘友们认真学习。原本这道题的灵感来源于bilibili上讲的canvas实现钟表中刻度的实现,虽然没用它的方法,因为他的方法会导致刻度变形,不是扇形的能量条,但是它旋转坐标轴的想法让我大受启发。