請大家動動小手,給我一個免費的 Star 吧~
大家如果發現了 Bug,歡迎來提 Issue 喲~
github原始碼
gitee原始碼
示例地址
不知不覺來到第 10 章了,感覺接近尾聲了。。。
對齊線
先看效果:
這裡互動有兩個部分:
1、節點之間的對齊線
2、對齊磁貼
多選的情況下,效果是一樣的:
主要邏輯會放在控制“選擇”的程式碼檔案裡:
src\Render\handlers\SelectionHandlers.ts
這裡需要一些輔助都定義:
interface SortItem {
id?: number // 有 id 就是其他節點,否則就是 選擇目標
value: number // 左、垂直中、右的 x 座標值; 上、水平中、下的 y 座標值;
}
type SortItemPair = [SortItem, SortItem]
嘗試畫個圖說明一下上面的含義:
這裡以縱向(基於 x 座標值)為例:
這裡的 x1~x9,就是 SortItem,橫向(基於 y 座標值)同理,特別地,如果是正在拖動的目標節點,會把該節點的 _id 記錄在 SortItem 以示區分。
會存在一個處理,把一個方向上的所有 x 座標進行從小到大的排序,然後一雙一雙的遍歷,需要符合以下條件“必須分別屬於相鄰的兩個節點”的 SortItem 對,也就是 SortItemPair。
在查詢所有 SortItemPair 的同時,只會更新並記錄節點距離最短的那些 SortItemPair(可能會存在多個)。
核心邏輯程式碼:
// 磁吸邏輯
attract = (newPos: Konva.Vector2d) => {
// 對齊線清除
this.alignLinesClear()
// stage 狀態
const stageState = this.render.getStageState()
const width = this.render.transformer.width()
const height = this.render.transformer.height()
let newPosX = newPos.x
let newPosY = newPos.y
let isAttract = false
let pairX: SortItemPair | null = null
let pairY: SortItemPair | null = null
// 對齊線 磁吸邏輯
if (this.render.config.attractNode) {
// 橫向所有需要判斷對齊的 x 座標
const sortX: Array<SortItem> = []
// 縱向向所有需要判斷對齊的 y 座標
const sortY: Array<SortItem> = []
// 選擇目標所有的對齊 x
sortX.push(
{
value: this.render.toStageValue(newPos.x - stageState.x) // 左
},
{
value: this.render.toStageValue(newPos.x - stageState.x + width / 2) // 垂直中
},
{
value: this.render.toStageValue(newPos.x - stageState.x + width) // 右
}
)
// 選擇目標所有的對齊 y
sortY.push(
{
value: this.render.toStageValue(newPos.y - stageState.y) // 上
},
{
value: this.render.toStageValue(newPos.y - stageState.y + height / 2) // 水平中
},
{
value: this.render.toStageValue(newPos.y - stageState.y + height) // 下
}
)
// 拖動目標
const targetIds = this.render.selectionTool.selectingNodes.map((o) => o._id)
// 除拖動目標的其他
const otherNodes = this.render.layer.getChildren((node) => !targetIds.includes(node._id))
// 其他節點所有的 x / y 座標
for (const node of otherNodes) {
// x
sortX.push(
{
id: node._id,
value: node.x() // 左
},
{
id: node._id,
value: node.x() + node.width() / 2 // 垂直中
},
{
id: node._id,
value: node.x() + node.width() // 右
}
)
// y
sortY.push(
{
id: node._id,
value: node.y() // 上
},
{
id: node._id,
value: node.y() + node.height() / 2 // 水平中
},
{
id: node._id,
value: node.y() + node.height() // 下
}
)
}
// 排序
sortX.sort((a, b) => a.value - b.value)
sortY.sort((a, b) => a.value - b.value)
// x 最短距離
let XMin = Infinity
// x 最短距離的【對】(多個)
let pairXMin: Array<SortItemPair> = []
// y 最短距離
let YMin = Infinity
// y 最短距離的【對】(多個)
let pairYMin: Array<SortItemPair> = []
// 一對對比較距離,記錄最短距離的【對】
// 必須是 選擇目標 與 其他節點 成【對】
// 可能有多個這樣的【對】
for (let i = 0; i < sortX.length - 1; i++) {
// 相鄰兩個點,必須為 目標節點 + 非目標節點
if (
(sortX[i].id === void 0 && sortX[i + 1].id !== void 0) ||
(sortX[i].id !== void 0 && sortX[i + 1].id === void 0)
) {
// 相鄰兩個點的 x 距離
const offset = Math.abs(sortX[i].value - sortX[i + 1].value)
if (offset < XMin) {
// 更新 x 最短距離 記錄
XMin = offset
// 更新 x 最短距離的【對】 記錄
pairXMin = [[sortX[i], sortX[i + 1]]]
} else if (offset === XMin) {
// 存在多個 x 最短距離
pairXMin.push([sortX[i], sortX[i + 1]])
}
}
}
for (let i = 0; i < sortY.length - 1; i++) {
// 相鄰兩個點,必須為 目標節點 + 非目標節點
if (
(sortY[i].id === void 0 && sortY[i + 1].id !== void 0) ||
(sortY[i].id !== void 0 && sortY[i + 1].id === void 0)
) {
// 相鄰兩個點的 y 距離
const offset = Math.abs(sortY[i].value - sortY[i + 1].value)
if (offset < YMin) {
// 更新 y 最短距離 記錄
YMin = offset
// 更新 y 最短距離的【對】 記錄
pairYMin = [[sortY[i], sortY[i + 1]]]
} else if (offset === YMin) {
// 存在多個 y 最短距離
pairYMin.push([sortY[i], sortY[i + 1]])
}
}
}
// 取第一【對】,用於判斷距離是否在閾值內
if (pairXMin[0]) {
if (Math.abs(pairXMin[0][0].value - pairXMin[0][1].value) < this.render.bgSize / 2) {
pairX = pairXMin[0]
}
}
if (pairYMin[0]) {
if (Math.abs(pairYMin[0][0].value - pairYMin[0][1].value) < this.render.bgSize / 2) {
pairY = pairYMin[0]
}
}
// 優先對齊節點
// 存在 1或多個 x 最短距離 滿足閾值
if (pairX?.length === 2) {
for (const pair of pairXMin) {
// 【對】裡的那個非目標節點
const other = pair.find((o) => o.id !== void 0)
if (other) {
// x 對齊線
const line = new Konva.Line({
points: _.flatten([
[other.value, this.render.toStageValue(-stageState.y)],
[other.value, this.render.toStageValue(this.render.stage.height() - stageState.y)]
]),
stroke: 'blue',
strokeWidth: this.render.toStageValue(1),
dash: [4, 4],
listening: false
})
this.alignLines.push(line)
this.render.layerCover.add(line)
}
}
// 磁貼第一個【對】
const target = pairX.find((o) => o.id === void 0)
const other = pairX.find((o) => o.id !== void 0)
if (target && other) {
// 磁鐵座標值
newPosX = newPosX - this.render.toBoardValue(target.value - other.value)
isAttract = true
}
}
// 存在 1或多個 y 最短距離 滿足閾值
if (pairY?.length === 2) {
for (const pair of pairYMin) {
// 【對】裡的那個非目標節點
const other = pair.find((o) => o.id !== void 0)
if (other) {
// y 對齊線
const line = new Konva.Line({
points: _.flatten([
[this.render.toStageValue(-stageState.x), other.value],
[this.render.toStageValue(this.render.stage.width() - stageState.x), other.value]
]),
stroke: 'blue',
strokeWidth: this.render.toStageValue(1),
dash: [4, 4],
listening: false
})
this.alignLines.push(line)
this.render.layerCover.add(line)
}
}
// 磁貼第一個【對】
const target = pairY.find((o) => o.id === void 0)
const other = pairY.find((o) => o.id !== void 0)
if (target && other) {
// 磁鐵座標值
newPosY = newPosY - this.render.toBoardValue(target.value - other.value)
isAttract = true
}
}
}
雖然程式碼比較冗長,不過邏輯相對還是比較清晰,找到滿足條件(小於閾值,足夠近,這裡閾值為背景網格的一半大小)的 SortItemPair,就可以根據記錄的座標值大小,定義並繪製相應的線條(新增到 layerCover 中),記錄在某個變數中:
// 對齊線
alignLines: Konva.Line[] = []
// 對齊線清除
alignLinesClear() {
for (const line of this.alignLines) {
line.remove()
}
this.alignLines = []
}
在適合的時候,執行 alignLinesClear 清空失效的對齊線即可。
接下來,計劃實現下面這些功能:
- 連線線
- 等等。。。
More Stars please!勾勾手指~
原始碼
gitee原始碼
示例地址