这一章补充一个效果,在多选的情况下,对目标进行对齐。基于多选整体区域对齐的基础上,还支持基于其中一个节点进行对齐。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

基于整体的对齐

垂直居中

水平居中

左对齐

右对齐

上对齐

下对齐

基于目标节点的对齐

垂直居中(基于目标节点)

水平居中(基于目标节点)

左对齐(基于目标节点)

右对齐(基于目标节点)

上对齐(基于目标节点)

下对齐(基于目标节点)

对齐逻辑

放在 src/Render/tools/AlignTool.ts

  1. import { Render } from '../index'
  2. //
  3. import * as Types from '../types'
  4. import * as Draws from '../draws'
  5. import Konva from 'konva'
  6. export class AlignTool {
  7. static readonly name = 'AlignTool'
  8. private render: Render
  9. constructor(render: Render) {
  10. this.render = render
  11. }
  12. // 对齐参考点
  13. getAlignPoints(target?: Konva.Node | Konva.Transformer): { [index: string]: number } {
  14. let width = 0,
  15. height = 0,
  16. x = 0,
  17. y = 0
  18. if (target instanceof Konva.Transformer) {
  19. // 选择器
  20. // 转为 逻辑觉尺寸
  21. ;[width, height] = [
  22. this.render.toStageValue(target.width()),
  23. this.render.toStageValue(target.height())
  24. ]
  25. ;[x, y] = [
  26. this.render.toStageValue(target.x()) - this.render.rulerSize,
  27. this.render.toStageValue(target.y()) - this.render.rulerSize
  28. ]
  29. } else if (target !== void 0) {
  30. // 节点
  31. // 逻辑尺寸
  32. ;[width, height] = [target.width(), target.height()]
  33. ;[x, y] = [target.x(), target.y()]
  34. } else {
  35. // 默认为选择器
  36. return this.getAlignPoints(this.render.transformer)
  37. }
  38. return {
  39. [Types.AlignType.垂直居中]: x + width / 2,
  40. [Types.AlignType.左对齐]: x,
  41. [Types.AlignType.右对齐]: x + width,
  42. [Types.AlignType.水平居中]: y + height / 2,
  43. [Types.AlignType.上对齐]: y,
  44. [Types.AlignType.下对齐]: y + height
  45. }
  46. }
  47. align(type: Types.AlignType, target?: Konva.Node) {
  48. // 对齐参考点(所有)
  49. const points = this.getAlignPoints(target)
  50. // 对齐参考点
  51. const point = points[type]
  52. // 需要移动的节点
  53. const nodes = this.render.transformer.nodes().filter((node) => node !== target)
  54. // 移动逻辑
  55. switch (type) {
  56. case Types.AlignType.垂直居中:
  57. for (const node of nodes) {
  58. node.x(point - node.width() / 2)
  59. }
  60. break
  61. case Types.AlignType.水平居中:
  62. for (const node of nodes) {
  63. node.y(point - node.height() / 2)
  64. }
  65. break
  66. case Types.AlignType.左对齐:
  67. for (const node of nodes) {
  68. node.x(point)
  69. }
  70. break
  71. case Types.AlignType.右对齐:
  72. for (const node of nodes) {
  73. node.x(point - node.width())
  74. }
  75. break
  76. case Types.AlignType.上对齐:
  77. for (const node of nodes) {
  78. node.y(point)
  79. }
  80. break
  81. case Types.AlignType.下对齐:
  82. for (const node of nodes) {
  83. node.y(point - node.height())
  84. }
  85. break
  86. }
  87. // 更新历史
  88. this.render.updateHistory()
  89. // 更新预览
  90. this.render.draws[Draws.PreviewDraw.name].draw()
  91. }
  92. }

还是比较容易理解的,要注意的主要是 transformer 获得的 size 和 position 是视觉尺寸,需要转为逻辑尺寸。

功能入口

准备些枚举值:

  1. export enum AlignType {
  2. 垂直居中 = 'Middle',
  3. 左对齐 = 'Left',
  4. 右对齐 = 'Right',
  5. 水平居中 = 'Center',
  6. 上对齐 = 'Top',
  7. 下对齐 = 'Bottom'
  8. }

按钮

  1. <button @click="onRestore">导入</button>
  2. <button @click="onSave">导出</button>
  3. <button @click="onSavePNG">另存为图片</button>
  4. <button @click="onSaveSvg">另存为Svg</button>
  5. <button @click="onPrev" :disabled="historyIndex <= 0">上一步</button>
  6. <button @click="onNext" :disabled="historyIndex >= history.length - 1">下一步</button>
  7. <!-- 新增 -->
  8. <button @click="onAlign(Types.AlignType.垂直居中)" :disabled="noAlign">垂直居中</button>
  9. <button @click="onAlign(Types.AlignType.左对齐)" :disabled="noAlign">左对齐</button>
  10. <button @click="onAlign(Types.AlignType.右对齐)" :disabled="noAlign">右对齐</button>
  11. <button @click="onAlign(Types.AlignType.水平居中)" :disabled="noAlign">水平居中</button>
  12. <button @click="onAlign(Types.AlignType.上对齐)" :disabled="noAlign">上对齐</button>
  13. <button @click="onAlign(Types.AlignType.下对齐)" :disabled="noAlign">下对齐</button>

按键生效的条件是,必须是多选,所以 render 需要暴露一个事件,跟踪选择节点:

  1. render = new Render(stageElement.value!, {
  2. // 略
  3. //
  4. on: {
  5. historyChange: (records: string[], index: number) => {
  6. history.value = records
  7. historyIndex.value = index
  8. },
  9. // 新增
  10. selectionChange: (nodes: Konva.Node[]) => {
  11. selection.value = nodes
  12. }
  13. }
  14. })

条件判断:

  1. // 选择项
  2. const selection: Ref<Konva.Node[]> = ref([])
  3. // 是否可以进行对齐
  4. const noAlign = computed(() => selection.value.length <= 1)
  5. // 对齐方法
  6. function onAlign(type: Types.AlignType) {
  7. render?.alignTool.align(type)
  8. }

触发事件的地方:

src/Render/tools/SelectionTool.ts

  1. // 清空已选
  2. selectingClear() {
  3. // 选择变化了
  4. if (this.selectingNodes.length > 0) {
  5. this.render.config.on?.selectionChange?.([])
  6. }
  7. // 略
  8. }
  9. // 选择节点
  10. select(nodes: Konva.Node[]) {
  11. // 选择变化了
  12. if (nodes.length !== this.selectingNodes.length) {
  13. this.render.config.on?.selectionChange?.(nodes)
  14. }
  15. // 略
  16. }

右键菜单



在多选区域的空白处的时候右键,功能与按钮一样,不多赘述。

右键菜单(基于目标节点)



基于目标,比较特别,在多选的情况下,给内部的节点增加一个 hover 效果。

首先,拖入元素的时候,给每个节点准备一个 Konva.Rect 作为 hover 效果,默认不显示,且列入忽略的部分。

src/Render/handlers/DragOutsideHandlers.ts:

  1. // hover 框(多选时才显示)
  2. group.add(
  3. new Konva.Rect({
  4. id: 'hoverRect',
  5. width: image.width(),
  6. height: image.height(),
  7. fill: 'rgba(0,255,0,0.3)',
  8. visible: false
  9. })
  10. )
  11. // 隐藏 hover 框
  12. group.on('mouseleave', () => {
  13. group.findOne('#hoverRect')?.visible(false)
  14. })

src/Render/index.ts:

  1. // 忽略非素材
  2. ignore(node: Konva.Node) {
  3. // 素材有各自根 group
  4. const isGroup = node instanceof Konva.Group
  5. return (
  6. !isGroup || node.id() === 'selectRect' || node.id() === 'hoverRect' || this.ignoreDraw(node)
  7. )
  8. }

src/Render/handlers/SelectionHandlers.ts:

  1. // 子节点 hover
  2. mousemove: () => {
  3. const pos = this.render.stage.getPointerPosition()
  4. if (pos) {
  5. // 获取所有图形
  6. const shapes = this.render.transformer.nodes()
  7. // 隐藏 hover 框
  8. for (const shape of shapes) {
  9. if (shape instanceof Konva.Group) {
  10. shape.findOne('#hoverRect')?.visible(false)
  11. }
  12. }
  13. // 多选
  14. if (shapes.length > 1) {
  15. // zIndex 倒序(大的优先)
  16. shapes.sort((a, b) => b.zIndex() - a.zIndex())
  17. // 提取重叠目标
  18. const selected = shapes.find((shape) =>
  19. // 关键 api
  20. Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
  21. )
  22. // 显示 hover 框
  23. if (selected) {
  24. if (selected instanceof Konva.Group) {
  25. selected.findOne('#hoverRect')?.visible(true)
  26. }
  27. }
  28. }
  29. }
  30. },
  31. mouseleave: () => {
  32. // 隐藏 hover 框
  33. for (const shape of this.render.transformer.nodes()) {
  34. if (shape instanceof Konva.Group) {
  35. shape.findOne('#hoverRect')?.visible(false)
  36. }
  37. }
  38. }

需要注意的是,hover 优先级是基于节点的 zIndex,所以判断 hover 之前,需要进行一次排序。

判断 hover,这里使用 Konva.Util.haveIntersection,判断两个 rect 是否重叠,鼠标表达为大小为 1 的 rect。

用 find 找到 hover 的目标节点,使用 find 找到第一个即可,第一个就是 zIndex 最大最上层那个。

把 hover 的目标节点内部的 hoverRect 显示出来就行了。

同样的,就可以判断是基于目标节点的右键菜单:

src/Render/draws/ContextmenuDraw.ts:

  1. if (target instanceof Konva.Transformer) {
  2. const pos = this.render.stage.getPointerPosition()
  3. if (pos) {
  4. // 获取所有图形
  5. const shapes = target.nodes()
  6. if (shapes.length > 1) {
  7. // zIndex 倒序(大的优先)
  8. shapes.sort((a, b) => b.zIndex() - a.zIndex())
  9. // 提取重叠目标
  10. const selected = shapes.find((shape) =>
  11. // 关键 api
  12. Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
  13. )
  14. // 对齐菜单
  15. menus.push({
  16. name: '垂直居中' + (selected ? '于目标' : ''),
  17. action: () => {
  18. this.render.alignTool.align(Types.AlignType.垂直居中, selected)
  19. }
  20. })
  21. menus.push({
  22. name: '左对齐' + (selected ? '于目标' : ''),
  23. action: () => {
  24. this.render.alignTool.align(Types.AlignType.左对齐, selected)
  25. }
  26. })
  27. menus.push({
  28. name: '右对齐' + (selected ? '于目标' : ''),
  29. action: () => {
  30. this.render.alignTool.align(Types.AlignType.右对齐, selected)
  31. }
  32. })
  33. menus.push({
  34. name: '水平居中' + (selected ? '于目标' : ''),
  35. action: () => {
  36. this.render.alignTool.align(Types.AlignType.水平居中, selected)
  37. }
  38. })
  39. menus.push({
  40. name: '上对齐' + (selected ? '于目标' : ''),
  41. action: () => {
  42. this.render.alignTool.align(Types.AlignType.上对齐, selected)
  43. }
  44. })
  45. menus.push({
  46. name: '下对齐' + (selected ? '于目标' : ''),
  47. action: () => {
  48. this.render.alignTool.align(Types.AlignType.下对齐, selected)
  49. }
  50. })
  51. }
  52. }
  53. }

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

  • 连接线
  • 等等。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

前端使用 Konva 实现可视化设计器(11)- 对齐效果的更多相关文章

  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. 流程设计器jQuery + svg/vml(Demo7 - 设计器与引擎及表单一起应用例子)

    去年就完成了流程设计器及流程引擎的开发,本想着把流程设计器好好整理一下,形成一个一步一步的开发案例,结果才整理了一点点,发现写文章比写代码还累,加上有事情要忙,结果就.. 明天要去外包驻场了,现把流程 ...

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

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

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

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

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

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

  9. F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)

          Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...

  10. 纯JS Web在线可拖拽的流程设计器

    F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...

随机推荐

  1. C# 面试问答

    引用:https://www.cnblogs.com/zh7791/p/13705434.html   1.什么是 COM? COM 代表组件对象模型.COM 是微软技术之一.使用这项技术,我们可以开 ...

  2. c++ 暂停2秒,等待2秒

    std::chrono::milliseconds stopTime(2000); std::this_thread::sleep_for(stopTime);

  3. c# apollo订阅与发布(一)(迁)

    前言 apollo 翻译过来是阿波罗的意思,准确的说是:apache apollo,看了apache基本可以放心,因为它不像微软. 安装 下面我以windows 为例. https://activem ...

  4. 重新整理数据结构与算法(c#)—— 堆排序[二十一]

    前言 将下面按照从小到大排序: int[] arr = { 4, 6, 8, 5, 9 }; 这时候可以通过冒泡排序,计数排序等. 但是一但数据arr很大,那么会产生排序过于缓慢,堆排序就是一个很好的 ...

  5. arp 的基础概念

    前言 打算整理网络这一块,先把概念写完. 就是有一个问题,那就是为什么有ip地址还有mac地址呢? 原因是这样子的,我们知道ip协议是第三层,那么有一个问题了,如果只有第三层的ip是否能过识别到主机? ...

  6. Redis为什么是单线程还支持高并发

    Redis为什么设计成单线程模式因为redis是基于内存的读写操作,所以CPU不是性能瓶颈,而单线程更好实现,所以就设计成单线程模式 单线程模式省却了CPU上下文切换带来的开销问题,也不用去考虑各种锁 ...

  7. Ubuntu下部署gitlab

    1.安装gitlab服务 1.安装依赖 在ubuntu下使用快捷键ctrl+alt+T打开命令行窗口,然后运行下面命令 sudo apt update sudo apt-get upgrade sud ...

  8. Oracle sql 判断全角字符

    (lengthb(MC) - length(MC))<>(lengthb(to_single_byte(MC)) - length(to_single_byte(MC)))

  9. dbeaver导出结果集中乱码

    重要的一步 需要点击

  10. 力扣665(java)-非递减数列(中等)

    题目: 给你一个长度为 n 的整数数组 nums ,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列. 我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= ...