github/gitee Star 终于有几个了!

从这章开始,难度算是(或者说细节较多)升级,是不是值得更多的 Star 呢?!

继续求 Star ,希望大家多多一键三连,十分感谢大家的支持~

创作不易,Star 50 个,创作加速!

github源码

gitee源码

示例地址

选择框

准备工作

想要拖动一个元素,可以考虑使用节点的 draggable 属性。

不过,想要拖动多个元素,可以使用 transformer,官网也是简单的示例 Basic demo

按设计思路统一通过 transformer 移动/缩放所选,也意味着,元素要先选后动。

先准备一个 group、transformer、selectRect:

  // 多选器层
 groupTransformer: Konva.Group = new Konva.Group()

 // 多选器
 transformer: Konva.Transformer = new Konva.Transformer({
   shouldOverdrawWholeArea: true,
   borderDash: [4, 4],
   padding: 1,
   rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315, 360]
})

 // 选择框
 selectRect: Konva.Rect = new Konva.Rect({
   id: 'selectRect',
   fill: 'rgba(0,0,255,0.1)',
   visible: false
})

先说 transformer,设置 shouldOverdrawWholeArea 为了选择所选的空白处也能拖动;rotationSnaps 就是官方提供的 rotate 时的磁贴交互。

然后,selectRect 就是选择框,参考的就是上面提到的 Basic demo

最后,上面的 group 比较特别,它承载了上面的 transformer 和 selectRect,且置于第一章提到的 layerCover

    // 辅助层 - 顶层
   this.groupTransformer.add(this.transformer)
   this.groupTransformer.add(this.selectRect)
   this.layerCover.add(this.groupTransformer)

selectRect 不应该被“交互”,所以加个排查判断:

  // 忽略非素材
 ignore(node: Konva.Node) {
   // 素材有各自根 group
   const isGroup = node instanceof Konva.Group
   return !isGroup || node.id() === 'selectRect' || this.ignoreDraw(node)
}

选择

准备一些状态变量:

  // selectRect 拉动的开始和结束坐标
 selectRectStartX = 0
 selectRectStartY = 0
 selectRectEndX = 0
 selectRectEndY = 0
 // 是否正在使用 selectRect
 selecting = false

选择开始,处理 stage 的 mousedown 事件:

    mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
       // 略

       if (e.target === this.render.stage) {
         // 点击空白处

         // 清除选择
         // 外部也需要此操作,统一放在 selectionTool中
         // 后面会提到
         this.render.selectionTool.selectingClear()

         // 选择框
         if (e.evt.button === Types.MouseButton.左键) {
           const pos = this.render.stage.getPointerPosition()
           if (pos) {
             // 初始化状态值
             this.selectRectStartX = pos.x
             this.selectRectStartY = pos.y
             this.selectRectEndX = pos.x
             this.selectRectEndY = pos.y
          }

// 初始化大小
           this.render.selectRect.width(0)
           this.render.selectRect.height(0)

// 开始选择
           this.selecting = true
        }
      } else if (parent instanceof Konva.Transformer) {
         // transformer 点击事件交给 transformer 自己的 handler
      } else if (parent instanceof Konva.Group) {
         // 略
      }
    }

接着,处理 stage 的 mousemove 事件:

    mousemove: () => {
       // stage 状态
       const stageState = this.render.getStageState()

       // 选择框
       if (this.selecting) {
         // 选择区域中
         const pos = this.render.stage.getPointerPosition()
         if (pos) {
           // 选择移动后的坐标
           this.selectRectEndX = pos.x
           this.selectRectEndY = pos.y
        }

         // 调整【选择框】的位置和大小
         this.render.selectRect.setAttrs({
           visible: true, // 显示
           x: this.render.toStageValue(
             Math.min(this.selectRectStartX, this.selectRectEndX) - stageState.x
          ),
           y: this.render.toStageValue(
             Math.min(this.selectRectStartY, this.selectRectEndY) - stageState.y
          ),
           width: this.render.toStageValue(Math.abs(this.selectRectEndX - this.selectRectStartX)),
           height: this.render.toStageValue(Math.abs(this.selectRectEndY - this.selectRectStartY))
        })
      }
    }

稍微说一下,调整【选择框】的位置和大小,关于 toStageValue 可以看看上一章。 width 和 height 比较好理解,开始位置 和 结束位置 相减就可以得出。

x 和 y,需从 开始位置 和 结束位置 选数值小的作为【选择框】的 rect 起点,最后要扣除 stage 的视觉位移,毕竟它们是放在 stage 里面的,就是 相对位置 和 视觉位置 的转换。

结束选择,处理 stage 的 mouseup 事件:

    mouseup: () => {
       // 选择框

       // 重叠计算
       const box = this.render.selectRect.getClientRect()
       if (box.width > 0 && box.height > 0) {
         // 区域有面积

         // 获取所有图形
         const shapes = this.render.layer.getChildren((node) => {
           return !this.render.ignore(node)
        })
         
         // 提取重叠部分
         const selected = shapes.filter((shape) =>
           // 关键 api
           Konva.Util.haveIntersection(box, shape.getClientRect())
        )

         // 多选
         // 统一放在 selectionTool中,对外暴露 api
         this.render.selectionTool.select(selected)
      }

       // 重置
       this.render.selectRect.setAttrs({
         visible: false, // 隐藏
         x: 0,
         y: 0,
         width: 0,
         height: 0
      })

       // 选择区域结束
       this.selecting = false
    }

【选择框】的主要处理的事件就是这些,接着,看看关键的 selectionTool.selectingClear、selectionTool.select,直接上代码:

  // 选择节点
 select(nodes: Konva.Node[]) {
   // 选之前,清一下
   this.selectingClear()

   if (nodes.length > 0) {
     // 用于撑开 transformer
     // 如果到这一章就到此为止,是不需要selectingNodesArea 这个 group
     // 卖个关子,留着后面解释
     this.selectingNodesArea = new Konva.Group({
       visible: false,
       listening: false
    })

     // 最大zIndex
     const maxZIndex = Math.max(
       ...this.render.layer
        .getChildren((node) => {
           return !this.render.ignore(node)
        })
        .map((o) => o.zIndex())
    )

     // 记录状态
     for (const node of nodes) {
       node.setAttrs({
         nodeMousedownPos: node.position(), // 后面用于移动所选
         lastOpacity: node.opacity(), // 选中时,下面会使其变透明,记录原有的透明度
         lastZIndex: node.zIndex() // 记录原有的层次,后面暂时提升所选节点的层次
      })
    }

     // 设置透明度、提升层次、不可交互
     for (const node of nodes.sort((a, b) => a.zIndex() - b.zIndex())) {
       const copy = node.clone()

       this.selectingNodesArea.add(copy)

       node.setAttrs({
         listening: false,
         opacity: node.opacity() * 0.8,
         zIndex: maxZIndex
      })
    }

 // 选中的节点
     this.selectingNodes = nodes

     // 放进 transformer 所在的层
     this.render.groupTransformer.add(this.selectingNodesArea)

     // 选中的节点,放进 transformer
     this.render.transformer.nodes([...this.selectingNodes, this.selectingNodesArea])
  }
}
  // 清空已选
selectingClear() {
// 清空选择
this.render.transformer.nodes([]) // 移除 selectingNodesArea
this.selectingNodesArea?.remove()
this.selectingNodesArea = null // 恢复透明度、层次、可交互
for (const node of this.selectingNodes.sort(
(a, b) => a.attrs.lastZIndex - b.attrs.lastZIndex
)) {
node.setAttrs({
listening: true,
opacity: node.attrs.lastOpacity ?? 1,
zIndex: node.attrs.lastZIndex
})
} // 清空状态
for (const node of this.selectingNodes) {
node.setAttrs({
nodeMousedownPos: undefined,
lastOpacity: undefined,
lastZIndex: undefined,
selectingZIndex: undefined
})
} // 清空选择节点
this.selectingNodes = []
}

值得一提,Konva 关于 zIndex 的处理比较特别,始终从 1 到 N,意味着,改变一个节点的 zIndex,将影响其他节点的 zIndex,举个例子,假如有下面节点,数字就是对应的 zIndex:

a-1、b-2、c-3、d-4

此时我改 b 到 4(最大 zIndex),即 b-4,此时 c、d 会自动适应 zIndex,变成:

a-1、c-2、d-3、b-4

所以,上面需要两次的 this.selectingNodes.sort 处理,举个例子:

a/1、b/2、c/3、d/4,此时我选中 b 和 c

先置顶 b,即 a-1、c-2、d-3、b-4

后置顶 c,即 a-1、d-2、b-3、c-4

这样就可以保证原来 b 和 c 的相对位置的基础上,置顶 b 和 c

这样,通过【选择框】多选目标的交互就完成了。

点选

处理【未选择】节点

除了用【选择框】,也可以通过 ctrl + 点击 选择节点。

回到 stage 的 mousedown 事件处理:

	mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
const parent = e.target.getParent() if (e.target === this.render.stage) {
// 略
} else if (parent instanceof Konva.Transformer) {
// transformer 点击事件交给 transformer 自己的 handler
} else if (parent instanceof Konva.Group) {
if (e.evt.button === Types.MouseButton.左键) {
if (!this.render.ignore(parent) && !this.render.ignoreDraw(e.target)) {
if (e.evt.ctrlKey) {
// 新增多选
this.render.selectionTool.select([
...this.render.selectionTool.selectingNodes,
parent
])
} else {
// 单选
this.render.selectionTool.select([parent])
}
}
} else {
this.render.selectionTool.selectingClear()
}
}
}

这里比较简单,就是处理一下已选的数组。

处理【已选择】节点

      // 记录初始状态
mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
const anchor = this.render.transformer.getActiveAnchor()
if (!anchor) {
// 非变换
if (e.evt.ctrlKey) {
// 选择
if (this.render.selectionTool.selectingNodesArea) {
const pos = this.render.stage.getPointerPosition()
if (pos) {
const keeps: Konva.Node[] = []
const removes: Konva.Node[] = [] // 从高到低,逐个判断 已选节点 和 鼠标点击位置 是否重叠
let finded = false
for (const node of this.render.selectionTool.selectingNodes.sort(
(a, b) => b.zIndex() - a.zIndex()
)) {
if (
!finded &&
Konva.Util.haveIntersection(node.getClientRect(), {
...pos,
width: 1,
height: 1
})
) {
// 记录需要移除选择的节点
removes.unshift(node)
finded = true
} else {
keeps.unshift(node)
}
} if (removes.length > 0) {
// 取消选择
this.render.selectionTool.select(keeps)
} else {
// 从高到低,逐个判断 未选节点 和 鼠标点击位置 是否重叠
let finded = false
const adds: Konva.Node[] = []
for (const node of this.render.layer
.getChildren()
.filter((node) => !this.render.ignore(node))
.sort((a, b) => b.zIndex() - a.zIndex())) {
if (
!finded &&
Konva.Util.haveIntersection(node.getClientRect(), {
...pos,
width: 1,
height: 1
})
) {
// 记录需要增加选择的节点
adds.unshift(node)
finded = true
}
}
if (adds.length > 0) {
// 新增选择
this.render.selectionTool.select([
...this.render.selectionTool.selectingNodes,
...adds
])
}
}
}
}
} else {
// 略
}
} else {
// 略
}
}

效果:

移动节点

准备工作

相关状态变量:

  // 拖动前的位置
transformerMousedownPos: Konva.Vector2d = { x: 0, y: 0 } // 拖动偏移
groupImmediateLocOffset: Konva.Vector2d = { x: 0, y: 0 }

相关方法,处理 transformer 事件中会使用到:

  // 通过偏移量(selectingNodesArea)移动【目标节点】
selectingNodesPositionByOffset(offset: Konva.Vector2d) {
for (const node of this.render.selectionTool.selectingNodes) {
const x = node.attrs.nodeMousedownPos.x + offset.x
const y = node.attrs.nodeMousedownPos.y + offset.y
node.x(x)
node.y(y)
} const area = this.render.selectionTool.selectingNodesArea
if (area) {
area.x(area.attrs.areaMousedownPos.x + offset.x)
area.y(area.attrs.areaMousedownPos.y + offset.y)
}
} // 重置【目标节点】的 nodeMousedownPos
selectingNodesPositionReset() {
for (const node of this.render.selectionTool.selectingNodes) {
node.attrs.nodeMousedownPos.x = node.x()
node.attrs.nodeMousedownPos.y = node.y()
}
} // 重置 transformer 状态
transformerStateReset() {
// 记录 transformer pos
this.transformerMousedownPos = this.render.transformer.position()
} // 重置 selectingNodesArea 状态
selectingNodesAreaReset() {
this.render.selectionTool.selectingNodesArea?.setAttrs({
areaMousedownPos: {
x: 0,
y: 0
}
})
} // 重置
reset() {
this.transformerStateReset()
this.selectingNodesPositionReset()
this.selectingNodesAreaReset()
}

主要通过处理 transformer 的事件:

      transformend: () => {
// 变换结束 // 重置状态
this.reset()
},
//
dragstart: () => {
this.render.selectionTool.selectingNodesArea?.setAttrs({
areaMousedownPos: this.render.selectionTool.selectingNodesArea?.position()
})
},
// 拖动
dragmove: () => {
// 拖动中
this.selectingNodesPositionByOffset(this.groupImmediateLocOffset)
},
dragend: () => {
// 拖动结束 this.selectingNodesPositionByOffset(this.groupImmediateLocOffset) // 重置状态
this.reset()
}

还有这:

      // 记录初始状态
mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
const anchor = this.render.transformer.getActiveAnchor()
if (!anchor) {
// 非变换
if (e.evt.ctrlKey) {
// 略
} else {
if (this.render.selectionTool.selectingNodesArea) {
// 拖动前
// 重置状态
this.reset()
}
}
} else {
// 变换前 // 重置状态
this.reset()
}
}

还要处理 transformer 的配置 dragBoundFunc,从它获得 groupImmediateLocOffset 偏移量:

    // 拖动中
dragBoundFunc: (pos: Konva.Vector2d) => {
// transform pos 偏移
const transformPosOffsetX = pos.x - this.transformerMousedownPos.x
const transformPosOffsetY = pos.y - this.transformerMousedownPos.y // group loc 偏移
this.groupImmediateLocOffset = {
x: this.render.toStageValue(transformPosOffsetX),
y: this.render.toStageValue(transformPosOffsetY)
} return pos // 接着到 dragmove 事件处理
}

接下来,计划实现下面这些功能:

  • 放大缩小所选的“磁贴效果”(基于网格)
  • 拖动所选的“磁贴效果”(基于网格)
  • 节点层次单个、批量调整
  • 键盘复制、粘贴
  • 等等。。。

是不是更加有趣呢?是不是值得更多的 Star 呢?勾勾手指~

源码

gitee源码

示例地址

前端使用 Konva 实现可视化设计器(3)的更多相关文章

  1. 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!

    官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...

  2. (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用

    一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...

  3. Windows Phone 十二、设计器同步

    在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...

  4. WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?

    目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...

  5. ActiveReports 9 新功能:可视化查询设计器(VQD)介绍

    在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...

  6. VS2015 android 设计器不能可视化问题解决。

    近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...

  7. 可视化流程设计——流程设计器演示(基于Silverlight)

    上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...

  8. Type Script 在流程设计器的落地实践

    流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...

  9. .net erp(办公oa)开发平台架构概要说明之表单设计器

    背景:搭建一个适合公司erp业务的开发平台.   架构概要图: 表单设计开发部署示例图    表单设计开发部署示例说明1)每个开发人员可以自己部署表单设计至本地一份(当然也可以共用一套开发环境,但是如 ...

  10. 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器

    企业管理软件包含一些公共的组件,这些基础的组件在每个新项目立项阶段就必须考虑.核心的稳定不变功能,方便系统开发与维护,也为系统二次开发提供了诸多便利.比如通用权限管理系统,通用附件管理,通用查询等组件 ...

随机推荐

  1. 【Azure 应用服务】 在App Service中无法上传证书[Private Key Certificates (.pfx)],导入Azure Key Vault中的证书也无法成功

    问题描述 在App Service的TLS/SSL settings页面,切换到Private Key Certificates (.pfx),通过Import Key Vault Certifica ...

  2. 【Azure 应用服务】用App Service部署运行 Vue.js 编写的项目,应该怎么部署运行呢?

    问题描述 用App Service部署运行 Vue.js 编写的项目,应该怎么部署运行呢? 问题解答 VUE通常是运行在客户端侧的JS框架. App Service 在这种场景中是以静态文件的形式提供 ...

  3. 【Azure 应用服务】如何查看App Service中的私网IP地址?

    问题描述 在使用App Service服务时,可以通过Azure 门户中的属性功能查看出站IP列表. 如果把App Service与虚拟网络(VNET)集成后,它就可以直接访问虚拟网络内部资源,那么如 ...

  4. 【Azure Redis 缓存】Redis性能指标之Server Load

    Server Load描述 在Redis的官方介绍中,Server Load指标是Redis 服务器忙于处理消息并且非空闲等待消息的周期百分比. 如果此计数器达到 100,则意味着 Redis 服务器 ...

  5. 【Azure Developer】使用 Powershell az account get-access-token 命令获取Access Token (使用用户名+密码)

    问题描述 在上篇的文章中,我们使用了JAVA SDK,根据用户名和密码来获取Azure AD的Access Token,这节,我们将使用Powershell az 命令来获取Access Token. ...

  6. 连接 AI,NebulaGraph Python ORM 项目 Carina 简化 Web 开发

    作者:Steam & Hao 本文整理自社区第 7 期会议中 13'21″ 到 44'11″ 的 Python ORM 的分享,视频见 https://www.bilibili.com/vid ...

  7. 火柴 基于everything的搜索软件 软件推荐 Ctrl+Ctrl 显示 tab转换 本机搜索和网络搜索

    https://www.huochaipro.com/

  8. 没有 Release 文件的解决方法

    https://blog.csdn.net/weixin_44903509/article/details/108825738 sudo apt-get update 出现问题 E: 仓库 " ...

  9. 【开发】操作系统应用基础-Linux常用Shell命令

    一 Linux操作系统和Shell 简介 操作系统(Operating Systems, OS)实际上是一种用于计算机的软.硬件资源管理调度的系统级软件,它的主体是内核(Kernel),其主要负责进程 ...

  10. 基于可穿戴的GPS定位存储模块方案特色解析

    前记   GPS作为一个位置定位手段,在日常生活中扮演着非常重要的角色.在研发动物可穿戴产品的同时.团队一直在做产品和模块标准化的事情,尽量把研发出来的东西标准化.按照任老板的说法,在追求理想主义的路 ...