在 Cocos Creator 里画个炫酷的雷达图
前言
?️雷达图(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
动手吧
我的思路是:
- 将当前的数据保存到当前实例的
this.curDatas
中 - 接收到新的数据时,使用
cc.tween
对this.curData
的属性进行缓动 - 在
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);
}
计划通
?数值和样式都动起来了:
点击文章底部“阅读原文”即可获取完整雷达图组件。
更多分享
- 分区表的一个持续改进方案(r9笔记第53天)
- python jieba分词(结巴分词)、提取词,加载词,修改词频,定义词库
- MySQL中的NULL和空串比较 (r9笔记第52天)
- Core-periphery decomposition--核心-外围模型R代码整理
- 停止数据库没有响应的问题分析(r9笔记第51天)
- Hive——巧用transform处理复杂的字符串问题
- 停止数据库没有响应的问题分析(r9笔记第50天)
- 一个SQL语句引发的ORA-00600错误排查(一) (r9笔记第64天)
- 一个SQL语句引发的ORA-00600错误排查(二)(r9笔记第65天)
- 关于all_procedures的问题分析 (r9笔记第61天)
- 半自动化搭建Data Guard的想法和实践(一) (r9笔记第74天)
- Go语言位操作实例
- 通过Snapshot Standby来精确评估SQL性能 (r9笔记第73天)
- mongodb11天之屠龙宝刀(三)基本操作:增删改查与mysql对比
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Azure内容审查器之羞羞图审查
- Clickhouse简介和性能对比
- Clickhouse创建分布式表以及表引擎介绍
- Azure 内容审查器之文本审查
- Redis中String数据类型原理实现
- Clickhouse分布式集群搭建
- Redis过期策略以及淘汰机制
- 几行代码就可以轻松给你的程序加上进度条
- git禁止在master分支push和commit
- 记录一次mybatis缓存和事务传播行为导致ut挂的排查过程
- appium教程_3.启动appium-server
- appium教程_4.adb常用命令
- Python中的高阶概念属性:五个你应该搞明白的知识点
- 一次奇怪的http状态码改变
- Salesforce LWC学习(二十七) File Upload