您现在的位置是:首页 > 短信大全

前端使用 Konva 实现可视化设计器(6)- 复制粘贴、删除、位置、zIndex调整

作者:康由时间:2024-05-01 13:50:18分类:短信大全

简介  文章浏览阅读819次,点赞15次,收藏8次。请大家动动小手,给我一个免费的 Star 吧~这一章处理一下复制、粘贴、删除、画布归位、层次调整,通过右键菜单控制。

点击全文阅读

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

这一章处理一下复制、粘贴、删除、画布归位、层次调整,通过右键菜单控制。

github源码

gitee源码

示例地址

复制粘贴

复制粘贴(通过快捷键)

在这里插入图片描述

  // 复制暂存  pasteCache: Konva.Node[] = [];  // 粘贴次数(用于定义新节点的偏移距离)  pasteCount = 1;  // 复制  pasteStart() {    this.pasteCache = this.render.selectionTool.selectingNodes.map((o) => {      const copy = o.clone();      // 恢复透明度、可交互      copy.setAttrs({        listening: true,        opacity: copy.attrs.lastOpacity ?? 1,      });      // 清空状态      copy.setAttrs({        nodeMousedownPos: undefined,        lastOpacity: undefined,        lastZIndex: undefined,        selectingZIndex: undefined,      });      return copy;    });    this.pasteCount = 1;  }  // 粘贴  pasteEnd() {    if (this.pasteCache.length > 0) {      this.render.selectionTool.selectingClear();      this.copy(this.pasteCache);      this.pasteCount++;    }  }

快捷键处理:

    keydown: (e: GlobalEventHandlersEventMap['keydown']) => {        if (e.ctrlKey) {          if (e.code === Types.ShutcutKey.C) {            this.render.copyTool.pasteStart() // 复制          } else if (e.code === Types.ShutcutKey.V) {            this.render.copyTool.pasteEnd() // 粘贴          }        }      }    }

逻辑比较简单,可以关注代码中的注释。

复制粘贴(右键)

在这里插入图片描述

  /**   * 复制粘贴   * @param nodes 节点数组   * @param skip 跳过检查   * @returns 复制的元素   */  copy(nodes: Konva.Node[]) {    const arr: Konva.Node[] = [];    for (const node of nodes) {      if (node instanceof Konva.Transformer) {        // 复制已选择        const backup = [...this.render.selectionTool.selectingNodes];        this.render.selectionTool.selectingClear();        this.copy(backup);      } else {        // 复制未选择        const copy = node.clone();        // 使新节点产生偏移        copy.setAttrs({          x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,          y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,        });        // 插入新节点        this.render.layer.add(copy);        // 选中复制内容        this.render.selectionTool.select([...this.render.selectionTool.selectingNodes, copy]);      }    }    return arr;  }

逻辑比较简单,可以关注代码中的注释。

删除

在这里插入图片描述

处理方法:

  // 移除元素  remove(nodes: Konva.Node[]) {    for (const node of nodes) {      if (node instanceof Konva.Transformer) {        // 移除已选择的节点        this.remove(this.selectionTool.selectingNodes);        // 清除选择        this.selectionTool.selectingClear();      } else {        // 移除未选择的节点        node.remove();      }    }  }

事件处理:

      keydown: (e: GlobalEventHandlersEventMap['keydown']) => {        if (e.ctrlKey) {          // 略        } else if (e.code === Types.ShutcutKey.删除) {          this.render.remove(this.render.selectionTool.selectingNodes)        }      }

画布归位

逻辑比较简单,恢复画布比例和偏移量:

  // 恢复位置大小  positionZoomReset() {    this.render.stage.setAttrs({      scale: { x: 1, y: 1 }    })    this.positionReset()  }  // 恢复位置  positionReset() {    this.render.stage.setAttrs({      x: this.render.rulerSize,      y: this.render.rulerSize    })    // 更新背景    this.render.draws[Draws.BgDraw.name].draw()    // 更新比例尺    this.render.draws[Draws.RulerDraw.name].draw()    // 更新参考线    this.render.draws[Draws.RefLineDraw.name].draw()  }

稍微说明一下,初始位置需要考虑比例尺的大小。

层次调整

关于层次的调整,相对比较晦涩。

在这里插入图片描述

一些辅助方法

获取需要处理的节点,主要是处理 transformer 内部的节点:

  // 获取移动节点  getNodes(nodes: Konva.Node[]) {    const targets: Konva.Node[] = []    for (const node of nodes) {      if (node instanceof Konva.Transformer) {        // 已选择的节点        targets.push(...this.render.selectionTool.selectingNodes)      } else {        // 未选择的节点        targets.push(node)      }    }    return targets  }

获得计算所需的最大、最小 zIndex:

  // 最大 zIndex  getMaxZIndex() {    return Math.max(      ...this.render.layer        .getChildren((node) => {          return !this.render.ignore(node)        })        .map((o) => o.zIndex())    )  }  // 最小 zIndex  getMinZIndex() {    return Math.min(      ...this.render.layer        .getChildren((node) => {          return !this.render.ignore(node)        })        .map((o) => o.zIndex())    )  }

记录选择之前的 zIndex

由于被选择的节点会被临时置顶,会影响节点层次的调整,所以选择之前需要记录一下选择之前的 zIndex:

  // 更新 zIndex 缓存  updateLastZindex(nodes: Konva.Node[]) {    for (const node of nodes) {      node.setAttrs({        lastZIndex: node.zIndex()      })    }  }

处理 transformer 的置顶影响

通过 transformer 选择的时候,所选节点的层次已经被置顶。

所以调整时需要有个步骤:

记录已经被 transformer 影响的每个节点的 zIndex(其实就是记录置顶状态)调整节点的层次恢复被 transformer 选择的节点的 zIndex(其实就是恢复置顶状态)

举例子:

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7

记录选择 C D E 之前的 lastZIndex:C/3 D/4 E/5

选择后,“临时置顶” C D E:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

此时置底了 C D E,由于上面记录了选择之前的 lastZIndex,直接计算 lastZIndex,变成 C/1 D/2 E/3

在 selectingClear 的时候,会根据 lastZIndex 让 zIndex 的调整生效:

逐步变化:

0、A/1 B/2 F/3 G/4 C/5 D/6 E/7 改变 C/5 -> C/1
1、C/1 A/2 B/3 F/4 G/5 D/6 E/7 改变 D/6 -> D/2
2、C/1 D/2 A/3 B/4 F/5 G/6 E/7 改变 E/7 -> E/3
3、C/1 D/2 E/3 A/4 B/5 F/6 G/7 完成调整

因为 transformer 的存在,调整完还要恢复原来的“临时置顶”:

A/1 B/2 F/3 G/4 C/5 D/6 E/7

下面是记录选择之前的 zIndex 状态、恢复调整之后的 zIndex 状态的方法:

  // 记录选择期间的 zIndex  updateSelectingZIndex(nodes: Konva.Node[]) {    for (const node of nodes) {      node.setAttrs({        selectingZIndex: node.zIndex()      })    }  }  // 恢复选择期间的 zIndex  resetSelectingZIndex(nodes: Konva.Node[]) {    nodes.sort((a, b) => a.zIndex() - b.zIndex())    for (const node of nodes) {      node.zIndex(node.attrs.selectingZIndex)    }  }

关于 zIndex 的调整

主要分两种情况:已选的节点、未选的节点

已选:如上面所说,调整之余,还要处理 transformer 的置顶影响未选:直接调整即可
  // 上移  up(nodes: Konva.Node[]) {    // 最大zIndex    const maxZIndex = this.getMaxZIndex()    const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex())    // 上移    let lastNode: Konva.Node | null = null    if (this.render.selectionTool.selectingNodes.length > 0) {      this.updateSelectingZIndex(sorted)      for (const node of sorted) {        if (          node.attrs.lastZIndex < maxZIndex &&          (lastNode === null || node.attrs.lastZIndex < lastNode.attrs.lastZIndex - 1)        ) {          node.setAttrs({            lastZIndex: node.attrs.lastZIndex + 1          })        }        lastNode = node      }      this.resetSelectingZIndex(sorted)    } else {      // 直接调整      for (const node of sorted) {        if (          node.zIndex() < maxZIndex &&          (lastNode === null || node.zIndex() < lastNode.zIndex() - 1)        ) {          node.zIndex(node.zIndex() + 1)        }        lastNode = node      }      this.updateLastZindex(sorted)    }  }

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,上移 D F

执行一次:

移动F,A/1 B/2 C/3 D/4 E/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 D/5 G/6 F/7

再执行一次:

移动F,已经到头了,不变,A/1 B/2 C/3 E/4 D/5 G/6 F/7

移动D,A/1 B/2 C/3 E/4 G/5 D/6 F/7

再执行一次:

移动F,已经到尾了,不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

移动D,已经贴着 F 了,为了保持 D F 的相对顺序,也不变,A/1 B/2 C/3 E/4 G/5 D/6 F/7

结束

  // 下移  down(nodes: Konva.Node[]) {    // 最小 zIndex    const minZIndex = this.getMinZIndex()    const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex())    // 下移    let lastNode: Konva.Node | null = null    if (this.render.selectionTool.selectingNodes.length > 0) {      this.updateSelectingZIndex(sorted)      for (const node of sorted) {        if (          node.attrs.lastZIndex > minZIndex &&          (lastNode === null || node.attrs.lastZIndex > lastNode.attrs.lastZIndex + 1)        ) {          node.setAttrs({            lastZIndex: node.attrs.lastZIndex - 1          })        }        lastNode = node      }      this.resetSelectingZIndex(sorted)    } else {      // 直接调整      for (const node of sorted) {        if (          node.zIndex() > minZIndex &&          (lastNode === null || node.zIndex() > lastNode.zIndex() + 1)        ) {          node.zIndex(node.zIndex() - 1)        }        lastNode = node      }      this.updateLastZindex(sorted)    }  }

直接举例子(忽略 transformer 的置顶影响):

现在有节点:

A/1 B/2 C/3 D/4 E/5 F/6 G/7,下移 B D

执行一次:

移动B,B/1 A/2 C/3 D/4 E/5 F/6 G/7

移动D,B/1 A/2 D/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 A/2 D/3 C/4 E/5 F/6 G/7

移动D,B/1 D/2 A/3 C/4 E/5 F/6 G/7

再执行一次:

移动B,已经到头了,不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

移动D,已经贴着 B 了,为了保持 B D 的相对顺序,也不变,B/1 D/2 A/3 C/4 E/5 F/6 G/7

结束

  // 置顶  top(nodes: Konva.Node[]) {    // 最大 zIndex    let maxZIndex = this.getMaxZIndex()    const sorted = this.getNodes(nodes).sort((a, b) => b.zIndex() - a.zIndex())    if (this.render.selectionTool.selectingNodes.length > 0) {      // 先选中再调整      this.updateSelectingZIndex(sorted)      // 置顶      for (const node of sorted) {        node.setAttrs({          lastZIndex: maxZIndex--        })      }      this.resetSelectingZIndex(sorted)    } else {      // 直接调整      for (const node of sorted) {        node.zIndex(maxZIndex)      }      this.updateLastZindex(sorted)    }  }

从高到低,逐个移动,每次移动递减 1

  // 置底  bottom(nodes: Konva.Node[]) {    // 最小 zIndex    let minZIndex = this.getMinZIndex()    const sorted = this.getNodes(nodes).sort((a, b) => a.zIndex() - b.zIndex())    if (this.render.selectionTool.selectingNodes.length > 0) {      // 先选中再调整      this.updateSelectingZIndex(sorted)      // 置底      for (const node of sorted) {        node.setAttrs({          lastZIndex: minZIndex++        })      }      this.resetSelectingZIndex(sorted)    } else {      // 直接调整      for (const node of sorted) {        node.zIndex(minZIndex)      }      this.updateLastZindex(sorted)    }  }

从低到高,逐个移动,每次移动递增 1

调整 zIndex 的思路比较个性化,所以晦涩。要符合 konva 的 zIndex 特定,且达到目的,算法可以自行调整。

右键菜单

事件处理

      mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {        this.state.lastPos = this.render.stage.getPointerPosition()        if (e.evt.button === Types.MouseButton.左键) {          if (!this.state.menuIsMousedown) {            // 没有按下菜单,清除菜单            this.state.target = null            this.draw()          }        } else if (e.evt.button === Types.MouseButton.右键) {          // 右键按下          this.state.right = true        }      },      mousemove: () => {        if (this.state.target && this.state.right) {          // 拖动画布时(右键),清除菜单          this.state.target = null          this.draw()        }      },      mouseup: () => {        this.state.right = false      },      contextmenu: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['contextmenu']>) => {        const pos = this.render.stage.getPointerPosition()        if (pos && this.state.lastPos) {          // 右键目标          if (pos.x === this.state.lastPos.x || pos.y === this.state.lastPos.y) {            this.state.target = e.target          } else {            this.state.target = null          }          this.draw()        }      },      wheel: () => {        // 画布缩放时,清除菜单        this.state.target = null        this.draw()      }

逻辑说明都在注释里了,主要处理的是右键菜单出现的位置,以及出现和消失的时机,最后是右键的目标。

  override draw() {    this.clear()    if (this.state.target) {      // 菜单数组      const menus: Array<{        name: string        action: (e: Konva.KonvaEventObject<MouseEvent>) => void      }> = []      if (this.state.target === this.render.stage) {        // 空白处        menus.push({          name: '恢复位置',          action: () => {            this.render.positionTool.positionReset()          }        })        menus.push({          name: '恢复大小位置',          action: () => {            this.render.positionTool.positionZoomReset()          }        })      } else {        // 未选择:真实节点,即素材的容器 group         // 已选择:transformer        const target = this.state.target.parent        // 目标        menus.push({          name: '复制',          action: () => {            if (target) {              this.render.copyTool.copy([target])            }          }        })        menus.push({          name: '删除',          action: () => {            if (target) {              this.render.remove([target])            }          }        })        menus.push({          name: '置顶',          action: () => {            if (target) {              this.render.zIndexTool.top([target])            }          }        })        menus.push({          name: '上一层',          action: () => {            if (target) {              this.render.zIndexTool.up([target])            }          }        })        menus.push({          name: '下一层',          action: () => {            if (target) {              this.render.zIndexTool.down([target])            }          }        })        menus.push({          name: '置底',          action: () => {            if (target) {              this.render.zIndexTool.bottom([target])            }          }        })      }      // stage 状态      const stageState = this.render.getStageState()      // 绘制右键菜单      const group = new Konva.Group({        name: 'contextmenu',        width: stageState.width,        height: stageState.height      })      let top = 0      // 菜单每项高度      const lineHeight = 30      const pos = this.render.stage.getPointerPosition()      if (pos) {        for (const menu of menus) {          // 框          const rect = new Konva.Rect({            x: this.render.toStageValue(pos.x - stageState.x),            y: this.render.toStageValue(pos.y + top - stageState.y),            width: this.render.toStageValue(100),            height: this.render.toStageValue(lineHeight),            fill: '#fff',            stroke: '#999',            strokeWidth: this.render.toStageValue(1),            name: 'contextmenu'          })          // 标题          const text = new Konva.Text({            x: this.render.toStageValue(pos.x - stageState.x),            y: this.render.toStageValue(pos.y + top - stageState.y),            text: menu.name,            name: 'contextmenu',            listening: false,            fontSize: this.render.toStageValue(16),            fill: '#333',            width: this.render.toStageValue(100),            height: this.render.toStageValue(lineHeight),            align: 'center',            verticalAlign: 'middle'          })          group.add(rect)          group.add(text)          // 菜单事件          rect.on('click', (e) => {            if (e.evt.button === Types.MouseButton.左键) {              // 触发事件              menu.action(e)              // 移除菜单              this.group.removeChildren()              this.state.target = null            }            e.evt.preventDefault()            e.evt.stopPropagation()          })          rect.on('mousedown', (e) => {            if (e.evt.button === Types.MouseButton.左键) {              this.state.menuIsMousedown = true              // 按下效果              rect.fill('#dfdfdf')            }            e.evt.preventDefault()            e.evt.stopPropagation()          })          rect.on('mouseup', (e) => {            if (e.evt.button === Types.MouseButton.左键) {              this.state.menuIsMousedown = false            }          })          rect.on('mouseenter', (e) => {            if (this.state.menuIsMousedown) {              rect.fill('#dfdfdf')            } else {              // hover in              rect.fill('#efefef')            }            e.evt.preventDefault()            e.evt.stopPropagation()          })          rect.on('mouseout', () => {            // hover out            rect.fill('#fff')          })          rect.on('contextmenu', (e) => {            e.evt.preventDefault()            e.evt.stopPropagation()          })          top += lineHeight - 1        }      }      this.group.add(group)    }  }

逻辑也不复杂,根据右键的目标分配相应的菜单项

空白处:恢复位置、大小

节点:复制、删除、上移、下移、置顶、置底

绘制右键菜单

右键的目标有二种情况:空白处、单个/多选节点。

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

实时预览窗导出、导入对齐效果等等。。。

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

源码

gitee源码

示例地址

点击全文阅读

郑重声明:

本站所有活动均为互联网所得,如有侵权请联系本站删除处理

上一篇:防溺水教育提醒制度

下一篇:返回列表

我来说两句