某日,进入掘金我的倔友分看到五个维度的分数图,遂想着实现以下。
五边形,对应五个维度:社区基础,社区学习,社区规范,社区影响力,社区活跃。
那么,我能否是个满级的五边形战士呢?就像下面这样:
So, It's my show time now~ Hold On.
五边形特效实现的效果如下 GIF
图:
实现的功能参考掘金我的倔友分,使用
canvas
技术实现
html
代码的骨架很简单:
<canvas id="canvas" width="374" height="342" style="background-color: rgb(35, 35, 35)"></canvas>
宽 374,高 342。为什么选择这个数值呢?抄的掘金
canvas
数值~
接下来的就是重头戏了,编写 javascript
。
看过我之前文章 - 天幕:六边形特效 的读者可能比较好理解。之前,我们绘制的是六边形:
不同的点是,这次我们绘制的是五边形,道理都一样,不明白的可以移步天幕:六边形特效文章了解,这里不赘述。
(function() {
let canvas = document.getElementById("canvas"); // 获取画布
let ctx = canvas.getContext("2d"); // 设置画笔
ctx.strokeStyle = "#fff";
ctx.lineWidth = 1;
function init() {
for(let i = 0; i < 5; i += 1) {
// 使用半透明颜色来模拟细线条效果
ctx.globalAlpha = 0.2;
// 绘制五边形
drawPentagon(canvas.width/2, canvas.height/2, i * 20);
if(i === 4) {
// 将五边形连接起来
drawPentagonBetweenLine(canvas.width/2, canvas.height/2, i * 20);
}
// 恢复透明度
ctx.globalAlpha = 1;
}
}
init();
function drawPentagon(x, y, radius) {
ctx.strokeStyle = "#fff";
ctx.beginPath();
ctx.moveTo(x + Math.cos(-90 * Math.PI / 180) * radius, y + Math.sin(-90 * Math.PI / 180) * radius);
ctx.arc(x + Math.cos(-90 * Math.PI / 180) * radius, y + Math.sin(-90 * Math.PI / 180) * radius, 0.5, 0, 2 * Math.PI);
for(let i = 1; i <= 4; i++) {
let angle = (-90 * Math.PI / 180) + i * (2 * Math.PI / 5);
ctx.lineTo(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius);
ctx.arc(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius, 0.5, 0, 2 * Math.PI);
}
ctx.closePath();
ctx.stroke();
}
function drawPentagonBetweenLine(x, y, radius) {
ctx.beginPath();
ctx.moveTo(x, y);
for(let i = 0; i < 5; i += 1) {
let angle = (-90 * Math.PI / 180) + i * (2 * Math.PI / 5);
ctx.lineTo(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius);
ctx.moveTo(x, y);
}
ctx.closePath();
ctx.stroke();
}
})()
上面的代码,我们将使用了一个 for
循环,将四层五边形绘制出来,然后调用 drawPentagonBetweenLine
方法,将这四层五边形连接起来。
OK,我们设定五个维度的维度值。
在此之前,我们得设定下五个维度的文本,我们在 drawPentagonBetweenLine
函数上添加:
// 恢复透明度
ctx.globalAlpha = 1;
ctx.beginPath();
for(let i = 0; i < 5; i += 1) {
let angle = (-90 * Math.PI / 180) + i * (2 * Math.PI / 5);
let _text = "";
let _x = x + Math.cos(angle) * radius;
let _y = y + Math.sin(angle) * radius;
if(i === 0) {
_text = "社区基础"
}
if(i === 1) {
_text = "社区学习"
}
if(i === 2) {
_text = "社区规范"
}
if(i === 3) {
_text = "社区影响力"
}
if(i === 4) {
_text = "社区活跃"
}
const textWidth = ctx.measureText(_text).width;
if(i === 0) {
_y -= 10;
}
if(i === 1) {
_x += (textWidth / 2 + 10);
}
if(i === 2) {
_y += 10;
}
if(i === 3) {
_y += 10;
}
if(i === 4) {
_x -= (textWidth / 2 + 10);
}
ctx.fillText(_text, _x, _y);
}
ctx.closePath();
嗯~,我们这里简单使用了 if
语句进行判断并赋值。
现在,我们可以设定五个维度对应的值了,拿社区基础为例子:
// 设定指定的维度点值
function drawPoints() {
let x = canvas.width/2, y = canvas.height/2;
ctx.strokeStyle = "#FDB452";
ctx.fillStyle = "#FDB452";
ctx.beginPath();
let point0_x = x + Math.cos((-90 * Math.PI / 180) + 0 * (2 * Math.PI / 5)) * 4 * 20;
let point0_y = y + Math.sin((-90 * Math.PI / 180) + 0 * (2 * Math.PI / 5)) * 4 * 20;
ctx.arc(point0_x, point0_y, 2, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
这里,我们计算了维度 社区基础
值点所在的位置 point0_x
和 point0_y
,即对应的 x
和 y
轴,然后调用 ctx.arc()
方法进行绘制。以此类推,我们可以得到对应的其他四个维度的点坐标并绘制。
五维度的动效:先显示各个维度的点,然后从中心向四周点进行扩散。
首先,我们先将五个维度的点连线起来:
let scale = 0; // 是否正在变大, 0 -> 1
function drawLine() {
clearCanvas();
let point0_x = x + Math.cos((-90 * Math.PI / 180) + 0 * (2 * Math.PI / 5)) * 4 * 20 * scale;
let point0_y = y + Math.sin((-90 * Math.PI / 180) + 0 * (2 * Math.PI / 5)) * 4 * 20 * scale;
// ... other code
// 连线
ctx.beginPath();
ctx.moveTo(point0_x, point0_y);
ctx.lineTo(point1_x, point1_y);
ctx.lineTo(point2_x, point2_y);
ctx.lineTo(point3_x, point3_y);
ctx.lineTo(point4_x, point4_y);
ctx.closePath();
ctx.stroke();
ctx.fill(); // 填充
if(scale < 1) {
scale += 0.03;
requestAnimationFrame(drawLine); // 动效
}
}
drawLine();
// 清空画布
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
这里的 (point0_x, point0_y)
,point1_x, point1_y
...点的坐标,对应上面👆 - 「五维度点设定」 计算出来的点坐标进行修改,追加了 scale
的参数。
然后,我们根据 scale
的大小进行动效操作,这里我们以 0.03
的倍数进行重绘。
最后,我们就是进行动效的切换了:点击 - 我们得分 区域,对五个维度的值进行隐藏或展示。
首先,我们先将这个点击区域绘制出来,也就是画线和填充文本:
let showHightLight = true; // 展示五边形高亮
// 绘制 hint 线条和文本
function drawHintLine() {
const _text = "我的得分";
const _textWidth = ctx.measureText(_text).width;
const _textOffsetLeft = 6;
const _lineWidth = 20;
const _offsetBottom = 20;
ctx.fillStyle = "#fff";
// 根据条件展示高亮线条颜色
if(showHightLight) {
ctx.strokeStyle = "#FDB452";
} else {
ctx.strokeStyle = "#fff";
}
ctx.textAlign = "left";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(canvas.width / 2 - (_textWidth + _lineWidth + _textOffsetLeft) / 2, canvas.height - _offsetBottom);
ctx.lineTo(canvas.width / 2 - (_textWidth + _lineWidth + _textOffsetLeft) / 2 + _lineWidth, canvas.height - _offsetBottom);
ctx.fillText(_text, canvas.width / 2 - (_textWidth + _lineWidth + _textOffsetLeft) / 2 + _lineWidth + _textOffsetLeft, canvas.height - _offsetBottom);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#fff";
}
接下来就是监听点击区域了,我们这里会使用到 mousemove
和 click
的方法,结合 isHoverTextHint
变量:
let isHoverTextHint = false; // 是否鼠标在 我的分数提示上
// 监听鼠标移动
canvas.addEventListener("mousemove", function(event) {
const _text = "我的得分";
const _textWidth = ctx.measureText(_text).width; // 获取文本的宽度
const _textHeight = ctx.measureText(_text).actualBoundingBoxAscent + ctx.measureText(_text).actualBoundingBoxDescent; // 获取文本的高度
const _textOffsetLeft = 6;
const _lineWidth = 20;
const _offsetBottom = 20;
// 获取鼠标在Canvas中的坐标
var mouseX = event.clientX - canvas.offsetLeft;
var mouseY = event.clientY - canvas.offsetTop;
if(mouseX >= (canvas.width / 2 - (_textWidth + _textOffsetLeft + _lineWidth) / 2) && mouseX <= ((canvas.width / 2 - (_textWidth + _textOffsetLeft + _lineWidth) / 2) + _textWidth + _textOffsetLeft + _lineWidth) && mouseY >= (canvas.height - _textHeight - _offsetBottom) && mouseY <= (canvas.height + _textHeight)) {
canvas.style.cursor = "pointer";
isHoverTextHint = true;
} else {
canvas.style.cursor = "default";
isHoverTextHint = false;
}
})
// 监听鼠标滚动
canvas.addEventListener("click", function() {
if(isHoverTextHint) {
if(showHightLight) {
clearCanvas();
init();
showHightLight = false;
drawHintLine();
} else {
clearCanvas();
init();
scale = 0;
drawPoints();
drawLine();
showHightLight = true;
}
}
})
当鼠标在 canvas
上移动的时候,我们监听。当鼠标位置在指定的位置区间,我们设置鼠标的样式为 canvas.style.cursor="pointer"
,否则为默认样式。
当鼠标在指定的位置进行点击的时候,我们就要进行效果的切换了。
Yeah,终于是无所不能的五边形战士了:
后话,当然,上面的代码,我并没有进行进一步的优化,如果读者感兴趣,可以根据步骤来实现,并进行相关的优化