前端使用 Konva 实现可视化设计器(8)- 预览框

konva · 浏览次数 : 0

小编点评

**代码展示** ```js // ... // 9种情况的绘制逻辑 let points: Array<[number, number]> = []; // 上超出,不绘制“上边” if (x1 > minX && x1 < maxX && x2 > minX && x2 < maxX && y1 > minY && y1 < maxY && y2 > minY && y2 < maxY) { points.push([x1, y1], [x2, y1], [x2, y2], [x1, y2]); } // 下超出,不绘制“下边” ... // 右下超出,不绘制“下边”、“右边” ... // 其他情况绘制线 ... // 监听鼠标点击事件,绘制提示框 this.group.add(new Konva.Line({ // ... }); // 复制提取的节点,用作预览 for (const node of nodes) { const copy = node.clone(); // ... this.group.add(copy); } // 将所有图形添加到舞台中 this.group.add(this.group); ``` **排版** 为了使文章更易于阅读,可以采用以下排版方法: * 使用缩进来缩短文字长度。 * 使用标题来区分不同的部分。 * 使用脚注来引用代码中的注释。 **代码示例地址** 代码示例地址已在代码代码中提供,请参阅代码。 **其他补充** * 可以根据需要修改线颜色、宽度和其他参数。 * 可以根据需要添加鼠标点击事件监听。

正文

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

大家如果发现了明显的 Bug,可以提 Issue 哟~

这一章我们实现一个预览框,实时、可交互定位的。

github源码

gitee源码

示例地址

预览框

image
image

定位方法

移动画布,将传入 x,y 作为画布中心:

  // 更新中心位置
  updateCenter(x = 0, y = 0) {
    // stage 状态
    const stageState = this.render.getStageState()

    // 提取节点
    const nodes = this.render.layer.getChildren((node) => {
      return !this.render.ignore(node)
    })

    // 计算节点占用的区域(计算起点即可)
    let minX = 0
    let minY = 0
    for (const node of nodes) {
      const x = node.x()
      const y = node.y()

      if (x < minX) {
        minX = x
      }
      if (y < minY) {
        minY = y
      }
    }

    // 居中画布
    this.render.stage.setAttrs({
      x: stageState.width / 2 - this.render.toBoardValue(minX) - this.render.toBoardValue(x),
      y: stageState.height / 2 - this.render.toBoardValue(minY) - this.render.toBoardValue(y)
    })

    // 更新背景
    this.render.draws[Draws.BgDraw.name].draw()
    // 更新比例尺
    this.render.draws[Draws.RulerDraw.name].draw()
    // 更新参考线
    this.render.draws[Draws.RefLineDraw.name].draw()
    // 更新预览
    this.render.draws[Draws.PreviewDraw.name].draw()
  }

比较难表达,尝试画个图说明:
image

为了简化,维持画布初始位置,可以把 minX 和 minY 视为 0。

"前"即是 stage 起始位置 也是可视区域,可视区域是固定的,当点击 x,y 坐标时,为了使移动之前 x,y 相对 stage 的位置,移动到可视区域居中位置,stage 就要如图那样“反向”偏移。

分解步骤,可以分为 3 步,"前"、“中”、“后”,对应计算“居中画布”处。

绘制预览框

下面的代码比较长,添加了必要的注释,会重点解释 move 和“可视区域提示框”两部分逻辑。

  override draw() {
    if (this.render.config.showPreview) {
      this.clear()

      // stage 状态
      const stageState = this.render.getStageState()

      // 预览框的外边距
      const previewMargin = 20

      // 预览框 group
      const group = new Konva.Group({
        name: 'preview',
        scale: {
          x: this.render.toStageValue(this.option.size),
          y: this.render.toStageValue(this.option.size)
        },
        width: stageState.width,
        height: stageState.height
      })

      const main = this.render.stage.find('#main')[0] as Konva.Layer

      // 提取节点
      const nodes = main.getChildren((node) => {
        return !this.render.ignore(node)
      })

      // 计算节点占用的区域
      let minX = 0
      let maxX = group.width()
      let minY = 0
      let maxY = group.height()
      for (const node of nodes) {
        const x = node.x()
        const y = node.y()
        const width = node.width()
        const height = node.height()

        if (x < minX) {
          minX = x
        }
        if (x + width > maxX) {
          maxX = x + width
        }
        if (y < minY) {
          minY = y
        }
        if (y + height > maxY) {
          maxY = y + height
        }
      }

      // 根据占用的区域调整预览框的大小
      group.setAttrs({
        x: this.render.toStageValue(
          -stageState.x + stageState.width - maxX * this.option.size - previewMargin
        ),
        y: this.render.toStageValue(
          -stageState.y + stageState.height - maxY * this.option.size - previewMargin
        ),
        width: maxX - minX,
        height: maxY - minY
      })

      // 预览框背景
      const bg = new Konva.Rect({
        name: this.constructor.name,
        x: minX,
        y: minY,
        width: group.width(),
        height: group.height(),
        stroke: '#666',
        strokeWidth: this.render.toStageValue(1),
        fill: '#eee'
      })

      // 根据预览框内部拖动,同步画布的移动
      const move = () => {
        // 略,下面有单独说明
      }

      // 预览框内拖动事件处理
      bg.on('mousedown', (e) => {
        if (e.evt.button === Types.MouseButton.左键) {
          move()
        }
        e.evt.preventDefault()
      })
      bg.on('mousemove', (e) => {
        if (this.state.moving) {
          move()
        }
        e.evt.preventDefault()
      })
      bg.on('mouseup', () => {
        this.state.moving = false
      })

      group.add(bg)

      // 预览框 边框
      group.add(
        new Konva.Rect({
          name: this.constructor.name,
          x: 0,
          y: 0,
          width: stageState.width,
          height: stageState.height,
          stroke: 'rgba(255,0,0,0.2)',
          strokeWidth: 1 / this.option.size,
          listening: false
        })
      )

      // 复制提取的节点,用作预览
      for (const node of nodes) {
        const copy = node.clone()
        // 不可交互
        copy.listening(false)
        // 设置名称用于 ignore
        copy.name(this.constructor.name)
        group.add(copy)
      }

      // 放大的时候,显示当前可视区域提示框
      if (stageState.scale > 1) {
        // 略,下面有单独说明
      }

      this.group.add(group)
    }
  }

通过预览框移动画布

image
上面介绍了“定位方法”,基于它,通过预览框也可以使画布同步移动,前提就是要把“预览框”内部的坐标转换成“画布”的坐标。

      // 根据预览框内部拖动,同步画布的移动
      const move = () => {
        this.state.moving = true

        const pos = this.render.stage.getPointerPosition()
        if (pos) {
          const pWidth = group.width() * this.option.size
          const pHeight = group.height() * this.option.size

          const pOffsetX = pWidth - (stageState.width - pos.x - previewMargin)
          const pOffsetY = pHeight - (stageState.height - pos.y - previewMargin)

          const offsetX = pOffsetX / this.option.size
          const offsetY = pOffsetY / this.option.size

          // 点击预览框,点击位置作为画布中心
          this.render.positionTool.updateCenter(offsetX, offsetY)
        }
      }

上面转换的思路就是:

1、通过 group 和倍数反推计算占用的区域可视大小
2、计算可视居中坐标
3、计算逻辑居中坐标(使用倍数恢复)
4、通过 updateCenter 居中

可视区域提示框

当放大的时候,会显示当前可视区域提示框
image

      // 放大的时候,显示当前可视区域提示框
      if (stageState.scale > 1) {
        // 画布可视区域起点坐标(左上)
        let x1 = this.render.toStageValue(-stageState.x + this.render.rulerSize)
        let y1 = this.render.toStageValue(-stageState.y + this.render.rulerSize)
        // 限制可视区域提示框不能超出预览区域
        x1 = x1 > minX ? x1 : minX
        x1 = x1 < maxX ? x1 : maxX
        y1 = y1 > minY ? y1 : minY
        y1 = y1 < maxY ? y1 : maxY

        // 画布可视区域起点坐标(右下)
        let x2 =
          this.render.toStageValue(-stageState.x + this.render.rulerSize) +
          this.render.toStageValue(stageState.width)
        let y2 =
          this.render.toStageValue(-stageState.y + this.render.rulerSize) +
          this.render.toStageValue(stageState.height)
        // 限制可视区域提示框不能超出预览区域
        x2 = x2 > minX ? x2 : minX
        x2 = x2 < maxX ? x2 : maxX
        y2 = y2 > minY ? y2 : minY
        y2 = y2 < maxY ? y2 : maxY

        // 可视区域提示框 连线坐标序列
        let points: Array<[x: number, y: number]> = []

        // 可视区域提示框“超出”预览区域影响的“边”不做绘制
        // "超出"(上面实际处理:把超过的坐标设置为上/下线,判断方式如[以正则表达式表示]:(x|y)(1|2) === (min|max)(X|Y))
        //
        // 简单直接穷举 9 种情况:
        // 不超出
        // 上超出 下超出
        // 左超出 右超出
        // 左上超出 右上超出
        // 左下超出 右下超出

        // 不超出,绘制完整矩形
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x1, y1],
            [x2, y1],
            [x2, y2],
            [x1, y2],
            [x1, y1]
          ]
        }
        // 上超出,不绘制“上边”
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 === minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x2, y1],
            [x2, y2],
            [x1, y2],
            [x1, y1]
          ]
        }
        // 下超出,不绘制“下边”
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 === maxY
        ) {
          points = [
            [x1, y2],
            [x1, y1],
            [x2, y1],
            [x2, y2]
          ]
        }
        // 左超出,不绘制“左边”
        if (
          x1 === minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x1, y1],
            [x2, y1],
            [x2, y2],
            [x1, y2]
          ]
        }
        // 右超出,不绘制“右边”
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 === maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x2, y1],
            [x1, y1],
            [x1, y2],
            [x2, y2]
          ]
        }
        // 左上超出,不绘制“上边”、“左边”
        if (
          x1 === minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 === minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x2, y1],
            [x2, y2],
            [x1, y2]
          ]
        }
        // 右上超出,不绘制“上边”、“右边”
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 === maxX &&
          y1 === minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 < maxY
        ) {
          points = [
            [x2, y2],
            [x1, y2],
            [x1, y1]
          ]
        }
        // 左下超出,不绘制“下边”、“左边”
        if (
          x1 === minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 < maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 === maxY
        ) {
          points = [
            [x1, y1],
            [x2, y1],
            [x2, y2]
          ]
        }
        // 右下超出,不绘制“下边”、“右边”
        if (
          x1 > minX &&
          x1 < maxX &&
          x2 > minX &&
          x2 === maxX &&
          y1 > minY &&
          y1 < maxY &&
          y2 > minY &&
          y2 === maxY
        ) {
          points = [
            [x2, y1],
            [x1, y1],
            [x1, y2]
          ]
        }

        // 可视区域提示框
        group.add(
          new Konva.Line({
            name: this.constructor.name,
            points: _.flatten(points),
            stroke: 'blue',
            strokeWidth: 1 / this.option.size,
            listening: false
          })
        )
      }

      // 复制提取的节点,用作预览
      for (const node of nodes) {
        const copy = node.clone()
        // 不可交互
        copy.listening(false)
        // 设置名称用于 ignore
        copy.name(this.constructor.name)
        group.add(copy)
      }

      this.group.add(group)
    }
  }

除了上面必要的注释,还是画一张图表示这 9 种情况:
image
实际上,不希望“提示框”超出“预览框”,于是才有上面“穷举”的处理逻辑。
image
image

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

  • 对齐效果
  • 连接线
  • 等等。。。

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

源码

gitee源码

示例地址

与前端使用 Konva 实现可视化设计器(8)- 预览框相似的内容:

前端使用 Konva 实现可视化设计器(8)- 预览框

请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了明显的 Bug,可以提 Issue 哟~ 这一章我们实现一个预览框,实时、可交互定位的。 github源码 gitee源码 示例地址 预览框 定位方法 移动画布,将传入 x,y 作为画布中心: // 更新中心位置 updateCenter

前端使用 Konva 实现可视化设计器(16)- 旋转对齐、触摸板操作的优化

这一章解决两个缺陷,一是调整一些快捷键,使得 Mac 触摸板可以正常操作;二是修复一个 Issue,使得即使素材节点即使被旋转之后,也能正常触发磁贴对齐效果,有个小坑需要注意。

前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化

本章将处理一些缺陷的同时,实现支持连接点的自定义,一个节点可以定义多个连接点,最终可以满足类似图元接线的效果。

前端使用 Konva 实现可视化设计器(14)- 折线 - 最优路径应用【代码篇】

话接上回[《前端使用 Konva 实现可视化设计器(13)- 折线 - 最优路径应用【思路篇】》](https://www.cnblogs.com/xachary/p/18238704),这一章继续说说相关的代码如何构思的,如何一步步构建数据模型可供 AStar 算法进行路径规划,最终画出节点之间的...

前端使用 Konva 实现可视化设计器(13)- 折线 - 最优路径应用【思路篇】

这一章把直线连接改为折线连接,沿用原来连接点的关系信息。关于折线的计算,使用的是开源的 AStar 算法进行路径规划,启发方式为 曼哈顿距离,且不允许对角线移动。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示

前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示例地址 相关定义 连接点 记

前端使用 Konva 实现可视化设计器(11)- 对齐效果

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

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

前端使用 Konva 实现可视化设计器,这次实现对齐线的交互功能,单个、多个、多选都可以对齐,同时还能磁贴。

前端使用 Konva 实现可视化设计器(9)- 另存为SVG

请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示例地址 另存为SVG 这一章增强了另存为的能力,实现“另存为SVG”,大概是全网唯一的实例分享了吧。 灵感来源:react-konva-custom-context

前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步

请大家动动小手,给我一个免费的 Star 吧~ 这一章实现导入导出为JSON文件、另存为图片、上一步、下一步。 github源码 gitee源码 示例地址 导出为JSON文件 提取需要导出的内容 getView() { // 复制画布 const copy = this.render.stage.c