前端使用 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三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
随机推荐
- 7.16考试总结(NOIP模拟17)[世界线·时间机器·weight]
车如流水马如龙,花月正春风 前言 其实,一开始 T1 是看错了题的,我以为是无向图来着,就想直接搞到每一个联通块的完全图,然后减去总边数就好了. 发现错误之后,码了个暴力,想得 40pts 来着,没想 ...
- flutter 调用环信sdk 实现即时通讯
首先下载依赖 导包 import 'package:im_flutter_sdk/im_flutter_sdk.dart';登录 import 'package:flutter/material.da ...
- LocalDateTime日期相互转换字符串
/** LocalDateTime日期相互转换字符串 * 默认的时间日期样式 */ public static final String YYYYMMDDHHMMSSS_PATTERN = " ...
- boltdb 原理
简介 介绍及简单使用:https://www.cnblogs.com/daemon365/p/17690167.html 源码地址:https://github.com/etcd-io/bbolt p ...
- electron 安装遇到的各种奇怪问题解决
在国内,electron经常遇到各种问题,导致无法安装 在安装electron官网的快速入门步骤,到npx electron-forge import的时候,遇到了各种问题 npm install - ...
- JSP四个作用域和九个对象
一.四个作用域 (1)Requset 请求作用域,就是客户端的一次请求 (2)Session 会话作用域,当用户首次访问时,产生一个新的会话,以后服务器就可以记住这个会话状态.生命周期:会话超时,或者 ...
- Apache Kylin(二)在EMR上搭建Kylin
EMR上搭建kylin 1. 启动EMR集群 根据官网说明: http://kylin.apache.org/docs21/install/kylin_aws_emr.html 启动EMR时,若是 h ...
- .NET Core WebApi接口ip限流实践
.NET Core WebApi接口ip限流实践 前言 之前一直想实现接口限流,但一直没去实现,然后刚好看到一篇文章是基于AspNetCoreRateLimit 组件的限流策略.这个组件不做多的介绍, ...
- 在Linux驱动中使用LED子系统
在Linux驱动中使用LED子系统 原文:https://blog.csdn.net/hanp_linux/article/details/79037684 前提配置device driver下面的L ...
- Android 编译系统 defconfig文件的确定
Android 编译系统 defconfig文件的确定 背景 经常在驱动改动的时候,同时改动2个文件,才知道他们分别对应不同的编译结果. 路径 对应版本 kernel/msm-4.4/arch/arm ...