前端使用 Konva 实现可视化设计器(17)- 素材嵌套 - 生成阶段

本章主要实现素材的嵌套(生成阶段)这意味着可以拖入画布的对象,不只是图片素材,还可以是嵌套的图片和图形。在未来的章节中,应该可以实现素材成组/解散的效果。

最近难以抽出时间继续本示例更新,以至于拖到今天才更新这一章…

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

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

一些调整和优化

1、原本分散在各处的不同层的 draw 方法调用,现在基本上统一调用 render 的 redraw 方法,简化代码逻辑(暂未发现明显的性能问题)。
2、修复右键无法删除连接线的问题,主要是 stage 的 contextmenu 事件实测 target 无法得到指向的 Line 实例,目前使用 Konva.Util.haveIntersection 处理,以下是该逻辑的代码片段:

// src/Render/draws/ContextmenuDraw.tsconst linkGroup = this.render.layerCover.find(`.${Draws.LinkDraw.name}`)[0] as Konva.Group// 右键目标可能为 连接线let lineSelection: Konva.Node | null = nullif (linkGroup) {const linkLines = linkGroup.find('.link-line')for (const line of linkLines) {if (Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, line.getClientRect())) {// 右键目标为 连接线lineSelection = linebreak}}}if (pos.x === this.state.lastPos.x || pos.y === this.state.lastPos.y) {// 右键 连接线/其它目标this.state.target = lineSelection ?? e.target} else {this.state.target = null}

3、原来的对齐逻辑没有考虑目标被 rotate 后的情况,现已经修复支持了,实现方式还是使用三角函数计算:

在这里插入图片描述

红框是 rotate 后的占用区域,通过旋转角度就可以计算该区域的宽高,left、right、top、bottom 也是用于计算占用区域的 x,y 坐标:
在这里插入图片描述

// src/Render/tools/AlignTool.tscalcNodeRotationInfo(node: Konva.Node) {const rotate = node.rotation()const offsetLeft = node.height() * Math.sin((rotate * Math.PI) / 180)const offsetRight = node.width() * Math.cos((rotate * Math.PI) / 180)const offsetTop = node.height() * Math.cos((rotate * Math.PI) / 180)const offsetBottom = node.width() * Math.sin((rotate * Math.PI) / 180)const width = Math.abs(offsetLeft) + Math.abs(offsetRight)const height = Math.abs(offsetTop) + Math.abs(offsetBottom)let x = node.x()if ((rotate >= 0 && rotate < 90) || (rotate >= -360 && rotate < -270)) {x = x - Math.abs(offsetLeft)} else if ((rotate >= 90 && rotate < 180) || (rotate >= -270 && rotate < -180)) {x = x - width} else if ((rotate >= 180 && rotate < 270) || (rotate >= -180 && rotate < -90)) {x = x - Math.abs(offsetRight)} else if ((rotate >= 270 && rotate < 360) || (rotate >= -90 && rotate < 0)) {// 无需处理}let y = node.y()if ((rotate >= 0 && rotate < 90) || (rotate >= -360 && rotate < -270)) {// 无需处理} else if ((rotate >= 90 && rotate < 180) || (rotate >= -270 && rotate < -180)) {y = y - Math.abs(offsetTop)} else if ((rotate >= 180 && rotate < 270) || (rotate >= -180 && rotate < -90)) {y = y - height} else if ((rotate >= 270 && rotate < 360) || (rotate >= -90 && rotate < 0)) {y = y - Math.abs(offsetBottom)}return {x,y,width,height}}

进入正题

调整资源的定义

由于嵌套之后的素材,不是图片,需要额外一个封面图片 avatar,用于左侧菜单的显示:

// src/Render/types.ts
export interface AssetInfo {url: stringavatar?: string // 子素材需要额外的封面points?: Array<AssetInfoPoint>
}

增加示例数据:

// src/App.vue
const assetsModules: Array<Types.AssetInfo> = [{ "url": "./json/1.json", avatar: './json/1.png' },{ "url": "./json/2.json", avatar: './json/2.png' },{ "url": "./json/3.json", avatar: './json/3.png' },{ "url": "./json/4.json", avatar: './json/4.png' },// 略
}

上面的示例数据是嵌套生成,稍后再细说实现。生成一个嵌套素材,分 2 步骤:

      <button @click="onSaveAsset">另存为元素</button><button @click="onSaveAssetPNG">另存为元素图片</button>

另存为元素,输出的就是 json 文件;另存为元素图片,就是上面的封面 avatar。

// src/App.vue// 另存为元素
function onSaveAsset() {if (render) {const a = document.createElement('a')const event = new MouseEvent('click')a.download = 'asset.json'a.href = window.URL.createObjectURL(new Blob([render.importExportTool.getAsset()]))a.dispatchEvent(event)a.remove()}
}// 另存为元素图片
function onSaveAssetPNG() {if (render) {// 3倍尺寸、白色背景const url = render.importExportTool.getAssetImage(3, '#ffffff')const a = document.createElement('a')const event = new MouseEvent('click')a.download = 'image'a.href = urla.dispatchEvent(event)a.remove()}
}

关键就是方法 getAsset、getAssetImage,实现逻辑后面细说。

则,左侧菜单素材,优先显示 avatar 封面:

<img :src="item.avatar || item.url" />

将当前多个素材,组合成单个素材

上面提到了 getAsset,它是生成满足需求的 json 内容的,依赖关系:

getAsset(新) <- getAssetView(新) <- getView(已有)

已经存在的 getView 方法,已经用于 导出 json、另存为图片、另存为 svg,此次仅做了一些优化,它的作用基本上依然是:

1、clone 当前 stage
2、移除被认为应该 ignore 的节点
3、如需要,重新加入连接线的 Line 实例(操作过程中,连接线是实时绘制的)
4、计算并处理节点(们)占用的区域
5、返回处理好的 clone

基于 getView,为了组合多个素材,需要实现一个 getAssetView:

// src/Render/tools/ImportExportTool.ts/*** 获得显示内容(用于另存为元素)* @returns Konva.Stage*/getAssetView() {const copy = this.getView(true)const children = copy.getChildren()[0].getChildren()const nodes: Konva.Stage | Konva.Layer | Konva.Group | Konva.Node[] = [...children]let minX = Infinity,maxX = -Infinity,minY = Infinity,maxY = -Infinity,minStartX = Infinity,minStartY = Infinityfor (const node of nodes) {if (node instanceof Konva.Group) {if (node.x() < minX) {minX = node.x()}if (node.x() + node.width() > maxX) {maxX = node.x() + node.width()}if (node.y() < minY) {minY = node.y()}if (node.y() + node.height() > maxY) {maxY = node.y() + node.height()}if (node.x() < minStartX) {minStartX = node.x()}if (node.y() < minStartY) {minStartY = node.y()}// 移除辅助元素if (node instanceof Konva.Group) {const clickMask = node.findOne('#click-mask')if (clickMask) {clickMask.destroy()}}} else if (node instanceof Konva.Line && node.name() === 'link-line') {// 连线占用空间const points = node.points()for (let i = 0; i < points.length; i += 2) {const [x, y] = [points[i], points[i + 1]]if (x < minX) {minX = x - 1}if (x > maxX) {maxX = x + 1}if (y < minY) {minY = y - 1}if (y > maxY) {maxY = y + 1}if (x < minStartX) {minStartX = x - 1}if (y < minStartY) {minStartY = y - 1}}}}for (const node of nodes) {if (node instanceof Konva.Group) {node.x(node.x() - minStartX)node.y(node.y() - minStartY)} else if (node instanceof Konva.Line && node.name() === 'link-line') {const points = node.points()for (let i = 0; i < points.length; i += 2) {points[i] = points[i] - minStartXpoints[i + 1] = points[i + 1] - minStartY}node.points(points)}}copy.x(0)copy.y(0)copy.width(maxX - minX)copy.height(maxY - minY)return copy}

区别于 getView,计算占用区域是有差异的(绿色:getView,红色:getAssetView):
在这里插入图片描述
因为,经过组合的素材,是包含连接线 Line 的实例的,所以还要考虑连接线的占用区域:
在这里插入图片描述
最后,导出之前,把所有内容,都进行一次移动,整体移动至 0,0 点。
在这里插入图片描述
接着是实现 getAsset:

// src/Render/tools/ImportExportTool.ts/*** 获得元素(用于另存为元素)* @returns Konva.Stage*/getAsset() {const copy = this.getAssetView()const json = copy.toJSON()const obj = JSON.parse(json)const assets = obj.children[0].childrenfor (const asset of assets) {if (asset.attrs.name === 'asset') {asset.attrs.name = 'sub-asset'}if (asset.attrs.selected) {asset.attrs.selected = false}}this.render.linkTool.jsonIdCover(assets)// 通过 stage api 导出 jsonconst result = JSON.stringify({...obj.children[0],className: 'Group',attrs: {width: copy.width(),height: copy.height(),x: 0,y: 0}})copy.destroy()return result}

主要逻辑有 2 个:
1、把 getAssetView 处理好的 stage 导出为 json。
2、把组合前多个 asset 的 name 改为 sub-asset,意为“子素材”,与组合后的 asset 区别开。
3、把组合前多个 asset 的内部 id 刷新一遍,通过 jsonIdCover 方法。

// src/Render/tools/LinkTool.ts// 刷新 json 的 id、事件jsonIdCover(assets: any[]) {let deepAssets = [...assets]const idMap = new Map()while (deepAssets.length > 0) {const asset = deepAssets.shift()if (asset) {if (Array.isArray(asset.attrs.points)) {for (const point of asset.attrs.points) {if (Array.isArray(point.pairs)) {for (const pair of point.pairs) {if (pair.from.groupId && !idMap.has(pair.from.groupId)) {idMap.set(pair.from.groupId, 'g:' + nanoid())}if (pair.to.groupId && !idMap.has(pair.to.groupId)) {idMap.set(pair.to.groupId, 'g:' + nanoid())}if (pair.from.pointId && !idMap.has(pair.from.pointId)) {idMap.set(pair.from.pointId, 'p:' + nanoid())}if (pair.to.pointId && !idMap.has(pair.to.pointId)) {idMap.set(pair.to.pointId, 'p:' + nanoid())}}}if (point.id) {if (!idMap.has(point.id)) {idMap.set(point.id, 'p:' + nanoid())}}if (point.groupId) {if (!idMap.has(point.groupId)) {idMap.set(point.groupId, 'g:' + nanoid())}}}}if (asset.attrs.id) {if (!idMap.has(asset.attrs.id)) {idMap.set(asset.attrs.id, 'n:' + nanoid())}}if (Array.isArray(asset.children)) {deepAssets.push(...asset.children)}}}deepAssets = [...assets]while (deepAssets.length > 0) {const asset = deepAssets.shift()if (asset) {if (idMap.has(asset.attrs.id)) {asset.attrs.id = idMap.get(asset.attrs.id)}if (Array.isArray(asset.attrs.points)) {for (const point of asset.attrs.points) {if (Array.isArray(point.pairs)) {for (const pair of point.pairs) {pair.disabled = trueif (pair.id) {pair.id = 'pr:' + nanoid()}if (idMap.has(pair.from.groupId)) {pair.from.groupId = idMap.get(pair.from.groupId)}if (idMap.has(pair.to.groupId)) {pair.to.groupId = idMap.get(pair.to.groupId)}if (idMap.has(pair.from.pointId)) {pair.from.pointId = idMap.get(pair.from.pointId)}if (idMap.has(pair.to.pointId)) {pair.to.pointId = idMap.get(pair.to.pointId)}}}if (idMap.has(point.id)) {const anchor = asset.children.find((o: any) => o.attrs.id === point.id)point.id = idMap.get(point.id)if (anchor) {anchor.attrs.id = point.id}}if (idMap.has(point.groupId)) {point.groupId = idMap.get(point.groupId)}}}if (Array.isArray(asset.children)) {deepAssets.push(...asset.children)}}}}

jsonIdCover 对 json 结构进行一次广度优先遍历,把所有 asset 的 id(groupId)刷新一遍,连带关联的 point、pair、anchor 的 id、groupId 要同步更新。

特别地,将 pair.disabled 记录为 true,原因是,在组合的时候,连接线直接当作 Line 实例也包含进来了,在保留该 pair 记录的同时,标记后,在后续的 Link draw 的时候会忽略这些 pair。

此时,另存为元素的 json 已经处理好,可以导出了。

组合后的素材封面

生成封面,也是依赖 getAssetView 处理好的 stage 克隆,基本与原来的 getImage 一样:

// src/Render/tools/ImportExportTool.ts// 获取元素图片getAssetImage(pixelRatio = 1, bgColor?: string) {// 获取可视节点和 layerconst copy = this.getAssetView()// 背景层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))const url = copy.toDataURL({ pixelRatio })copy.destroy()// 通过 stage api 导出图片return url}

通过 getAsset、getAssetImage 就可以获得组合后的素材的 json 文件、封面图片了。

生成的示例,可以参考左侧菜单新增的 4 个素材,放在静态目录 public/json 中。

在这里插入图片描述

示例 3 = 示例 1 + 示例 2;示例 4 = 示例 3 + 其他,嵌套了多层。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1487214.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C语言数据结构课设:基于EasyX前端界面的飞机订票系统

数据结构课程设计说明书 学 院、系&#xff1a; 软件学院 专 业&#xff1a; 软件工程 班 级&#xff1a; 学 生 姓 名&#xff1a; 范 学 号&#xff1a; 设 计 题 目&#xff1a; 飞机订票系统 起 迄 日 期: 2024年6月18日~ 20…

兰州交通大学电子与信息工程学院师资能力提升培训班圆满结束

7月21日&#xff0c;兰州交通大学电子与信息工程学院携手泰迪智能科技开展的“师资能力提升培训班&#xff08;兰州交通大学电子与信息工程学院专场&#xff09;”圆满结束&#xff0c;电子与信息工程学院副院长申东、泰迪智能科技区域总监曹玉红&#xff0c;教学组代表杨惠及电…

今日科普:什么是脑血管畸形,该怎么治疗?

谈及脑血管疾病&#xff0c;人们往往存在一种误解&#xff0c;认为这是老年群体的专属问题。然而&#xff0c;事实并非如此&#xff0c;尤其是脑动静脉畸形&#xff08;AVM&#xff09;这一特殊类型&#xff0c;它更倾向于侵袭20至40岁的青壮年人群。那么&#xff0c;脑血管畸形…

Transformer是什么?如何理解Transformer?

一、Transformer是什么 Transformer是一种深度学习模型架构&#xff0c;最初由Google的研究团队在2017年提出。这种架构最早用于自然语言处理&#xff08;NLP&#xff09;&#xff0c;但后来也在其他领域表现出色。Transformer的关键特点是其自注意力机制&#xff08;Self-Att…

第20讲:EtherCAT网络基础

EtherCAT概述 一、定义 二、EtherCAT原理 1、以太网帧通过到站不停车的方式进行数据交换 (1)如图,当中走过的就是以太网帧。当它经过从站的时候不会停留,但是它会跟从站进行信息交互。 即会把从站需要发送的信息给到了以太网帧里面去,然后把从站需要的信息,从以太网帧里…

河南萌新联赛2024第(二)场:南阳理工学院

A 国际旅行Ⅰ D A*BBBB F 水灵灵的小学弟 H 狼狼的备忘录 I 重生之zbk要拿回属于他的一切 J 这是签到 ##A 国际旅行Ⅰ 链接&#xff1a;https://ac.nowcoder.com/acm/contest/87255/A 来源&#xff1a;牛客网 题目描述 很久很久以前&#xff0c;有 n n n 个国家&#xff0c;第…

构建一个具有深色模式的简单React Web应用

在当今的Web开发世界里,创建一个既美观又功能丰富的用户界面是至关重要的。在本文中,我们将探讨如何使用React构建一个简单但功能强大的Web应用,它包含导航栏、内容展示区域和深色模式切换功能。 项目概述 我们的目标是创建一个具有以下特性的Web应用: 左侧导航栏,包含四个链…

MySQL 约束 (constraint)

文章目录 约束&#xff08;constraint)列级约束和表级约束给约束起名字&#xff08;constraint)非空约束&#xff08;no null)检查约束&#xff08;check)唯一性约束 (unique)主键约束 (primary key)主键分类单一主键复合主键主键自增 &#xff08;auto_increment) 外键约束外什…

IP协议和路由转发

文章目录 IP协议IP报头网段划分特殊的IP私有IP和公有IP IP分片 路由 IP协议 IP协议提供了一种能力&#xff0c;将数据报从A主机送到B主机&#xff0c;TCP可以保证可靠性&#xff0c;所以TCP/IP协议可以将数据可靠的从A主机送到B主机。 IP报头 4位版本号(version): 指定IP协议…

Unity UGUI 之 Slider

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 1.Slider是什么 滑块&#xff0c;由三部分组成&#xff1a;背景 填充条 手柄 填充条就是…

vue3前端开发-小兔鲜项目-产品详情基础数据渲染

vue3前端开发-小兔鲜项目-产品详情基础数据渲染&#xff01;这一次内容比较多&#xff0c;我们分开写。第一步先完成详情页面的基础数据的渲染。然后再去做一下右侧的热门产品的列表内容。 第一步&#xff0c;还是老规矩&#xff0c;先准备好接口函数。方便我们的页面组件拿到对…

华盈生物-小分子靶点筛选服务:助力药物发现的利器

在药物发现的过程中&#xff0c;确定小分子化合物的靶点是至关重要的一步。华盈生物为科学家们提供了两种高效的小分子靶点筛选方案&#xff0c;助力研究人员精准锁定靶点&#xff0c;加速新药研发进程。 方案一&#xff1a;荧光标记与HuProt人类蛋白质组芯片结合 华盈生物的H…

软件设计之Java入门视频(19)

软件设计之Java入门视频(19) 视频教程来自B站尚硅谷&#xff1a; 尚硅谷Java入门视频教程&#xff0c;宋红康java基础视频 相关文件资料&#xff08;百度网盘&#xff09; 提取密码&#xff1a;8op3 idea 下载可以关注 软件管家 公众号 学习内容&#xff1a; 该视频共分为1-7…

三种方法加密图纸!2024如何对CAD图纸进行加密?

在2024年的今天&#xff0c;随着企业对数据安全意识的不断提高&#xff0c;对CAD图纸进行加密成为了保护知识产权和商业机密的重要手段。无论是建筑设计、机械制造&#xff0c;还是电子工程领域&#xff0c;CAD图纸都承载着核心的设计理念和技术细节&#xff0c;因此&#xff0…

音视频入门基础:PCM专题(3)——使用Audacity工具分析PCM音频文件

音视频入门基础&#xff1a;PCM专题系列文章&#xff1a; 音视频入门基础&#xff1a;PCM专题&#xff08;1&#xff09;——使用FFmpeg命令生成PCM音频文件并播放 音视频入门基础&#xff1a;PCM专题&#xff08;2&#xff09;——使用Qt播放PCM音频文件 音视频入门基础&am…

LabVIEW多种测试仪器集成控制系统

在现代工业生产与科研领域&#xff0c;对测试设备的需求日益增长。传统的手动操作测试不仅效率低下&#xff0c;而且易出错。本项目通过集成控制系统&#xff0c;实现了自动化控制&#xff0c;降低操作复杂度和错误率&#xff0c;提高生产和研究效率。 系统组成与硬件选择 系…

人工智能学习笔记 - 初级篇Ⅱ - 图形可视化 - 第5节-设置刻度、刻度标签和网格

微信公众号&#xff1a;御风研墨 关注可了解更多。问题或建议&#xff0c;请公众号留言 文章目录 设置刻度、刻度标签和网格应用背景准备工作操作步骤工作原理补充说明最后 设置刻度、刻度标签和网格 应用背景 在数据可视化中&#xff0c;合理设置刻度、刻度标签和网格是提高…

如何学习EMR:糙快猛的大数据之路(建立整体框架)

目录 初学EMREMR是什么&#xff1f;我的EMR学习故事糙快猛学习法则代码示例: 你的第一个EMR任务学习EMR的深入步骤EMR进阶技巧实用资源推荐常见挑战和解决方案 EMR生态EMR生态系统深度探索1. EMR上的Hadoop生态系统2. EMR Studio3. EMR on EKS 高级EMR配置和优化1. EMR实例集策…

《Milvus Cloud向量数据库指南》——开源许可证的开放度:塑造AI开发合作与创新的双刃剑

在人工智能(AI)技术日新月异的今天,开源软件作为推动技术创新的重要力量,其许可证的开放度成为了影响AI开发合作、创新模式乃至整个行业生态的关键因素。不同的开源许可证模型,以其各自独特的开放程度,不仅决定了软件项目的可访问性和可定制性,还深刻影响着AI领域内的合…

(7) cmake 编译C++程序(二)

文章目录 概要整体代码结构整体代码小结 概要 在ubuntu下&#xff0c;通过cmake编译一个稍微复杂的管理程序 整体代码结构 整体代码 boss.cpp #include "boss.h"Boss::Boss(int id, string name, int dId) {this->Id id;this->Name name;this->DeptId …