当前位置: 首页 > news >正文

前端使用 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)- 连接线 - 直线、折线

本章响应小伙伴的反馈&#xff0c;除了算法自动画连接线&#xff08;仍需优化完善&#xff09;&#xff0c;实现了可以手动绘制直线、折线连接线功能。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug&#xff0c;欢迎来提 Issue 哟~ github源码 gitee…...

C#:通用方法总结—第15集

大家好&#xff0c;今天继续分享我们的通用方法系列。 下面是今天的通用方法&#xff1a; &#xff08;1&#xff09;这个通用方法为用文件流写数据 /// <summary> /// 用文件流写数据 /// </summary> /// <param name"data"></param> //…...

LoadRunner12 添加事务并添加检查点

1、先要添加事务开始函数lr_start_transaction("登陆事务");&#xff0c;在接口上方右击点击-插入-开始事务。输入事务名称&#xff1b; 2、在某个接口想法 右击点击-插入-结束事务&#xff0c;输入事务名称&#xff0c;与开始事务名称要保持一致&#xff0c;lr_end_…...

python中的文件

绝对路径和相对路径 一般情况下绝对路径就是从根目录开始描述的路径 相对路径就是相对于当前目录 . 没错,就是一个点,表示的是当前文件夹;.. 两个点表示的是上一层文件夹 os模块与os.path os 和 os.path 是两个非常重要的标准库模块,它们分别用于操作系统相关的功能操…...

Powerdesigner连接mysql数据库,逆向工程生成ER图 (保姆级教程:下载->连接->配置)看这一篇就够了

一、下载powerdesigner 下载的教程请看如下链接&#xff0c;我太懒了&#xff0c;直接借鉴&#xff01; 把别大佬的博客搬过来了嘿嘿~我真聪明&#xff01;ㄟ( ▔, ▔ )ㄏ 操作到完成汉化就好&#xff01;&#xff01;第5步不看了&#xff0c;别按那个走&#xff0c;因为新手…...

商家转账到零钱分销返佣申请方案及驳回处理办法

分销返佣场景是商家申请最多的场景&#xff0c;因而申请被驳回也是最多的&#xff0c;根据我们上万次成功开通商家转账到零钱的经验&#xff0c;当商家转账到零钱的分销返佣场景被驳回时&#xff0c;按照以下步骤&#xff0c;商家都可以快速过审&#xff1a; 一、分析驳回原因 …...

荟萃科技:国外问卷调查有没有实时更新的题库?

有的&#xff0c;口子查和渠道查都是。 口子查的题目都是国外的公司发放在网络上&#xff0c;都是实时发布&#xff0c;所以我们需要去国外的各大社交平台做题。 这些题目不是集中的&#xff0c;而是散布在网站里面&#xff0c;需要我们去找&#xff0c;都是老外上班实时发放…...

【课程总结】Day18:Seq2Seq的深入了解

前言 在上一章【课程总结】Day17&#xff08;下&#xff09;&#xff1a;初始Seq2Seq模型中&#xff0c;我们初步了解了Seq2Seq模型的基本情况及代码运行效果&#xff0c;本章内容将深入了解Seq2Seq模型的代码&#xff0c;梳理代码的框架图、各部分组成部分以及运行流程。 框…...

C++利用开发人员命令提示工具查看对象模型

1.跳转文件路径 cd 具体路径 2.输入c1 /d1 reportSingleClassLayout类名 文件名 操作示例如下图&#xff1a;...

白骑士的PyCharm教学高级篇 3.4 服务器部署与配置

系列目录 上一篇&#xff1a;白骑士的PyCharm教学高级篇 3.3 Web开发支持 在开发完成后&#xff0c;将代码部署到服务器上是一个关键步骤。PyCharm不仅提供了强大的本地开发支持&#xff0c;还为远程服务器配置与部署、自动化部署流程提供了便捷的工具和功能。本文将详细介绍如…...

数据库管理-第226期 内存至超线程(20240805)

数据库管理226期 2024-08-05 数据库管理-第226期 内存至超线程&#xff08;20240805&#xff09;1 CPU内缓存结构2 缓存与内存3 单核单线程4 超线程5 超线程的利弊总结 数据库管理-第226期 内存至超线程&#xff08;20240805&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xf…...

Django学习-数据迁移与数据导入导出

文章目录 一、数据迁移二、数据导入导出1. 数据导出2. 数据导入 一、数据迁移 数据迁移是将项目里定义的模型生成相应的数据表。主要的迁移指令如下&#xff1a; # 第一次生成自定义模型与django admin自带模型迁移文件&#xff0c;后续只生成新增模型迁移文件。后面加App名…...

【Nuxt】编程式导航和动态路由

编程式导航 navigateTo&#xff1a; 更多用法&#xff1a;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题 实现以字符串形式输入的简单表达式求值 得分点&#xff08;必背&#xff09; 题解 1. 初始化和变量定义 2. 获取第一个数字并存入队列 3. 遍历表达式字符串&#xff0c;处理运算符和数字 4. 初始化 count 并处理加减法运算 代码详解 &#x1f308; 嗨&#xf…...

Kylin系列-入门

Kylin系列-入门 Apache Kylin是一个开源的分布式分析引擎&#xff0c;提供Hadoop/Spark之上的SQL查询接口及多维分析&#xff08;OLAP&#xff09;能力&#xff0c;以支持超大规模数据。以下是对Kylin系列的入门介绍&#xff1a; 一、基本概念 1. 定义 Apache Kylin是由eBa…...

力扣-46.全排列

刷力扣热题–第二十六天:46.全排列 新手第二十六天 奋战敲代码&#xff0c;持之以恒&#xff0c;见证成长 1.题目简介 2.题目解答 这道题目想了会,思路比较好想,但一直没调试成功,所以就参考了力扣官网的代码,积累一下回溯算法的实现和基本实现思路,即先试探后回溯,结果在下面…...

博物馆展厅AI交互数字人,解锁创新的文化交互体验

在智能化时代&#xff0c;博物馆展厅融入AI交互数字人&#xff0c;可以为游客给予实时交互的旅游服务&#xff0c;AI交互数字人可以承担智能引导、讲解、接待、客服与导游等多重角色&#xff0c;为游客塑造崭新的旅游体验。 AI交互数字人相比传统的录屏解说相比&#xff0c;AI…...

DS18B20数字温度传感器操作解析

文章目录 引言特点工作原理引脚说明配置寄存器温度寄存器时序初始化时序写时序读时序 引言 DS18B20 是一种广泛使用的数字温度传感器&#xff0c;具有高精度和易用性。是Dallas Semiconductor公司&#xff08;现为Maxim Integrated公司&#xff09;生产的单总线数字温度传感器…...

你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉,你却浑然不觉。

据统计&#xff0c;2024年全球总体通货膨胀率预计达到5.8%&#xff0c;这意味着&#xff1a;你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉&#xff0c;你却浑然不觉。 数据来源&#xff1a;国际货币基金组织 如何跑赢通货膨胀&#xff1f; 家庭财富的积累速度&#xff0c;要…...

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

服务器硬防的应用场景都有哪些?

服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式&#xff0c;避免服务器受到各种恶意攻击和网络威胁&#xff0c;那么&#xff0c;服务器硬防通常都会应用在哪些场景当中呢&#xff1f; 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...

在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module

1、为什么要修改 CONNECT 报文&#xff1f; 多租户隔离&#xff1a;自动为接入设备追加租户前缀&#xff0c;后端按 ClientID 拆分队列。零代码鉴权&#xff1a;将入站用户名替换为 OAuth Access-Token&#xff0c;后端 Broker 统一校验。灰度发布&#xff1a;根据 IP/地理位写…...

【算法训练营Day07】字符串part1

文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接&#xff1a;344. 反转字符串 双指针法&#xff0c;两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

如何在最短时间内提升打ctf(web)的水平?

刚刚刷完2遍 bugku 的 web 题&#xff0c;前来答题。 每个人对刷题理解是不同&#xff0c;有的人是看了writeup就等于刷了&#xff0c;有的人是收藏了writeup就等于刷了&#xff0c;有的人是跟着writeup做了一遍就等于刷了&#xff0c;还有的人是独立思考做了一遍就等于刷了。…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…...