前端使用 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三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
随机推荐
- react 网络请求 axios
react中通过npm来安装axios扩展 cnpm i -S axios 发起请求 import React, { Component } from 'react' import axios fro ...
- 剑指Offer-47.求1+2+3+...+n(C++/Java)
题目: 求1+2+3+...+n,要求不能使用乘除法.for.while.if.else.switch.case等关键字及条件判断语句(A?B:C). 分析: 利用短路与来判断n是否大于0,从而实现递 ...
- C#.NET 微信上传电子小票
HttpWebRequest 时,不认图片的Content-Type.Content-Type 实际是有传的. 报错内容:{"code":"PARAM_ERROR&quo ...
- 别想宰我,怎么查看云厂商是否超卖?详解 cpu steal time
据说有些云厂商会超卖,宿主有 96 个核心,结果卖出去 100 多个 vCPU,如果这些虚机负载都不高,大家相安无事,如果这些虚机同时运行一些高负载的任务,相互之间就会抢占 CPU,对应用程序有较大影 ...
- jwt 加密和解密demo
jwt 加密和解密demo JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息.导入jar <dependency&g ...
- 免费的Java主流jdk发行版本有哪些?
Java的特点是百花齐放,不像c#或者go只有一家主导.oracle jdk收费了,没关系,不是只有它可用.java还有很多免费的主流的jdk发行版本,记录下来备忘. OpenJDK - 官方网站 - ...
- 颠覆传统编程,用ChatGPT十倍提升生产力
我们即将见证一个新的时代!这是最好的时代,也是最坏的时代! 需求背景 背景: 平时会编写博客,并且会把这个博客上传到github上,然后自己买一个域名挂到github上. 我平时编写的博客会有一些图片 ...
- python重拾第六天-面向对象基础
本节内容: 面向对象编程介绍 为什么要用面向对象进行开发? 面向对象的特性:封装.继承.多态 类.方法. 引子 你现在是一家游戏公司的开发人员,现在需要你开发一款叫做<人狗大战>的 ...
- 嵌入式编程的 4 种模型:轮询、中断、DMA、通道
轮询方式 对I/O设备的程序轮询的方式,是早期的计算机系统对I/O设备的一种管理方式.它定时对各种设备轮流询问一遍有无处理要求.轮流询问之后,有要求的,则加以处理.在处理I/O设备的要求之后,处理机返 ...
- Codeforces Round 894 (Div. 3) A-E cd 894 div3
A. Gift Carpet 每道题都是伸缩代码框有ac代码请不要漏掉 --------------------------题解----------------------------- 按先行便然后 ...