前端使用 Konva 实现可视化设计器(11)- 对齐效果
这一章补充一个效果,在多选的情况下,对目标进行对齐。基于多选整体区域对齐的基础上,还支持基于其中一个节点进行对齐。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
基于整体的对齐
垂直居中
水平居中
左对齐
右对齐
上对齐
下对齐
基于目标节点的对齐
垂直居中(基于目标节点)
水平居中(基于目标节点)
左对齐(基于目标节点)
右对齐(基于目标节点)
上对齐(基于目标节点)
下对齐(基于目标节点)
对齐逻辑
放在 src/Render/tools/AlignTool.ts
import { Render } from '../index'
//
import * as Types from '../types'
import * as Draws from '../draws'
import Konva from 'konva'
export class AlignTool {
static readonly name = 'AlignTool'
private render: Render
constructor(render: Render) {
this.render = render
}
// 对齐参考点
getAlignPoints(target?: Konva.Node | Konva.Transformer): { [index: string]: number } {
let width = 0,
height = 0,
x = 0,
y = 0
if (target instanceof Konva.Transformer) {
// 选择器
// 转为 逻辑觉尺寸
;[width, height] = [
this.render.toStageValue(target.width()),
this.render.toStageValue(target.height())
]
;[x, y] = [
this.render.toStageValue(target.x()) - this.render.rulerSize,
this.render.toStageValue(target.y()) - this.render.rulerSize
]
} else if (target !== void 0) {
// 节点
// 逻辑尺寸
;[width, height] = [target.width(), target.height()]
;[x, y] = [target.x(), target.y()]
} else {
// 默认为选择器
return this.getAlignPoints(this.render.transformer)
}
return {
[Types.AlignType.垂直居中]: x + width / 2,
[Types.AlignType.左对齐]: x,
[Types.AlignType.右对齐]: x + width,
[Types.AlignType.水平居中]: y + height / 2,
[Types.AlignType.上对齐]: y,
[Types.AlignType.下对齐]: y + height
}
}
align(type: Types.AlignType, target?: Konva.Node) {
// 对齐参考点(所有)
const points = this.getAlignPoints(target)
// 对齐参考点
const point = points[type]
// 需要移动的节点
const nodes = this.render.transformer.nodes().filter((node) => node !== target)
// 移动逻辑
switch (type) {
case Types.AlignType.垂直居中:
for (const node of nodes) {
node.x(point - node.width() / 2)
}
break
case Types.AlignType.水平居中:
for (const node of nodes) {
node.y(point - node.height() / 2)
}
break
case Types.AlignType.左对齐:
for (const node of nodes) {
node.x(point)
}
break
case Types.AlignType.右对齐:
for (const node of nodes) {
node.x(point - node.width())
}
break
case Types.AlignType.上对齐:
for (const node of nodes) {
node.y(point)
}
break
case Types.AlignType.下对齐:
for (const node of nodes) {
node.y(point - node.height())
}
break
}
// 更新历史
this.render.updateHistory()
// 更新预览
this.render.draws[Draws.PreviewDraw.name].draw()
}
}
还是比较容易理解的,要注意的主要是 transformer 获得的 size 和 position 是视觉尺寸,需要转为逻辑尺寸。
功能入口
准备些枚举值:
export enum AlignType {
垂直居中 = 'Middle',
左对齐 = 'Left',
右对齐 = 'Right',
水平居中 = 'Center',
上对齐 = 'Top',
下对齐 = 'Bottom'
}
按钮
<button @click="onRestore">导入</button>
<button @click="onSave">导出</button>
<button @click="onSavePNG">另存为图片</button>
<button @click="onSaveSvg">另存为Svg</button>
<button @click="onPrev" :disabled="historyIndex <= 0">上一步</button>
<button @click="onNext" :disabled="historyIndex >= history.length - 1">下一步</button>
<!-- 新增 -->
<button @click="onAlign(Types.AlignType.垂直居中)" :disabled="noAlign">垂直居中</button>
<button @click="onAlign(Types.AlignType.左对齐)" :disabled="noAlign">左对齐</button>
<button @click="onAlign(Types.AlignType.右对齐)" :disabled="noAlign">右对齐</button>
<button @click="onAlign(Types.AlignType.水平居中)" :disabled="noAlign">水平居中</button>
<button @click="onAlign(Types.AlignType.上对齐)" :disabled="noAlign">上对齐</button>
<button @click="onAlign(Types.AlignType.下对齐)" :disabled="noAlign">下对齐</button>
按键生效的条件是,必须是多选,所以 render 需要暴露一个事件,跟踪选择节点:
render = new Render(stageElement.value!, {
// 略
//
on: {
historyChange: (records: string[], index: number) => {
history.value = records
historyIndex.value = index
},
// 新增
selectionChange: (nodes: Konva.Node[]) => {
selection.value = nodes
}
}
})
条件判断:
// 选择项
const selection: Ref<Konva.Node[]> = ref([])
// 是否可以进行对齐
const noAlign = computed(() => selection.value.length <= 1)
// 对齐方法
function onAlign(type: Types.AlignType) {
render?.alignTool.align(type)
}
触发事件的地方:
src/Render/tools/SelectionTool.ts
// 清空已选
selectingClear() {
// 选择变化了
if (this.selectingNodes.length > 0) {
this.render.config.on?.selectionChange?.([])
}
// 略
}
// 选择节点
select(nodes: Konva.Node[]) {
// 选择变化了
if (nodes.length !== this.selectingNodes.length) {
this.render.config.on?.selectionChange?.(nodes)
}
// 略
}
右键菜单
在多选区域的空白处的时候右键,功能与按钮一样,不多赘述。
右键菜单(基于目标节点)
基于目标,比较特别,在多选的情况下,给内部的节点增加一个 hover 效果。
首先,拖入元素的时候,给每个节点准备一个 Konva.Rect 作为 hover 效果,默认不显示,且列入忽略的部分。
src/Render/handlers/DragOutsideHandlers.ts:
// hover 框(多选时才显示)
group.add(
new Konva.Rect({
id: 'hoverRect',
width: image.width(),
height: image.height(),
fill: 'rgba(0,255,0,0.3)',
visible: false
})
)
// 隐藏 hover 框
group.on('mouseleave', () => {
group.findOne('#hoverRect')?.visible(false)
})
src/Render/index.ts:
// 忽略非素材
ignore(node: Konva.Node) {
// 素材有各自根 group
const isGroup = node instanceof Konva.Group
return (
!isGroup || node.id() === 'selectRect' || node.id() === 'hoverRect' || this.ignoreDraw(node)
)
}
src/Render/handlers/SelectionHandlers.ts:
// 子节点 hover
mousemove: () => {
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 获取所有图形
const shapes = this.render.transformer.nodes()
// 隐藏 hover 框
for (const shape of shapes) {
if (shape instanceof Konva.Group) {
shape.findOne('#hoverRect')?.visible(false)
}
}
// 多选
if (shapes.length > 1) {
// zIndex 倒序(大的优先)
shapes.sort((a, b) => b.zIndex() - a.zIndex())
// 提取重叠目标
const selected = shapes.find((shape) =>
// 关键 api
Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
)
// 显示 hover 框
if (selected) {
if (selected instanceof Konva.Group) {
selected.findOne('#hoverRect')?.visible(true)
}
}
}
}
},
mouseleave: () => {
// 隐藏 hover 框
for (const shape of this.render.transformer.nodes()) {
if (shape instanceof Konva.Group) {
shape.findOne('#hoverRect')?.visible(false)
}
}
}
需要注意的是,hover 优先级是基于节点的 zIndex,所以判断 hover 之前,需要进行一次排序。
判断 hover,这里使用 Konva.Util.haveIntersection,判断两个 rect 是否重叠,鼠标表达为大小为 1 的 rect。
用 find 找到 hover 的目标节点,使用 find 找到第一个即可,第一个就是 zIndex 最大最上层那个。
把 hover 的目标节点内部的 hoverRect 显示出来就行了。
同样的,就可以判断是基于目标节点的右键菜单:
src/Render/draws/ContextmenuDraw.ts:
if (target instanceof Konva.Transformer) {
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 获取所有图形
const shapes = target.nodes()
if (shapes.length > 1) {
// zIndex 倒序(大的优先)
shapes.sort((a, b) => b.zIndex() - a.zIndex())
// 提取重叠目标
const selected = shapes.find((shape) =>
// 关键 api
Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
)
// 对齐菜单
menus.push({
name: '垂直居中' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.垂直居中, selected)
}
})
menus.push({
name: '左对齐' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.左对齐, selected)
}
})
menus.push({
name: '右对齐' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.右对齐, selected)
}
})
menus.push({
name: '水平居中' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.水平居中, selected)
}
})
menus.push({
name: '上对齐' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.上对齐, selected)
}
})
menus.push({
name: '下对齐' + (selected ? '于目标' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.下对齐, selected)
}
})
}
}
}
接下来,计划实现下面这些功能:
- 连接线
- 等等。。。
More Stars please!勾勾手指~
前端使用 Konva 实现可视化设计器(11)- 对齐效果的更多相关文章
- 惊闻企业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 ...
- 流程设计器jQuery + svg/vml(Demo7 - 设计器与引擎及表单一起应用例子)
去年就完成了流程设计器及流程引擎的开发,本想着把流程设计器好好整理一下,形成一个一步一步的开发案例,结果才整理了一点点,发现写文章比写代码还累,加上有事情要忙,结果就.. 明天要去外包驻场了,现把流程 ...
- 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路分支.会签.聚合.多重聚合.退回. ...
随机推荐
- Python 条件和 if 语句
Python支持来自数学的通常逻辑条件: 等于:a == b 不等于:a != b 小于:a < b 小于或等于:a <= b 大于:a > b 大于或等于:a >= b 这些 ...
- 教你构建一个优秀的SD Prompt
构建一个优秀的Prompt 在使用Stable Diffusion AI时,构建一个有效的提示(Prompt)是至关重要的第一步.这个过程涉及到创造性的尝试和对AI行为的理解.这里我会对如何构建一个好 ...
- Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用
Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用 SpringCloud学习教程 SpringCloud Spring Cloud OpenFeign 是 ...
- redis 简单整理——持久化的问题定位和优化[二十一]
前言 Redis持久化功能一直是影响Redis性能的高发地,简单介绍一下持久化的问题定位和优化. 正文 当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创 建子进程,对于大多 ...
- 《c#高级编程》第2章C#2.0中的更改(四)——空值类型
一.概念 在C#中,空值类型(Nullable Types)是一种特殊的数据类型,它可以包含正常值或者一个null值.当我们需要在程序中处理可能不存在的值时,空值类型就会派上用场. 空值类型的语法形式 ...
- Vue-flask 展示小电影
显示小电影 前端Vue <body> <div id="app"> <button @click="handleLoad"> ...
- 力扣455(java&python)-分发饼干(简单)
题目: 假设你是一位很棒的家长,想要给你的孩子们一些小饼干.但是,每个孩子最多只能给一块饼干. 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸:并且每块饼干 j,都有 ...
- 全面提升易用性:OpenClusterManagement 0.7 版本发布
简介:千呼万唤始出来,三月末 OpenClusterManagement 社区正式发布了 v0.7 版本.在新的版本有一系列新的功能特性欢迎感兴趣的读者体验探索,同时在这个版本中社区维护者对目前已有 ...
- 块存储监控与服务压测调优利器-EBS Lens发布
简介:SLS团队联合EBS团队发布了EBS Lens,针对块存储提供数据分析.资源监控的功能,可以帮助用户获取云上块存储资源信息与性能监控数据.提升云上块存储资源的管理效率.高效分析业务波动与资源性 ...
- 如何避免JS内存泄漏?
简介: 很多开发者可能平时并不关心自己维护的页面是否存在内存泄漏,原因可能是刚开始简单的页面内存泄漏的速度很缓慢,在造成严重卡顿之前可能就被用户刷新了,问题也就被隐藏了,但是随着页面越来越复杂,尤 ...