前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步
请大家动动小手,给我一个免费的 Star 吧~
这一章实现导入导出为JSON文件、另存为图片、上一步、下一步。

导出为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 呢?勾勾手指~
前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步的更多相关文章
- 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!
		官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ... 
- ActiveReports 9 新功能:可视化查询设计器(VQD)介绍
		在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ... 
- (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用
		一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ... 
- Windows Phone 十二、设计器同步
		在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ... 
- WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?
		目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ... 
- C#基础系列:开发自己的窗体设计器(PropertyGrid显示中文属性名)
		既然是一个窗体设计器,那就应该能够设置控件的属性,设置属性最好的当然是PropertyGrid了,我们仅仅需要使用一个PropertyGrid.SelectedObject = Control就可以搞 ... 
- 通过用 .NET 生成自定义窗体设计器来定制应用程序
		通过用 .NET 生成自定义窗体设计器来定制应用程序 https://www.microsoft.com/china/MSDN/library/netFramework/netframework/Cu ... 
- Qt编写自定义控件属性设计器
		以前做.NET开发中,.NET直接就集成了属性设计器,VS不愧是宇宙第一IDE,你能够想到的都给你封装好了,用起来不要太爽!因为项目需要自从全面转Qt开发已经6年有余,在工业控制领域,有一些应用场景需 ... 
- VS2015 android 设计器不能可视化问题解决。
		近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ... 
- 可视化流程设计——流程设计器演示(基于Silverlight)
		上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ... 
随机推荐
- [.NET项目实战] Elsa开源工作流组件应用(二):内核解读
			@ 目录 定义 变量 内存寄存器类 寄存器中的存储区块类 变量到存储的映射类 上下文对象 活动上下文(ActivityExecutionContext) 工作流执行上下文(WorkflowExecut ... 
- objective-c之Class底层结构探索
			isa 走位图 在讲 OC->Class 底层类结构之前,先看下下面这张图: 通过isa走位图 得出的结论是: 1,类,父类,元类都包含了 isa, superclass 2,对象isa指向类对 ... 
- drools执行指定的规则
			目录 1.背景 2.方案 2.1 通过AgendaFilter来实现 2.2 通过entry-point来实现 3.实现 3.1 需求 3.2 drl 文件编写 3.3 部分java代码 3.4 运行 ... 
- vue项目 nginx部署
			nginx.conf中的server配置片段 server { listen 8080 ;#默认端口是80,如果端口没被占用可以不用修改 server_name localhost; #charset ... 
- verilog之时钟信号的编写2
			verilog之时钟信号的编写2 1.时钟信号的特点 时钟信号除了可以根据时序一个个变化列举出来,还可以利用其循环的变化的特点,使用循环执行语句always来实现.这种方法实现的时钟信号可以一直执行且 ... 
- Oracle通过数据库链连接KingbaseES
			测试环境: ip 数据库版本 192.168.254.135 oracle 11g 192.168.254.137 V008R006C005B0023 通过oracle官网得知,Oracle使用DG4 ... 
- hadoop集群实现分发文件命令xsync脚本文件
			1 #!/bin/bash 2 3 #1. 判断参数个数 4 if [ $# -lt 1 ] 5 then 6 echo Not Enough Arguement! 7 exit; 8 fi 9 10 ... 
- Codeforces Round #726 (Div. 2)
			CF1537A Arithmetic Array 洛谷传送门 CF1537A 分析 用这 \(n\) 个数的总和 \(sum\) 判断: 如果 \(sum<n\) 直接用 \(n-sum+1\) ... 
- #树形dp#洛谷 1272 重建道路
			题目 给出一个大小为 \(n\) 的树, 问至少断掉多少条边使得存在一个大小为 \(m\) 的连通块 \(n\leq 150\) 分析 设 \(dp[x][s]\) 表示以 \(x\) 为根的子树至少 ... 
- 安装HTMLTestRunner库
			安装 HTMLTestRunner 库的方法非常简单,直接 pip 就可以了 pip install html-testRunner 在 https://pypi.org/ 中可以直接搜索到,并且官 ... 
