本章主要實現素材的巢狀(載入階段)這意味著可以拖入畫布的物件,不只是圖片素材,還可以是巢狀的圖片和圖形。
請大家動動小手,給我一個免費的 Star 吧~
大家如果發現了 Bug,歡迎來提 Issue 喲~
github原始碼
gitee原始碼
示例地址
在原來的 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!勾勾手指~
原始碼
gitee原始碼
示例地址