如何在CocosCreator裡畫個炫酷的雷達圖

前言

雷達圖(Radar Chart) 也稱為網絡圖、星圖或蜘蛛網圖。

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

適用於顯示三個或更多的維度的變量。

雷達圖常用於📚數據統計或對比,對於查看哪些變量具有相似的值、變量之間是否有異常值都很有用。

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

例如王者榮耀中的對戰資料中就用到瞭:

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

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

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


預覽

先來看看效果吧~

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

以上就是如何在CocosCreator裡畫個炫酷的雷達圖的詳細內容,更多關於CocosCreator畫個雷達圖的資料請關註WalkonNet其它相關文章!

推薦閱讀:

    None Found