上篇文章我们实现了节点路径的绘制、缩放和平移,本篇主要是一些额外的细节。
上篇地址:# 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,
}
路径中其他内容
箭头指向
两个节点之间,生成一定数量的箭头,指向前进方向。箭头节点间隔为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 构建,支持硬件加速,非常适合开发需要流畅动画和大量图形渲染的应用,如游戏和互动式广告。