使用 konva 实现一个设计器交互,首先考虑实现设计器的画布。
一个基本的画布:
【展示】网格、比例尺
【交互】拖拽、缩放
“拖拽”是无尽的,“缩放”是基于鼠标焦点的。
最终效果(示例地址):



基本思路:
设计区域 HTML 由两个节点构成,内层挂载一个 Konva.stage 作为画布的开始。
<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。
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 模拟无限的网格画布。
// 格子大小 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)
关于“比例尺”,与“网格背景”思路差不多,在绘制“刻度”和“数值”的时候相对麻烦一些,例如绘制“数值”的时候,需要动态判断应该使用多大的字体。
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 的位置即可。
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 个,将很快更新下一篇章。
源码在这,望多多支持