前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步

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

示例地址

posted @ 2024-04-24 22:25  xachary  阅读(302)  评论(0编辑  收藏  举报