PixiJs:巡线机器节点路径绘制(2)

622 阅读4分钟

上篇文章我们实现了节点路径的绘制、缩放和平移,本篇主要是一些额外的细节。

pixijs.gif

上篇地址:# PixiJs:巡线机器节点路径绘制

调整

锯齿和模糊

我们绘制的节点是圆形的,节点中还有序号文本。从之前的图上可以看出锯齿比较严重,文本不够清晰。app.init时,可以设置对应的参数:

app.init({background: '#f5f5f5', , antialias: true, autoDensity: true, resolution: window.devicePixelRatio, resizeTo: mapRef.value })

节点重绘

每一个节点,都是一个Graphics,重绘的时候,本质上是节点的位置发生了变化,所以我们只要把节点移动到对应的坐标上就行了:

function drawNodes(points: PointType[], type?: string) {
  if (type === 'rerender') {
    nodesGraphics.forEach((graphics, i) => {
      const point = points[i]
      graphics.position.set(point.x + point.offsetX, point.y + point.offsetY)
    })
  } else {
    points.forEach((point, i) => {
      drawNode({ x: point.x + point.offsetX, y: point.y + point.offsetY, uid: i }, `${i + 1}`)
    })
  }
}

样式

重新定义了一组颜色,用来区分不同节点状态:

enum ColorEnum {
  Black = 0x0f0f0f,
  Green = 0x1bb98c,
  Blue = 0x3a8ee6,
  Gray = 0xc7c7c7,
  LightGreen = 0xbee5e1,
  LightBlue = 0x6aa1ff,
}

路径中其他内容

pixijs1208.gif

箭头指向

两个节点之间,生成一定数量的箭头,指向前进方向。箭头节点间隔为RAIUDS + ARROW_SIZE,根据间隔和节点长度,计算出绘制箭头的个数及箭头的坐标:

function drawArrows(points: PointType[], color = ColorEnum.Black) {
  const graphics = new Graphics({ zIndex: -1 }) // 默认箭头层级-1
  for (let i = 0, len = points.length; i < len; i++) {
    if (i < len - 1) {
      const point = points[i]
      const nextPoint = points[i + 1]
      // 节点长度
      const length = Math.sqrt(Math.pow(nextPoint.x - point.x, 2) + Math.pow(nextPoint.y - point.y, 2))
      if (length > RADIUS * 2 + ARROW_SIZE) {
        const num = Math.floor(length / (RADIUS + ARROW_SIZE)) // 箭头数量
        const arrowPoints = [...new Array(num)].map((_, i) => {
          return getArrowPoints(point, (i + 1) * (RADIUS + ARROW_SIZE))
        })
        // 绘制箭头
        arrowPoints.forEach(arrow => {
          graphics.moveTo(arrow[0].x, arrow[0].y)
          graphics.lineTo(arrow[1].x, arrow[1].y)
          graphics.lineTo(arrow[2].x, arrow[2].y)
        })
      }
    }
  }
  graphics.stroke({ width: 3, color })

  return graphics
}
// 计算箭头的3个点的坐标--直角箭头
function getArrowPoints(point: PointType, distance: number) {
  const radian = (point.angle / 180) * Math.PI // 弧度
  const dx = Math.round(distance * Math.cos(radian)) // 差值
  const dy = Math.round(distance * Math.sin(radian)) // 差值
  const middlePoint = { x: point.x + dx, y: point.y - dy } // 箭头顶点
  
  const startRadian = Math.PI - (Math.PI * 0.25 - radian) // 开始点弧度
  const endRadian = startRadian + Math.PI * 0.5 // 结束点弧度
  const startDx = Math.round(ARROW_SIZE * Math.cos(startRadian))
  const startDy = Math.round(ARROW_SIZE * Math.sin(startRadian))
  const endDx = Math.round(ARROW_SIZE * Math.cos(endRadian))
  const endDy = Math.round(ARROW_SIZE * Math.sin(endRadian))
  const startPoint = { x: middlePoint.x + startDx, y: middlePoint.y - startDy }
  const endPoint = { x: middlePoint.x + endDx, y: middlePoint.y - endDy }
  
  return [startPoint, middlePoint, endPoint]
}

节点不同状态样式

不同的样式可以区分:当前行走到哪个节点,已走过的节点和未走过的节点

  • 节点:未走过--Black,已走过--Green,当前节点--Blue
  • 路径:未走过--Gray,已走过--LightGreen,当前节点--LightBlue
  • 箭头:未走过--Black,已走过--Green,当前节点--Black

可以通过点击按钮,生成不同的下标,来模拟变化

<button @click="onClick"> index {{ index !== undefined ? index + 1 : '' }}</button>

const index = ref<number>()
const onClick = () => {
  index.value = Math.floor(Math.random() * 50)
}
watch(index, (newIndex, oldIndex) => {
  // 重置上次绘制
  if (oldIndex !== undefined) {
    clearStyle(oldIndex)
  }
  // 绘制已走节点和路径以及当前节点
  if (newIndex !== undefined) {
    setStyle(newIndex)
  }
})
function clearStyle(index: number) {
  for (let i = 0; i <= index; i++) {
    // 连续设置fill,第一次起作用
    nodesGraphics[i].fill({ color: ColorEnum.Black }).stroke({ width: 2, color: '#ffffff' })
    // 当前节点
    if (i === index) {
      nodesGraphics[index].scale.set(1)
      nodesGraphics[index].zIndex = 0
    }
  }
  // 清掉上次绘制的donePath和doneArrow
  container.removeChild(donePath, doneArrow)
}
function setStyle(index: number) {
  for (let i = 0; i < index; i++) {
    nodesGraphics[i].fill(ColorEnum.Green).stroke({ width: 2, color: '#ffffff' })
  }
  // 当前节点
  nodesGraphics[index].fill(ColorEnum.Blue).stroke({ width: 2, color: '#ffffff' })
  nodesGraphics[index].scale.set(1.25) // 放大
  nodesGraphics[index].zIndex = 9 // 层级
  
  // 绘制已走路径+箭头
  donePath = drawPath(scaledPoints.slice(0, index + 1), ColorEnum.LightGreen)
  doneArrow = drawArrows(scaledPoints.slice(0, index + 1), ColorEnum.Green)
  container.addChild(donePath, doneArrow)
}

进行中节点

进行中的节点,样式、大小、层级都进行了设置,还要添加一个前进效果:

function drawAnimatePath(index?: number) {
  clearInterval(animateTimer) // 清除定时器
  animatePath.clear() // 清除绘制内容
  if (index !== undefined && scaledPoints.length > 1) {
    const animatePoints = getAnimatePoints(index) // 生成一组坐标
    if (animatePoints.length > 1) {
      let i = 0
      // 每500ms前进一格
      animateTimer = window.setInterval(() => {
        if (i === 0) {
          animatePath.clear()
          animatePath.moveTo(animatePoints[i].x, animatePoints[i].y)
          animatePath.lineTo(animatePoints[i + 1].x, animatePoints[i + 1].y)
          animatePath.stroke({ width: 14, color: ColorEnum.LightBlue })
        } else {
          animatePath.lineTo(animatePoints[i + 1].x, animatePoints[i + 1].y)
          animatePath.stroke({ width: 14, color: ColorEnum.LightBlue })
        }
        i++
        if (i >= animatePoints.length - 1) {
          i = 0
        }
      }, 500)
    }
  }
}
function getAnimatePoints(index: number) {
  const startPoint = scaledPoints[index]
  const endPoint = scaledPoints[index + 1]
  if (!endPoint) return []
  const length = Math.round(Math.sqrt((endPoint.x - startPoint.x) ** 2 + (endPoint.y - startPoint.y) ** 2))
  const stepLength = RADIUS + ARROW_SIZE // 每一格长度
  if (length < stepLength) return []
  const points = [startPoint]
  let i = 0
  let remainingLength = length
  // 计算坐标
  while (remainingLength > stepLength) {
    const nextPoint = {
      x: points[i].x + Math.round(Math.cos((startPoint.angle / 180) * Math.PI) * stepLength),
      y: points[i].y - Math.round(Math.sin((startPoint.angle / 180) * Math.PI) * stepLength),
      offsetX: 0,
      offsetY: 0,
      angle: startPoint.angle,
    }
    points.push(nextPoint)

    i++
    remainingLength -= stepLength
  }
  points.push(endPoint)

  return points
}

其他调整

绘制地图

绘制地图时,需要判断index,做相应的样式调整(缩放时)

 function drawMap(points: PointType[], type?: string) {
    // ...
    if (index.value !== undefined) {
      setStyle(index.value)
      // 当前节点路径前进效果
      drawAnimatePath(index.value)
    }
  }

节点点击

可以通过点击节点,改变index

function drawNodes(points: PointType[], type?: string) {
  // ...
  // 初始渲染时
  if (!type) {
    nodesGraphics.forEach((graphics, i) => {
      graphics.cursor = 'pointer'
      graphics.eventMode = 'static'
      graphics.on('pointerdown', e => {
        index.value = i
      })
    })
  }
}

总结

通过上面一系列处理,我们实现了一个,满足基本需求的节点路径。实际需求中,可能还包括重置位置、异常结束、不同类型的节点使用不同的图标、设置初始方向等,根据实际需求调整即可。

PixiJS 作为一个高性能的 2D 渲染库,它基于 WebGL 构建,支持硬件加速,非常适合开发需要流畅动画和大量图形渲染的应用,如游戏和互动式广告。