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 应用服务】Python3.7项目在引用pandas 模块后,部署报错 

    问题描述 参考"快速入门:在 Linux 上的 Azure 应用服务中创建 Python 应用" 文档,在App Service For Linux环境中部署Python应用,在添 ...

  2. 【Azure 应用服务】App Service 进入后台管理(Kudu)页面,因为文件过多而显示不全的问题

    问题描述 当App Service 应用发布到Azure上后,需要查看某一个日志文件时候,如果一个文件夹中的文件内容过多,则会出现错误消息提醒: Full error Message: There a ...

  3. [Python] 子线程退出孙线程不退出

    遇到了一个大坑! 如图,在子线程ThreadFunc退出之后,ThreadFunc2依旧在运行... 根本不会结束 但是官方文档中说明了,只要设置了daemon不为None 就能设置子线程是守护线程, ...

  4. TCP和UDP可以使用同一个端口号吗?

    TCP和UDP可以使用同一个端口号吗? 首先说答案:可以.怎么理解呢? 我想这个问题要从计算机网络通信谈起,学过计算机网络的同学,可能都还记得7层或者4层网络模型,TCP/UDP属于其中的传输层协议, ...

  5. C++ //内建函数对象 算数仿函数 关系仿函数 //逻辑仿函数

    1 //内建函数对象 算数仿函数 关系仿函数 //逻辑仿函数 2 #include<iostream> 3 #include<string> 4 #include<fun ...

  6. 手机使用termux部署alist(一起体验alist挂载云盘)

    termux安装alist 安装termux 软件Termux:https://f-droid.org/packages/com.termux/ pkg install vim pkg install ...

  7. 【技术积累】Java 8 新特性

    一.Lambda表达式 Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递).可以写出更简洁.更灵活的代码.作为一种更紧凑的代码风格,使J ...

  8. go程序在mac下的交叉编译

    主页 微信公众号:密码应用技术实战 博客园首页:https://www.cnblogs.com/informatics/ 背景 go语言的一大优势就是跨平台,go语言是编译型语言,与Java.C#等语 ...

  9. audio currentTime 设置后,重置成0,解决方案(流文件-下载文件)

    audio currentTime 设置后,重置成0,解决方案 第一步-流文件-下载文件: 先查看你的mp3文件是 流文件,还是下载文件. 检测方式,就是放到浏览器回车.在线播放就是流文件,直接下载了 ...

  10. 3DCAT+上汽奥迪:打造新零售汽车配置器实时云渲染解决方案

    在 5G.云计算等技术飞速发展的加持下,云渲染技术迎来了突飞猛进的发展.在这样的背景下,3DCAT应运而生,成为了业内知名的实时云渲染服务商之一. 交互式3D实时云看车作为云渲染技术的一种使用场景,也 ...