前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线
这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
相关定义
- 连接点
记录了连接点相关信息,并不作为素材而存在,仅记录信息,即导出导入的时候,并不会出现所谓的连接点节点。
它存放在节点身上,因此导出、导入自然而然就可以持久化了。
src/Render/draws/LinkDraw.ts
// 连接点
export interface LinkDrawPoint {
id: string
groupId: string
visible: boolean
pairs: LinkDrawPair[]
x: number
y: number
}
- 连接对
一个连接点,记录从该点出发的多条连接线信息,作为连接对信息存在。
src/Render/draws/LinkDraw.ts
// 连接对
export interface LinkDrawPair {
id: string
from: {
groupId: string
pointId: string
}
to: {
groupId: string
pointId: string
}
}
- 连接点(锚点)
它是拖入素材的时候生成的真实节点,归属于所在的节点中,存在却不可见,关键作用是同步连接点真实坐标,尤其是节点发生 transform 时候,必须依赖它获得 transform 后连接点变化。
src/Render/handlers/DragOutsideHandlers.ts
// 略
drop: (e: GlobalEventHandlersEventMap['drop']) => {
// 略
const points = [
// 左
{ x: 0, y: group.height() / 2 },
// 右
{
x: group.width(),
y: group.height() / 2
},
// 上
{ x: group.width() / 2, y: 0 },
// 下
{
x: group.width() / 2,
y: group.height()
}
]
// 连接点信息
group.setAttrs({
points: points.map(
(o) =>
({
...o,
id: nanoid(),
groupId: group.id(),
visible: true,
pairs: []
}) as LinkDrawPoint
)
})
// 连接点(锚点)
for (const point of group.getAttr('points') ?? []) {
group.add(
new Konva.Circle({
name: 'link-anchor',
id: point.id,
x: point.x,
y: point.y,
radius: this.render.toStageValue(1),
stroke: 'rgba(0,0,255,1)',
strokeWidth: this.render.toStageValue(2),
visible: false
})
)
}
group.on('mouseenter', () => {
// 显示 连接点
this.render.linkTool.pointsVisible(true, group)
})
// hover 框(多选时才显示)
group.add(
new Konva.Rect({
id: 'hoverRect',
width: image.width(),
height: image.height(),
fill: 'rgba(0,255,0,0.3)',
visible: false
})
)
group.on('mouseleave', () => {
// 隐藏 连接点
this.render.linkTool.pointsVisible(false, group)
// 隐藏 hover 框
group.findOne('#hoverRect')?.visible(false)
})
// 略
}
// 略
- 连接线
根据连接点信息,绘制的线条,也不作为素材而存在,导出导入的时候,也不会出现所谓的连接点节点。不过,在导出图片、SVG和用于预览框的时候,会直接利用线条节点导出、显示。
src/Render/tools/ImportExportTool.ts
// 略
/**
* 获得显示内容
* @param withLink 是否包含线条
* @returns
*/
getView(withLink: boolean = false) {
// 复制画布
const copy = this.render.stage.clone()
// 提取 main layer 备用
const main = copy.find('#main')[0] as Konva.Layer
const cover = copy.find('#cover')[0] as Konva.Layer
// 暂时清空所有 layer
copy.removeChildren()
// 提取节点
let nodes = main.getChildren((node) => {
return !this.render.ignore(node)
})
if (withLink) {
nodes = nodes.concat(
cover.getChildren((node) => {
return node.name() === Draws.LinkDraw.name
})
)
}
// 略
}
// 略
src/Render/draws/PreviewDraw.ts
override draw() {
// 略
const main = this.render.stage.find('#main')[0] as Konva.Layer
const cover = this.render.stage.find('#cover')[0] as Konva.Layer
// 提取节点
const nodes = [
...main.getChildren((node) => {
return !this.render.ignore(node)
}),
// 补充连线
...cover.getChildren((node) => {
return node.name() === Draws.LinkDraw.name
})
]
// 略
}
- 连接线(临时)
起点鼠标按下 -> 拖动显示线条 -> 终点鼠标释放 -> 产生连接信息 LinkDrawPoint. LinkDrawPair
// 连接线(临时)
export interface LinkDrawState {
linkingLine: {
group: Konva.Group
circle: Konva.Circle
line: Konva.Line
} | null
}
代码文件
新增几个关键的代码文件:
src/Render/draws/LinkDraw.ts
根据 连接点.链接对 绘制 连接点、连接线,及其相关的事件处理
它的绘制顺序,应该放在绘制 比例尺、预览框之前。
src/Render/handlers/LinkHandlers.ts
根据 连接线(临时)信息,绘制/移除 连接线(临时)
src/Render/tools/LinkTool.ts
移除连接线,控制 连接点 的显示/隐藏
移除连接线,实际上就是移除其 连接对 信息
// 略
export class LinkTool {
// 略
pointsVisible(visible: boolean, group?: Konva.Group) {
if (group) {
this.pointsVisibleEach(visible, group)
} else {
const groups = this.render.layer.find('.asset') as Konva.Group[]
for (const group of groups) {
this.pointsVisibleEach(visible, group)
}
}
// 更新连线
this.render.draws[Draws.LinkDraw.name].draw()
// 更新预览
this.render.draws[Draws.PreviewDraw.name].draw()
}
remove(line: Konva.Line) {
const { groupId, pointId, pairId } = line.getAttrs()
if (groupId && pointId && pairId) {
const group = this.render.layer.findOne(`#${groupId}`) as Konva.Group
if (group) {
const points = (group.getAttr('points') ?? []) as LinkDrawPoint[]
const point = points.find((o) => o.id === pointId)
if (point) {
const pairIndex = (point.pairs ?? ([] as LinkDrawPair[])).findIndex(
(o) => o.id === pairId
)
if (pairIndex > -1) {
point.pairs.splice(pairIndex, 1)
group.setAttr('points', points)
// 更新连线
this.render.draws[Draws.LinkDraw.name].draw()
// 更新预览
this.render.draws[Draws.PreviewDraw.name].draw()
}
}
}
}
}
}
关键逻辑
- 绘制 连接线(临时)
src/Render/draws/LinkDraw.ts
起点鼠标按下 'mousedown' -> 略 -> 终点鼠标释放 'mouseup'
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
this.clear()
// stage 状态
const stageState = this.render.getStageState()
const groups = this.render.layer.find('.asset') as Konva.Group[]
const points = groups.reduce((ps, group) => {
return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
}, [] as LinkDrawPoint[])
const pairs = points.reduce((ps, point) => {
return ps.concat(point.pairs ? point.pairs : [])
}, [] as LinkDrawPair[])
// 略
// 连接点
for (const point of points) {
const group = groups.find((o) => o.id() === point.groupId)
// 非 选择中
if (group && !group.getAttr('selected')) {
const anchor = this.render.layer.findOne(`#${point.id}`)
if (anchor) {
const circle = new Konva.Circle({
id: point.id,
groupId: group.id(),
x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
radius: this.render.toStageValue(this.option.size),
stroke: 'rgba(255,0,0,0.2)',
strokeWidth: this.render.toStageValue(1),
name: 'link-point',
opacity: point.visible ? 1 : 0
})
// 略
circle.on('mousedown', () => {
this.render.selectionTool.selectingClear()
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 临时 连接线 画
this.state.linkingLine = {
group: group,
circle: circle,
line: new Konva.Line({
name: 'linking-line',
points: _.flatten([
[circle.x(), circle.y()],
[
this.render.toStageValue(pos.x - stageState.x),
this.render.toStageValue(pos.y - stageState.y)
]
]),
stroke: 'blue',
strokeWidth: 1
})
}
this.layer.add(this.state.linkingLine.line)
}
})
// 略
}
}
}
}
src/Render/handlers/LinkHandlers.ts
拖动显示线条、移除 连接线(临时)
从起点到鼠标当前位置
handlers = {
stage: {
mouseup: () => {
const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
// 临时 连接线 移除
linkDrawState.linkingLine?.line.remove()
linkDrawState.linkingLine = null
},
mousemove: () => {
const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
const pos = this.render.stage.getPointerPosition()
if (pos) {
// stage 状态
const stageState = this.render.getStageState()
// 临时 连接线 画
if (linkDrawState.linkingLine) {
const { circle, line } = linkDrawState.linkingLine
line.points(
_.flatten([
[circle.x(), circle.y()],
[
this.render.toStageValue(pos.x - stageState.x),
this.render.toStageValue(pos.y - stageState.y)
]
])
)
}
}
}
}
}
- 产生连接信息
src/Render/draws/LinkDraw.ts
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
this.clear()
// stage 状态
const stageState = this.render.getStageState()
const groups = this.render.layer.find('.asset') as Konva.Group[]
const points = groups.reduce((ps, group) => {
return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
}, [] as LinkDrawPoint[])
const pairs = points.reduce((ps, point) => {
return ps.concat(point.pairs ? point.pairs : [])
}, [] as LinkDrawPair[])
// 略
// 连接点
for (const point of points) {
const group = groups.find((o) => o.id() === point.groupId)
// 非 选择中
if (group && !group.getAttr('selected')) {
const anchor = this.render.layer.findOne(`#${point.id}`)
if (anchor) {
const circle = new Konva.Circle({
id: point.id,
groupId: group.id(),
x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
radius: this.render.toStageValue(this.option.size),
stroke: 'rgba(255,0,0,0.2)',
strokeWidth: this.render.toStageValue(1),
name: 'link-point',
opacity: point.visible ? 1 : 0
})
// 略
circle.on('mouseup', () => {
if (this.state.linkingLine) {
const line = this.state.linkingLine
// 不同连接点
if (line.circle.id() !== circle.id()) {
const toGroup = groups.find((o) => o.id() === circle.getAttr('groupId'))
if (toGroup) {
const fromPoints = (
Array.isArray(line.group.getAttr('points')) ? line.group.getAttr('points') : []
) as LinkDrawPoint[]
const fromPoint = fromPoints.find((o) => o.id === line.circle.id())
if (fromPoint) {
const toPoints = (
Array.isArray(toGroup.getAttr('points')) ? toGroup.getAttr('points') : []
) as LinkDrawPoint[]
const toPoint = toPoints.find((o) => o.id === circle.id())
if (toPoint) {
if (Array.isArray(fromPoint.pairs)) {
fromPoint.pairs = [
...fromPoint.pairs,
{
id: nanoid(),
from: {
groupId: line.group.id(),
pointId: line.circle.id()
},
to: {
groupId: circle.getAttr('groupId'),
pointId: circle.id()
}
}
]
}
// 更新历史
this.render.updateHistory()
this.draw()
// 更新预览
this.render.draws[Draws.PreviewDraw.name].draw()
}
}
}
}
// 临时 连接线 移除
this.state.linkingLine?.line.remove()
this.state.linkingLine = null
}
})
this.group.add(circle)
}
// 略
}
}
}
}
- 绘制 连接线
src/Render/draws/LinkDraw.ts
这里就是利用了上面提到的 连接点(锚点),通过它的 absolutePosition 获得真实位置。
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
this.clear()
// stage 状态
const stageState = this.render.getStageState()
const groups = this.render.layer.find('.asset') as Konva.Group[]
const points = groups.reduce((ps, group) => {
return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
}, [] as LinkDrawPoint[])
const pairs = points.reduce((ps, point) => {
return ps.concat(point.pairs ? point.pairs : [])
}, [] as LinkDrawPair[])
// 连接线
for (const pair of pairs) {
const fromGroup = groups.find((o) => o.id() === pair.from.groupId)
const fromPoint = points.find((o) => o.id === pair.from.pointId)
const toGroup = groups.find((o) => o.id() === pair.to.groupId)
const toPoint = points.find((o) => o.id === pair.to.pointId)
if (fromGroup && toGroup && fromPoint && toPoint) {
const fromAnchor = this.render.layer.findOne(`#${fromPoint.id}`)
const toAnchor = this.render.layer.findOne(`#${toPoint.id}`)
if (fromAnchor && toAnchor) {
const line = new Konva.Line({
name: 'link-line',
// 用于删除连接线
groupId: fromGroup.id(),
pointId: fromPoint.id,
pairId: pair.id,
//
points: _.flatten([
[
this.render.toStageValue(fromAnchor.absolutePosition().x - stageState.x),
this.render.toStageValue(fromAnchor.absolutePosition().y - stageState.y)
],
[
this.render.toStageValue(toAnchor.absolutePosition().x - stageState.x),
this.render.toStageValue(toAnchor.absolutePosition().y - stageState.y)
]
]),
stroke: 'red',
strokeWidth: 2
})
this.group.add(line)
// 连接线 hover 效果
line.on('mouseenter', () => {
line.stroke('rgba(255,0,0,0.6)')
document.body.style.cursor = 'pointer'
})
line.on('mouseleave', () => {
line.stroke('red')
document.body.style.cursor = 'default'
})
}
}
}
// 略
}
}
- 绘制 连接点
src/Render/draws/LinkDraw.ts
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
// 略
override draw() {
this.clear()
// stage 状态
const stageState = this.render.getStageState()
const groups = this.render.layer.find('.asset') as Konva.Group[]
const points = groups.reduce((ps, group) => {
return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
}, [] as LinkDrawPoint[])
const pairs = points.reduce((ps, point) => {
return ps.concat(point.pairs ? point.pairs : [])
}, [] as LinkDrawPair[])
// 略
// 连接点
for (const point of points) {
const group = groups.find((o) => o.id() === point.groupId)
// 非 选择中
if (group && !group.getAttr('selected')) {
const anchor = this.render.layer.findOne(`#${point.id}`)
if (anchor) {
const circle = new Konva.Circle({
id: point.id,
groupId: group.id(),
x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
radius: this.render.toStageValue(this.option.size),
stroke: 'rgba(255,0,0,0.2)',
strokeWidth: this.render.toStageValue(1),
name: 'link-point',
opacity: point.visible ? 1 : 0
})
// hover 效果
circle.on('mouseenter', () => {
circle.stroke('rgba(255,0,0,0.5)')
circle.opacity(1)
document.body.style.cursor = 'pointer'
})
circle.on('mouseleave', () => {
circle.stroke('rgba(255,0,0,0.2)')
circle.opacity(0)
document.body.style.cursor = 'default'
})
// 略
}
}
}
}
- 复制
有几个关键:
- 更新 id,包括:节点、连接点、锚点、连接对
- 重新绑定相关事件
src/Render/tools/CopyTool.ts
// 略
export class CopyTool {
// 略
/**
* 复制粘贴
* @param nodes 节点数组
* @param skip 跳过检查
* @returns 复制的元素
*/
copy(nodes: Konva.Node[]) {
const clones: Konva.Group[] = []
for (const node of nodes) {
if (node instanceof Konva.Transformer) {
// 复制已选择
const backup = [...this.render.selectionTool.selectingNodes]
this.render.selectionTool.selectingClear()
this.copy(backup)
return
} else {
// 复制未选择(先记录,后处理)
clones.push(node.clone())
}
}
// 处理克隆节点
// 新旧 id 映射
const groupIdChanges: { [index: string]: string } = {}
const pointIdChanges: { [index: string]: string } = {}
// 新 id、新事件
for (const copy of clones) {
const gid = nanoid()
groupIdChanges[copy.id()] = gid
copy.id(gid)
const pointsClone = _.cloneDeep(copy.getAttr('points') ?? [])
copy.setAttr('points', pointsClone)
for (const point of pointsClone) {
const pid = nanoid()
pointIdChanges[point.id] = pid
const anchor = copy.findOne(`#${point.id}`)
anchor?.id(pid)
point.id = pid
point.groupId = copy.id()
point.visible = false
}
copy.off('mouseenter')
copy.on('mouseenter', () => {
// 显示 连接点
this.render.linkTool.pointsVisible(true, copy)
})
copy.off('mouseleave')
copy.on('mouseleave', () => {
// 隐藏 连接点
this.render.linkTool.pointsVisible(false, copy)
// 隐藏 hover 框
copy.findOne('#hoverRect')?.visible(false)
})
// 使新节点产生偏移
copy.setAttrs({
x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount
})
}
// pairs 新 id
for (const copy of clones) {
const points = copy.getAttr('points') ?? []
for (const point of points) {
for (const pair of point.pairs) {
// id 换新
pair.id = nanoid()
pair.from.groupId = groupIdChanges[pair.from.groupId]
pair.from.pointId = pointIdChanges[pair.from.pointId]
pair.to.groupId = groupIdChanges[pair.to.groupId]
pair.to.pointId = pointIdChanges[pair.to.pointId]
}
}
}
// 略
}
}
接下来,计划实现下面这些功能:
- 连接线 - 折线(头疼)
- 等等。。。
More Stars please!勾勾手指~
前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线的更多相关文章
- 惊闻企业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)
上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...
- 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器
企业管理软件包含一些公共的组件,这些基础的组件在每个新项目立项阶段就必须考虑.核心的稳定不变功能,方便系统开发与维护,也为系统二次开发提供了诸多便利.比如通用权限管理系统,通用附件管理,通用查询等组件 ...
- F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)
Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...
- 纯JS Web在线可拖拽的流程设计器
F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...
随机推荐
- 重新整理.net core 计1400篇[七] (.net core 中的依赖注入)
前言 请阅读第六篇,对于理解.net core 中的依赖注入很关键. 和我们上一篇不同的是,.net core服务注入保存在IServiceCollection 中,而将集合创建的依赖注入容器体现为I ...
- 老夫当年手写的js动画库
前言 当年我学习js的时候,那时候学生时代不知道有jquery,所以手写了一些东西,留下的不多作为回忆. 正文 ``` javascript window.onload = function () { ...
- 低成本FPGA的MIPI测试GOWIN和LATTICE CROSSLINK
本次实验MIPI屏,2.0寸,分辨率是240*320 RGB888, 接口如下: 接上IO就是RST和MIPI的时钟和数据接口,另外就是电源和地. 一:GOWIN的测试方案 Gowin的案例中,首先是 ...
- linux中nginx的https证书过期替换
linux中nginx的https证书过期替换 工作记录,不然老是忘 一般提示这个就说明过期了 首先把新的证书换上去,最好和之前的文件名字一样,这样就不用改配置文件了 路径就自己找了需要,不过一般挺好 ...
- 力扣627(MySQL)-变更性别(简单)
题目: Salary 表: 请你编写一个 SQL 查询来交换所有的 'f' 和 'm' (即,将所有 'f' 变为 'm' ,反之亦然),仅使用 单个 update 语句 ,且不产生中间临时表. 注意 ...
- 表格存储 SQL 查询多元索引
简介: 多元索引是表格存储产品中一个重要的功能,多元索引使用倒排索引技术为表格存储提供了非主键列上的快速检索功能,另外也提供了统计聚合功能.表格存储近期开放了SQL查询功能,SQL引擎默认从原始表格 ...
- [GPT] php查询mongo,触发了 operation exceeded time limit
"operation exceeded time limit"错误通常意味着查询所需的时间超过了MongoDB实例配置的操作超时限制. 这可以是由于查询需要处理大量数据或没有正 ...
- [GPT] 数据分析工具可以使用机器学习技术来预测未来趋势和提供数据可视化?
数据分析工具使用机器学习技术来预测未来趋势和提供数据可视化是靠谱的. 机器学习算法可以通过对历史数据的学习来发现数据中的模式和趋势,并利用这些模式和趋势来预测未来的趋势.这种方法已经被广泛应用于许 ...
- 2019-2-21-PowerShell-通过-WMI-获取补丁
title author date CreateTime categories PowerShell 通过 WMI 获取补丁 lindexi 2019-02-21 20:39:51 +0800 201 ...
- 【学习笔记】Python 使用 matplotlib 画图
目录 安装 中文显示 折线图.点线图 柱状图.堆积柱状图 坐标轴断点 参考资料 本文将介绍如何使用 Python 的 matplotlib 库画图,记录一些常用的画图 demo 代码 安装 # 建议先 ...