前端使用 Konva 實現視覺化設計器(7)- 匯入匯出、上一步、下一步

xachary發表於2024-04-24

請大家動動小手,給我一個免費的 Star 吧~

這一章實現匯入匯出為JSON檔案、另存為圖片、上一步、下一步。

github原始碼

gitee原始碼

示例地址

image

匯出為JSON檔案

提取需要匯出的內容

  getView() {
    // 複製畫布
    const copy = this.render.stage.clone()
    // 提取 main layer 備用
    const main = copy.find('#main')[0] as Konva.Layer
    // 暫時清空所有 layer
    copy.removeChildren()

    // 提取節點
    let nodes = main.getChildren((node) => {
      return !this.render.ignore(node) && !this.render.ignoreDraw(node)
    })

    // 重新裝載節點
    const layer = new Konva.Layer()
    layer.add(...nodes)
    nodes = layer.getChildren()

    // 計算節點佔用的區域
    let minX = 0
    let maxX = copy.width()
    let minY = 0
    let maxY = copy.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
      }

      if (node.attrs.nodeMousedownPos) {
        // 修正正在選中的節點透明度
        node.setAttrs({
          opacity: copy.attrs.lastOpacity ?? 1
        })
      }
    }

    // 重新裝載 layer
    copy.add(layer)

    // 節點佔用的區域
    copy.setAttrs({
      x: -minX,
      y: -minY,
      scale: { x: 1, y: 1 },
      width: maxX - minX,
      height: maxY - minY
    })

    // 返回可視節點和 layer
    return copy
  }

1、首先複製一份畫布
2、取出 main layer
3、篩選目標節點
4、計算目標節點佔用區域
5、調整複製畫布的位置和大小

匯出 JSON

使用 stage 的 toJSON 即可。

  // 儲存
  save() {
    const copy = this.getView()

    // 透過 stage api 匯出 json
    return copy.toJSON()
  }

匯入 JSON,恢復畫布

相比匯出,過程會比較複雜一些。

恢復節點結構

  // 恢復
  async restore(json: string, silent = false) {
    try {
      // 清空選擇
      this.render.selectionTool.selectingClear()

      // 清空 main layer 節點
      this.render.layer.removeChildren()

      // 載入 json,提取節點
      const container = document.createElement('div')
      const stage = Konva.Node.create(json, container)
      const main = stage.getChildren()[0]
      const nodes = main.getChildren()

      // 恢復節點圖片素材
      await this.restoreImage(nodes)

      // 往 main layer 插入新節點
      this.render.layer.add(...nodes)

      // 上一步、下一步 無需更新 history 記錄
      if (!silent) {
        // 更新歷史
        this.render.updateHistory()
      }
    } catch (e) {
      console.error(e)
    }
  }

1、清空選擇
2、清空 main layer 節點
3、建立臨時 stage
4、透過 Konva.Node.create 恢復 JSON 定義的節點結構
5、恢復圖片素材(關鍵)

恢復圖片素材

  // 載入 image(用於匯入)
  loadImage(src: string) {
    return new Promise<HTMLImageElement | null>((resolve) => {
      const img = new Image()
      img.onload = () => {
        // 返回載入完成的圖片 element
        resolve(img)
      }
      img.onerror = () => {
        resolve(null)
      }
      img.src = src
    })
  }
  // 恢復圖片(用於匯入)
  async restoreImage(nodes: Konva.Node[] = []) {
    for (const node of nodes) {
      if (node instanceof Konva.Group) {
        // 遞迴
        await this.restoreImage(node.getChildren())
      } else if (node instanceof Konva.Image) {
        // 處理圖片
        if (node.attrs.svgXML) {
          // svg 素材
          const blob = new Blob([node.attrs.svgXML], { type: 'image/svg+xml' })
          // dataurl
          const url = URL.createObjectURL(blob)
          // 載入為圖片 element
          const image = await this.loadImage(url)
          if (image) {
            // 設定圖片
            node.image(image)
          }
        } else if (node.attrs.gif) {
          // gif 素材
          const imageNode = await this.render.assetTool.loadGif(node.attrs.gif)
          if (imageNode) {
            // 設定圖片
            node.image(imageNode.image())
          }
        } else if (node.attrs.src) {
          // 其他圖片素材
          const image = await this.loadImage(node.attrs.src)
          if (image) {
            // 設定圖片
            node.image(image)
          }
        }
      }
    }
  }

關於恢復 svg,關鍵在於拖入 svg 的時候,記錄了完整的 svg xml 在屬性 svgXML 中。

關於恢復 gif、其他圖片,拖入的時候記錄其 src 地址,就可以恢復到節點中。

上一步、下一步

其實就是需要記錄歷史記錄

歷史記錄

  history: string[] = []
  historyIndex = -1

  updateHistory() {
    this.history.splice(this.historyIndex + 1)
    this.history.push(this.importExportTool.save())
    this.historyIndex = this.history.length - 1
    // 歷史變化事件
    this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
  }

1、從當前歷史位置,捨棄後面的記錄
2、從當前歷史位置,覆蓋最新的 JSON 記錄
3、更新位置
4、暴露事件(用於外部判斷歷史狀態,以此禁用、啟用上一步、下一步)

更新歷史記錄

一切會產生變動的位置都執行 updateHistory,如拖入素材、移動節點、改變節點位置、改變節點大小、複製貼上節點、刪除節點、改變節點的層次。具體程式碼就不貼了,只是在影響的地方執行一句:

this.render.updateHistory()

上一步、下一步方法

  prevHistory() {
    const record = this.history[this.historyIndex - 1]
    if (record) {
      this.importExportTool.restore(record, true)
      this.historyIndex--
      // 歷史變化事件
      this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
    }
  }

  nextHistory() {
    const record = this.history[this.historyIndex + 1]
    if (record) {
      this.importExportTool.restore(record, true)
      this.historyIndex++
      // 歷史變化事件
      this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
    }
  }

另存為圖片

  // 獲取圖片
  getImage(pixelRatio = 1, bgColor?: string) {
    // 獲取可視節點和 layer
    const copy = this.getView()

    // 背景層
    const bgLayer = new Konva.Layer()

    // 背景矩形
    const bg = new Konva.Rect({
      listening: false
    })
    bg.setAttrs({
      x: -copy.x(),
      y: -copy.y(),
      width: copy.width(),
      height: copy.height(),
      fill: bgColor
    })

    // 新增背景
    bgLayer.add(bg)

    // 插入背景
    const children = copy.getChildren()
    copy.removeChildren()
    copy.add(bgLayer)
    copy.add(children[0], ...children.slice(1))

    // 透過 stage api 匯出圖片
    return copy.toDataURL({ pixelRatio })
  }

主要關注有2點:
1、插入背景層
2、設定匯出圖片的尺寸

匯出的時候,其實就是把當前向量、非向量素材統一輸出為非向量的圖片,設定匯出圖片的尺寸越大,可以保留更多的向量素材細節。

接下來,計劃實現下面這些功能:

  • 實時預覽窗
  • 對齊效果
  • 連線線
  • 等等。。。

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

原始碼

gitee原始碼

示例地址

相關文章