首页 > 其他分享 >前端使用 Konva 实现可视化设计器(3)

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

时间:2024-04-10 23:34:47浏览次数:26  
标签:node transformer const render Konva 可视化 selectionTool 前端

github/gitee Star 终于有几个了!

从这章开始,难度算是(或者说细节较多)升级,是不是值得更多的 Star 呢?!

继续求 Star ,希望大家多多一键三连,十分感谢大家的支持~

创作不易,Star 50 个,创作加速!

github源码

gitee源码

示例地址

选择框

image

准备工作

想要拖动一个元素,可以考虑使用节点的 draggable 属性。

不过,想要拖动多个元素,可以使用 transformer,官网也是简单的示例 Basic demo

按设计思路统一通过 transformer 移动/缩放所选,也意味着,元素要先选后动。

先准备一个 group、transformer、selectRect:

  // 多选器层
  groupTransformer: Konva.Group = new Konva.Group()
​
  // 多选器
  transformer: Konva.Transformer = new Konva.Transformer({
    shouldOverdrawWholeArea: true,
    borderDash: [4, 4],
    padding: 1,
    rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315, 360]
  })
​
  // 选择框
  selectRect: Konva.Rect = new Konva.Rect({
    id: 'selectRect',
    fill: 'rgba(0,0,255,0.1)',
    visible: false
  })

先说 transformer,设置 shouldOverdrawWholeArea 为了选择所选的空白处也能拖动;rotationSnaps 就是官方提供的 rotate 时的磁贴交互。

然后,selectRect 就是选择框,参考的就是上面提到的 Basic demo

最后,上面的 group 比较特别,它承载了上面的 transformer 和 selectRect,且置于第一章提到的 layerCover

    // 辅助层 - 顶层
    this.groupTransformer.add(this.transformer)
    this.groupTransformer.add(this.selectRect)
    this.layerCover.add(this.groupTransformer)

selectRect 不应该被“交互”,所以加个排查判断:

  // 忽略非素材
  ignore(node: Konva.Node) {
    // 素材有各自根 group
    const isGroup = node instanceof Konva.Group
    return !isGroup || node.id() === 'selectRect' || this.ignoreDraw(node)
  }
​

选择

准备一些状态变量:

  // selectRect 拉动的开始和结束坐标
  selectRectStartX = 0
  selectRectStartY = 0
  selectRectEndX = 0
  selectRectEndY = 0
  // 是否正在使用 selectRect
  selecting = false

选择开始,处理 stage 的 mousedown 事件:

    mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        // 略
​
        if (e.target === this.render.stage) {
          // 点击空白处
​
          // 清除选择
          // 外部也需要此操作,统一放在 selectionTool中
          // 后面会提到
          this.render.selectionTool.selectingClear()
​
          // 选择框
          if (e.evt.button === Types.MouseButton.左键) {
            const pos = this.render.stage.getPointerPosition()
            if (pos) {
              // 初始化状态值
              this.selectRectStartX = pos.x
              this.selectRectStartY = pos.y
              this.selectRectEndX = pos.x
              this.selectRectEndY = pos.y
            }
​
            // 初始化大小
            this.render.selectRect.width(0)
            this.render.selectRect.height(0)
​
            // 开始选择
            this.selecting = true
          }
        } else if (parent instanceof Konva.Transformer) {
          // transformer 点击事件交给 transformer 自己的 handler
        } else if (parent instanceof Konva.Group) {
          // 略
        }
      }

接着,处理 stage 的 mousemove 事件:

    mousemove: () => {
        // stage 状态
        const stageState = this.render.getStageState()
​
        // 选择框
        if (this.selecting) {
          // 选择区域中
          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            // 选择移动后的坐标
            this.selectRectEndX = pos.x
            this.selectRectEndY = pos.y
          }
​
          // 调整【选择框】的位置和大小
          this.render.selectRect.setAttrs({
            visible: true, // 显示
            x: this.render.toStageValue(
              Math.min(this.selectRectStartX, this.selectRectEndX) - stageState.x
            ),
            y: this.render.toStageValue(
              Math.min(this.selectRectStartY, this.selectRectEndY) - stageState.y
            ),
            width: this.render.toStageValue(Math.abs(this.selectRectEndX - this.selectRectStartX)),
            height: this.render.toStageValue(Math.abs(this.selectRectEndY - this.selectRectStartY))
          })
        }
      }

稍微说一下,调整【选择框】的位置和大小,关于 toStageValue 可以看看上一章。 width 和 height 比较好理解,开始位置 和 结束位置 相减就可以得出。

x 和 y,需从 开始位置 和 结束位置 选数值小的作为【选择框】的 rect 起点,最后要扣除 stage 的视觉位移,毕竟它们是放在 stage 里面的,就是 相对位置 和 视觉位置 的转换。

结束选择,处理 stage 的 mouseup 事件:

    mouseup: () => {
        // 选择框
​
        // 重叠计算
        const box = this.render.selectRect.getClientRect()
        if (box.width > 0 && box.height > 0) {
          // 区域有面积
​
          // 获取所有图形
          const shapes = this.render.layer.getChildren((node) => {
            return !this.render.ignore(node)
          })
          
          // 提取重叠部分
          const selected = shapes.filter((shape) =>
            // 关键 api
            Konva.Util.haveIntersection(box, shape.getClientRect())
          )
​
          // 多选
          // 统一放在 selectionTool中,对外暴露 api
          this.render.selectionTool.select(selected)
        }
​
        // 重置
        this.render.selectRect.setAttrs({
          visible: false, // 隐藏
          x: 0,
          y: 0,
          width: 0,
          height: 0
        })
​
        // 选择区域结束
        this.selecting = false
      }

【选择框】的主要处理的事件就是这些,接着,看看关键的 selectionTool.selectingClear、selectionTool.select,直接上代码:

  // 选择节点
  select(nodes: Konva.Node[]) {
    // 选之前,清一下
    this.selectingClear()
​
    if (nodes.length > 0) {
      // 用于撑开 transformer
      // 如果到这一章就到此为止,是不需要selectingNodesArea 这个 group
      // 卖个关子,留着后面解释
      this.selectingNodesArea = new Konva.Group({
        visible: false,
        listening: false
      })
​
      // 最大zIndex
      const maxZIndex = Math.max(
        ...this.render.layer
          .getChildren((node) => {
            return !this.render.ignore(node)
          })
          .map((o) => o.zIndex())
      )
​
      // 记录状态
      for (const node of nodes) {
        node.setAttrs({
          nodeMousedownPos: node.position(), // 后面用于移动所选
          lastOpacity: node.opacity(), // 选中时,下面会使其变透明,记录原有的透明度
          lastZIndex: node.zIndex() // 记录原有的层次,后面暂时提升所选节点的层次
        })
      }
​
      // 设置透明度、提升层次、不可交互
      for (const node of nodes.sort((a, b) => a.zIndex() - b.zIndex())) {
        const copy = node.clone()
​
        this.selectingNodesArea.add(copy)
​
        node.setAttrs({
          listening: false,
          opacity: node.opacity() * 0.8,
          zIndex: maxZIndex
        })
      }
​
      // 选中的节点
      this.selectingNodes = nodes
​
      // 放进 transformer 所在的层
      this.render.groupTransformer.add(this.selectingNodesArea)
​
      // 选中的节点,放进 transformer
      this.render.transformer.nodes([...this.selectingNodes, this.selectingNodesArea])
    }
  }
  // 清空已选
  selectingClear() {
    // 清空选择
    this.render.transformer.nodes([])

	// 移除 selectingNodesArea 
    this.selectingNodesArea?.remove()
    this.selectingNodesArea = null

    // 恢复透明度、层次、可交互
    for (const node of this.selectingNodes.sort(
      (a, b) => a.attrs.lastZIndex - b.attrs.lastZIndex
    )) {
      node.setAttrs({
        listening: true,
        opacity: node.attrs.lastOpacity ?? 1,
        zIndex: node.attrs.lastZIndex
      })
    }
    
    // 清空状态
    for (const node of this.selectingNodes) {
      node.setAttrs({
        nodeMousedownPos: undefined,
        lastOpacity: undefined,
        lastZIndex: undefined,
        selectingZIndex: undefined
      })
    }

	// 清空选择节点
    this.selectingNodes = []
  }

值得一提,Konva 关于 zIndex 的处理比较特别,始终从 1 到 N,意味着,改变一个节点的 zIndex,将影响其他节点的 zIndex,举个例子,假如有下面节点,数字就是对应的 zIndex:

a-1、b-2、c-3、d-4

此时我改 b 到 4(最大 zIndex),即 b-4,此时 c、d 会自动适应 zIndex,变成:

a-1、c-2、d-3、b-4

所以,上面需要两次的 this.selectingNodes.sort 处理,举个例子:

a/1、b/2、c/3、d/4,此时我选中 b 和 c

先置顶 b,即 a-1、c-2、d-3、b-4

后置顶 c,即 a-1、d-2、b-3、c-4

这样就可以保证原来 b 和 c 的相对位置的基础上,置顶 b 和 c

这样,通过【选择框】多选目标的交互就完成了。

点选

image

处理【未选择】节点

除了用【选择框】,也可以通过 ctrl + 点击 选择节点。

回到 stage 的 mousedown 事件处理:

	mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        const parent = e.target.getParent()

        if (e.target === this.render.stage) {
          // 略
        } else if (parent instanceof Konva.Transformer) {
          // transformer 点击事件交给 transformer 自己的 handler
        } else if (parent instanceof Konva.Group) {
          if (e.evt.button === Types.MouseButton.左键) {
            if (!this.render.ignore(parent) && !this.render.ignoreDraw(e.target)) {
              if (e.evt.ctrlKey) {
                // 新增多选
                this.render.selectionTool.select([
                  ...this.render.selectionTool.selectingNodes,
                  parent
                ])
              } else {
                // 单选
                this.render.selectionTool.select([parent])
              }
            }
          } else {
            this.render.selectionTool.selectingClear()
          }
        }
      }

这里比较简单,就是处理一下已选的数组。

处理【已选择】节点

      // 记录初始状态
      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        const anchor = this.render.transformer.getActiveAnchor()
        if (!anchor) {
          // 非变换
          if (e.evt.ctrlKey) {
            // 选择
            if (this.render.selectionTool.selectingNodesArea) {
              const pos = this.render.stage.getPointerPosition()
              if (pos) {
                const keeps: Konva.Node[] = []
                const removes: Konva.Node[] = []

                // 从高到低,逐个判断 已选节点 和 鼠标点击位置 是否重叠
                let finded = false
                for (const node of this.render.selectionTool.selectingNodes.sort(
                  (a, b) => b.zIndex() - a.zIndex()
                )) {
                  if (
                    !finded &&
                    Konva.Util.haveIntersection(node.getClientRect(), {
                      ...pos,
                      width: 1,
                      height: 1
                    })
                  ) {
                    // 记录需要移除选择的节点
                    removes.unshift(node)
                    finded = true
                  } else {
                    keeps.unshift(node)
                  }
                }

                if (removes.length > 0) {
                  // 取消选择
                  this.render.selectionTool.select(keeps)
                } else {
                  // 从高到低,逐个判断 未选节点 和 鼠标点击位置 是否重叠
                  let finded = false
                  const adds: Konva.Node[] = []
                  for (const node of this.render.layer
                    .getChildren()
                    .filter((node) => !this.render.ignore(node))
                    .sort((a, b) => b.zIndex() - a.zIndex())) {
                    if (
                      !finded &&
                      Konva.Util.haveIntersection(node.getClientRect(), {
                        ...pos,
                        width: 1,
                        height: 1
                      })
                    ) {
                      // 记录需要增加选择的节点
                      adds.unshift(node)
                      finded = true
                    }
                  }
                  if (adds.length > 0) {
                    // 新增选择
                    this.render.selectionTool.select([
                      ...this.render.selectionTool.selectingNodes,
                      ...adds
                    ])
                  }
                }
              }
            }
          } else {
            // 略
          }
        } else {
          // 略
        }
      }

效果:

image

移动节点

准备工作

相关状态变量:

  // 拖动前的位置
  transformerMousedownPos: Konva.Vector2d = { x: 0, y: 0 }

  // 拖动偏移
  groupImmediateLocOffset: Konva.Vector2d = { x: 0, y: 0 }

相关方法,处理 transformer 事件中会使用到:

  // 通过偏移量(selectingNodesArea)移动【目标节点】
  selectingNodesPositionByOffset(offset: Konva.Vector2d) {
    for (const node of this.render.selectionTool.selectingNodes) {
      const x = node.attrs.nodeMousedownPos.x + offset.x
      const y = node.attrs.nodeMousedownPos.y + offset.y
      node.x(x)
      node.y(y)
    }

    const area = this.render.selectionTool.selectingNodesArea
    if (area) {
      area.x(area.attrs.areaMousedownPos.x + offset.x)
      area.y(area.attrs.areaMousedownPos.y + offset.y)
    }
  }

  // 重置【目标节点】的 nodeMousedownPos
  selectingNodesPositionReset() {
    for (const node of this.render.selectionTool.selectingNodes) {
      node.attrs.nodeMousedownPos.x = node.x()
      node.attrs.nodeMousedownPos.y = node.y()
    }
  }

  // 重置 transformer 状态
  transformerStateReset() {
    // 记录 transformer pos
    this.transformerMousedownPos = this.render.transformer.position()
  }

  // 重置 selectingNodesArea 状态
  selectingNodesAreaReset() {
    this.render.selectionTool.selectingNodesArea?.setAttrs({
      areaMousedownPos: {
        x: 0,
        y: 0
      }
    })
  }

  // 重置
  reset() {
    this.transformerStateReset()
    this.selectingNodesPositionReset()
    this.selectingNodesAreaReset()
  }

主要通过处理 transformer 的事件:

      transformend: () => {
        // 变换结束

        // 重置状态
        this.reset()
      },
      //
      dragstart: () => {
        this.render.selectionTool.selectingNodesArea?.setAttrs({
          areaMousedownPos: this.render.selectionTool.selectingNodesArea?.position()
        })
      },
      // 拖动
      dragmove: () => {
        // 拖动中
        this.selectingNodesPositionByOffset(this.groupImmediateLocOffset)
      },
      dragend: () => {
        // 拖动结束

        this.selectingNodesPositionByOffset(this.groupImmediateLocOffset)

        // 重置状态
        this.reset()
      }

还有这:

      // 记录初始状态
      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {
        const anchor = this.render.transformer.getActiveAnchor()
        if (!anchor) {
          // 非变换
          if (e.evt.ctrlKey) {
            // 略
          } else {
            if (this.render.selectionTool.selectingNodesArea) {
              // 拖动前
              // 重置状态
              this.reset()
            }
          }
        } else {
          // 变换前

          // 重置状态
          this.reset()
        }
      }

还要处理 transformer 的配置 dragBoundFunc,从它获得 groupImmediateLocOffset 偏移量:

    // 拖动中
    dragBoundFunc: (pos: Konva.Vector2d) => {
      // transform pos 偏移
      const transformPosOffsetX = pos.x - this.transformerMousedownPos.x
      const transformPosOffsetY = pos.y - this.transformerMousedownPos.y

      // group loc 偏移
      this.groupImmediateLocOffset = {
        x: this.render.toStageValue(transformPosOffsetX),
        y: this.render.toStageValue(transformPosOffsetY)
      }

      return pos

      // 接着到 dragmove 事件处理
    }

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

  • 放大缩小所选的“磁贴效果”(基于网格)
  • 拖动所选的“磁贴效果”(基于网格)
  • 节点层次单个、批量调整
  • 键盘复制、粘贴
  • 等等。。。

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

源码

gitee源码

示例地址

标签:node,transformer,const,render,Konva,可视化,selectionTool,前端
From: https://www.cnblogs.com/xachary/p/18127777

相关文章

  • 前后端分离开发和接口文档管理平台YAPI以及前端工程化(Vue-cli)
    前后端分离开发和接口文档管理平台YAPI以及前端工程化(Vue-cli)前后端分离开发需求分析=>接口定义(API接口文档)=>前后端并行开发(遵守规范)=>测试(前端、后端)=>前后端联调测试YApi1.介绍:YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服......
  • 前端学习-vue视频学习014-组件通信
    尚硅谷视频教程通信方式1propsparent传child--只能传递非函数//Parent.vue<template><divclass="parent"><h3>parent</h3> <h3>car:{{car}}</h3> <Child:car="car"/></div></templat......
  • 前端开发之el-table(vue2中)固定列fixed滚动条被固定列盖住
    固定列fixed滚动条被固定列盖住效果图前言解决方案效果图前言在使用fixed固定列的时候会出现滚动条被盖住的情况解决方案改变el-table固定列的计算高度即可.el-table{.el-table__fixed-right,.el-table__fixed{height:auto!important;......
  • 3 突破编程_前端_SVG(rect 矩形)
    1rect元素的基本属性和用法在SVG中,<rect>元素用于创建矩形。<rect>元素有一些基本的属性,可以用来定义矩形的形状、位置、颜色等。以下是这些属性的详细解释:x和y:这两个属性定义矩形左上角的位置。x是矩形左上角到SVG画布左边缘的水平距离,y是矩形左上角到SVG画......
  • 前端开发命名规范
    前言优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。一.常见命名种类目前收集到的常......
  • 前端流程引擎有哪些框架推荐
    前端流程引擎通常是指用于设计、建模、执行和可视化业务流程、工作流或流程图的软件组件,它们通常包含图形化的设计器、流程模型解析与执行引擎、以及与用户交互的可视化界面。以下是一些推荐的前端流程引擎框架:bpmn-js:开源项目,基于BPMN2.0标准,提供流程图的绘制、解析......
  • ADS1299模拟前端(AFE)代替料LHE7909
    今天我和大家分享一颗国产可以代替ADS1299的料——LHE7909,这是一颗由领慧立芯设计生产的一款具备颅外脑电图(EEG)和心电图(ECG)应用所需的全部常用功能的模拟前端芯片,并且凭借其高集成度和出色的性能,能够创建多种可扩展的医疗仪器系统,而尺寸,功耗和总成本却大大降低。以下是详......
  • 遥感降水、气温数据的处理与可视化、ERA5大气再分析数据的统计分析、干旱监测及SCI论
    深度探讨人工智能在大气科学中的应用,特别是如何结合最新AI模型与Python技术处理和分析气候数据。课程介绍包括GPT-4等先进AI工具,旨在帮助学员掌握这些工具的功能及应用范围。内容覆盖使用GPT处理数据、生成论文摘要、文献综述、技术方法分析等实战案例,使学员能够将AI技术广泛应......
  • 前端性能优化
    性能指标方案rail性能模型,WebVitals性能测试工具1.Lighthouse的基本使用在谷歌浏览器的开发者工具中2.WebPageTest:[https://www.webpagetest.org/]只能够测试已经发布的网址3.ChromeDevTools测试性能、网络请求阻止,开发者工具网络中ctrl+shift+p开启面板,然后输入block,......
  • 前端面试题 — 前端页面性能优化
    1.减少HTTP请求:合并和压缩CSS、JavaScript和图片等静态资源。使用雪碧图来减少图片请求次数。使用字体图标代替图片图标,以减少HTTP请求。使用CSSSprites技术将多个小图标合并成一张大图来减少HTTP请求。2.优化资源加载:将JavaScript放在页面底部,以确保页面内容先加载。......