请大家动动小手,给我一个免费的 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 呢?勾勾手指~