首页/文章列表/文章详情

前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段

编程知识1802024-07-22评论

本章主要实现素材的嵌套(加载阶段)这意味着可以拖入画布的对象,不只是图片素材,还可以是嵌套的图片和图形。

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

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

github源码

gitee源码

示例地址

在原来的 drop 处理基础上,增加一个 json 类型素材的处理入口:

// src/Render/handlers/DragOutsideHandlers.tsdrop: (e: GlobalEventHandlersEventMap['drop']) => { // 略 this.render.assetTool[ type === 'svg' ? `loadSvg` : type === 'gif' ? 'loadGif' : type === 'json' ? 'loadJson' // 新增,处理 json 类型素材 : 'loadImg' ](src).then((target: Konva.Image | Konva.Group) => { // 图片素材 if (target instanceof Konva.Image) { // 略 } else { // json 素材 target.id(nanoid()) target.name('asset') group = target this.render.linkTool.groupIdCover(group) } }) // 略}

drop 原逻辑基本不变,关键逻辑在 loadJson 中:

// src/Render/tools/AssetTool.ts // 加载节点 json async loadJson(src: string) { try { // 读取 json内容 const json = JSON.parse(await (await fetch(src)).text()) // 子素材 const assets = json.children // 刷新id this.render.linkTool.jsonIdCover(assets) // 生成空白 stage+layer const stageEmpty = new Konva.Stage({ container: document.createElement('div') }) const layerEmpty = new Konva.Layer() stageEmpty.add(layerEmpty) // 空白 json 根 const jsonRoot = JSON.parse(stageEmpty.toJSON()) jsonRoot.children[0].children = [json] // 重新加载 stage const stageReload = Konva.Node.create(JSON.stringify(jsonRoot), document.createElement('div')) // 目标 group(即 json 转化后的节点) const groupTarget = stageReload.children[0].children[0] as Konva.Group // 释放内存 stageEmpty.destroy() groupTarget.remove() stageReload.destroy() // 深度遍历加载子素材 const nodes: { target: Konva.Stage | Konva.Layer | Konva.Group | Konva.Node parent?: Konva.Stage | Konva.Layer | Konva.Group | Konva.Node }[] = [{ target: groupTarget }] while (nodes.length > 0) { const item = nodes.shift() if (item) { const node = item.target if (node instanceof Konva.Image) { if (node.attrs.svgXML) { const n = await this.loadSvgXML(node.attrs.svgXML) n.listening(false) node.parent?.add(n) node.remove() } else if (node.attrs.gif) { const n = await this.loadGif(node.attrs.gif) n.listening(false) node.parent?.add(n) node.remove() } else if (node.attrs.src) { const n = await this.loadImg(node.attrs.src) n.listening(false) node.parent?.add(n) node.remove() } } if ( node instanceof Konva.Stage || node instanceof Konva.Layer || node instanceof Konva.Group ) { nodes.push( ...node.getChildren().map((o) => ({ target: o, parent: node })) ) } } } // 作用:点击空白区域可选择 const clickMask = new Konva.Rect({ id: 'click-mask', width: groupTarget.width(), height: groupTarget.height() }) groupTarget.add(clickMask) clickMask.zIndex(1) return groupTarget } catch (e) { console.error(e) return new Konva.Group() } }

loadJson,关键逻辑说明:

1、jsonIdCover 把加载到的 json 内部的 id 们刷新一遍

2、借一个空 stage 得到一个 空 stage 的 json 结构(由于素材 json 只包含素材自身结构,需要补充上层 json 结构)

3、加载拼接好的 json,得到一个新 stage

4、从 3 的 stage 中提取目标素材 group

5、加载该 group 内部的图片素材

6、插入一个透明 Rect,使其点击 sub-asset 们之间的空白,也能选中整个 asset

最后,进行一次 linkTool.groupIdCover 处理:

// src/Render/tools/LinkTool.ts // 把深层 group 的 id 统一为顶层 group 的 id groupIdCover(group: Konva.Group) { const groupId = group.id() const subGroups = group.find('.sub-asset') as Konva.Group[] while (subGroups.length > 0) { const subGroup = subGroups.shift() as Konva.Group | undefined if (subGroup) { const points = subGroup.attrs.points if (Array.isArray(points)) { for (const point of points) { point.rawGroupId = point.groupId point.groupId = groupId for (const pair of point.pairs) { pair.from.rawGroupId = pair.from.groupId pair.from.groupId = groupId pair.to.rawGroupId = pair.to.groupId pair.to.groupId = groupId } } } subGroups.push(...(subGroup.find('.sub-asset') as Konva.Group[])) } } }

这里的逻辑就是把 顶层 asset 的新id,通过广度优先遍历,下发到下面所有的 point 和 pair 上,并保留原来的 groupId(上面的 rawGroupId)为日后备用。groupId 更新之后,在连接线算法执行的时候,会忽略同个 asset 下不同 sub-asset 的 pair 关系,即不会重复绘制内部不同 sub-asset 之间实时连接线(连接线在另存为素材 json 的时候,已经直接固化成 Line 实例了,往后将跟随 根 asset 行动,特别是 transform 变换)。

接着,因为这次的实现,内部属于各 sub-asset 的 point 依旧有效,首先,调整一下 pointsVisible,使其在 hover 根 asset 的时候,内部所有 point 都会显现:

// src/Render/tools/LinkTool.ts pointsVisible(visible: boolean, group?: Konva.Group) { const start = group ?? this.render.layer // 查找深层 points for (const asset of [ ...(['asset', 'sub-asset'].includes(start.name()) ? [start] : []), ...start.find('.asset'), ...start.find('.sub-asset') ]) { const points = asset.getAttr('points') ?? [] asset.setAttrs({ points: points.map((o: any) => ({ ...o, visible })) }) } // 重绘 this.render.redraw() }

然后,关键要调整 LinkDraw:

// src/Render/draws/LinkDraw.tsoverride draw() { // 略 // 所有层级的素材 const groups = [ ...(this.render.layer.find('.asset') as Konva.Group[]), ...(this.render.layer.find('.sub-asset') as Konva.Group[]) ] // 略 const pairs = points.reduce((ps, point) => { return ps.concat(point.pairs ? point.pairs.filter((o) => !o.disabled) : []) }, [] as LinkDrawPair[]) // 略 // 连接线 for (const pair of pairs) { // 多层素材,需要排除内部 pair 对 // pair 也不能为 disabled if (pair.from.groupId !== pair.to.groupId && !pair.disabled) { // 略 } }}

1、groups 查询要增加包含 sub-asset

2、过滤掉 disabled 的 pair 纪录

3、过滤掉同 asset 的 pair 纪录

其他逻辑,基本不变。

至此,关于“素材嵌套”的逻辑基本已实现。

整体代码对比上个功能版本,改变的并不多,对之前的代码影响不大。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

博客园

这个人很懒...

用户评论 (0)

发表评论

captcha