前端使用 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源码

示例地址

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel5 小时前
在二维 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·服务器·前端