使用 konva 实现一个设计器交互,首先考虑实现设计器的画布。
一个基本的画布:
【展示】网格、比例尺
【交互】拖拽、缩放
"拖拽"是无尽的,"缩放"是基于鼠标焦点的。
最终效果:
基本思路:
设计区域 HTML 由两个节点构成,内层挂载一个 Konva.stage 作为画布的开始。
html
<template>
<div class="page">
<header></header>
<section>
<header></header>
<section ref="boardElement">
<div ref="stageElement"></div>
</section>
<footer></footer>
</section>
<footer></footer>
</div>
</template>
Konva.stage 暂时先设计3个 Konva.Layer,分别用于绘制背景、所有素材、比例尺。
通过 ResizeObserver 使 Konva.stage 的大小与外层 boardElement 保持一致。
为了显示"比例尺" Konva.stage 默认会偏移一些距离,这里定义"比例尺"尺寸为 40px。
ts
this.stage = new Konva.Stage({
container: stageEle,
x: this.rulerSize,
y: this.rulerSize,
width: config.width,
height: config.height
})
关于"网格背景",是按照当前设计区域大小、缩放大小、偏移量,计算横向、纵向分别需要绘制多少条 Konva.Line(横向、纵向分别多加1条),同时根据 Konva.stage 的 x,y 进行偏移,用有限的 Konva.Line 模拟无限的网格画布。
ts
// 格子大小
const cellSize = this.option.size
//
const width = this.stage.width()
const height = this.stage.height()
const scaleX = this.stage.scaleX()
const scaleY = this.stage.scaleY()
const stageX = this.stage.x()
const stageY = this.stage.y()
// 列数
const lenX = Math.ceil(width / scaleX / cellSize)
// 行数
const lenY = Math.ceil(height / scaleY / cellSize)
const startX = -Math.ceil(stageX / scaleX / cellSize)
const startY = -Math.ceil(stageY / scaleY / cellSize)
const group = new Konva.Group()
group.add(
new Konva.Rect({
name: this.constructor.name,
x: 0,
y: 0,
width: width,
height: height,
stroke: 'rgba(255,0,0,0.1)',
strokeWidth: 2 / scaleY,
listening: false,
dash: [4, 4]
})
)
// 竖线
for (let x = startX; x < lenX + startX + 1; x++) {
group.add(
new Konva.Line({
name: this.constructor.name,
points: _.flatten([
[cellSize * x, -stageY / scaleY],
[cellSize * x, (height - stageY) / scaleY]
]),
stroke: '#ddd',
strokeWidth: 1 / scaleY,
listening: false
})
)
}
// 横线
for (let y = startY; y < lenY + startY + 1; y++) {
group.add(
new Konva.Line({
name: this.constructor.name,
points: _.flatten([
[-stageX / scaleX, cellSize * y],
[(width - stageX) / scaleX, cellSize * y]
]),
stroke: '#ddd',
strokeWidth: 1 / scaleX,
listening: false
})
)
}
this.group.add(group)
关于"比例尺",与"网格背景"思路差不多,在绘制"刻度"和"数值"的时候相对麻烦一些,例如绘制"数值"的时候,需要动态判断应该使用多大的字体。
ts
let fontSize = fontSizeMax
const text = new Konva.Text({
name: this.constructor.name,
y: this.option.size / scaleY / 2 - fontSize / scaleY,
text: (x * cellSize).toString(),
fontSize: fontSize / scaleY,
fill: '#999',
align: 'center',
verticalAlign: 'bottom',
lineHeight: 1.6
})
while (text.width() / scaleY > (cellSize / scaleY) * 4.6) {
fontSize -= 1
text.fontSize(fontSize / scaleY)
text.y(this.option.size / scaleY / 2 - fontSize / scaleY)
}
text.x(nx - text.width() / 2)
关于"拖拽",这里设计的是通过鼠标右键拖拽画布,通过记录 mousedown 时 Konva.stage 起始位置、鼠标位置,mousemove 时将鼠标位置偏移与Konva.stage 起始位置计算最新的 Konva.stage 的位置即可。
ts
mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
if (e.evt.button === Types.MouseButton.右键) {
// 鼠标右键
this.mousedownRight = true
this.mousedownPosition = { x: this.render.stage.x(), y: this.render.stage.y() }
const pos = this.render.stage.getPointerPosition()
if (pos) {
this.mousedownPointerPosition = { x: pos.x, y: pos.y }
}
document.body.style.cursor = 'pointer'
}
},
mouseup: () => {
this.mousedownRight = false
document.body.style.cursor = 'default'
},
mousemove: () => {
if (this.mousedownRight) {
// 鼠标右键拖动
const pos = this.render.stage.getPointerPosition()
if (pos) {
const offsetX = pos.x - this.mousedownPointerPosition.x
const offsetY = pos.y - this.mousedownPointerPosition.y
this.render.stage.position({
x: this.mousedownPosition.x + offsetX,
y: this.mousedownPosition.y + offsetY
})
// 更新背景
this.render.draws[Draws.BgDraw.name].draw()
// 更新比例尺
this.render.draws[Draws.RulerDraw.name].draw()
}
}
}
关于"缩放",可以参考 konva 官网的缩放示例,思路是差不多的,只是根据实际情况调整了逻辑。
接下来,计划增加下面功能:
- 坐标参考线
- 从左侧图片素材拖入节点
- 鼠标、键盘移动节点
- 鼠标、键盘单选、多选节点
- 键盘复制、粘贴
- 节点层次单个、批量调整
- 等等。。。
如果 github Star 能超过 20 个,将很快更新下一篇章。
源码在这,望多多支持