前端使用 Konva 实现可视化设计器(5)- 磁贴效果
1.前端使用 Konva 实现可视化设计器(2)- 参考线、svg、gif图片加载2.前端使用 Konva 实现可视化设计器(1)- 无限画布、比例尺3.前端使用 Konva 实现可视化设计器(4)- 快捷键移动元素4.前端使用 Konva 实现可视化设计器(3)- 单选、多选、选择框
5.前端使用 Konva 实现可视化设计器(5)- 磁贴效果
6.前端使用 Konva 实现可视化设计器(6)- 复制粘贴、删除、位置、zIndex调整7.前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步8.前端使用 Konva 实现可视化设计器(8)- 预览框9.前端使用 Konva 实现可视化设计器(9)- 另存为SVG10.前端使用 Konva 实现可视化设计器(10)- 对齐线11.前端使用 Konva 实现可视化设计器(11)- 对齐效果12.前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线13.前端使用 Konva 实现可视化设计器(13)- 折线 - 最优路径应用【思路篇】14.前端使用 Konva 实现可视化设计器(14)- 折线 - 最优路径应用【代码篇】15.前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化16.前端使用 Konva 实现可视化设计器(16)- 旋转对齐、触摸板操作的优化17.前端使用 Konva 实现可视化设计器(17)- 素材嵌套 - 生成阶段18.前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段19.前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线20.前端使用 Konva 实现可视化设计器(20)- 性能优化、UI 美化21.前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)22.前端使用 Konva 实现可视化设计器(22)- 绘制图形(矩形、直线、折线)23.前端使用 Konva 实现可视化设计器(23)- 绘制曲线、属性面板关于第三章提到的 selectingNodesArea,在后续的实现中已经精简掉了。
而 transformer 的 dragBoundFunc 中的逻辑,也直接移动 transformer 的 dragmove 事件中处理。
请大家动动小手,给我一个免费的 Star 吧~
这一章花了比较多的时间调试,创作不易~
磁贴效果
放大缩小点磁贴网格效果
官方提供的便捷的 api 可以实现该效果,就是 transformer 的 anchorDragBoundFunc,官方实例,在此基础上,根据当前设计进行实现。
// 变换中
anchorDragBoundFunc: (oldPos: Konva.Vector2d, newPos: Konva.Vector2d) => {
// 磁贴逻辑
if (this.render.config.attractResize) {
// transformer 锚点按钮
const anchor = this.render.transformer.getActiveAnchor()
// 非旋转(就是放大缩小时)
if (anchor && anchor !== 'rotater') {
// stage 状态
const stageState = this.render.getStageState()
const logicX = this.render.toStageValue(newPos.x - stageState.x) // x坐标
const logicNumX = Math.round(logicX / this.render.bgSize) // x单元格个数
const logicClosestX = logicNumX * this.render.bgSize // x磁贴目标坐标
const logicDiffX = Math.abs(logicX - logicClosestX) // x磁贴偏移量
const snappedX = /-(left|right)$/.test(anchor) && logicDiffX < 5 // x磁贴阈值
const logicY = this.render.toStageValue(newPos.y - stageState.y) // y坐标
const logicNumY = Math.round(logicY / this.render.bgSize) // y单元格个数
const logicClosestY = logicNumY * this.render.bgSize // y磁贴目标坐标
const logicDiffY = Math.abs(logicY - logicClosestY) // y磁贴偏移量
const snappedY = /^(top|bottom)-/.test(anchor) && logicDiffY < 5 // y磁贴阈值
if (snappedX && !snappedY) {
// x磁贴
return {
x: this.render.toBoardValue(logicClosestX) + stageState.x,
y: oldPos.y
}
} else if (snappedY && !snappedX) {
// y磁贴
return {
x: oldPos.x,
y: this.render.toBoardValue(logicClosestY) + stageState.y
}
} else if (snappedX && snappedY) {
// xy磁贴
return {
x: this.render.toBoardValue(logicClosestX) + stageState.x,
y: this.render.toBoardValue(logicClosestY) + stageState.y
}
}
}
}
// 不磁贴
return newPos
}
主要的逻辑:根据最新的坐标,找到最接近的网格,达到设计的阈值就按官方 api 的定义,返回修正过的坐标(视觉上),所以返回之前,把计算好的“逻辑坐标”用 toBoardValue 恢复成“视觉坐标”。
移动磁贴网格效果
这个功能实现起来比较麻烦,官方是没有像类似 anchorDragBoundFunc 这样的 api,需要在 transformer 的 dragmove 介入修改。官方有个对齐线示例,也是“磁贴”相关的,证明在 transformer 的 dragmove 入手是合理的。主要差异是,示例是针对单个节点控制的,本设计是要控制在 transformer 中的多个节点的。
主要流程:
- 通过 transformer 的 dragmove 获得拖动期间的坐标
- 计算离四周网格的距离和偏移量
- 横向、纵向分别找到达到接近阈值,且距离最近的那个网格坐标(偏移量最小)
- 把选中的所有节点进行坐标修正
核心逻辑:
// 磁吸逻辑
attract = (newPos: Konva.Vector2d) => {
// stage 状态
const stageState = this.render.getStageState()
const width = this.render.transformer.width()
const height = this.render.transformer.height()
let newPosX = newPos.x
let newPosY = newPos.y
let isAttract = false
if (this.render.config.attractBg) {
const logicLeftX = this.render.toStageValue(newPos.x - stageState.x) // x坐标
const logicNumLeftX = Math.round(logicLeftX / this.render.bgSize) // x单元格个数
const logicClosestLeftX = logicNumLeftX * this.render.bgSize // x磁贴目标坐标
const logicDiffLeftX = Math.abs(logicLeftX - logicClosestLeftX) // x磁贴偏移量
const logicRightX = this.render.toStageValue(newPos.x + width - stageState.x) // x坐标
const logicNumRightX = Math.round(logicRightX / this.render.bgSize) // x单元格个数
const logicClosestRightX = logicNumRightX * this.render.bgSize // x磁贴目标坐标
const logicDiffRightX = Math.abs(logicRightX - logicClosestRightX) // x磁贴偏移量
const logicTopY = this.render.toStageValue(newPos.y - stageState.y) // y坐标
const logicNumTopY = Math.round(logicTopY / this.render.bgSize) // y单元格个数
const logicClosestTopY = logicNumTopY * this.render.bgSize // y磁贴目标坐标
const logicDiffTopY = Math.abs(logicTopY - logicClosestTopY) // y磁贴偏移量
const logicBottomY = this.render.toStageValue(newPos.y + height - stageState.y) // y坐标
const logicNumBottomY = Math.round(logicBottomY / this.render.bgSize) // y单元格个数
const logicClosestBottomY = logicNumBottomY * this.render.bgSize // y磁贴目标坐标
const logicDiffBottomY = Math.abs(logicBottomY - logicClosestBottomY) // y磁贴偏移量
// 距离近优先
for (const diff of [
{ type: 'leftX', value: logicDiffLeftX },
{ type: 'rightX', value: logicDiffRightX }
].sort((a, b) => a.value - b.value)) {
if (diff.value < 5) {
if (diff.type === 'leftX') {
newPosX = this.render.toBoardValue(logicClosestLeftX) + stageState.x
} else if (diff.type === 'rightX') {
newPosX = this.render.toBoardValue(logicClosestRightX) + stageState.x - width
}
isAttract = true
break
}
}
for (const diff of [
{ type: 'topY', value: logicDiffTopY },
{ type: 'bottomY', value: logicDiffBottomY }
].sort((a, b) => a.value - b.value)) {
if (diff.value < 5) {
if (diff.type === 'topY') {
newPosY = this.render.toBoardValue(logicClosestTopY) + stageState.y
} else if (diff.type === 'bottomY') {
newPosY = this.render.toBoardValue(logicClosestBottomY) + stageState.y - height
}
isAttract = true
break
}
}
}
return {
pos: {
x: newPosX,
y: newPosY
},
isAttract
}
}
这段逻辑及其相关事件的改动,不下 5 次,才勉强达到预期的效果。
接下来,计划实现下面这些功能:
- 实时预览窗
- 导出、导入
- 节点层次单个、批量调整
- 键盘复制、粘贴
- 对齐效果
- 等等。。。
是不是值得更多的 Star 呢?勾勾手指~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?