這一章補充一個效果,在多選的情況下,對目標進行對齊。基於多選整體區域對齊的基礎上,還支持基於其中一個節點進行對齊。
請大家動動小手,給我一個免費的 Star 吧~
大家如果發現了 Bug,歡迎來提 Issue 喲~
基於整體的對齊
垂直居中
水平居中
左對齊
右對齊
上對齊
下對齊
基於目標節點的對齊
垂直居中(基於目標節點)
水平居中(基於目標節點)
左對齊(基於目標節點)
右對齊(基於目標節點)
上對齊(基於目標節點)
下對齊(基於目標節點)
對齊邏輯
放在 src/Render/tools/AlignTool.ts
import { Render } from '../index'
//
import * as Types from '../types'
import * as Draws from '../draws'
import Konva from 'konva'
export class AlignTool {
static readonly name = 'AlignTool'
private render: Render
constructor(render: Render) {
this.render = render
}
// 對齊參考點
getAlignPoints(target?: Konva.Node | Konva.Transformer): { [index: string]: number } {
let width = 0,
height = 0,
x = 0,
y = 0
if (target instanceof Konva.Transformer) {
// 選擇器
// 轉爲 邏輯覺尺寸
;[width, height] = [
this.render.toStageValue(target.width()),
this.render.toStageValue(target.height())
]
;[x, y] = [
this.render.toStageValue(target.x()) - this.render.rulerSize,
this.render.toStageValue(target.y()) - this.render.rulerSize
]
} else if (target !== void 0) {
// 節點
// 邏輯尺寸
;[width, height] = [target.width(), target.height()]
;[x, y] = [target.x(), target.y()]
} else {
// 默認爲選擇器
return this.getAlignPoints(this.render.transformer)
}
return {
[Types.AlignType.垂直居中]: x + width / 2,
[Types.AlignType.左對齊]: x,
[Types.AlignType.右對齊]: x + width,
[Types.AlignType.水平居中]: y + height / 2,
[Types.AlignType.上對齊]: y,
[Types.AlignType.下對齊]: y + height
}
}
align(type: Types.AlignType, target?: Konva.Node) {
// 對齊參考點(所有)
const points = this.getAlignPoints(target)
// 對齊參考點
const point = points[type]
// 需要移動的節點
const nodes = this.render.transformer.nodes().filter((node) => node !== target)
// 移動邏輯
switch (type) {
case Types.AlignType.垂直居中:
for (const node of nodes) {
node.x(point - node.width() / 2)
}
break
case Types.AlignType.水平居中:
for (const node of nodes) {
node.y(point - node.height() / 2)
}
break
case Types.AlignType.左對齊:
for (const node of nodes) {
node.x(point)
}
break
case Types.AlignType.右對齊:
for (const node of nodes) {
node.x(point - node.width())
}
break
case Types.AlignType.上對齊:
for (const node of nodes) {
node.y(point)
}
break
case Types.AlignType.下對齊:
for (const node of nodes) {
node.y(point - node.height())
}
break
}
// 更新歷史
this.render.updateHistory()
// 更新預覽
this.render.draws[Draws.PreviewDraw.name].draw()
}
}
還是比較容易理解的,要注意的主要是 transformer 獲得的 size 和 position 是視覺尺寸,需要轉爲邏輯尺寸。
功能入口
準備些枚舉值:
export enum AlignType {
垂直居中 = 'Middle',
左對齊 = 'Left',
右對齊 = 'Right',
水平居中 = 'Center',
上對齊 = 'Top',
下對齊 = 'Bottom'
}
按鈕
<button @click="onRestore">導入</button>
<button @click="onSave">導出</button>
<button @click="onSavePNG">另存爲圖片</button>
<button @click="onSaveSvg">另存爲Svg</button>
<button @click="onPrev" :disabled="historyIndex <= 0">上一步</button>
<button @click="onNext" :disabled="historyIndex >= history.length - 1">下一步</button>
<!-- 新增 -->
<button @click="onAlign(Types.AlignType.垂直居中)" :disabled="noAlign">垂直居中</button>
<button @click="onAlign(Types.AlignType.左對齊)" :disabled="noAlign">左對齊</button>
<button @click="onAlign(Types.AlignType.右對齊)" :disabled="noAlign">右對齊</button>
<button @click="onAlign(Types.AlignType.水平居中)" :disabled="noAlign">水平居中</button>
<button @click="onAlign(Types.AlignType.上對齊)" :disabled="noAlign">上對齊</button>
<button @click="onAlign(Types.AlignType.下對齊)" :disabled="noAlign">下對齊</button>
按鍵生效的條件是,必須是多選,所以 render 需要暴露一個事件,跟蹤選擇節點:
render = new Render(stageElement.value!, {
// 略
//
on: {
historyChange: (records: string[], index: number) => {
history.value = records
historyIndex.value = index
},
// 新增
selectionChange: (nodes: Konva.Node[]) => {
selection.value = nodes
}
}
})
條件判斷:
// 選擇項
const selection: Ref<Konva.Node[]> = ref([])
// 是否可以進行對齊
const noAlign = computed(() => selection.value.length <= 1)
// 對齊方法
function onAlign(type: Types.AlignType) {
render?.alignTool.align(type)
}
觸發事件的地方:
src/Render/tools/SelectionTool.ts
// 清空已選
selectingClear() {
// 選擇變化了
if (this.selectingNodes.length > 0) {
this.render.config.on?.selectionChange?.([])
}
// 略
}
// 選擇節點
select(nodes: Konva.Node[]) {
// 選擇變化了
if (nodes.length !== this.selectingNodes.length) {
this.render.config.on?.selectionChange?.(nodes)
}
// 略
}
右鍵菜單
在多選區域的空白處的時候右鍵,功能與按鈕一樣,不多贅述。
右鍵菜單(基於目標節點)
基於目標,比較特別,在多選的情況下,給內部的節點增加一個 hover 效果。
首先,拖入元素的時候,給每個節點準備一個 Konva.Rect 作爲 hover 效果,默認不顯示,且列入忽略的部分。
src/Render/handlers/DragOutsideHandlers.ts:
// hover 框(多選時才顯示)
group.add(
new Konva.Rect({
id: 'hoverRect',
width: image.width(),
height: image.height(),
fill: 'rgba(0,255,0,0.3)',
visible: false
})
)
// 隱藏 hover 框
group.on('mouseleave', () => {
group.findOne('#hoverRect')?.visible(false)
})
src/Render/index.ts:
// 忽略非素材
ignore(node: Konva.Node) {
// 素材有各自根 group
const isGroup = node instanceof Konva.Group
return (
!isGroup || node.id() === 'selectRect' || node.id() === 'hoverRect' || this.ignoreDraw(node)
)
}
src/Render/handlers/SelectionHandlers.ts:
// 子節點 hover
mousemove: () => {
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 獲取所有圖形
const shapes = this.render.transformer.nodes()
// 隱藏 hover 框
for (const shape of shapes) {
if (shape instanceof Konva.Group) {
shape.findOne('#hoverRect')?.visible(false)
}
}
// 多選
if (shapes.length > 1) {
// zIndex 倒序(大的優先)
shapes.sort((a, b) => b.zIndex() - a.zIndex())
// 提取重疊目標
const selected = shapes.find((shape) =>
// 關鍵 api
Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
)
// 顯示 hover 框
if (selected) {
if (selected instanceof Konva.Group) {
selected.findOne('#hoverRect')?.visible(true)
}
}
}
}
},
mouseleave: () => {
// 隱藏 hover 框
for (const shape of this.render.transformer.nodes()) {
if (shape instanceof Konva.Group) {
shape.findOne('#hoverRect')?.visible(false)
}
}
}
需要注意的是,hover 優先級是基於節點的 zIndex,所以判斷 hover 之前,需要進行一次排序。
判斷 hover,這裏使用 Konva.Util.haveIntersection,判斷兩個 rect 是否重疊,鼠標表達爲大小爲 1 的 rect。
用 find 找到 hover 的目標節點,使用 find 找到第一個即可,第一個就是 zIndex 最大最上層那個。
把 hover 的目標節點內部的 hoverRect 顯示出來就行了。
同樣的,就可以判斷是基於目標節點的右鍵菜單:
src/Render/draws/ContextmenuDraw.ts:
if (target instanceof Konva.Transformer) {
const pos = this.render.stage.getPointerPosition()
if (pos) {
// 獲取所有圖形
const shapes = target.nodes()
if (shapes.length > 1) {
// zIndex 倒序(大的優先)
shapes.sort((a, b) => b.zIndex() - a.zIndex())
// 提取重疊目標
const selected = shapes.find((shape) =>
// 關鍵 api
Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
)
// 對齊菜單
menus.push({
name: '垂直居中' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.垂直居中, selected)
}
})
menus.push({
name: '左對齊' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.左對齊, selected)
}
})
menus.push({
name: '右對齊' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.右對齊, selected)
}
})
menus.push({
name: '水平居中' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.水平居中, selected)
}
})
menus.push({
name: '上對齊' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.上對齊, selected)
}
})
menus.push({
name: '下對齊' + (selected ? '於目標' : ''),
action: () => {
this.render.alignTool.align(Types.AlignType.下對齊, selected)
}
})
}
}
}
接下來,計劃實現下面這些功能:
- 連接線
- 等等。。。
More Stars please!勾勾手指~