前端使用 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三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
随机推荐
- Python多线程、多进程编程
1 简介 参考:https://www.bilibili.com/video/BV1bK411A7tV?spm_id_from=333.999.0.0 python线程池ThreadPoolExecu ...
- LeetCode 347. Top K Frequent Elements 前 K 个高频元素 (Java)
题目: Given a non-empty array of integers, return the k most frequent elements. Example 1: Input: nums ...
- kali linux主题美化
Kali 主题美化 先放张安装了主题的图片: 执行下面命令下载主题文件: git clone https://github.com/daniruiz/flat-remix-gtk.git git cl ...
- WIN11 WINDOWS 11 INTEL 12-13代大小核默认调度策略
WIN11 WINDOWS 11 INTEL 12-13代大小核默认调度策略 生效的异类策略:使用异类策略0. 异类线程调度策略:自动. 异类短运行线程调度策略:自动.
- work03
第一题: 1.定义一个包含十个元素的数组.数组元素自己给出 2.遍历打印出数组元素 3.求出数组当中的最小值打印出来 4.求出数组当中的最大值打印出来 5,求数组当中 第二大 值 第二题: 1.定义一 ...
- CentOS7学习笔记(四) 系统运行级别
什么是运行级别 在CentOS系统中包含七种运行级别,例如命令行或图形化界面就是最常用的运行级别 运行级别的两种表示方式及作用 运行级别 运行级别 作用说明 0 poweroff.target 关机 ...
- spring的问题-能耗、学习曲线
说实话,在过去将近20年中,spring对于it行业的帮助还是很大的,尤其是信息系统建设方面. 但在我看来,spring的发展也许进入了一个困局. 开始的时候,spring的确是一个还是算小巧的工具, ...
- JAVA-poi导出excel到http响应流
导出结果为excel是相对常见的业务需求,大部分情况下只需要导出简单的格式即可,所以有许多可以采用的方案.有些方案还是很容易实现的. 一.可用的解决方案 目前可以有几类解决方案: 字处理企业提供的解决 ...
- linux下后台运行程序
文章目录 背景 nohup命令 setsid命令 pm2 背景 后台运行程序的时候,如果退出当前的终端(session),你运行的所有程序(包括后台程序),都将被关闭. 原因是:你运行的程序都是你的终 ...
- 使用 GPU 进行 Lightmap 烘焙 - 简单 demo
作者:i_dovelemon 日期:2024-06-16 主题:Lightmap, PathTracer, Compute Shader 引言 一直以来,我都对离线 bake lightmap 操作很 ...