首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

编程知识
2024年08月01日 22:02

本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

模式切换

image

前置工作

连接线 模式种类

// 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 // 记录 连接线 类型
                          }
                        ]
                      }
                      // 略
                    }
                  }
                }
              }
              // 略
            }
          })
          // 略
        }
      }
    }
  }
}

直线

image

绘制直线相对简单,通过判断 连接对(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 {
          // 略,原算法画连接线逻辑
        }
    }
  }
}

折线

image

绘制折线,先人为定义 3 种“点”: 1、连接点,就是原来就有的。 2、拐点(待拐),蓝色的,从未拖动过的,一旦拖动,会新增拐点记录。 3、拐点(已拐),绿色的,已经拖动过的,依然可以拖动,但不会新增拐点记录。

image

请留意下方代码的注释,关键:

  • 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源码

示例地址

From:https://www.cnblogs.com/xachary/p/18337764
本文地址: http://www.shuzixingkong.net/article/682
0评论
提交 加载更多评论
其他文章 如何通过PowerShell批量修改O365用户的office phone属性值
我的博客园:https://www.cnblogs.com/CQman/ 如何通过PowerShell批量修改O365用户的office phone属性值? 需求信息: 组织中的O365用户在创建时,已手动录入了办公电话(Office phone),现在需要在办公电话前面加上统一的数字,如“0571
如何通过PowerShell批量修改O365用户的office phone属性值 如何通过PowerShell批量修改O365用户的office phone属性值 如何通过PowerShell批量修改O365用户的office phone属性值
P5665 [CSP-S2019] 划分
讲解 P5665 [CSP-S2019] 划分。 由朴素 dp 入手,先用二分优化,然后用走指针优化,之后注意到单调性,将状态数压缩,然后使用单调队列优化转移。
架构演化学习思考(3)
架构演化学习思考(3) 接上一篇我们继续对命令模式进行学习。 在这节内容中,我们聊一下经典的命令模式,还记得上一篇文章开头我们实现的简单的命令模式吗?来看代码,非常简单易解。 public interface ICommand { void Execute(); } public class Pla
架构演化学习思考(3)
运行期加载时共享库路径搜索优先级实验
目录前言实验环境目录说明单独测试不配置路径默认路径ld.so.cacheRUNPATHLD_LIBRARY_PATHRPATH优先级测试附录库文件源码主程序源码makefile脚本run_nonerun_defaultrun_ld_so_cacherun_runpathrun_ld_library_
SLF4J2.0.x与Logback1.3.x的绑定变动还是很大的,不要乱点鸳鸯谱
开心一刻 今天跟我姐聊天 我:我喜欢上了我们公司的一个女同事,她好漂亮,我心动了,怎么办 姐:喜欢一个女孩子不能只看她的外表 我:我知道,还要看她的内在嘛 姐:你想多了,还要看看自己的外表 背景介绍 在 SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 原理分析那部分,我
SLF4J2.0.x与Logback1.3.x的绑定变动还是很大的,不要乱点鸳鸯谱 SLF4J2.0.x与Logback1.3.x的绑定变动还是很大的,不要乱点鸳鸯谱 SLF4J2.0.x与Logback1.3.x的绑定变动还是很大的,不要乱点鸳鸯谱
产品、开发、测试人手一份:升级上线检查清单大全
在软件开发过程中,尤其是在准备将新功能或修复后的版本上线之前,进行详尽的自测和上线前检查是至关重要的。以下是一个从多个维度综合考量的上线升级检查清单(Checklist),旨在帮助团队确保软件质量、稳定性和安全性: 1、代码质量与构建检查 代码审查已完成 所有代码变更已通过单元测试,特别是与升级相关
还在为找开源项目发愁么?或许这个项目能帮助你
大家好,我是晓凡。 有很多小伙伴尤其是在校大学生或者想转软件开发的小伙伴,经常会问:准备找工作了,没有项目经验怎么办呢? 这时候上网找开源项目学习,就是一个获取项目经验比较靠谱的途径。 这时候又有小伙伴问了,去哪找开源项目呢? 当然是全球最大的的同性交友网站 GitHub 上找了。 这时候又有小伙伴
还在为找开源项目发愁么?或许这个项目能帮助你 还在为找开源项目发愁么?或许这个项目能帮助你 还在为找开源项目发愁么?或许这个项目能帮助你
用了组合式 (Composition) API 后代码变得更乱了,怎么办?
组合式 (Composition) API 的一大特点是“非常灵活”,但也因为非常灵活,也可能导致我们的代码变得愈发混乱,最终到达无法维护的地步。
用了组合式 (Composition) API 后代码变得更乱了,怎么办? 用了组合式 (Composition) API 后代码变得更乱了,怎么办? 用了组合式 (Composition) API 后代码变得更乱了,怎么办?