说在前面
大家对于雷达图应该都不陌生了吧,比如龙珠中这个经典的龙珠雷达,今天让我们一起来看看怎么快速实现一个雷达图。
效果展示
圆形网格雷达图
矩形网格雷达图
代码实现
HTML & CSS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>雷达扫描效果</title>
<style>
.radar {
width: 100%;
height: 98vh;
display: flex;
}
.radar-content {
margin: auto;
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<div class="radar">
<canvas class="radar-content"></canvas>
</div>
</body>
HTML部分定义了页面的基本结构,外层<div>
元素添加radar
类,通过CSS设置其宽度占满屏幕、高度占据98%视口高度,采用弹性布局实现内部元素垂直居中。内部的<canvas>
元素添加radar-content
类,设置固定宽高为500px,并通过margin: auto
实现水平和垂直方向的居中,作为绘制雷达扫描效果的核心区域。
雷达配置
const canvas = document.querySelector(".radar-content");
const ctx = canvas.getContext("2d");
const backaroundColor = "rgba(0,0,0,0.7)";
const gridCount = 5;
const sideLength = 500;
const gridColor = "#2EB74E";
const gridType = 1;
const rowCount = 10;
const tickCount = 4;
const centerX = sideLength / 2;
const centerY = sideLength / 2;
const radius = sideLength / 2;
const scanSpeed = 0.01;
const scanColor = ["rgba(46, 183, 78, 0.5)", "rgba(46, 183, 78, 0.1)"];
const points = [
{ x: -50, y: -50 },
{ x: 50, y: 50, isScanShow: false, color: "red" },
{ x: -70, y: 50, isScanShow: false, color: "skyblue", radius: 10 },
{ x: 80, y: 80 },
{ x: 20, y: -20 },
{ x: -30, y: 30 },
];
const scanAreaAngle = 30;
let scanAngle = 0;
JavaScript代码首先获取<canvas>
元素及其2D绘图上下文,为后续绘图操作奠定基础。随后定义了一系列关键变量:
- 基础样式与布局变量:如背景颜色
backaroundColor
、网格数量gridCount
、雷达区域边长sideLength
、网格颜色gridColor
等,用于确定雷达图的基本外观; - 可定制参数变量:
gridType
用于控制网格类型(1为圆形网格,2为矩形网格),rowCount
针对矩形网格设置行数; - 位置与尺寸变量:
centerX
、centerY
确定雷达图圆心坐标,radius
定义雷达半径; - 动画相关变量:
scanSpeed
控制扫描扇形的旋转速度,scanColor
定义扫描区域的渐变颜色,points
数组存储扫描过程中需要显示的点信息,每个点对象包含坐标、半径、颜色、是否在扫描时显示
等属性; - 动画状态变量:
scanAreaAngle
表示扫描区域的角度范围,scanAngle
记录当前扫描角度。
雷达网格绘制
圆形网格
function drawCircleGird() {
const gridGap = radius / gridCount;
for (let i = 1; i <= gridCount; i++) {
const gridRadius = gridGap * i;
ctx.beginPath();
ctx.arc(centerX, centerY, gridRadius, 0, Math.PI * 2, true);
ctx.strokeStyle = gridColor;
ctx.stroke();
ctx.closePath();
}
const tickGap = (Math.PI * 2) / tickCount;
for (let i = 0; i < tickCount; i++) {
const angle = tickGap * i;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(
centerX + Math.cos(angle) * radius,
centerY + Math.sin(angle) * radius
);
ctx.strokeStyle = gridColor;
ctx.stroke();
ctx.closePath();
}
}
- 先计算网格间距
gridGap
,通过循环确定每个圆形网格的半径gridRadius
,使用ctx.arc
方法绘制圆形网格,并设置描边颜色,最后通过ctx.stroke
描边; - 再计算刻度角度间隔
tickGap
,通过循环以圆心为起点,利用三角函数计算刻度终点坐标,使用ctx.moveTo
和ctx.lineTo
绘制刻度线并描边。
矩形网格
function drawRectGird() {
ctx.strokeStyle = gridColor; // 网格颜色
const width = sideLength / rowCount; // 每个格子的宽度
// 绘制横线
const drawRow = (i) => {
const y = width * i;
const x = Math.sqrt(radius * radius - (centerY - y) ** 2); // 计算对应的x坐标
ctx.beginPath();
ctx.moveTo(radius - x, y);
ctx.lineTo(radius + x, y);
ctx.stroke();
ctx.closePath();
};
// 绘制竖线
const drawCol = (i) => {
const x = width * i;
const y = Math.sqrt(radius * radius - (centerX - x) ** 2); // 计算对应的y坐标
ctx.beginPath();
ctx.moveTo(x, radius - y);
ctx.lineTo(x, radius + y);
ctx.stroke();
ctx.closePath();
};
// 绘制横线
for (let i = 1; i <= rowCount; i++) {
drawCol(i);
drawRow(i);
}
}
- 首先绘制雷达图的外圆轮廓;
- 接着计算每个矩形格子的宽度
width
,定义内部函数drawRow
和drawCol
分别用于绘制横线和竖线。在绘制横线和竖线时,通过勾股定理计算与圆相交位置的坐标,从而确定线段起止点,最后完成横线和竖线的绘制。
雷达图绘制
function drawRadar() {
canvas.width = sideLength;
canvas.height = sideLength;
ctx.clearRect(0, 0, sideLength, sideLength);
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
ctx.fillStyle = backaroundColor;
ctx.fill();
ctx.closePath();
if (gridType === 1) {
drawCircleGird();
}
if (gridType === 2) {
drawRectGird();
}
drawPoints();
}
- 先重置
画布
的宽高,并清空绘图区域,确保每次绘制都是在干净的画布上进行; - 根据
backaroundColor
给画布绘制上对于的背景 - 根据
gridType
的值判断需要绘制的网格类型,分别调用drawCircleGird
或drawRectGird
函数绘制相应网格及刻度; - 最后调用
drawPoints
函数绘制扫描点。
扫描点绘制
function drawPoints() {
points.forEach((point) => {
const {
x,
y,
radius = "5",
color = "green",
isScanShow = true,
} = point;
if (isScanShow) {
let angle = Math.atan2(y, x);
angle += Math.PI * 2;
angle %= Math.PI * 2;
if (
angle < scanAngle ||
angle > scanAngle + Math.PI / (180 / scanAreaAngle)
) {
return;
}
}
ctx.beginPath();
ctx.arc(centerX + x, centerY + y, radius, 0, Math.PI * 2, true);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
});
}
drawPoints
函数遍历points
数组中的每个点对象:
- 提取点的坐标、半径、颜色和显示控制属性;
- 对于需要在扫描时显示的点,计算该点相对于圆心的角度,并将角度调整到0到
2π
范围内,判断该点是否处于当前扫描区域内,若不在则跳过绘制; - 若在扫描区域内,则使用
ctx.arc
方法以点的坐标为圆心绘制圆形,设置填充颜色后通过ctx.fill
填充圆形,完成点的绘制。
扫描动画
function radarScan() {
let angle = 0;
const scanAnimation = () => {
drawRadar();
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(
centerX,
centerY,
radius,
angle,
angle + Math.PI / (180 / scanAreaAngle),
false
);
const gradient = ctx.createLinearGradient(
centerX,
centerY,
centerX + Math.cos(angle) * radius,
centerY + Math.sin(angle) * radius
);
gradient.addColorStop(0, scanColor[0]);
gradient.addColorStop(1, scanColor[1]);
ctx.fillStyle = gradient;
ctx.fill();
ctx.closePath();
angle += scanSpeed;
if (angle >= Math.PI * 2) {
angle = 0;
}
scanAngle = angle;
requestAnimationFrame(scanAnimation);
};
scanAnimation();
}
radarScan();
radarScan
函数实现雷达扫描的核心动画逻辑:
- 定义初始角度
angle
,并创建内部递归函数scanAnimation
; - 在
scanAnimation
函数中,首先调用drawRadar
函数重新绘制雷达图的静态部分; - 接着使用
ctx.arc
绘制扫描扇形区域,通过ctx.createLinearGradient
创建线性渐变对象,设置扫描区域从起始颜色到结束颜色的渐变效果,然后填充扇形区域; - 更新扫描角度
angle
,当角度达到2π
(即旋转一圈)时重置为0,并同步更新全局的scanAngle
; - 最后通过
requestAnimationFrame
递归调用scanAnimation
,实现流畅的动画循环,radarScan()
的调用则启动整个扫描动画过程。
源码
gitee
- 🌟觉得有点意思的可以点个star~
- 🖊有什么问题或错误可以指出,欢迎pr~
- 📬有什么想要实现的功能或想法可以联系我~
codePen
码上掘金
代码地址:code.juejin.cn/pen/7498064…
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。