前端使用 Konva 实现可视化设计器(1)- 无限画布、比例尺

使用 konva 实现一个设计器交互,首先考虑实现设计器的画布。

一个基本的画布:

【展示】网格、比例尺

【交互】拖拽、缩放

“拖拽”是无尽的,“缩放”是基于鼠标焦点的。

最终效果(示例地址):

image

image

image

基本思路:

设计区域 HTML 由两个节点构成,内层挂载一个 Konva.stage 作为画布的开始。

<template>
  <div class="page">
    <header></header>
    <section>
      <header></header>
      <section ref="boardElement">
        <div ref="stageElement"></div>
      </section>
      <footer></footer>
    </section>
    <footer></footer>
  </div>
</template>

image

Konva.stage 暂时先设计3个 Konva.Layer,分别用于绘制背景、所有素材、比例尺。

image

通过 ResizeObserver 使 Konva.stage 的大小与外层 boardElement 保持一致。

为了显示“比例尺” Konva.stage 默认会偏移一些距离,这里定义“比例尺”尺寸为 40px。

    this.stage = new Konva.Stage({
      container: stageEle,
      x: this.rulerSize,
      y: this.rulerSize,
      width: config.width,
      height: config.height
    })

关于“网格背景”,是按照当前设计区域大小、缩放大小、偏移量,计算横向、纵向分别需要绘制多少条 Konva.Line(横向、纵向分别多加1条),同时根据 Konva.stage 的 x,y 进行偏移,用有限的 Konva.Line 模拟无限的网格画布。

      // 格子大小
      const cellSize = this.option.size
      //
      const width = this.stage.width()
      const height = this.stage.height()
      const scaleX = this.stage.scaleX()
      const scaleY = this.stage.scaleY()
      const stageX = this.stage.x()
      const stageY = this.stage.y()

      // 列数
      const lenX = Math.ceil(width / scaleX / cellSize)
      // 行数
      const lenY = Math.ceil(height / scaleY / cellSize)

      const startX = -Math.ceil(stageX / scaleX / cellSize)
      const startY = -Math.ceil(stageY / scaleY / cellSize)

      const group = new Konva.Group()

      group.add(
        new Konva.Rect({
          name: this.constructor.name,
          x: 0,
          y: 0,
          width: width,
          height: height,
          stroke: 'rgba(255,0,0,0.1)',
          strokeWidth: 2 / scaleY,
          listening: false,
          dash: [4, 4]
        })
      )

      // 竖线
      for (let x = startX; x < lenX + startX + 1; x++) {
        group.add(
          new Konva.Line({
            name: this.constructor.name,
            points: _.flatten([
              [cellSize * x, -stageY / scaleY],
              [cellSize * x, (height - stageY) / scaleY]
            ]),
            stroke: '#ddd',
            strokeWidth: 1 / scaleY,
            listening: false
          })
        )
      }

      // 横线
      for (let y = startY; y < lenY + startY + 1; y++) {
        group.add(
          new Konva.Line({
            name: this.constructor.name,
            points: _.flatten([
              [-stageX / scaleX, cellSize * y],
              [(width - stageX) / scaleX, cellSize * y]
            ]),
            stroke: '#ddd',
            strokeWidth: 1 / scaleX,
            listening: false
          })
        )
      }

      this.group.add(group)

关于“比例尺”,与“网格背景”思路差不多,在绘制“刻度”和“数值”的时候相对麻烦一些,例如绘制“数值”的时候,需要动态判断应该使用多大的字体。

              let fontSize = fontSizeMax

              const text = new Konva.Text({
                name: this.constructor.name,
                y: this.option.size / scaleY / 2 - fontSize / scaleY,
                text: (x * cellSize).toString(),
                fontSize: fontSize / scaleY,
                fill: '#999',
                align: 'center',
                verticalAlign: 'bottom',
                lineHeight: 1.6
              })

              while (text.width() / scaleY > (cellSize / scaleY) * 4.6) {
                fontSize -= 1
                text.fontSize(fontSize / scaleY)
                text.y(this.option.size / scaleY / 2 - fontSize / scaleY)
              }
              text.x(nx - text.width() / 2)

关于“拖拽”,这里设计的是通过鼠标右键拖拽画布,通过记录 mousedown 时 Konva.stage 起始位置、鼠标位置,mousemove 时将鼠标位置偏移与Konva.stage 起始位置计算最新的 Konva.stage 的位置即可。

      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        if (e.evt.button === Types.MouseButton.右键) {
          // 鼠标右键
          this.mousedownRight = true

          this.mousedownPosition = { x: this.render.stage.x(), y: this.render.stage.y() }
          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            this.mousedownPointerPosition = { x: pos.x, y: pos.y }
          }

          document.body.style.cursor = 'pointer'
        }
      },
      mouseup: () => {
        this.mousedownRight = false

        document.body.style.cursor = 'default'
      },
      mousemove: () => {
        if (this.mousedownRight) {
          // 鼠标右键拖动
          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            const offsetX = pos.x - this.mousedownPointerPosition.x
            const offsetY = pos.y - this.mousedownPointerPosition.y
            this.render.stage.position({
              x: this.mousedownPosition.x + offsetX,
              y: this.mousedownPosition.y + offsetY
            })

            // 更新背景
            this.render.draws[Draws.BgDraw.name].draw()
            // 更新比例尺
            this.render.draws[Draws.RulerDraw.name].draw()
          }
        }
      }

关于“缩放”,可以参考 konva 官网的缩放示例,思路是差不多的,只是根据实际情况调整了逻辑。

接下来,计划增加下面功能:

  • 鼠标、键盘移动节点
  • 鼠标、键盘单选、多选节点
  • 单个、多个节点拖动时的磁贴交互
  • 键盘复制、粘贴
  • 节点层次单个、批量调整
  • 等等。。。

如果 github Star 能超过 20 个,将很快更新下一篇章。

源码在这,望多多支持

posted @ 2024-04-05 09:20  xachary  阅读(1763)  评论(2编辑  收藏  举报