今天遇到一个很有意思的面试题,面试官给我一道题目,要我实现之前它们公司之前写的一个组件。
首先我介绍下这道题,首先我是先想到用 flex 布局来写头像分布,因为 grid 布局不能实现头像最后一排不能居中的效果。
然后这道题的 重点 来了,我一开始以为它头像上的边框是死的,是张贴图。然后我去问面试官,他说是一个能量条,能根据投票的数量进行改变。我脑袋有点懵,问 ai 也没结果,生成的非常垃圾,然后就开始思考怎么才能实现。首先想到的是 echats,但没有找到合适的,我就开始想 echats 是用 canvas 写的,我就想用 canvas 写下,在 bilibili 上看了下 canvas 的使用方法,于是就想到了这道题的解法。这是我的成果。
我就不做过多的讲解关于 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'); var ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2); ctx.rotate(-Math.PI / 2);
ctx.strokeStyle = 'rgb(144, 211, 205)'; ctx.lineWidth = 20; ctx.lineCap = "butt";
for (let i = 0; i < 17; i++) { 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>
成品代码
最后的成品我是用 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); }); });
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";
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 { 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 实现钟表中刻度的实现,虽然没用它的方法,因为他的方法会导致刻度变形,不是扇形的能量条,但是它旋转坐标轴的想法让我大受启发。