本章響應小夥伴的反饋,除了演算法自動畫連線線(仍需最佳化完善),實現了可以手動繪製直線、折線連線線功能。
請大家動動小手,給我一個免費的 Star 吧~
大家如果發現了 Bug,歡迎來提 Issue 喲~
github原始碼
gitee原始碼
示例地址
模式切換
前置工作
連線線 模式種類
// src/Render/types.ts
export enum LinkType {
'auto' = 'auto',
'straight' = 'straight', // 直線
'manual' = 'manual' // 手動折線
}
連線線 模式狀態
// src/Render/draws/LinkDraw.ts
// 連線線(臨時)
export interface LinkDrawState {
// 略
linkType: Types.LinkType // 連線線型別
linkManualing: boolean // 是否 正在操作拐點
}
連線線 模式切換方法
// src/Render/draws/LinkDraw.ts
/**
* 修改當前連線線型別
* @param linkType Types.LinkType
*/
changeLinkType(linkType: Types.LinkType) {
this.state.linkType = linkType
this.render.config?.on?.linkTypeChange?.(this.state.linkType)
}
連線線 模式切換按鈕
<!-- src/App.vue -->
<button @click="onLinkTypeChange(Types.LinkType.auto)"
:disabled="currentLinkType === Types.LinkType.auto">連線線:自動</button>
<button @click="onLinkTypeChange(Types.LinkType.straight)"
:disabled="currentLinkType === Types.LinkType.straight">連線線:直線</button>
<button @click="onLinkTypeChange(Types.LinkType.manual)"
:disabled="currentLinkType === Types.LinkType.manual">連線線:手動</button>
連線線 模式切換事件
// src/App.vue
const currentLinkType = ref(Types.LinkType.auto)
function onLinkTypeChange(linkType: Types.LinkType) {
(render?.draws[Draws.LinkDraw.name] as Draws.LinkDraw).changeLinkType(linkType)
}
當前 連線對(pair) 記錄當前 連線線 模式
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
// 略
// 連線點
for (const point of points) {
// 略
// 非 選擇中
if (group && !group.getAttr('selected')) {
// 略
const anchor = this.render.layer.findOne(`#${point.id}`)
if (anchor) {
// 略
circle.on('mouseup', () => {
if (this.state.linkingLine) {
// 略
// 不同連線點
if (line.circle.id() !== circle.id()) {
// 略
if (toGroup) {
// 略
if (fromPoint) {
// 略
if (toPoint) {
if (Array.isArray(fromPoint.pairs)) {
fromPoint.pairs = [
...fromPoint.pairs,
{
// 略
linkType: this.state.linkType // 記錄 連線線 型別
}
]
}
// 略
}
}
}
}
// 略
}
})
// 略
}
}
}
}
}
直線
繪製直線相對簡單,透過判斷 連線對(pair)記錄的 連線線 模式,從起點繪製一條 Line 到終點即可:
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
// 略
// 連線線
for (const pair of pairs) {
if (pair.linkType === Types.LinkType.manual) {
// 略,手動折線
} else if (pair.linkType === Types.LinkType.straight) {
// 直線
if (fromGroup && toGroup && fromPoint && toPoint) {
const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`)
const toAnchor = toGroup.findOne(`#${toPoint.id}`)
// 錨點資訊
const fromAnchorPos = this.getAnchorPos(fromAnchor)
const toAnchorPos = this.getAnchorPos(toAnchor)
const linkLine = new Konva.Line({
name: 'link-line',
// 用於刪除連線線
groupId: fromGroup.id(),
pointId: fromPoint.id,
pairId: pair.id,
linkType: pair.linkType,
points: _.flatten([
[
this.render.toStageValue(fromAnchorPos.x),
this.render.toStageValue(fromAnchorPos.y)
],
[this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)]
]),
stroke: 'red',
strokeWidth: 2
})
this.group.add(linkLine)
}
} else {
// 略,原演算法畫連線線邏輯
}
}
}
}
折線
繪製折線,先人為定義 3 種“點”: 1、連線點,就是原來就有的。 2、拐點(待拐),藍色的,從未拖動過的,一旦拖動,會新增拐點記錄。 3、拐點(已拐),綠色的,已經拖動過的,依然可以拖動,但不會新增拐點記錄。
請留意下方程式碼的註釋,關鍵:
- fromGroup 會記錄 拐點 manualPoints。
- 連線線 的繪製是從 起點 -> 拐點(們)-> 終點(linkPoints)。
- 拐點正在拖動時,繪製臨時的虛線 Line。
- 分別處理 拐點(待拐)和 拐點(已拐)兩種情況。
處理 拐點(待拐)和 拐點(已拐)主要區別是:
- 處理 拐點(待拐),遍歷 linkPoints 的時候,是成對遍歷的。
- 處理 拐點(已拐),遍歷 linkPoints 的時候,是跳過 起點 和 終點 的。
- 拖動 拐點(待拐),會新增拐點記錄。
- 拖動 拐點(已拐),不會新增拐點記錄。
// src/Render/draws/LinkDraw.ts
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
// 略
// 連線線
for (const pair of pairs) {
if (pair.linkType === Types.LinkType.manual) {
// 手動折線
if (fromGroup && toGroup && fromPoint && toPoint) {
const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`)
const toAnchor = toGroup.findOne(`#${toPoint.id}`)
// 錨點資訊
const fromAnchorPos = this.getAnchorPos(fromAnchor)
const toAnchorPos = this.getAnchorPos(toAnchor)
// 拐點(已拐)記錄
const manualPoints: Array<{ x: number; y: number }> = Array.isArray(
fromGroup.getAttr('manualPoints')
)
? fromGroup.getAttr('manualPoints')
: []
// 連線點 + 拐點
const linkPoints = [
[
this.render.toStageValue(fromAnchorPos.x),
this.render.toStageValue(fromAnchorPos.y)
],
...manualPoints.map((o) => [o.x, o.y]),
[this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)]
]
// 連線線
const linkLine = new Konva.Line({
name: 'link-line',
// 用於刪除連線線
groupId: fromGroup.id(),
pointId: fromPoint.id,
pairId: pair.id,
linkType: pair.linkType,
points: _.flatten(linkPoints),
stroke: 'red',
strokeWidth: 2
})
this.group.add(linkLine)
// 正在拖動效果
const manualingLine = new Konva.Line({
stroke: '#ff0000',
strokeWidth: 2,
points: [],
dash: [4, 4]
})
this.group.add(manualingLine)
// 拐點
// 拐點(待拐)
for (let i = 0; i < linkPoints.length - 1; i++) {
const circle = new Konva.Circle({
id: nanoid(),
pairId: pair.id,
x: (linkPoints[i][0] + linkPoints[i + 1][0]) / 2,
y: (linkPoints[i][1] + linkPoints[i + 1][1]) / 2,
radius: this.render.toStageValue(this.render.bgSize / 2),
stroke: 'rgba(0,0,255,0.1)',
strokeWidth: this.render.toStageValue(1),
name: 'link-manual-point',
// opacity: 0,
linkManualIndex: i // 當前拐點位置
})
// hover 效果
circle.on('mouseenter', () => {
circle.stroke('rgba(0,0,255,0.8)')
document.body.style.cursor = 'pointer'
})
circle.on('mouseleave', () => {
if (!circle.attrs.dragStart) {
circle.stroke('rgba(0,0,255,0.1)')
document.body.style.cursor = 'default'
}
})
// 拐點操作
circle.on('mousedown', () => {
const pos = circle.getAbsolutePosition()
// 記錄操作開始狀態
circle.setAttrs({
// 開始座標
dragStartX: pos.x,
dragStartY: pos.y,
// 正在操作
dragStart: true
})
// 標記狀態 - 正在操作拐點
this.state.linkManualing = true
})
this.render.stage.on('mousemove', () => {
if (circle.attrs.dragStart) {
// 正在操作
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 磁貼
const { pos: transformerPos } = this.render.attractTool.attract({
x: pos.x,
y: pos.y,
width: 1,
height: 1
})
// 移動拐點
circle.setAbsolutePosition(transformerPos)
// 正在拖動效果
const tempPoints = [...linkPoints]
tempPoints.splice(circle.attrs.linkManualIndex + 1, 0, [
this.render.toStageValue(transformerPos.x - stageState.x),
this.render.toStageValue(transformerPos.y - stageState.y)
])
manualingLine.points(_.flatten(tempPoints))
}
}
})
circle.on('mouseup', () => {
const pos = circle.getAbsolutePosition()
if (
Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size ||
Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size
) {
// 操作移動距離達到閾值
// stage 狀態
const stageState = this.render.getStageState()
// 記錄(插入)拐點
manualPoints.splice(circle.attrs.linkManualIndex, 0, {
x: this.render.toStageValue(pos.x - stageState.x),
y: this.render.toStageValue(pos.y - stageState.y)
})
fromGroup.setAttr('manualPoints', manualPoints)
}
// 操作結束
circle.setAttrs({
dragStart: false
})
// state 操作結束
this.state.linkManualing = false
// 銷燬
circle.destroy()
manualingLine.destroy()
// 更新歷史
this.render.updateHistory()
// 重繪
this.render.redraw()
})
this.group.add(circle)
}
// 拐點(已拐)
for (let i = 1; i < linkPoints.length - 1; i++) {
const circle = new Konva.Circle({
id: nanoid(),
pairId: pair.id,
x: linkPoints[i][0],
y: linkPoints[i][1],
radius: this.render.toStageValue(this.render.bgSize / 2),
stroke: 'rgba(0,100,0,0.1)',
strokeWidth: this.render.toStageValue(1),
name: 'link-manual-point',
// opacity: 0,
linkManualIndex: i // 當前拐點位置
})
// hover 效果
circle.on('mouseenter', () => {
circle.stroke('rgba(0,100,0,1)')
document.body.style.cursor = 'pointer'
})
circle.on('mouseleave', () => {
if (!circle.attrs.dragStart) {
circle.stroke('rgba(0,100,0,0.1)')
document.body.style.cursor = 'default'
}
})
// 拐點操作
circle.on('mousedown', () => {
const pos = circle.getAbsolutePosition()
// 記錄操作開始狀態
circle.setAttrs({
dragStartX: pos.x,
dragStartY: pos.y,
dragStart: true
})
// 標記狀態 - 正在操作拐點
this.state.linkManualing = true
})
this.render.stage.on('mousemove', () => {
if (circle.attrs.dragStart) {
// 正在操作
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 磁貼
const { pos: transformerPos } = this.render.attractTool.attract({
x: pos.x,
y: pos.y,
width: 1,
height: 1
})
// 移動拐點
circle.setAbsolutePosition(transformerPos)
// 正在拖動效果
const tempPoints = [...linkPoints]
tempPoints[circle.attrs.linkManualIndex] = [
this.render.toStageValue(transformerPos.x - stageState.x),
this.render.toStageValue(transformerPos.y - stageState.y)
]
manualingLine.points(_.flatten(tempPoints))
}
}
})
circle.on('mouseup', () => {
const pos = circle.getAbsolutePosition()
if (
Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size ||
Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size
) {
// 操作移動距離達到閾值
// stage 狀態
const stageState = this.render.getStageState()
// 記錄(更新)拐點
manualPoints[circle.attrs.linkManualIndex - 1] = {
x: this.render.toStageValue(pos.x - stageState.x),
y: this.render.toStageValue(pos.y - stageState.y)
}
fromGroup.setAttr('manualPoints', manualPoints)
}
// 操作結束
circle.setAttrs({
dragStart: false
})
// state 操作結束
this.state.linkManualing = false
// 銷燬
circle.destroy()
manualingLine.destroy()
// 更新歷史
this.render.updateHistory()
// 重繪
this.render.redraw()
})
this.group.add(circle)
}
}
} else if (pair.linkType === Types.LinkType.straight) {
// 略,直線
} else {
// 略,原演算法畫連線線邏輯
}
}
}
}
最後,關於 linkManualing 狀態,會用在 2 個地方,避免和其它互動產生衝突:
// src/Render/handlers/DragHandlers.ts
// 略
export class DragHandlers implements Types.Handler {
// 略
handlers = {
stage: {
mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
// 拐點操作中,防止異常拖動
if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {
// 略
}
},
// 略
}
}
}
// src/Render/tools/LinkTool.ts
// 略
export class LinkTool {
// 略
pointsVisible(visible: boolean, group?: Konva.Group) {
// 略
// 拐點操作中,此處不重繪
if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {
// 重繪
this.render.redraw()
}
}
// 略
}
Done!
More Stars please!勾勾手指~
原始碼
gitee原始碼
示例地址