前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
本章主要实现素材的嵌套(加载阶段)这意味着可以拖入画布的对象,不只是图片素材,还可以是嵌套的图片和图形。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
在原来的 drop 处理基础上,增加一个 json 类型素材的处理入口:
// src/Render/handlers/DragOutsideHandlers.ts
drop: (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.ts
override 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!勾勾手指~