前端使用 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 個,將很快更新下一篇章。

源碼在這,望多多支持

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章