前端使用 Konva 实现可视化设计器(1)

使用 konva 实现一个设计器交互,首先考虑实现设计器的画布。

一个基本的画布:

【展示】网格、比例尺

【交互】拖拽、缩放

“拖拽”是无尽的,“缩放”是基于鼠标焦点的。

最终效果(示例地址):

前端使用 Konva 实现可视化设计器(1)

前端使用 Konva 实现可视化设计器(1)

前端使用 Konva 实现可视化设计器(1)

基本思路:

设计区域 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 实现可视化设计器(1)

Konva.stage 暂时先设计3个 Konva.Layer,分别用于绘制背景、所有素材、比例尺。

前端使用 Konva 实现可视化设计器(1)

通过 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 个,将很快更新下一篇章。

源码在这,望多多支持

发表评论

评论已关闭。

相关文章