canvas实现一个雷达扫描效果

365 阅读6分钟

说在前面

大家对于雷达图应该都不陌生了吧,比如龙珠中这个经典的龙珠雷达,今天让我们一起来看看怎么快速实现一个雷达图。

效果展示

圆形网格雷达图

矩形网格雷达图

代码实现

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针对矩形网格设置行数;
  • 位置与尺寸变量centerXcenterY确定雷达图圆心坐标,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.moveToctx.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,定义内部函数drawRowdrawCol分别用于绘制横线和竖线。在绘制横线和竖线时,通过勾股定理计算与圆相交位置的坐标,从而确定线段起止点,最后完成横线和竖线的绘制。

雷达图绘制

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的值判断需要绘制的网格类型,分别调用drawCircleGirddrawRectGird函数绘制相应网格及刻度;
  • 最后调用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到范围内,判断该点是否处于当前扫描区域内,若不在则跳过绘制;
  • 若在扫描区域内,则使用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,当角度达到(即旋转一圈)时重置为0,并同步更新全局的scanAngle
  • 最后通过requestAnimationFrame递归调用scanAnimation,实现流畅的动画循环,radarScan()的调用则启动整个扫描动画过程。

源码

gitee

仓库地址:gitee.com/zheng_yongt…


  • 🌟觉得有点意思的可以点个star~
  • 🖊有什么问题或错误可以指出,欢迎pr~
  • 📬有什么想要实现的功能或想法可以联系我~

codePen

代码地址:codepen.io/yongtaozhen…

码上掘金

代码地址:code.juejin.cn/pen/7498064…

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。