在 Cocos Creator 里畫個炫酷的雷達圖

前言

???雷達圖(Radar Chart) 也稱為網絡圖府蛇、星圖或蜘蛛網圖矿辽。

是以從同一點開始的軸上表示的三個或更多個定量變量的二維圖表的形式顯示多元數據的圖形方法芭毙。

適用于顯示三個或更多的維度的變量嘱丢。

網上偷的圖(侵刪)

???雷達圖常用于??數據統(tǒng)計或對比,對于查看哪些變量具有相似的值伟桅、變量之間是否有異常值都很有用敞掘。

??同時在不少游戲中都有雷達圖的身影,可以很直觀地展示并對比一些數據贿讹。

例如王者榮耀中的對戰(zhàn)資料中就用到了:

網上偷的圖(侵刪)

??那么在本篇文章中渐逃,皮皮就來分享下在 Cocos Creator 中如何利用 Graphics 組件來繪制炫酷的雷達圖~

文中會對原始代碼進行一定的削減以保證閱讀體驗够掠。

雷達圖組件:https://gitee.com/ifaswind/eazax-ccc/blob/master/components/RadarChart.ts


預覽

??先來看看效果吧~

在線預覽:https://ifaswind.gitee.io/eazax-cases/?case=radarChart

??兩條數據

image

??緩動數據

image

??花里胡哨

image

??藝術就是爆炸

image

??逐漸偏離主題

image

?? 沒有人

?? 比我

?? 更懂

?? 花里胡哨

(??川老師直呼內行)


正文

??Graphics 組件

在我們正式開始制作雷達圖之前民褂,讓我們先來大概了解一下 Cocos Creator 引擎中的 Graphics 組件。

Graphics 組件繼承于 cc.RenderComponent疯潭,利用該組件我們可以實現(xiàn)畫板和表格之類的功能赊堪。

屬性(Properties)

下面是我們本次將會用到的屬性:

  • lineCap:設置或返回線條兩端的樣式(無、圓形線帽或方形線帽)
  • lineJoin:設置或返回兩條線相交時的拐角樣式(斜角竖哩、圓角或尖角)
  • lineWidth:設置或返回當前畫筆的粗細(線條的寬度)
  • strokeColor:設置或返回當前畫筆的顏色
  • fillColor:設置或返回填充用的顏色(油漆桶)

函數(Functions)

下面是我們本次將會用到的函數:

  • moveTo(x, y):抬起畫筆并移動到指定位置(不創(chuàng)建線條)
  • lineTo(x, y):放下畫筆并創(chuàng)建一條直線至指定位置
  • circle(cx, cy, r):在指定位置(圓心)畫一個圓
  • close():閉合已創(chuàng)建的線條(相當于 lineTo(起點)
  • stroke():繪制已創(chuàng)建(但未被繪制)的線條(將線條想象成默認透明的哭廉,此行為則是賦予線條顏色)
  • fill():填充當前線條包圍的區(qū)域(如果線條沒有閉合則會嘗試”模擬閉合“起點和終點)
  • clear():擦掉當前畫板上的所有東西

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

??畫網格

捋一捋

先來看看一個標準的雷達圖有啥特點:

網上偷的圖(侵刪)

??發(fā)現(xiàn)了嗎?雷達圖的基本特點如下:

  • 有 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 個或以上的刻度(不包含中心點)

image

所以我們需使用一個二維數組來保存所有刻度的坐標,從最外層(即軸線的末端)的刻度開始記錄增淹,方便我們繪制時讀却环谩:

// 創(chuàng)建一個二維數組
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);
    // 創(chuàng)建線條
    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++) {
    // 創(chuàng)建線條
    this.graphics.lineTo(scalesSet[0][i].x, scalesSet[0][i].y);
}
// 閉合當前線條(外網格線)
this.graphics.close();
填充并繪制

這里需要注意先填充顏色再繪制線條,要不然軸線和網格線就被擋住了:

// 填充線條包圍的空白區(qū)域
this.graphics.fill();
// 繪制已創(chuàng)建的線條(軸線和外網格線)
this.graphics.stroke();

??于是現(xiàn)在我們就有了這么個玩意兒:

image

繪制內網格線

當刻度大于 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++) {
            // 創(chuàng)建線條
            this.graphics.lineTo(scalesSet[i][j].x, scalesSet[i][j].y);
        }
        // 閉合當前線條(內網格線)
        this.graphics.close();
    }
    // 繪制已創(chuàng)建的線條(內網格線)
    this.graphics.stroke();
}

??就這樣我們雷達圖的底子就畫好啦:

image

??畫數據

捋一捋

編寫畫線邏輯之前成玫,先確定一下我們需要的數據結構:

  • 數值數組(必須,小數形式的比例拳喻,至少包含 3 個值)
  • 線的寬度(可選哭当,不指定則使用默認值)
  • 線的顏色(可選,不指定則使用默認值)
  • 填充的顏色(可選冗澈,不指定則使用默認值)
  • 節(jié)點的顏色(可選钦勘,不指定則使用默認值)

具體的數據結構如下(導出類型方便外部使用):

/**
 * 雷達圖數據
 */
export interface RadarChartData {

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

    /** 線的寬度 */
    lineWidth?: number;

    /** 線的顏色 */
    lineColor?: cc.Color;

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

    /** 節(jié)點的顏色 */
    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;

        // 計算節(jié)點坐標
        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);
        }

        // 創(chuàng)建線條
        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(); // 閉合線條
        
        // 填充包圍區(qū)域
        this.graphics.fill();
        // 繪制線條
        this.graphics.stroke();

        // 繪制數據節(jié)點
        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();
        }

    }
}

??到這里我們已經成功制作了一個可用的雷達圖:

image

??但是!我們的征途是星辰大海颊亮!必須加點料柴梆!

??加料不加價

動起來?

??完全靜態(tài)的雷達圖實在是太無趣太普通终惑,得想想辦法讓它動起來绍在!

??我們的雷達圖數據的數值是數組形式,想到怎么樣才能讓這些數值動起來了嗎雹有?

??別 擔 心 偿渡!

??得益于 Cocos Creator 為我們提供的 Tween 緩動系統(tǒng),讓復雜的數據動起來變得異常簡單霸奕!

??我們只需要這樣溜宽,這樣,然后那樣质帅,是不是很簡單适揉?

cc.tween 支持緩動任意對象的任意屬性

緩動系統(tǒng):http://docs.cocos.com/creator/manual/zh/scripting/tween.html

另外我在《一個全能的挖孔 Shader》中也是使用了緩動系統(tǒng)來讓挖孔動起來~

在線預覽: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);
}

計劃通

??數值和樣式都動起來了:

image

雷達圖組件:https://gitee.com/ifaswind/eazax-ccc/blob/master/components/RadarChart.ts


傳送門

微信推文版本

個人博客:菜鳥小棧

開源主頁:陳皮皮

Eazax-CCC 游戲開發(fā)腳手架

Eazax-CCC 示例在線預覽

雷達圖組件


更多分享

《為什么選擇使用 TypeScript 瓣俯?》

《高斯模糊 Shader》

《一文看懂 YAML》

《Cocos Creator 性能優(yōu)化:DrawCall》

《互聯(lián)網運營術語掃盲》


公眾號

菜鳥小棧

我是陳皮皮,這是我的個人公眾號兵怯,專注但不僅限于游戲開發(fā)彩匕、前端和后端技術記錄與分享。

每一篇原創(chuàng)都非常用心摇零,你的關注就是我原創(chuàng)的動力推掸!

Input and output.

[圖片上傳失敗...(image-86261-1597845635091)]

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驻仅,隨后出現(xiàn)的幾起案子谅畅,更是在濱河造成了極大的恐慌,老刑警劉巖噪服,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毡泻,死亡現(xiàn)場離奇詭異,居然都是意外死亡粘优,警方通過查閱死者的電腦和手機仇味,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門呻顽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丹墨,你說我怎么就攤上這事廊遍。” “怎么了贩挣?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵喉前,是天一觀的道長。 經常有香客問我王财,道長卵迂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任绒净,我火速辦了婚禮见咒,結果婚禮上,老公的妹妹穿的比我還像新娘挂疆。我一直安慰自己改览,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布囱嫩。 她就那樣靜靜地躺著恃疯,像睡著了一般漏设。 火紅的嫁衣襯著肌膚如雪墨闲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天郑口,我揣著相機與錄音鸳碧,去河邊找鬼。 笑死犬性,一個胖子當著我的面吹牛瞻离,可吹牛的內容都是我干的。 我是一名探鬼主播乒裆,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼套利,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鹤耍?” 一聲冷哼從身側響起肉迫,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稿黄,沒想到半個月后喊衫,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡杆怕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年族购,在試婚紗的時候發(fā)現(xiàn)自己被綠了壳贪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡寝杖,死狀恐怖违施,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情瑟幕,我是刑警寧澤醉拓,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站收苏,受9級特大地震影響亿卤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜鹿霸,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一排吴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧懦鼠,春花似錦钻哩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至睦袖,卻和暖如春珊肃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背馅笙。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工伦乔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人董习。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓烈和,卻偏偏與公主長得像,于是被迫代替她去往敵國和親皿淋。 傳聞我的和親對象是個殘疾皇子招刹,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345