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

请大家动动小手,给我一个免费的 Star 吧~

这一章实现导入导出为JSON文件、另存为图片、上一步、下一步。

github源码

gitee源码

示例地址

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

导出为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源码

示例地址

发表评论

评论已关闭。

相关文章