前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
模式切换

前置工作
连接线 模式种类
// 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!勾勾手指~
前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线的更多相关文章
- 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!
		
官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...
 - (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用
		
一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...
 - Windows Phone 十二、设计器同步
		
在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...
 - WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?
		
目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...
 - ActiveReports 9 新功能:可视化查询设计器(VQD)介绍
		
在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...
 - VS2015 android 设计器不能可视化问题解决。
		
近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...
 - 可视化流程设计——流程设计器演示(基于Silverlight)
		
上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...
 - F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)
		
Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...
 - 纯JS Web在线可拖拽的流程设计器
		
F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...
 - Type Script 在流程设计器的落地实践
		
流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
 
随机推荐
- Opencv笔记(11)随机数发生器cv::RNG
			
一个随机数对象(RNG)用来产生随机数的伪随机序列.这样做的好处是你可以方便地得到多重伪随机数流.一旦随机数发生器创建,就会开始按需提供产生随机数的"服务",无论是平均分布还是正态 ...
 - Centos安装Redis(极速安装)
			
下载 从官网找到下载文件,我下载的是redis-6.0.16.tar.gz. 安装 1. 解压文件 解压文件然后,进入解压文件夹: tar -zxvf redis-6.0.16.tar.gz cd r ...
 - 从零开始的常用MySQL语句练习大全
			
先说一些废话 很多时候深入学习固然很重要,但是想要写下一篇给新手都能看得懂看的很香,并且老鸟可以查漏补缺的的练习博客,还是挺有难度, 所以今天尝试写一些关于MySQL的语句练习大全,供想要从零开始练习 ...
 - 手摸手教你把Ingress Nginx集成进Skywalking
			
背景 在微服务大行其道的今天,如何观测众多微服务.快速理清服务间的依赖.如何对服务之间的调用性能进行衡量,成了摆在大家面前的难题.对此,Skywalking应运而生,它是托管在 Apache 基金会下 ...
 - 使用Wesky.Net.OpenTools包来快速实现嵌套型结构体数据转换功能
			
今天遇到有人提到结构体和byte数组互转的问题,我就顺便拿来水一篇.这是一个冷门的问题,估计使用的人不多.既然有需求,应该就有使用场景,那就顺便整一波. 为了达到效果,结构体.复杂结构体嵌套等都能实现 ...
 - 【大道至简】NetCore3.1快速开发框架一:介绍
			
新的一年开始,祝大家2020新年快乐! 去年开源了一个基于NetCore2.2的框架,好多好基友还是比较喜欢的 github地址:https://github.com/feiyit/FytSoaCms ...
 - 记录一次 OpenStack 集群在创建虚机后无法获取 IP 的问题
			
现象 所有的组件状态都是正常工作的,DHCP 服务正常工作,在个个虚拟网络设备端口抓包,发现 OpenVSwitch 上 DHCP 包可以通行,虚机获取 DHCP Discover 请求可以正常发出, ...
 - 卷积神经网络中nn.Conv2d()和nn.MaxPool2d()以及卷积神经网络实现minist数据集分类
			
卷积神经网络中nn.Conv2d()和nn.MaxPool2d() 卷积神经网络之Pythorch实现: nn.Conv2d()就是PyTorch中的卷积模块 参数列表 参数 作用 in_channe ...
 - 3.8折钜惠,瑞芯微RK3568J国产工业评估板“限时折扣”!
 - SpringBoot快速插入Mysql 1000万条数据
			
导读 有时候为了验证系统瓶颈,需要往数据库表中插入大量数据,可以写sheel脚本插入,前几天为了插入100万条数据,走的sheel脚本(点我直达),插入速度简直无法直视,花了3小时,才插入了10万条, ...