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

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

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

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

github源码

gitee源码

示例地址

预览框


定位方法

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

ts 复制代码
  // 更新中心位置
  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()
  }

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

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

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

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

绘制预览框

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

ts 复制代码
  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)
    }
  }

通过预览框移动画布

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

ts 复制代码
      // 根据预览框内部拖动,同步画布的移动
      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 居中

可视区域提示框

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

ts 复制代码
      // 放大的时候,显示当前可视区域提示框
      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 种情况:

实际上,不希望"提示框"超出"预览框",于是才有上面"穷举"的处理逻辑。


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

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

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

源码

gitee源码

示例地址

相关推荐
EnCi Zheng13 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen17 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技18 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人29 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实29 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha40 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习