在 Cocos Creator 里画个炫酷的雷达图(附源码)

news/2024/7/19 15:05:08 标签: js, 数据可视化, html, 游戏开发, opengl
htmledit_views">
html" title=js>js_content">

前言

????️雷达图(Radar Chart) 也称为网络图、星图或蜘蛛网图。

是以从同一点开始的轴上表示的三个或更多个定量变量的二维图表的形式显示多元数据的图形方法。

适用于显示三个或更多的维度的变量。

网上偷的图(侵删)

????️雷达图常用于????数据统计或对比,对于查看哪些变量具有相似的值、变量之间是否有异常值都很有用。

????同时在不少游戏中都有雷达图的身影,可以很直观地展示并对比一些数据。

例如王者荣耀中的对战资料中就用到了:

网上偷的图(侵删)

????那么在本篇文章中,皮皮就来分享下在 Cocos Creator 中如何利用 Graphics 组件来绘制炫酷的雷达图~

文中会对原始代码进行一定的削减以保证阅读体验。

需要完整代码文件的小伙伴可在文章底部“阅读原文”处获取。


预览

????先来看看效果吧~

在线预览:https://ifaswind.gitee.io/eazax-cases/?case=radarChart

????「两条数据」

????「缓动数据」

????「花里胡哨」

????「艺术就是爆炸」

????「逐渐偏离主题」

???? 没有人

???? 比我

???? 更懂

☝️ 花里胡哨

(????川老师直呼内行)


正文

????Graphics 组件

在我们正式开始制作雷达图之前,让我们先来大概了解一下 Cocos Creator 引擎中的 Graphics 组件。

Graphics 组件继承于 cc.RenderComponent,利用该组件我们可以实现画板和表格之类的功能。

属性(Properties)

下面是我们本次将会用到的属性:

  • lineCap:设置或返回线条两端的样式(无、圆形线帽或方形线帽)

  • lineJoin:设置或返回两条线相交时的拐角样式(斜角、圆角或尖角)

  • lineWidth:设置或返回当前画笔的粗细(线条的宽度)

  • strokeColor:设置或返回当前画笔的颜色

  • fillColor:设置或返回填充用的颜色(油漆桶)

函数(Functions)

下面是我们本次将会用到的函数:

  • moveTo(x, y):抬起画笔并移动到指定位置(不创建线条)

  • lineTo(x, y):放下画笔并创建一条直线至指定位置

  • circle(cx, cy, r):在指定位置(圆心)画一个圆

  • close():闭合已创建的线条(相当于 lineTo(起点)

  • stroke():绘制已创建(但未被绘制)的线条(将线条想象成默认透明的,此行为则是赋予线条颜色)

  • fill():填充当前线条包围的区域(如果线条没有闭合则会尝试”模拟闭合“起点和终点)

  • clear():擦掉当前画板上的所有东西

Graphics 组件文档:http://docs.cocos.com/creator/manual/zh/components/graphics.html?h=graphics

????画网格

捋一捋

先来看看一个标准的雷达图有啥特点:

网上偷的图(侵删)

????发现了吗?雷达图的基本特点如下:

  • 有 3 条或以上的轴线

  • 轴与轴之间的夹角相同

  • 每条轴上除中心点外应至少有 1 个刻度

  • 每条轴上都有相同的刻度

  • 刻度与刻度之间的距离也相同

  • 轴之间的刻度相连形成网格线

动手吧

计算轴线角度

先算出轴之间的夹角度数 [ 360 ÷ 轴数 ],再计算所有轴的角度:

this.angles = [];
// 轴间夹角
const iAngle = 360 / this.axes;
for (let i = 0; i < this.axes; i++) {
    // 计算
    const angle = iAngle * i;
    this.angles.push(angle);
}

计算刻度坐标

雷达图至少拥有 3 条轴,且「每条轴上都应有 1 个或以上的刻度(不包含中心点)」

所以我们需使用一个二维数组来保存所有刻度的坐标,从最外层(即轴线的末端)的刻度开始记录,方便我们绘制时读取:

// 创建一个二维数组
let scalesSet: cc.Vec2[][] = [];
for (let i = 0; i < 轴上刻度个数; i++) {
    // 用来保存当前层上的刻度坐标
    let scales = [];
    // 计算刻度在轴上的位置
    const length = 轴线长度 - (轴线长度 / 轴上刻度个数 * i);
    for (let j = 0; j < this.angles.length; j++) {
        // 将角度转为弧度
        const radian = (Math.PI / 180) * this.angles[j];
        // 根据三角公式计算刻度相对于中心点(0, 0)的坐标
        const pos = cc.v2(length * Math.cos(radian), length * Math.sin(radian));
        // 推进数组
        scales.push(pos);
    }
    // 推进二维数组
    scalesSet.push(scales);
}

绘制轴线和外网格线

轴线

连接中心点 (0, 0) 和最外层 scalesSet[0] 的刻度即为轴线:

// 遍历全部最外层的刻度
for (let i = 0; i < scalesSet[0].length; i++) {
    // 画笔移动至中心点
    this.graphics.moveTo(0, 0);
    // 创建线条
    this.graphics.lineTo(scalesSet[0][i].x, scalesSet[0][i].y);
}
外网格线

连接所有轴上最外层 scalesSet[0] 的刻度即形成外网格线:

// 画笔移动至第一个点
this.graphics.moveTo(scalesSet[0][0].x, scalesSet[0][0].y);
for (let i = 1; i < scalesSet[0].length; i++) {
    // 创建线条
    this.graphics.lineTo(scalesSet[0][i].x, scalesSet[0][i].y);
}
// 闭合当前线条(外网格线)
this.graphics.close();
填充并绘制

这里需要注意「先填充颜色再绘制线条」,要不然轴线和网格线就被挡住了:

// 填充线条包围的空白区域
this.graphics.fill();
// 绘制已创建的线条(轴线和外网格线)
this.graphics.stroke();

????于是现在我们就有了这么个玩意儿:

绘制内网格线

当刻度大于 1 个时就需要绘制内网格线,从刻度坐标集的下标 1 开始绘制:

// 刻度大于 1 个时才绘制内网格线
if (scalesSet.length > 1) {
    // 从下边 1 开始(下标 0 是外网格线)
    for (let i = 1; i < scalesSet.length; i++) {
        // 画笔移动至第一个点
        this.graphics.moveTo(scalesSet[i][0].x, scalesSet[i][0].y);
        for (let j = 1; j < scalesSet[i].length; j++) {
            // 创建线条
            this.graphics.lineTo(scalesSet[i][j].x, scalesSet[i][j].y);
        }
        // 闭合当前线条(内网格线)
        this.graphics.close();
    }
    // 绘制已创建的线条(内网格线)
    this.graphics.stroke();
}

????就这样我们雷达图的底子就画好啦:

????画数据

捋一捋

编写画线逻辑之前,先确定一下我们需要的数据结构:

  • 数值数组(必须,小数形式的比例,至少包含 3 个值)

  • 线的宽度(可选,不指定则使用默认值)

  • 线的颜色(可选,不指定则使用默认值)

  • 填充的颜色(可选,不指定则使用默认值)

  • 节点的颜色(可选,不指定则使用默认值)

具体的数据结构如下(导出类型方便外部使用):

/**
 * 雷达图数据
 */
export interface RadarChartData {

    /** 数值 */
    values: number[];

    /** 线的宽度 */
    lineWidth?: number;

    /** 线的颜色 */
    lineColor?: cc.Color;

    /** 填充的颜色 */
    fillColor?: cc.Color;

    /** 节点的颜色 */
    joinColor?: cc.Color;

}

动手吧

绘制数据比较简单,我们只需要算出数据点在图表中的位置,并将数据连起来就好了。

draw 函数中我们接收一份或以上的雷达图数据,并按照顺序遍历绘制出来(⚠️长代码警告):

/**
 * 绘制数据
 * @param data 数据
 */
public draw(data: RadarChartData | RadarChartData[]) {
    // 处理数据
    const datas = Array.isArray(data) ? data : [data];

    // 开始绘制数据
    for (let i = 0; i < datas.length; i++) {
        // 装填染料
        this.graphics.strokeColor = datas[i].lineColor || defaultOptions.lineColor;
        this.graphics.fillColor = datas[i].fillColor || defaultOptions.fillColor;
        this.graphics.lineWidth = datas[i].lineWidth || defaultOptions.lineWidth;

        // 计算节点坐标
        let coords = [];
        for (let j = 0; j < this.axes; j++) {
            const value = datas[i].values[j] > 1 ? 1 : datas[i].values[j];
            const length = value * this.axisLength;
            const radian = (Math.PI / 180) * this.angles[j];
            const pos = cc.v2(length * Math.cos(radian), length * Math.sin(radian))
            coords.push(pos);
        }

        // 创建线条
        this.graphics.moveTo(coords[0].x, coords[0].y);
        for (let j = 1; j < coords.length; j++) {
            this.graphics.lineTo(coords[j].x, coords[j].y);
        }
        this.graphics.close(); // 闭合线条
        
        // 填充包围区域
        this.graphics.fill();
        // 绘制线条
        this.graphics.stroke();

        // 绘制数据节点
        for (let j = 0; j < coords.length; j++) {
            // 大圆
            this.graphics.strokeColor = datas[i].lineColor || defaultOptions.lineColor;
            this.graphics.circle(coords[j].x, coords[j].y, 2);
            this.graphics.stroke();
            // 小圆
            this.graphics.strokeColor = datas[i].joinColor || defaultOptions.joinColor;
            this.graphics.circle(coords[j].x, coords[j].y, .65);
            this.graphics.stroke();
        }

    }
}

????到这里我们已经成功制作了一个可用的雷达图:

????「但是!我们的征途是星辰大海!必须加点料!」

????加料不加价

动起来?

????完全静态的雷达图实在是太无趣太普通,得想想办法让它动起来!

????我们的雷达图数据的数值是数组形式,想到怎么样才能让这些数值动起来了吗?

????「别 担 心 !」

????得益于 Cocos Creator 为我们提供的 「Tween 缓动系统」,让复杂的数据动起来变得异常简单!

????我们只需要这样,这样,然后那样,是不是很简单?

cc.tween 支持缓动任意对象的任意属性

缓动系统:http://docs.cocos.com/creator/manual/zh/scripting/tween.html

另外我在《一个全能的挖孔 Shader》中也是使用了缓动系统来让挖孔动起来~

在线预览:https://ifaswind.gitee.io/eazax-cases/?case=newGuide

动手吧

我的思路是:

  1. 将当前的数据保存到当前实例的 this.curDatas

  2. 接收到新的数据时,使用 cc.tweenthis.curData 的属性进行缓动

  3. update 中调用 draw 函数,每帧都重新绘制 this.curDatas 中的数据

每帧更新

// 当前雷达图数据
private curDatas: RadarChartData[] = [];

protected update() {
    if (!this.keepUpdating) return;
    // 绘制当前数据
    this.draw(this.curDatas);
}

缓动数据

/**
 * 缓动绘制
 * @param data 目标数据
 * @param duration 动画时长
 */
public to(data: RadarChartData | RadarChartData[], duration: number) {
    // 处理重复调用
    this.unscheduleAllCallbacks();
    
    // 包装单条数据
    const datas = Array.isArray(data) ? data : [data];

    // 打开每帧更新
    this.keepUpdating = true;

    // 动起来!
    for (let i = 0; i < datas.length; i++) {
        // 数值动起来!
        // 遍历数据中的全部数值,逐个让他们动起来!
        for (let j = 0; j < this.curDatas[i].values.length; j++) {
            // 限制最大值为 1(即 100%)
            const value = datas[i].values[j] > 1 ? 1 : datas[i].values[j];
            cc.tween(this.curDatas[i].values)
                .to(duration, { [j]: value })
                .start();
        }
        // 样式动起来!
        // 没有指定则使用原来的样式!
        cc.tween(this.curDatas[i])
            .to(duration, {
                lineWidth: datas[i].lineWidth || this.curDatas[i].lineWidth,
                lineColor: datas[i].lineColor || this.curDatas[i].lineColor,
                fillColor: datas[i].fillColor || this.curDatas[i].fillColor,
                joinColor: datas[i].joinColor || this.curDatas[i].joinColor
            })
            .start();
    }

    this.scheduleOnce(() => {
        // 关闭每帧更新
        this.keepUpdating = false;
    }, duration);
}

计划通

????数值和样式都动起来了:

源码仓库:https://gitee.com/ifaswind/eazax-ccc/blob/master/components/RadarChart.ts
点击文章底部“阅读原文”即可获取完整雷达图组件。


http://www.niftyadmin.cn/n/806347.html

相关文章

数据库优化:其余多种优化方式

应用优化 1 使用连接池 对于访问数据库来说&#xff0c;建立连接的代价是比较昂贵的&#xff0c;因为我们频繁的创建关闭连接&#xff0c;是比较耗费资源的&#xff0c;我们有必要建立 数据库连接池&#xff0c;以提高访问的性能。 2 减少对MySQL的访问 2.1 避免对数据进行…

【每日面试】YY直播 Java开发 一面

作者&#xff1a;许y愿 链接&#xff1a;https://www.nowcoder.com/discuss/735887?source_iddiscuss_experience_nctrack&channel-1 来源&#xff1a;牛客网 1、自我介绍 2、常用集合类&#xff0c;然后挑一个熟悉的介绍&#xff08;直接Hashmap&#xff09; 3、JVM垃圾…

盘点JavaScript中数组遍历的全部方式(上篇)

今日鸡汤天生我才必有用&#xff0c;千金散尽还复来。前言JavaScript想必大家都不陌生了&#xff0c;其中的字符串和数组大家经常都会用到&#xff0c;今天就让我们来说说这里面的数组对象的遍历吧&#xff0c;因为遍历经常使用的缘故&#xff0c;所以小编带着大家来解锁遍历的…

ubuntu系统安装FTP

Ubuntu安装vsftp软件 1.更新软件源 首先须要更新系统的软件源&#xff0c;便捷工具下载地址&#xff1a;http://help.aliyun.com/manual?spm0.0.0.0.zJ3dBU&helpId1692 2.安装vsftp 使用apt-get命令安装vsftp #apt-get install vsftpd -y 3.加入&#xfffd;ftp帐号和文件…

GT 大神 | 如何高效渲染流体效果(绝对干货)

流体效果 相信大家都不陌生&#xff0c;实现方式中的一种是将粒子渲染成 metaball 。什么是metaball metaball 就是粒子加上其周围的 密度场 (density field)。两个 metaball 靠近时&#xff0c;其密度场会叠加。当屏幕上某个像素的"密度"大于阈值时&#xff0c;将其…

七夕,互联网公司爱情故事

世界上有两种爱情&#xff1a;一般人的爱情和互联网人的爱情。互联网人的爱情&#xff0c;带着深深互联网公司的烙印&#xff1b;一般人&#xff0c;还真理解不了——1.欢快的脱单局-公关市场的联谊局&#xff0c;最后都会以互加微信、交换名片收场&#xff1b;目的是拓宽人脉&…

[Java聊天室server]实战之二 监听类

前言 学习不论什么一个稍有难度的技术&#xff0c;要对其有充分理性的分析&#xff0c;之后果断做出决定---->也就是人们常说的“多谋善断"&#xff1b;本系列尽管涉及的是socket相关的知识&#xff0c;但学习之前&#xff0c;更想和广大程序猿分享的是一种心境&#x…

【每日面试】2021美团优选Java一二面面经

作者&#xff1a;xin2801 链接&#xff1a;https://www.nowcoder.com/discuss/761529?source_iddiscuss_experience_nctrack&channel-1 来源&#xff1a;牛客网 ——9月26日 一面 55分钟 1.自我介绍 2.实习项目 &#xff08;讨论10-15分钟&#xff09; 3.Java集合类八股 …