cocos 使用Graphics(canvas)实现带过渡jojo雷达图

前言

虽然挺简单的,但还是纪念一下第一个,也应该是最后一个用cocos开发的游戏

注意事项

  • 需要多组数据可以参考: https://www.jb51.net/article/209761.htm
  • 不明原因导致第一次改变值不会触发过渡效果

最终效果

请添加图片描述

步骤

节点结构

在这里插入图片描述

挂载脚本

注释都挺清晰,感觉无需多余的解释

import { _decorator, Color, Component, EventTouch, Graphics, Label, Node, tween, UITransform, v2, v3, Vec2 } from 'cc'
import NetworkManager from '../global/NetworkManager'
import { createUINode, isEmpty } from '../utils'
import DataManager from '../global/DataManager'
import EventManager from '../global/EventManager'
import { EventEnum } from '../enum'
import { IActor, ISkill } from '../common'
const { ccclass, property } = _decorator

/**
 * 雷达图数据
 */
export interface RadarChartData {
  /** 数值 */
  values: string[]

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

  /** 线的颜色 */
  lineColor?: Color

  /** 填充的颜色 */
  fillColor?: Color
}

const radius = 150
const scale = 6 //刻度数
const axes = 6 //轴线数
const duration = 0.1

const defaultOptions = {
  lineWidth: 5,
  lineColor: new Color(255, 255, 255, 255),
  fillColor: new Color(222, 226, 230, 150),
}

// 参考 https://www.jb51.net/article/209761.htm
@ccclass('RadarChart')
export class RadarChart extends Component {
  graphics: Graphics
  angles: number[] = []
  valueItems: Label[] = []

  data: RadarChartData
  keepUpdating: boolean = false
  coords: Vec2[] = []
  currentCoords: Vec2[] = []

  protected onDestroy(): void {
    EventManager.Instance.off(EventEnum.renderChart, this.renderChart, this)
  }

  protected onLoad(): void {
    EventManager.Instance.on(EventEnum.renderChart, this.renderChart, this)
  }

  protected start(): void {
    this.graphics = this.node.getComponent(Graphics)

    this.currentCoords = new Array(axes).fill(v2(0, 0))

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

    // 非绘制部分
    const texts = ['破坏力', '成长性', '精密度', '持久力', '射程', '速度']
    const valueName = this.node.getChildByName('ValueName')
    for (let j = 0; j < this.angles.length; j++) {
      let angle = this.angles[j]
      const radian = (Math.PI / 180) * angle
      // 设置文本位置在圆的外围,稍微远一点
      const textPos = v3((radius + 40) * Math.cos(radian), (radius + 40) * Math.sin(radian))

      const valueNameItem = createUINode()
      valueNameItem.setPosition(textPos)
      valueNameItem.setParent(valueName)
      const label = valueNameItem.addComponent(Label)
      label.string = texts[j]
      label.fontSize = 20
      label.lineHeight = 20
      label.useSystemFont = false

      label.font = DataManager.Instance.fontFamily

      // 旋转文本使其朝向圆心
      angle -= 90
      if (j >= 2 && j <= 4) {
        angle += 180
      }
      valueNameItem.angle = angle
    }

    this.drawBase()

    // 破坏力 成长性 精密度 持久力 射程 速度
    // this.draw({ values: ['E', 'E', 'E', 'E', 'E', 'E'] })
  }

  protected update(dt: number): void {
    if (!this.keepUpdating) return
    // 绘制当前数据
    this.draw(this.data)
  }

  renderChart(actor: IActor, target: string) {
    // 取'Graphics1'为玩家
    if (target === this.node.name) {
      let attack = 0
      let grow = 0 //成长性 受持续能力、特殊能力影响
      let precision = 0 //精密度  受穿刺攻击、多范围攻击、破甲影响
      let defense = 0 //持久力  受防御力影响
      let range = 0 //射程  远程攻击数量
      let speed = 0 //速度  受闪避技能影响
      Object.keys(actor.skills).forEach((key) => {
        actor.skills[key].forEach((skill: ISkill) => {
          if (isEmpty(skill)) return
          if (skill.type.indexOf(4) !== -1) {
            grow++
            if (skill.defense) defense += skill.defense * 2 //持久力相当于加三遍
            if (skill.damage) attack += skill.damage * 2 //持久力相当于加三遍
            if (skill.pierce) precision += skill.damage * 2
            if (skill.broken) precision += skill.broken * 2
          } else if (skill.type.indexOf(5) !== -1) {
            grow++
          }

          if (skill.type.indexOf(3) !== -1) {
            speed++
          } else {
            if (skill.location) {
              if (skill.location != '0') speed++
            }
          }

          if (skill.defense) defense += skill.defense
          if (skill.damage) attack += skill.damage
          if (skill.longrang) range += 1

          if (skill.range) {
            skill.range.forEach((r) => {
              if (r.length > 1) precision += r.length - 1
            })
            if (skill.range.length >= 2) precision += skill.range.length - 1
          }
          if (skill.pierce) precision += skill.damage
          if (skill.broken) precision += skill.broken
        })
      })

      const max = {
        attack: 50,
        grow: 12,
        precision: 18,
        defense: 18,
        range: 7,
        speed: 6,
      }
      function getRating(value, maxValue) {
        const rate = value / maxValue
        if (rate >= 0.9) return 'S'
        if (rate >= 0.7) return 'A'
        if (rate >= 0.5) return 'B'
        if (rate >= 0.3) return 'C'
        if (rate >= 0.1) return 'D'
        return 'E'
      }
      // 根据比例计算所处等级
      const values = [
        getRating(attack, max.attack),
        getRating(grow, max.grow),
        getRating(precision, max.precision),
        getRating(defense, max.defense),
        getRating(range, max.range),
        getRating(speed, max.speed),
      ]

      // 计算节点坐标
      this.setValues(values)
      this.data = { values }

      //   过渡效果  --未解之谜,第一次改变没有过渡
      // 处理重复调用
      this.unscheduleAllCallbacks()
      // 打开每帧更新
      this.keepUpdating = true
      //   获取目标值
      for (let j = 0; j < axes; j++) {
        //   设置值
        const valueMap = { E: 1 / 6, D: 2 / 6, C: 3 / 6, B: 4 / 6, A: 5 / 6, S: 1 }
        let value = valueMap[values[j]]

        const length = value * radius
        const radian = (Math.PI / 180) * this.angles[j]
        const pos = v2(length * Math.cos(radian), length * Math.sin(radian))
        // this.coords.push(pos)

        tween(this.currentCoords)
          .to(duration, { [j]: v2(0, 0) })
          .to(duration, { [j]: pos })
          .start()
        // .sequence(tween().to(duration, { [j]: v2(0, 0) }), tween().to(duration, { [j]: pos }))
      }
      this.scheduleOnce(() => {
        // 关闭每帧更新
        this.keepUpdating = false
      }, duration * 2 + 0.5) // 太短了一开始不更新

      //   this.draw({ values })
    }
  }

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

    this.graphics.strokeColor = defaultOptions.lineColor
    this.graphics.fillColor = defaultOptions.fillColor
    this.graphics.lineWidth = defaultOptions.lineWidth

    this.graphics.circle(0, 0, radius)

    // 绘制轴线
    // 遍历全部最外层的刻度
    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)
    }

    //   刻度
    for (let i = 0; i < scalesSet.length; i++) {
      for (let j = 0; j < scalesSet.length; j++) {
        const angle = this.angles[j] - 90 // 使刻度垂直于轴线
        const pos = scalesSet[j]
        // 计算刻度的两个端点
        const x1 = pos[i].x + Math.cos((angle * Math.PI) / 180) * 5
        const y1 = pos[i].y + Math.sin((angle * Math.PI) / 180) * 5
        const x2 = pos[i].x - Math.cos((angle * Math.PI) / 180) * 5
        const y2 = pos[i].y - Math.sin((angle * Math.PI) / 180) * 5
        // 绘制刻度
        this.graphics.moveTo(x1, y1)
        this.graphics.lineTo(x2, y2)
      }
    }

    this.graphics.stroke()

    // 绘制外圈
    const count = 24 // 刻度数量
    const innerRadius = radius + 60 // 内圆半径
    const outerRadius = radius + 70 // 外圆半径
    this.graphics.circle(0, 0, innerRadius)
    this.graphics.circle(0, 0, outerRadius)
    this.graphics.stroke()

    // 画刻度
    this.graphics.lineWidth = 10
    for (let i = 0; i < count; i++) {
      const angle = (i * Math.PI * 2) / count
      const x1 = Math.cos(angle) * innerRadius
      const y1 = Math.sin(angle) * innerRadius
      const x2 = Math.cos(angle) * outerRadius
      const y2 = Math.sin(angle) * outerRadius

      this.graphics.moveTo(x1, y1)
      this.graphics.lineTo(x2, y2)
    }

    this.graphics.stroke()
  }

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

    // 开始绘制数据
    // 装填染料
    this.graphics.strokeColor = data.lineColor || defaultOptions.lineColor
    this.graphics.fillColor = data.fillColor || defaultOptions.fillColor
    this.graphics.lineWidth = data.lineWidth || defaultOptions.lineWidth

    // 创建线条
    this.graphics.moveTo(this.currentCoords[0].x, this.currentCoords[0].y)
    for (let j = 1; j < this.currentCoords.length; j++) {
      this.graphics.lineTo(this.currentCoords[j].x, this.currentCoords[j].y)
    }
    this.graphics.close() // 闭合线条

    // 填充包围区域
    this.graphics.fill()
    // 绘制线条
    this.graphics.stroke()
  }

  setValues(values: string[]) {
    const valueName = this.node.getChildByName('ValueName')

    // 已经存在的Label,直接修改
    if (this.valueItems.length > 0) {
      for (let j = 0; j < this.angles.length; j++) {
        this.valueItems[j].string = values[j]
      }
      return
    }

    for (let j = 0; j < this.angles.length; j++) {
      const angle = this.angles[j]
      const radian = (Math.PI / 180) * angle
      // 设置文本位置在圆的外围,稍微远一点
      const textPos = v3((radius + 20) * Math.cos(radian), (radius + 20) * Math.sin(radian))

      // 新建Label
      let valueNameItem = createUINode()
      valueNameItem.setPosition(textPos)
      valueNameItem.setParent(valueName)
      const label = valueNameItem.addComponent(Label)
      label.fontSize = 20
      label.lineHeight = 20
      this.valueItems.push(label)

      label.string = values[j] // 设置文本
    }
  }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值