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

这一章处理一下复制、粘贴、删除、画布归位、层次调整,通过右键菜单控制。

github源码

gitee源码

示例地址

复制粘贴

复制粘贴(通过快捷键)

  // 复制暂存
pasteCache: Konva.Node[] = [];
// 粘贴次数(用于定义新节点的偏移距离)
pasteCount = 1; // 复制
pasteStart() {
this.pasteCache = this.render.selectionTool.selectingNodes.map((o) => {
const copy = o.clone();
// 恢复透明度、可交互
copy.setAttrs({
listening: true,
opacity: copy.attrs.lastOpacity ?? 1,
});
// 清空状态
copy.setAttrs({
nodeMousedownPos: undefined,
lastOpacity: undefined,
lastZIndex: undefined,
selectingZIndex: undefined,
});
return copy;
});
this.pasteCount = 1;
} // 粘贴
pasteEnd() {
if (this.pasteCache.length > 0) {
this.render.selectionTool.selectingClear();
this.copy(this.pasteCache);
this.pasteCount++;
}
}

快捷键处理:

    keydown: (e: GlobalEventHandlersEventMap['keydown']) => {
if (e.ctrlKey) {
if (e.code === Types.ShutcutKey.C) {
this.render.copyTool.pasteStart() // 复制
} else if (e.code === Types.ShutcutKey.V) {
this.render.copyTool.pasteEnd() // 粘贴
}
}
}
}

逻辑比较简单,可以关注代码中的注释。

复制粘贴(右键)

  /**
* 复制粘贴
* @param nodes 节点数组
* @param skip 跳过检查
* @returns 复制的元素
*/
copy(nodes: Konva.Node[]) {
const arr: Konva.Node[] = []; for (const node of nodes) {
if (node instanceof Konva.Transformer) {
// 复制已选择
const backup = [...this.render.selectionTool.selectingNodes];
this.render.selectionTool.selectingClear();
this.copy(backup);
} else {
// 复制未选择
const copy = node.clone();
// 使新节点产生偏移
copy.setAttrs({
x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
});
// 插入新节点
this.render.layer.add(copy);
// 选中复制内容
this.render.selectionTool.select([...this.render.selectionTool.selectingNodes, copy]);
}
} return arr;
}

逻辑比较简单,可以关注代码中的注释。

删除

处理方法:

  // 移除元素
remove(nodes: Konva.Node[]) {
for (const node of nodes) {
if (node instanceof Konva.Transformer) {
// 移除已选择的节点
this.remove(this.selectionTool.selectingNodes);
// 清除选择
this.selectionTool.selectingClear();
} else {
// 移除未选择的节点
node.remove();
}
}
}

事件处理:

      keydown: (e: GlobalEventHandlersEventMap['keydown']) => {
if (e.ctrlKey) {
// 略
} else if (e.code === Types.ShutcutKey.删除) {
this.render.remove(this.render.selectionTool.selectingNodes)
}
}

画布归位

逻辑比较简单,恢复画布比例和偏移量:

  // 恢复位置大小
positionZoomReset() {
this.render.stage.setAttrs({
scale: { x: 1, y: 1 }
}) this.positionReset()
} // 恢复位置
positionReset() {
this.render.stage.setAttrs({
x: this.render.rulerSize,
y: this.render.rulerSize
}) // 更新背景
this.render.draws[Draws.BgDraw.name].draw()
// 更新比例尺
this.render.draws[Draws.RulerDraw.name].draw()
// 更新参考线
this.render.draws[Draws.RefLineDraw.name].draw()
}

稍微说明一下,初始位置需要考虑比例尺的大小。

层次调整

关于层次的调整,相对比较晦涩。

一些辅助方法

获取需要处理的节点,主要是处理 transformer 内部的节点:

  // 获取移动节点
getNodes(nodes: Konva.Node[]) {
const targets: Konva.Node[] = []
for (const node of nodes) {
if (node instanceof Konva.Transformer) {
// 已选择的节点
targets.push(...this.render.selectionTool.selectingNodes)
} else {
// 未选择的节点
targets.push(node)
}
}
return targets
}

获得计算所需的最大、最小 zIndex:

  // 最大 zIndex
getMaxZIndex() {
return Math.max(
...this.render.layer
.getChildren((node) => {
return !this.render.ignore(node)
})
.map((o) => o.zIndex())
)
} // 最小 zIndex
getMinZIndex() {
return Math.min(
...this.render.layer
.getChildren((node) => {
return !this.render.ignore(node)
})
.map((o) => o.zIndex())
)
}

记录选择之前的 zIndex

由于被选择的节点会被临时置顶,会影响节点层次的调整,所以选择之前需要记录一下选择之前的 zIndex:

  // 更新 zIndex 缓存
updateLastZindex(nodes: Konva.Node[]) {
for (const node of nodes) {
node.setAttrs({
lastZIndex: node.zIndex()
})
}
}

处理 transformer 的置顶影响

通过 transformer 选择的时候,所选节点的层次已经被置顶。

所以调整时需要有个步骤:

  • 记录已经被 transformer 影响的每个节点的 zIndex(其实就是记录置顶状态)
  • 调整节点的层次
  • 恢复被 transformer 选择的节点的 zIndex(其实就是恢复置顶状态)

举例子:

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7

记录选择 C D E 之前的 lastZIndex:C/3 D/4 E/5

选择后,“临时置顶” C D E:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

此时置底了 C D E,由于上面记录了选择之前的 lastZIndex,直接计算 lastZIndex,变成 C/1 D/2 E/3

在 selectingClear 的时候,会根据 lastZIndex 让 zIndex 的调整生效:

逐步变化:

0、A/1 B/2 F/3 G/4 C/5 D/6 E/7 改变 C/5 -> C/1

1、C/1 A/2 B/3 F/4 G/5 D/6 E/7 改变 D/6 -> D/2

2、C/1 D/2 A/3 B/4 F/5 G/6 E/7 改变 E/7 -> E/3

3、C/1 D/2 E/3 A/4 B/5 F/6 G/7 完成调整

因为 transformer 的存在,调整完还要恢复原来的“临时置顶”:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

下面是记录选择之前的 zIndex 状态、恢复调整之后的 zIndex 状态的方法:

  // 记录选择期间的 zIndex
updateSelectingZIndex(nodes: Konva.Node[]) {
for (const node of nodes) {
node.setAttrs({
selectingZIndex: node.zIndex()
})
}
} // 恢复选择期间的 zIndex
resetSelectingZIndex(nodes: Konva.Node[]) {
nodes.sort((a, b) => a.zIndex() - b.zIndex())
for (const node of nodes) {
node.zIndex(node.attrs.selectingZIndex)
}
}

关于 zIndex 的调整

主要分两种情况:已选的节点、未选的节点

  • 已选:如上面所说,调整之余,还要处理 transformer 的置顶影响
  • 未选:直接调整即可
  // 上移
up(nodes: Konva.Node[]) {
// 最大zIndex
const maxZIndex = this.getMaxZIndex() const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex()) // 上移
let lastNode: Konva.Node | null = null if (this.render.selectionTool.selectingNodes.length > 0) {
this.updateSelectingZIndex(sorted) for (const node of sorted) {
if (
node.attrs.lastZIndex < maxZIndex &&
(lastNode === null || node.attrs.lastZIndex < lastNode.attrs.lastZIndex - 1)
) {
node.setAttrs({
lastZIndex: node.attrs.lastZIndex + 1
})
}
lastNode = node
} this.resetSelectingZIndex(sorted)
} else {
// 直接调整
for (const node of sorted) {
if (
node.zIndex() < maxZIndex &&
(lastNode === null || node.zIndex() < lastNode.zIndex() - 1)
) {
node.zIndex(node.zIndex() + 1)
}
lastNode = node
} this.updateLastZindex(sorted)
}
}

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,上移 D F

执行一次:

移动F,A/1 B/2 C/3 D/4 E/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 D/5 G/6 F/7

再执行一次:

移动F,已经到头了,不变,A/1 B/2 C/3 E/4 D/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 G/5 D/6 F/7

再执行一次:

移动F,已经到尾了,不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

移动D,已经贴着 F 了,为了保持 D F 的相对顺序,也不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

结束

  // 下移
down(nodes: Konva.Node[]) {
// 最小 zIndex
const minZIndex = this.getMinZIndex() const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex()) // 下移
let lastNode: Konva.Node | null = null if (this.render.selectionTool.selectingNodes.length > 0) {
this.updateSelectingZIndex(sorted) for (const node of sorted) {
if (
node.attrs.lastZIndex > minZIndex &&
(lastNode === null || node.attrs.lastZIndex > lastNode.attrs.lastZIndex + 1)
) {
node.setAttrs({
lastZIndex: node.attrs.lastZIndex - 1
})
}
lastNode = node
} this.resetSelectingZIndex(sorted)
} else {
// 直接调整
for (const node of sorted) {
if (
node.zIndex() > minZIndex &&
(lastNode === null || node.zIndex() > lastNode.zIndex() + 1)
) {
node.zIndex(node.zIndex() - 1)
}
lastNode = node
} this.updateLastZindex(sorted)
}
}

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,下移 B D

执行一次:

移动B,B/1 A/2 C/3 D/4 E/5 F/6 G/7

移动D,B/1 A/2 D/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 A/2 D/3 C/4 E/5 F/6 G/7

移动D,B/1 D/2 A/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

移动D,已经贴着 B 了,为了保持 B D 的相对顺序,也不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

结束

  // 置顶
top(nodes: Konva.Node[]) {
// 最大 zIndex
let maxZIndex = this.getMaxZIndex() const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex()) if (this.render.selectionTool.selectingNodes.length > 0) {
// 先选中再调整
this.updateSelectingZIndex(sorted) // 置顶
for (const node of sorted) {
node.setAttrs({
lastZIndex: maxZIndex--
})
} this.resetSelectingZIndex(sorted)
} else {
// 直接调整 for (const node of sorted) {
node.zIndex(maxZIndex)
} this.updateLastZindex(sorted)
}
}

从高到低,逐个移动,每次移动递减 1

  // 置底
bottom(nodes: Konva.Node[]) {
// 最小 zIndex
let minZIndex = this.getMinZIndex() const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex()) if (this.render.selectionTool.selectingNodes.length > 0) {
// 先选中再调整
this.updateSelectingZIndex(sorted) // 置底
for (const node of sorted) {
node.setAttrs({
lastZIndex: minZIndex++
})
} this.resetSelectingZIndex(sorted)
} else {
// 直接调整 for (const node of sorted) {
node.zIndex(minZIndex)
} this.updateLastZindex(sorted)
}
}

从低到高,逐个移动,每次移动递增 1

调整 zIndex 的思路比较个性化,所以晦涩。要符合 konva 的 zIndex 特定,且达到目的,算法可以自行调整。

右键菜单

事件处理

      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
this.state.lastPos = this.render.stage.getPointerPosition() if (e.evt.button === Types.MouseButton.左键) {
if (!this.state.menuIsMousedown) {
// 没有按下菜单,清除菜单
this.state.target = null
this.draw()
}
} else if (e.evt.button === Types.MouseButton.右键) {
// 右键按下
this.state.right = true
}
},
mousemove: () => {
if (this.state.target && this.state.right) {
// 拖动画布时(右键),清除菜单
this.state.target = null
this.draw()
}
},
mouseup: () => {
this.state.right = false
},
contextmenu: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['contextmenu']>) => {
const pos = this.render.stage.getPointerPosition()
if (pos && this.state.lastPos) {
// 右键目标
if (pos.x === this.state.lastPos.x || pos.y === this.state.lastPos.y) {
this.state.target = e.target
} else {
this.state.target = null
}
this.draw()
}
},
wheel: () => {
// 画布缩放时,清除菜单
this.state.target = null
this.draw()
}

逻辑说明都在注释里了,主要处理的是右键菜单出现的位置,以及出现和消失的时机,最后是右键的目标。

  override draw() {
this.clear() if (this.state.target) {
// 菜单数组
const menus: Array<{
name: string
action: (e: Konva.KonvaEventObject<MouseEvent>) => void
}> = [] if (this.state.target === this.render.stage) {
// 空白处
menus.push({
name: '恢复位置',
action: () => {
this.render.positionTool.positionReset()
}
})
menus.push({
name: '恢复大小位置',
action: () => {
this.render.positionTool.positionZoomReset()
}
})
} else {
// 未选择:真实节点,即素材的容器 group
// 已选择:transformer
const target = this.state.target.parent // 目标
menus.push({
name: '复制',
action: () => {
if (target) {
this.render.copyTool.copy([target])
}
}
})
menus.push({
name: '删除',
action: () => {
if (target) {
this.render.remove([target])
}
}
})
menus.push({
name: '置顶',
action: () => {
if (target) {
this.render.zIndexTool.top([target])
}
}
})
menus.push({
name: '上一层',
action: () => {
if (target) {
this.render.zIndexTool.up([target])
}
}
})
menus.push({
name: '下一层',
action: () => {
if (target) {
this.render.zIndexTool.down([target])
}
}
})
menus.push({
name: '置底',
action: () => {
if (target) {
this.render.zIndexTool.bottom([target])
}
}
})
} // stage 状态
const stageState = this.render.getStageState() // 绘制右键菜单
const group = new Konva.Group({
name: 'contextmenu',
width: stageState.width,
height: stageState.height
}) let top = 0
// 菜单每项高度
const lineHeight = 30 const pos = this.render.stage.getPointerPosition()
if (pos) {
for (const menu of menus) {
// 框
const rect = new Konva.Rect({
x: this.render.toStageValue(pos.x - stageState.x),
y: this.render.toStageValue(pos.y + top - stageState.y),
width: this.render.toStageValue(100),
height: this.render.toStageValue(lineHeight),
fill: '#fff',
stroke: '#999',
strokeWidth: this.render.toStageValue(1),
name: 'contextmenu'
})
// 标题
const text = new Konva.Text({
x: this.render.toStageValue(pos.x - stageState.x),
y: this.render.toStageValue(pos.y + top - stageState.y),
text: menu.name,
name: 'contextmenu',
listening: false,
fontSize: this.render.toStageValue(16),
fill: '#333',
width: this.render.toStageValue(100),
height: this.render.toStageValue(lineHeight),
align: 'center',
verticalAlign: 'middle'
})
group.add(rect)
group.add(text) // 菜单事件
rect.on('click', (e) => {
if (e.evt.button === Types.MouseButton.左键) {
// 触发事件
menu.action(e) // 移除菜单
this.group.removeChildren()
this.state.target = null
} e.evt.preventDefault()
e.evt.stopPropagation()
})
rect.on('mousedown', (e) => {
if (e.evt.button === Types.MouseButton.左键) {
this.state.menuIsMousedown = true
// 按下效果
rect.fill('#dfdfdf')
} e.evt.preventDefault()
e.evt.stopPropagation()
})
rect.on('mouseup', (e) => {
if (e.evt.button === Types.MouseButton.左键) {
this.state.menuIsMousedown = false
}
})
rect.on('mouseenter', (e) => {
if (this.state.menuIsMousedown) {
rect.fill('#dfdfdf')
} else {
// hover in
rect.fill('#efefef')
} e.evt.preventDefault()
e.evt.stopPropagation()
})
rect.on('mouseout', () => {
// hover out
rect.fill('#fff')
})
rect.on('contextmenu', (e) => {
e.evt.preventDefault()
e.evt.stopPropagation()
}) top += lineHeight - 1
}
} this.group.add(group)
}
}

逻辑也不复杂,根据右键的目标分配相应的菜单项

空白处:恢复位置、大小

节点:复制、删除、上移、下移、置顶、置底

绘制右键菜单

右键的目标有二种情况:空白处、单个/多选节点。

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

  • 实时预览窗
  • 导出、导入
  • 对齐效果
  • 等等。。。

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

源码

gitee源码

示例地址

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

  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. 记spring boot启动出现Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.问题处理

    今天拉下了一个新的springboot工程,启动时出现了Unable to start web server; nested exception is org.springframework.cont ...

  2. KTL 最新支持Qt5窗口编程,一个支持C++14编辑公式的K线技术工具平台

    K,K线,Candle蜡烛图. T,技术分析,工具平台 L,公式Language语言使用c++14,Lite小巧简易. 项目仓库:https://github.com/bbqz007/KTL 国内仓库 ...

  3. NJUPT第一次积分赛

    NJUPT第一次积分赛 最近在忙第二次积分赛以及一些很复杂的队友关系(人际关系好复杂,好想电赛出个单机模式),但最后结果还是很满意的. 突然想起来第一次积分赛写的屎山,遂拿出来给大火闻闻 没啥很新颖的 ...

  4. django(web框架推导、简介、数据库初识)

    一 web框架推导 1 软件开发架构 cs架构bs架构bs本质是也是cs # HTTP协议:无状态,无连接,基于请求,基于tcp/ip的应用层协议 # mysql:c/s架构,底层基于soket,自己 ...

  5. 一天涨 23k Star 的开源项目「GitHub 热点速览」

    ​在 GitHub 上做过开源项目的小伙伴,可能都经历过截图自己项目 100 Star.1000 Star 的时刻,但有些时候事情发生的太快来不及截图,因为可能一觉醒来就破万了.这件事看似有些天方夜谭 ...

  6. 崩溃bug日志总结3

    目录介绍 1.1 OnErrorNotImplementedException[ Can't create handler inside thread that has not called Loop ...

  7. 【LeetCode刷题】239.滑动窗口最大值

    239.滑动窗口最大值(点击跳转LeetCode) 给你一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧.你只可以看到在滑动窗口内的k个数字.滑动窗口每次只向右移动一位. ...

  8. [Git]入门及其常用命令

    [版权声明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://www.cnblogs.com/cnb-yuchen/p/17993832 出自[进步*于辰的博客] Git 的难点 ...

  9. hdfs中acl权限管理的简单实用

    1.背景 在我们开发的过程中有这么一种场景, /projectA 目录是 hadoopdeploy用户创建的,他对这个目录有wrx权限,同时这个目录属于supergroup,在这个组中的用户也具有这个 ...

  10. CSS+HTML+flexible.js+rem实现屏幕缩放适配概念原理解释

    首先理解几个概念: (1)屏幕尺寸:屏幕对角线的长度,一般用英寸表示,1英寸=2.54cm. (2)dp((或者叫dip):设备独立像素,也就是设备屏幕上多少个点. (3)dpi:印刷行业术语,像素密 ...