当前业务线,有一种终端机器,是通过配置路径、分配任务的方式,来帮助用户做一些重复性工作。之前路径的展现方式,是链式的节点展现,类似下面这种:
这种方式不能直观的展示出路径是个什么样子,现在需要根据配置的路径节点,来呈现路径的整体面貌:
虽然节点类型有十几种,但总体上可以归结为2种:直线行走节点和转向节点。
基于V8版本
基础图形
app和canvas
import { Application } from 'pixi.js'
;(async () => {
const app = new Application()
await app.init({ background: '#1099bb', resizeTo: window })
document.body.appendChild(app.canvas)
})()
app.init是异步的,所以需要使用async包裹起来,这样的话生成的app就变成了局部变量。而且通常也会把它封装成一个组件,要不然直接指明width和height:app.init({width: 600, height: 400}),要不然resizeTo指向组件根元素:
<div ref="canvasRef"></div>
const canvasRef = ref<HTMLDivElement>()
onMounted(async () => {
const app = new Application()
await app.init({ background: '#1099bb', resizeTo: canvasRef.value })
canvasRef.value!.appendChild(app.canvas)
})
圆
const circle = new Graphics().circle(0, 0, 12).fill(0xffffff).stroke({width: 1, color: '#ff0000')
app.stage.addChild(circle)
这个circle,其实就是new Graphics()的返回值,一个graphics中可以绘制多个图形:
const graphics = new Graphics()
const circle = graphics.circle(0, 0, 12).fill(0xffffff).stroke({width: 1, color: '#ff0000')
const rect = graphics.rect(10, 10, 50, 50).fill('#00ff00').stroke({width: 2, color: 0x0000ff)
// circle === rect === graphics
app.stage.appChild(graphics)
文本
const text = new Text({
text: '这是一段文本',
style: {
fontSize: 14,
fill: 0xff00ff,
},
x: 100,
y: 100,
})
text.anchor.set(0.5)
app.stage.addChild(text)
文本是有锚点的,0.5表示中心点,上面这个文本的中心点,在 (100,100) 处
折线
const points = [{x: 100, y: 100}, {x: 100, y: 200}, {x: 200, y: 200}]
const graphics = new Graphics()
graphics.moveTo(points[0].x, points[0].y)
for (let i = 1, len = points.length; i < len; i++) {
graphics.lineTo(points[i].x, points[i].y)
}
graphics.stroke({ width: 2, color: 0xff0000 })
app.stage.addChild(graphics)
折线可以通过moveTo、lineTo来绘制,也可以通过绘制多边形的边来实现:
// close需要设置为false
const path = new Graphics().poly([100, 100, 100, 200, 200, 200], false)
.stroke({ width: 2, color: 'red' })
整体思路
要求:1. 生成的节点路径,初始时处于中心位置,大小合适;2. 平移和缩放,缩放时,节点大小和线宽不变
第一个要求的实现思路:
- 根据获取到的节点数据,生成各个点的坐标
- 获取路径范围
- 和canvas大小比较,得出缩放比例
- 根据缩放比例,计算缩放后的范围
- 计算全局坐标系下的范围
- 全局范围的中心点,和canvas中心点比较,计算出差值
- 设置container的位置
- 根据缩放比例,计算出缩放后每个点的坐标
- 绘制图形
第二个要求的实现思路:
- 事件中获取全局坐标
- 转成局部坐标
- 根据滚动方向,设置缩放值(0.9 | 1 / 0.9)
- 局部坐标scale缩放
- 全局坐标 - 局部坐标,得出差值
- 最新缩放值 = 初始缩放值 * 此次设定的缩放值(0.9 | 1 / 0.9)
- 根据最新缩放值,计算缩放后每个点的坐标
- 清除上次绘制的内容,重新绘制
- 设置container的位置
节点路径
0. Container
由于我们需要满足平移操作,所以使用一个Container来作为父级,通过调整container的位置来实现所有内容的移动
const container = new Container({x: 0, y: 0})
container.x = xxx
container.y = yyy
// 或者
container.position.x = xxx
container.position.y = yyy
// 或者
container.position.set(xxx, yyy)
1. 模拟数据
路径节点大体上就2种:直线节点和转向节点,而且转向通常都是90°。我们就用90°来模拟一条50个节点的路径
- line节点有length,turn节点有angle
- 默认初始方向为0°(朝向右)
- 第一个节点位置为(0, 0)
- 由于转向节点只转向,下一个节点的位置和转向节点完全重叠,所以对下一个节点做偏移
const RADIUS = 12
type PointType = {
x: number
y: number
offsetX: number
offsetY: number
angle: number
}
const nodes = getNodes() // 模拟接口获取到的节点数据
const initialPoints = getPoints(nodes) // 初始坐标
function getPoints(nodes: ReturnType<typeof getNodes>): PointType[] {
const points: PointType[] = []
nodes.forEach((node, i) => {
const point = { x: 0, y: 0, offsetX: 0, offsetY: 0, angle: 0 }
if (i === 0) {
point.angle = node.angle ?? 0
} else {
const prevNode = nodes[i - 1]
const prevPoint = points[i - 1]
point.x = prevPoint.x + Math.round((prevNode.length ?? 0) * Math.cos((prevPoint.angle / 180) * Math.PI))
point.y = prevPoint.y - Math.round((prevNode.length ?? 0) * Math.sin((prevPoint.angle / 180) * Math.PI))
if (prevNode.type === 'turn') {
point.offsetX = Math.round(Math.min((node.length ?? 0) / 2, RADIUS) * Math.cos((prevPoint.angle / 180) * Math.PI))
point.offsetY = -Math.round(Math.min((node.length ?? 0) / 2, RADIUS) * Math.sin((prevPoint.angle / 180) * Math.PI))
}
point.angle = prevPoint.angle + (node.angle ?? 0)
}
points.push(point)
})
return points
}
// 生成不连续的转向节点
function getNodes() {
let lastNum = 0
const nodes = [...new Array(50)].map(() => {
const n = Math.random()
if ((lastNum && lastNum >= 0.4) || n < 0.4) {
lastNum = n
return {
type: 'line',
length: Math.round(Math.random() * 100) + 20,
}
}
lastNum = n
return {
type: 'turn',
angle: n < 0.7 ? 90 : -90,
}
})
return nodes
}
2. 绘制路径
function drawPath(points: PointType[]) {
const graphics = new Graphics()
graphics.moveTo(points[0].x, points[0].y)
for (let i = 1, len = points.length; i < len; i++) {
graphics.lineTo(points[i].x + points[i].offsetX, points[i].y + points[i].offsetY)
}
graphics.stroke({ width: 2, color: 0xff0000 })
container.addChild(graphics)
}
3. 绘制节点和文本
- 圆和节点序号文本,属于一个整体
- graphics的位置是以左上角为锚点(graphics本身无法设置锚点)
- 圆的中心点设置为(0, 0),文本添加到graphics中,文本anchor设置为中心点
function drawNodes(points: PointType[]) {
points.forEach((point, i) => {
drawNode({ x: point.x + point.offsetX, y: point.y + point.offsetY }, `${i + 1}`)
})
}
function drawNode(position: { x: number; y: number }, text: string) {
const graphics = new Graphics(position)
graphics.circle(0, 0, RADIUS).fill(0xffffff).stroke({ width: 2, color: 0xff0000 })
const nodeText = new Text({
text: text,
style: {
fontSize: 14,
fill: 0xff00ff,
},
})
nodeText.anchor.set(0.5)
graphics.addChild(nodeText)
graphics.cursor = 'pointer'
container.addChild(graphics)
}
4.图形
5. 调整
- 生成路径的中心点,就是地图区域的中心点
- 所有的节点都可见
- 节点大小和路径宽度不变(类似我们使用地图时,缩放后,地图上的线和圆点,大小不变),也就是只有长度变化
1. 计算范围
function getBounds(points: PointType[]) {
let minX = 0
let minY = 0
let maxX = 0
let maxY = 0
points.forEach(({ x, y }) => {
if (x < minX) {
minX = x
} else if (x > maxX) {
maxX = x
}
if (y < minY) {
minY = y
} else if (y > maxY) {
maxY = y
}
})
return {
minX,
minY,
maxX,
maxY,
}
}
2. 计算缩放比例和中心点差值
function getTransform(bounds: BoundsType) {
const { width, height } = app.screen.getBounds() // global
const { maxX, maxY, minX, minY } = bounds // local
const scale = Math.min((width - RADIUS * 2) / (maxX - minX), (height - RADIUS * 2) / (maxY - minY))
// 局部坐标转全局坐标
const globalMaxPoint = container.localTransform.apply(new Point(maxX * scale, maxY * scale))
const globalMinPoint = container.localTransform.apply(new Point(minX * scale, minY * scale))
// 中心点差值
const dx = Math.round(width / 2 - (globalMaxPoint.x + globalMinPoint.x) / 2)
const dy = Math.round(height / 2 - (globalMaxPoint.y + globalMinPoint.y) / 2)
return { scale, dx, dy }
}
3. 计算缩放后的坐标
function getScaledPoints(points: PointType[], scale = 1) {
return points.map(({ x, y, offsetX, offsetY, angle }) => ({
x: Math.round(x * scale),
y: Math.round(y * scale),
offsetX: Math.round(Math.max(Math.min(offsetX * scale, RADIUS), RADIUS * -1)), // 偏移限制
offsetY: Math.round(Math.max(Math.min(offsetY * scale, RADIUS), RADIUS * -1)),
angle,
}))
}
4. 重绘和平移
container.removeChildren().forEach(child => {
child.destroy({ children: true })
})
drawMap(points) // drawPath(points),drawNodes(points)
container.position.set(container.x + dx, container.y + dy)
5. 整体初始化渲染
let initialPoints: PointType[] = []
let scale = 1
initialRender()
function initialRender() {
initialPoints = getPoints(nodes) // 初始坐标
const bounds = getBounds(initialPoints) // 范围
const { scale: initialScale, dx, dy } = getTransform(bounds) // 转换
scale = initialScale
const points = getScaledPoints(initialPoints, scale) // 缩放后的坐标
drawMap(points) // 绘制
container.position.set(container.x + dx, container.y + dy) // 位置
}
事件
1. 平移
平移就是计算前后两个点的差值,新位置 = 当前位置+差值:
我们是对container做平移,因为所有的内容都在container中
app.stage.eventMode = 'static'
app.stage.hitArea = app.screen
let lastPoint = { x: 0, y: 0 }
let isDown = false
app.stage.on('pointerdown', e => {
isDown = true
lastPoint = { x: e.global.x, y: e.global.y }
})
app.stage.on('pointermove', e => {
if (isDown) {
const point = { x: e.global.x, y: e.global.y }
const dx = point.x - lastPoint.x
const dy = point.y - lastPoint.y
lastPoint = point
container.position.set(container.x + dx, container.y + dy)
}
})
app.stage.on('pointerup', e => {
isDown = false
})
2. 缩放
app.stage.on('wheel', e => {
// 清除上次绘制内容
container.removeChildren().forEach(child => {
child.destroy({ children: true })
})
// 全局坐标转局部坐标
const localPoint = container.localTransform.applyInverse(e.global)
const curScale = e.deltaY > 0 ? 0.9 : 1 / 0.9
// 全局坐标减去局部坐标缩放倍数,相对于左上角(0, 0)原点的偏移值
const dx = e.global.x - localPoint.x * curScale
const dy = e.global.y - localPoint.y * curScale
scale *= curScale // 最终缩放倍数
// 缩放后的坐标
const points = getScaledPoints(initialPoints, scale)
// 重新绘制
drawMap(points)
// 设置偏移,dx、dy都是相对(0, 0)点的
container.position.set(dx, dy)
})
总结
通过对坐标点的缩放和整体的平移,我们实现了一个可以展示为合适大小、缩放不失真的节点路径图。
下一篇会对路径补充一些细节,如路径上的箭头指示,完成节点、进行中节点、未执行节点不同样式,进行中节点动画效果等。