前端使用 Konva 实现可视化设计器(10)- 对齐线

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

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

github源码

gitee源码

示例地址

不知不觉来到第 10 章了,感觉接近尾声了。。。

对齐线

先看效果:

这里交互有两个部分:

1、节点之间的对齐线

2、对齐磁贴

多选的情况下,效果是一样的:

主要逻辑会放在控制"选择"的代码文件里:

src\Render\handlers\SelectionHandlers.ts

这里需要一些辅助都定义:

ts 复制代码
interface SortItem {
  id?: number // 有 id 就是其他节点,否则就是 选择目标
  value: number // 左、垂直中、右的 x 坐标值; 上、水平中、下的 y 坐标值;
}

type SortItemPair = [SortItem, SortItem]

尝试画个图说明一下上面的含义:

这里以纵向(基于 x 坐标值)为例:

这里的 x1~x9,就是 SortItem,横向(基于 y 坐标值)同理,特别地,如果是正在拖动的目标节点,会把该节点的 _id 记录在 SortItem 以示区分。

会存在一个处理,把一个方向上的所有 x 坐标进行从小到大的排序,然后一双一双的遍历,需要符合以下条件"必须分别属于相邻的两个节点"的 SortItem 对,也就是 SortItemPair。

在查找所有 SortItemPair 的同时,只会更新并记录节点距离最短的那些 SortItemPair(可能会存在多个)。

核心逻辑代码:

ts 复制代码
  // 磁吸逻辑
  attract = (newPos: Konva.Vector2d) => {
    // 对齐线清除
    this.alignLinesClear()

    // 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

    let pairX: SortItemPair | null = null
    let pairY: SortItemPair | null = null

    // 对齐线 磁吸逻辑
    if (this.render.config.attractNode) {
      // 横向所有需要判断对齐的 x 坐标
      const sortX: Array<SortItem> = []
      // 纵向向所有需要判断对齐的 y 坐标
      const sortY: Array<SortItem> = []

      // 选择目标所有的对齐 x
      sortX.push(
        {
          value: this.render.toStageValue(newPos.x - stageState.x) // 左
        },
        {
          value: this.render.toStageValue(newPos.x - stageState.x + width / 2) // 垂直中
        },
        {
          value: this.render.toStageValue(newPos.x - stageState.x + width) // 右
        }
      )

      // 选择目标所有的对齐 y
      sortY.push(
        {
          value: this.render.toStageValue(newPos.y - stageState.y) // 上
        },
        {
          value: this.render.toStageValue(newPos.y - stageState.y + height / 2) // 水平中
        },
        {
          value: this.render.toStageValue(newPos.y - stageState.y + height) // 下
        }
      )

      // 拖动目标
      const targetIds = this.render.selectionTool.selectingNodes.map((o) => o._id)
      // 除拖动目标的其他
      const otherNodes = this.render.layer.getChildren((node) => !targetIds.includes(node._id))

      // 其他节点所有的 x / y 坐标
      for (const node of otherNodes) {
        // x
        sortX.push(
          {
            id: node._id,
            value: node.x() // 左
          },
          {
            id: node._id,
            value: node.x() + node.width() / 2 // 垂直中
          },
          {
            id: node._id,
            value: node.x() + node.width() // 右
          }
        )
        // y
        sortY.push(
          {
            id: node._id,
            value: node.y() // 上
          },
          {
            id: node._id,
            value: node.y() + node.height() / 2 // 水平中
          },
          {
            id: node._id,
            value: node.y() + node.height() // 下
          }
        )
      }

      // 排序
      sortX.sort((a, b) => a.value - b.value)
      sortY.sort((a, b) => a.value - b.value)

      // x 最短距离
      let XMin = Infinity
      // x 最短距离的【对】(多个)
      let pairXMin: Array<SortItemPair> = []

      // y 最短距离
      let YMin = Infinity
      // y 最短距离的【对】(多个)
      let pairYMin: Array<SortItemPair> = []

      // 一对对比较距离,记录最短距离的【对】
      // 必须是 选择目标 与 其他节点 成【对】
      // 可能有多个这样的【对】

      for (let i = 0; i < sortX.length - 1; i++) {
        // 相邻两个点,必须为 目标节点 + 非目标节点
        if (
          (sortX[i].id === void 0 && sortX[i + 1].id !== void 0) ||
          (sortX[i].id !== void 0 && sortX[i + 1].id === void 0)
        ) {
          // 相邻两个点的 x 距离
          const offset = Math.abs(sortX[i].value - sortX[i + 1].value)
          if (offset < XMin) {
            // 更新 x 最短距离 记录
            XMin = offset
            // 更新 x 最短距离的【对】 记录
            pairXMin = [[sortX[i], sortX[i + 1]]]
          } else if (offset === XMin) {
            // 存在多个 x 最短距离
            pairXMin.push([sortX[i], sortX[i + 1]])
          }
        }
      }

      for (let i = 0; i < sortY.length - 1; i++) {
        // 相邻两个点,必须为 目标节点 + 非目标节点
        if (
          (sortY[i].id === void 0 && sortY[i + 1].id !== void 0) ||
          (sortY[i].id !== void 0 && sortY[i + 1].id === void 0)
        ) {
          // 相邻两个点的 y 距离
          const offset = Math.abs(sortY[i].value - sortY[i + 1].value)
          if (offset < YMin) {
            // 更新 y 最短距离 记录
            YMin = offset
            // 更新 y 最短距离的【对】 记录
            pairYMin = [[sortY[i], sortY[i + 1]]]
          } else if (offset === YMin) {
            // 存在多个 y 最短距离
            pairYMin.push([sortY[i], sortY[i + 1]])
          }
        }
      }

      // 取第一【对】,用于判断距离是否在阈值内

      if (pairXMin[0]) {
        if (Math.abs(pairXMin[0][0].value - pairXMin[0][1].value) < this.render.bgSize / 2) {
          pairX = pairXMin[0]
        }
      }

      if (pairYMin[0]) {
        if (Math.abs(pairYMin[0][0].value - pairYMin[0][1].value) < this.render.bgSize / 2) {
          pairY = pairYMin[0]
        }
      }

      // 优先对齐节点

      // 存在 1或多个 x 最短距离 满足阈值
      if (pairX?.length === 2) {
        for (const pair of pairXMin) {
          // 【对】里的那个非目标节点
          const other = pair.find((o) => o.id !== void 0)
          if (other) {
            // x 对齐线
            const line = new Konva.Line({
              points: _.flatten([
                [other.value, this.render.toStageValue(-stageState.y)],
                [other.value, this.render.toStageValue(this.render.stage.height() - stageState.y)]
              ]),
              stroke: 'blue',
              strokeWidth: this.render.toStageValue(1),
              dash: [4, 4],
              listening: false
            })
            this.alignLines.push(line)
            this.render.layerCover.add(line)
          }
        }
        // 磁贴第一个【对】
        const target = pairX.find((o) => o.id === void 0)
        const other = pairX.find((o) => o.id !== void 0)
        if (target && other) {
          // 磁铁坐标值
          newPosX = newPosX - this.render.toBoardValue(target.value - other.value)
          isAttract = true
        }
      }

      // 存在 1或多个 y 最短距离 满足阈值
      if (pairY?.length === 2) {
        for (const pair of pairYMin) {
          // 【对】里的那个非目标节点
          const other = pair.find((o) => o.id !== void 0)
          if (other) {
            // y 对齐线
            const line = new Konva.Line({
              points: _.flatten([
                [this.render.toStageValue(-stageState.x), other.value],
                [this.render.toStageValue(this.render.stage.width() - stageState.x), other.value]
              ]),
              stroke: 'blue',
              strokeWidth: this.render.toStageValue(1),
              dash: [4, 4],
              listening: false
            })
            this.alignLines.push(line)
            this.render.layerCover.add(line)
          }
        }
        // 磁贴第一个【对】
        const target = pairY.find((o) => o.id === void 0)
        const other = pairY.find((o) => o.id !== void 0)
        if (target && other) {
          // 磁铁坐标值
          newPosY = newPosY - this.render.toBoardValue(target.value - other.value)
          isAttract = true
        }
      }
    }

虽然代码比较冗长,不过逻辑相对还是比较清晰,找到满足条件(小于阈值,足够近,这里阈值为背景网格的一半大小)的 SortItemPair,就可以根据记录的坐标值大小,定义并绘制相应的线条(添加到 layerCover 中),记录在某个变量中:

ts 复制代码
  // 对齐线
  alignLines: Konva.Line[] = []

  // 对齐线清除
  alignLinesClear() {
    for (const line of this.alignLines) {
      line.remove()
    }
    this.alignLines = []
  }

在适合的时候,执行 alignLinesClear 清空失效的对齐线即可。

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

  • 连接线
  • 等等。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼5 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端