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

    示例地址

  • 相关阅读:
    第二章《Java程序世界初探》第4节:实战第一仗--计算圆形面积
    【Java-LangChain:使用 ChatGPT API 搭建系统-3】评估输入-分类
    【机试题】LazyIterator迭代器懒加载问题
    沁恒 CH32V208(三): CH32V208 Ubuntu22.04 Makefile VSCode环境配置
    flink cdc原理与使用
    Mybatis动态SQL
    JDK1.8新特性(部分)【Lambda表达式、函数式接口】--学习JavaEE的day41
    和鲸 ModelWhale 与华为 OceanStor 2910 计算型存储完成兼容性测试
    Spring cloud 多种限流方案
    【算法】希尔排序
  • 原文地址:https://www.cnblogs.com/xachary/p/18156521