請大家動動小手,給我一個免費的 Star 吧~
大家如果發現了明顯的 Bug,可以提 Issue 喲~
這一章我們實現一個預覽框,實時、可互動定位的。
github原始碼
gitee原始碼
示例地址
預覽框
定位方法
移動畫布,將傳入 x,y 作為畫布中心:
// 更新中心位置
updateCenter(x = 0, y = 0) {
// stage 狀態
const stageState = this.render.getStageState()
// 提取節點
const nodes = this.render.layer.getChildren((node) => {
return !this.render.ignore(node)
})
// 計算節點佔用的區域(計算起點即可)
let minX = 0
let minY = 0
for (const node of nodes) {
const x = node.x()
const y = node.y()
if (x < minX) {
minX = x
}
if (y < minY) {
minY = y
}
}
// 居中畫布
this.render.stage.setAttrs({
x: stageState.width / 2 - this.render.toBoardValue(minX) - this.render.toBoardValue(x),
y: stageState.height / 2 - this.render.toBoardValue(minY) - this.render.toBoardValue(y)
})
// 更新背景
this.render.draws[Draws.BgDraw.name].draw()
// 更新比例尺
this.render.draws[Draws.RulerDraw.name].draw()
// 更新參考線
this.render.draws[Draws.RefLineDraw.name].draw()
// 更新預覽
this.render.draws[Draws.PreviewDraw.name].draw()
}
比較難表達,嘗試畫個圖說明:
為了簡化,維持畫布初始位置,可以把 minX 和 minY 視為 0。
"前"即是 stage 起始位置 也是可視區域,可視區域是固定的,當點選 x,y 座標時,為了使移動之前 x,y 相對 stage 的位置,移動到可視區域居中位置,stage 就要如圖那樣“反向”偏移。
分解步驟,可以分為 3 步,"前"、“中”、“後”,對應計算“居中畫布”處。
繪製預覽框
下面的程式碼比較長,新增了必要的註釋,會重點解釋 move 和“可視區域提示框”兩部分邏輯。
override draw() {
if (this.render.config.showPreview) {
this.clear()
// stage 狀態
const stageState = this.render.getStageState()
// 預覽框的外邊距
const previewMargin = 20
// 預覽框 group
const group = new Konva.Group({
name: 'preview',
scale: {
x: this.render.toStageValue(this.option.size),
y: this.render.toStageValue(this.option.size)
},
width: stageState.width,
height: stageState.height
})
const main = this.render.stage.find('#main')[0] as Konva.Layer
// 提取節點
const nodes = main.getChildren((node) => {
return !this.render.ignore(node)
})
// 計算節點佔用的區域
let minX = 0
let maxX = group.width()
let minY = 0
let maxY = group.height()
for (const node of nodes) {
const x = node.x()
const y = node.y()
const width = node.width()
const height = node.height()
if (x < minX) {
minX = x
}
if (x + width > maxX) {
maxX = x + width
}
if (y < minY) {
minY = y
}
if (y + height > maxY) {
maxY = y + height
}
}
// 根據佔用的區域調整預覽框的大小
group.setAttrs({
x: this.render.toStageValue(
-stageState.x + stageState.width - maxX * this.option.size - previewMargin
),
y: this.render.toStageValue(
-stageState.y + stageState.height - maxY * this.option.size - previewMargin
),
width: maxX - minX,
height: maxY - minY
})
// 預覽框背景
const bg = new Konva.Rect({
name: this.constructor.name,
x: minX,
y: minY,
width: group.width(),
height: group.height(),
stroke: '#666',
strokeWidth: this.render.toStageValue(1),
fill: '#eee'
})
// 根據預覽框內部拖動,同步畫布的移動
const move = () => {
// 略,下面有單獨說明
}
// 預覽框內拖動事件處理
bg.on('mousedown', (e) => {
if (e.evt.button === Types.MouseButton.左鍵) {
move()
}
e.evt.preventDefault()
})
bg.on('mousemove', (e) => {
if (this.state.moving) {
move()
}
e.evt.preventDefault()
})
bg.on('mouseup', () => {
this.state.moving = false
})
group.add(bg)
// 預覽框 邊框
group.add(
new Konva.Rect({
name: this.constructor.name,
x: 0,
y: 0,
width: stageState.width,
height: stageState.height,
stroke: 'rgba(255,0,0,0.2)',
strokeWidth: 1 / this.option.size,
listening: false
})
)
// 複製提取的節點,用作預覽
for (const node of nodes) {
const copy = node.clone()
// 不可互動
copy.listening(false)
// 設定名稱用於 ignore
copy.name(this.constructor.name)
group.add(copy)
}
// 放大的時候,顯示當前可視區域提示框
if (stageState.scale > 1) {
// 略,下面有單獨說明
}
this.group.add(group)
}
}
透過預覽框移動畫布
上面介紹了“定位方法”,基於它,透過預覽框也可以使畫布同步移動,前提就是要把“預覽框”內部的座標轉換成“畫布”的座標。
// 根據預覽框內部拖動,同步畫布的移動
const move = () => {
this.state.moving = true
const pos = this.render.stage.getPointerPosition()
if (pos) {
const pWidth = group.width() * this.option.size
const pHeight = group.height() * this.option.size
const pOffsetX = pWidth - (stageState.width - pos.x - previewMargin)
const pOffsetY = pHeight - (stageState.height - pos.y - previewMargin)
const offsetX = pOffsetX / this.option.size
const offsetY = pOffsetY / this.option.size
// 點選預覽框,點選位置作為畫布中心
this.render.positionTool.updateCenter(offsetX, offsetY)
}
}
上面轉換的思路就是:
1、透過 group 和倍數反推計算佔用的區域可視大小
2、計算可視居中座標
3、計算邏輯居中座標(使用倍數恢復)
4、透過 updateCenter 居中
可視區域提示框
當放大的時候,會顯示當前可視區域提示框
// 放大的時候,顯示當前可視區域提示框
if (stageState.scale > 1) {
// 畫布可視區域起點座標(左上)
let x1 = this.render.toStageValue(-stageState.x + this.render.rulerSize)
let y1 = this.render.toStageValue(-stageState.y + this.render.rulerSize)
// 限制可視區域提示框不能超出預覽區域
x1 = x1 > minX ? x1 : minX
x1 = x1 < maxX ? x1 : maxX
y1 = y1 > minY ? y1 : minY
y1 = y1 < maxY ? y1 : maxY
// 畫布可視區域起點座標(右下)
let x2 =
this.render.toStageValue(-stageState.x + this.render.rulerSize) +
this.render.toStageValue(stageState.width)
let y2 =
this.render.toStageValue(-stageState.y + this.render.rulerSize) +
this.render.toStageValue(stageState.height)
// 限制可視區域提示框不能超出預覽區域
x2 = x2 > minX ? x2 : minX
x2 = x2 < maxX ? x2 : maxX
y2 = y2 > minY ? y2 : minY
y2 = y2 < maxY ? y2 : maxY
// 可視區域提示框 連線座標序列
let points: Array<[x: number, y: number]> = []
// 可視區域提示框“超出”預覽區域影響的“邊”不做繪製
// "超出"(上面實際處理:把超過的座標設定為上/下線,判斷方式如[以正規表示式表示]:(x|y)(1|2) === (min|max)(X|Y))
//
// 簡單直接窮舉 9 種情況:
// 不超出
// 上超出 下超出
// 左超出 右超出
// 左上超出 右上超出
// 左下超出 右下超出
// 不超出,繪製完整矩形
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2],
[x1, y1]
]
}
// 上超出,不繪製“上邊”
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 === minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x2, y1],
[x2, y2],
[x1, y2],
[x1, y1]
]
}
// 下超出,不繪製“下邊”
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 === maxY
) {
points = [
[x1, y2],
[x1, y1],
[x2, y1],
[x2, y2]
]
}
// 左超出,不繪製“左邊”
if (
x1 === minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2]
]
}
// 右超出,不繪製“右邊”
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 === maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x2, y1],
[x1, y1],
[x1, y2],
[x2, y2]
]
}
// 左上超出,不繪製“上邊”、“左邊”
if (
x1 === minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 === minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x2, y1],
[x2, y2],
[x1, y2]
]
}
// 右上超出,不繪製“上邊”、“右邊”
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 === maxX &&
y1 === minY &&
y1 < maxY &&
y2 > minY &&
y2 < maxY
) {
points = [
[x2, y2],
[x1, y2],
[x1, y1]
]
}
// 左下超出,不繪製“下邊”、“左邊”
if (
x1 === minX &&
x1 < maxX &&
x2 > minX &&
x2 < maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 === maxY
) {
points = [
[x1, y1],
[x2, y1],
[x2, y2]
]
}
// 右下超出,不繪製“下邊”、“右邊”
if (
x1 > minX &&
x1 < maxX &&
x2 > minX &&
x2 === maxX &&
y1 > minY &&
y1 < maxY &&
y2 > minY &&
y2 === maxY
) {
points = [
[x2, y1],
[x1, y1],
[x1, y2]
]
}
// 可視區域提示框
group.add(
new Konva.Line({
name: this.constructor.name,
points: _.flatten(points),
stroke: 'blue',
strokeWidth: 1 / this.option.size,
listening: false
})
)
}
// 複製提取的節點,用作預覽
for (const node of nodes) {
const copy = node.clone()
// 不可互動
copy.listening(false)
// 設定名稱用於 ignore
copy.name(this.constructor.name)
group.add(copy)
}
this.group.add(group)
}
}
除了上面必要的註釋,還是畫一張圖表示這 9 種情況:
實際上,不希望“提示框”超出“預覽框”,於是才有上面“窮舉”的處理邏輯。
接下來,計劃實現下面這些功能:
- 對齊效果
- 連線線
- 等等。。。
是不是值得更多的 Star 呢?勾勾手指~
原始碼
gitee原始碼
示例地址