前端使用 Konva 实现可视化设计器(8)- 预览框

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了明显的 Bug,可以提 Issue 哟~

这一章我们实现一个预览框,实时、可交互定位的。

github源码

gitee源码

示例地址

预览框

前端使用 Konva 实现可视化设计器(8)- 预览框
前端使用 Konva 实现可视化设计器(8)- 预览框

定位方法

移动画布,将传入 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()   } 

比较难表达,尝试画个图说明:
前端使用 Konva 实现可视化设计器(8)- 预览框

为了简化,维持画布初始位置,可以把 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)     }   } 

通过预览框移动画布

前端使用 Konva 实现可视化设计器(8)- 预览框
上面介绍了“定位方法”,基于它,通过预览框也可以使画布同步移动,前提就是要把“预览框”内部的坐标转换成“画布”的坐标。

      // 根据预览框内部拖动,同步画布的移动       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 居中

可视区域提示框

当放大的时候,会显示当前可视区域提示框
前端使用 Konva 实现可视化设计器(8)- 预览框

      // 放大的时候,显示当前可视区域提示框       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 种情况:
前端使用 Konva 实现可视化设计器(8)- 预览框
实际上,不希望“提示框”超出“预览框”,于是才有上面“穷举”的处理逻辑。
前端使用 Konva 实现可视化设计器(8)- 预览框
前端使用 Konva 实现可视化设计器(8)- 预览框

接下来,计划实现下面这些功能:

  • 对齐效果
  • 连接线
  • 等等。。。

是不是值得更多的 Star 呢?勾勾手指~

源码

gitee源码

示例地址

发表评论

评论已关闭。

相关文章