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


    请大家动动小手,给我一个免费的 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源码

    示例地址

  • 相关阅读:
    Python&C++相互混合调用编程全面实战-24QT按钮事件的Open槽函数中调用python函数
    大语言模型(LLM)综述(二):开发大语言模型的公开可用资源
    SpringBoot bbs (二) 抓取标签<img>并且打包
    缩短从需求到上线的距离:集成多种工程实践的稳定框架 | 开源日报 No.55
    《windows核心编程》第3章 内核对象
    Context与Reducer
    博士论文答辩流程
    图像处理算法大全(基于libyuv或IPP)----RGB32(ARGB)转成yuv420,RGB24,nv12,yuv422集合
    [云原生案例2.3 ] Kubernetes的部署安装 【多master集群架构高可用 ---- (二进制安装部署)】
    算法37|738,714,968
  • 原文地址:https://www.cnblogs.com/xachary/p/18168732