请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了明显的 Bug,可以提 Issue 哟~
这一章我们实现一个预览框,实时、可交互定位的。
预览框


定位方法
移动画布,将传入 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 呢?勾勾手指~

