注册
web

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

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


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="'' + 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实现钟表中刻度的实现,虽然没用它的方法,因为他的方法会导致刻度变形,不是扇形的能量条,但是它旋转坐标轴的想法让我大受启发。


作者:睡觉zzz
来源:juejin.cn/post/7501568955498070016

0 个评论

要回复文章请先登录注册