前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。
请大家动动小手,给我一个免费的 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 = linkTypethis.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.tsexport 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.tsexport 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.tsexport 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源码
示例地址
相关文章:
前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee…...
C#:通用方法总结—第15集
大家好,今天继续分享我们的通用方法系列。 下面是今天的通用方法: (1)这个通用方法为用文件流写数据 /// <summary> /// 用文件流写数据 /// </summary> /// <param name"data"></param> //…...
LoadRunner12 添加事务并添加检查点
1、先要添加事务开始函数lr_start_transaction("登陆事务");,在接口上方右击点击-插入-开始事务。输入事务名称; 2、在某个接口想法 右击点击-插入-结束事务,输入事务名称,与开始事务名称要保持一致,lr_end_…...
python中的文件
绝对路径和相对路径 一般情况下绝对路径就是从根目录开始描述的路径 相对路径就是相对于当前目录 . 没错,就是一个点,表示的是当前文件夹;.. 两个点表示的是上一层文件夹 os模块与os.path os 和 os.path 是两个非常重要的标准库模块,它们分别用于操作系统相关的功能操…...
Powerdesigner连接mysql数据库,逆向工程生成ER图 (保姆级教程:下载->连接->配置)看这一篇就够了
一、下载powerdesigner 下载的教程请看如下链接,我太懒了,直接借鉴! 把别大佬的博客搬过来了嘿嘿~我真聪明!ㄟ( ▔, ▔ )ㄏ 操作到完成汉化就好!!第5步不看了,别按那个走,因为新手…...
商家转账到零钱分销返佣申请方案及驳回处理办法
分销返佣场景是商家申请最多的场景,因而申请被驳回也是最多的,根据我们上万次成功开通商家转账到零钱的经验,当商家转账到零钱的分销返佣场景被驳回时,按照以下步骤,商家都可以快速过审: 一、分析驳回原因 …...
荟萃科技:国外问卷调查有没有实时更新的题库?
有的,口子查和渠道查都是。 口子查的题目都是国外的公司发放在网络上,都是实时发布,所以我们需要去国外的各大社交平台做题。 这些题目不是集中的,而是散布在网站里面,需要我们去找,都是老外上班实时发放…...
【课程总结】Day18:Seq2Seq的深入了解
前言 在上一章【课程总结】Day17(下):初始Seq2Seq模型中,我们初步了解了Seq2Seq模型的基本情况及代码运行效果,本章内容将深入了解Seq2Seq模型的代码,梳理代码的框架图、各部分组成部分以及运行流程。 框…...
C++利用开发人员命令提示工具查看对象模型
1.跳转文件路径 cd 具体路径 2.输入c1 /d1 reportSingleClassLayout类名 文件名 操作示例如下图:...
白骑士的PyCharm教学高级篇 3.4 服务器部署与配置
系列目录 上一篇:白骑士的PyCharm教学高级篇 3.3 Web开发支持 在开发完成后,将代码部署到服务器上是一个关键步骤。PyCharm不仅提供了强大的本地开发支持,还为远程服务器配置与部署、自动化部署流程提供了便捷的工具和功能。本文将详细介绍如…...
数据库管理-第226期 内存至超线程(20240805)
数据库管理226期 2024-08-05 数据库管理-第226期 内存至超线程(20240805)1 CPU内缓存结构2 缓存与内存3 单核单线程4 超线程5 超线程的利弊总结 数据库管理-第226期 内存至超线程(20240805) 作者:胖头鱼的鱼缸…...
Django学习-数据迁移与数据导入导出
文章目录 一、数据迁移二、数据导入导出1. 数据导出2. 数据导入 一、数据迁移 数据迁移是将项目里定义的模型生成相应的数据表。主要的迁移指令如下: # 第一次生成自定义模型与django admin自带模型迁移文件,后续只生成新增模型迁移文件。后面加App名…...
【Nuxt】编程式导航和动态路由
编程式导航 navigateTo: 更多用法:navigateTo <template><div class"app-container"><button click"goToCategory">Category</button><NuxtPage/></div> </template> <script setup&…...
14. 计算机网络HTTPS协议(二)
1. 前言 上一章节中我们主要就 HTTPS 协议的前置知识进行介绍,下面会继续介绍 HTTPS 的通信过程以及抛出一些常见问题的探讨。因为候选人准备面试的时间和精力是比较有限的,我们在学习的过程要抓住重点,如果感觉对于细节缺乏了解,可以通过维基百科和查阅 StackOverflow 等…...
【算法设计题】实现以字符串形式输入的简单表达式求值,第2题(C/C++)
目录 第2题 实现以字符串形式输入的简单表达式求值 得分点(必背) 题解 1. 初始化和变量定义 2. 获取第一个数字并存入队列 3. 遍历表达式字符串,处理运算符和数字 4. 初始化 count 并处理加减法运算 代码详解 🌈 嗨…...
Kylin系列-入门
Kylin系列-入门 Apache Kylin是一个开源的分布式分析引擎,提供Hadoop/Spark之上的SQL查询接口及多维分析(OLAP)能力,以支持超大规模数据。以下是对Kylin系列的入门介绍: 一、基本概念 1. 定义 Apache Kylin是由eBa…...
力扣-46.全排列
刷力扣热题–第二十六天:46.全排列 新手第二十六天 奋战敲代码,持之以恒,见证成长 1.题目简介 2.题目解答 这道题目想了会,思路比较好想,但一直没调试成功,所以就参考了力扣官网的代码,积累一下回溯算法的实现和基本实现思路,即先试探后回溯,结果在下面…...
博物馆展厅AI交互数字人,解锁创新的文化交互体验
在智能化时代,博物馆展厅融入AI交互数字人,可以为游客给予实时交互的旅游服务,AI交互数字人可以承担智能引导、讲解、接待、客服与导游等多重角色,为游客塑造崭新的旅游体验。 AI交互数字人相比传统的录屏解说相比,AI…...
DS18B20数字温度传感器操作解析
文章目录 引言特点工作原理引脚说明配置寄存器温度寄存器时序初始化时序写时序读时序 引言 DS18B20 是一种广泛使用的数字温度传感器,具有高精度和易用性。是Dallas Semiconductor公司(现为Maxim Integrated公司)生产的单总线数字温度传感器…...
你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉,你却浑然不觉。
据统计,2024年全球总体通货膨胀率预计达到5.8%,这意味着:你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉,你却浑然不觉。 数据来源:国际货币基金组织 如何跑赢通货膨胀? 家庭财富的积累速度,要…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
