前端使用 Konva 实现可视化设计器(11)- 对齐效果

konva · 浏览次数 : 0

小编点评

**代码示例** ```typescript // Render组件 import { Render, useRef } from 'konva-react'; import { Types } from './types'; const render = useRef(null); // 事件处理函数 const onAlign = (type: Types.AlignType) => { render?.alignTool.align(type); }; // 右键菜单配置 const menus = [ { name: '垂直居中', action: onAlign, }, // 其他菜单项 ]; // 获取鼠标位置 const pos = render.current.stage.getPointerPosition(); // 判断是否在多个图形中找到重叠的元素 const selected = menus.find(menu => Konva.Util.haveIntersection({ ...pos, width: 1, height: 1, }, menu.name) ); // 显示鼠标指向的元素 if (selected) { selected.action(); } ``` **类型定义** ```typescript // types.ts enum Types { 垂直居中, 左对齐, 右对齐, 水平居中, 上对齐, 下对齐, } ``` **注释** * 使用 `useRef` 创建一个可复用的 `Render` 实例。 * 使用 `onAlign` 事件处理器捕获鼠标点击事件并调用 `alignTool.align` 方法。 * 右键菜单中包含多个选项,每个选项对应不同的 `alignType`。 * `haveIntersection` 方法用于检查两个 rect 是否重叠。 * 代码示例展示了如何使用右键菜单对图形进行调整。

正文

这一章补充一个效果,在多选的情况下,对目标进行对齐。基于多选整体区域对齐的基础上,还支持基于其中一个节点进行对齐。

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

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

基于整体的对齐

垂直居中

image

水平居中

image

左对齐

image

右对齐

image

上对齐

image

下对齐

image

基于目标节点的对齐

垂直居中(基于目标节点)

image

水平居中(基于目标节点)

image

左对齐(基于目标节点)

image

右对齐(基于目标节点)

image

上对齐(基于目标节点)

image

下对齐(基于目标节点)

image

对齐逻辑

放在 src/Render/tools/AlignTool.ts

import { Render } from '../index'
//
import * as Types from '../types'
import * as Draws from '../draws'
import Konva from 'konva'

export class AlignTool {
  static readonly name = 'AlignTool'

  private render: Render
  constructor(render: Render) {
    this.render = render
  }

  // 对齐参考点
  getAlignPoints(target?: Konva.Node | Konva.Transformer): { [index: string]: number } {
    let width = 0,
      height = 0,
      x = 0,
      y = 0

    if (target instanceof Konva.Transformer) {
      // 选择器
      // 转为 逻辑觉尺寸
      ;[width, height] = [
        this.render.toStageValue(target.width()),
        this.render.toStageValue(target.height())
      ]
      ;[x, y] = [
        this.render.toStageValue(target.x()) - this.render.rulerSize,
        this.render.toStageValue(target.y()) - this.render.rulerSize
      ]
    } else if (target !== void 0) {
      // 节点
      // 逻辑尺寸
      ;[width, height] = [target.width(), target.height()]
      ;[x, y] = [target.x(), target.y()]
    } else {
      // 默认为选择器
      return this.getAlignPoints(this.render.transformer)
    }

    return {
      [Types.AlignType.垂直居中]: x + width / 2,
      [Types.AlignType.左对齐]: x,
      [Types.AlignType.右对齐]: x + width,
      [Types.AlignType.水平居中]: y + height / 2,
      [Types.AlignType.上对齐]: y,
      [Types.AlignType.下对齐]: y + height
    }
  }

  align(type: Types.AlignType, target?: Konva.Node) {
    // 对齐参考点(所有)
    const points = this.getAlignPoints(target)

    // 对齐参考点
    const point = points[type]

    // 需要移动的节点
    const nodes = this.render.transformer.nodes().filter((node) => node !== target)

    // 移动逻辑
    switch (type) {
      case Types.AlignType.垂直居中:
        for (const node of nodes) {
          node.x(point - node.width() / 2)
        }
        break
      case Types.AlignType.水平居中:
        for (const node of nodes) {
          node.y(point - node.height() / 2)
        }
        break
      case Types.AlignType.左对齐:
        for (const node of nodes) {
          node.x(point)
        }
        break
      case Types.AlignType.右对齐:
        for (const node of nodes) {
          node.x(point - node.width())
        }
        break
      case Types.AlignType.上对齐:
        for (const node of nodes) {
          node.y(point)
        }
        break
      case Types.AlignType.下对齐:
        for (const node of nodes) {
          node.y(point - node.height())
        }
        break
    }
    // 更新历史
    this.render.updateHistory()
    // 更新预览
    this.render.draws[Draws.PreviewDraw.name].draw()
  }
}

还是比较容易理解的,要注意的主要是 transformer 获得的 size 和 position 是视觉尺寸,需要转为逻辑尺寸。

功能入口

准备些枚举值:

export enum AlignType {
  垂直居中 = 'Middle',
  左对齐 = 'Left',
  右对齐 = 'Right',
  水平居中 = 'Center',
  上对齐 = 'Top',
  下对齐 = 'Bottom'
}

按钮

在这里插入图片描述

      <button @click="onRestore">导入</button>
      <button @click="onSave">导出</button>
      <button @click="onSavePNG">另存为图片</button>
      <button @click="onSaveSvg">另存为Svg</button>
      <button @click="onPrev" :disabled="historyIndex <= 0">上一步</button>
      <button @click="onNext" :disabled="historyIndex >= history.length - 1">下一步</button>
      <!-- 新增 -->
      <button @click="onAlign(Types.AlignType.垂直居中)" :disabled="noAlign">垂直居中</button>
      <button @click="onAlign(Types.AlignType.左对齐)" :disabled="noAlign">左对齐</button>
      <button @click="onAlign(Types.AlignType.右对齐)" :disabled="noAlign">右对齐</button>
      <button @click="onAlign(Types.AlignType.水平居中)" :disabled="noAlign">水平居中</button>
      <button @click="onAlign(Types.AlignType.上对齐)" :disabled="noAlign">上对齐</button>
      <button @click="onAlign(Types.AlignType.下对齐)" :disabled="noAlign">下对齐</button>

按键生效的条件是,必须是多选,所以 render 需要暴露一个事件,跟踪选择节点:

		render = new Render(stageElement.value!, {
            // 略
            //
            on: {
              historyChange: (records: string[], index: number) => {
                history.value = records
                historyIndex.value = index
              },
              // 新增
              selectionChange: (nodes: Konva.Node[]) => {
                selection.value = nodes
              }
            }
          })

条件判断:

// 选择项
const selection: Ref<Konva.Node[]> = ref([])
// 是否可以进行对齐
const noAlign = computed(() => selection.value.length <= 1)
// 对齐方法
function onAlign(type: Types.AlignType) {
  render?.alignTool.align(type)
}

触发事件的地方:
src/Render/tools/SelectionTool.ts

  // 清空已选
  selectingClear() {
    // 选择变化了
    if (this.selectingNodes.length > 0) {
      this.render.config.on?.selectionChange?.([])
    }
    // 略
  }

  // 选择节点
  select(nodes: Konva.Node[]) {
    // 选择变化了
    if (nodes.length !== this.selectingNodes.length) {
      this.render.config.on?.selectionChange?.(nodes)
    }
    // 略
  }

右键菜单

在这里插入图片描述
在多选区域的空白处的时候右键,功能与按钮一样,不多赘述。

右键菜单(基于目标节点)

在这里插入图片描述
基于目标,比较特别,在多选的情况下,给内部的节点增加一个 hover 效果。
首先,拖入元素的时候,给每个节点准备一个 Konva.Rect 作为 hover 效果,默认不显示,且列入忽略的部分。

src/Render/handlers/DragOutsideHandlers.ts:

              // hover 框(多选时才显示)
              group.add(
                new Konva.Rect({
                  id: 'hoverRect',
                  width: image.width(),
                  height: image.height(),
                  fill: 'rgba(0,255,0,0.3)',
                  visible: false
                })
              )
              // 隐藏 hover 框
              group.on('mouseleave', () => {
                group.findOne('#hoverRect')?.visible(false)
              })

src/Render/index.ts:

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

src/Render/handlers/SelectionHandlers.ts:

 // 子节点 hover
      mousemove: () => {
        const pos = this.render.stage.getPointerPosition()
        if (pos) {
          // 获取所有图形
          const shapes = this.render.transformer.nodes()

          // 隐藏 hover 框
          for (const shape of shapes) {
            if (shape instanceof Konva.Group) {
              shape.findOne('#hoverRect')?.visible(false)
            }
          }

          // 多选
          if (shapes.length > 1) {
            // zIndex 倒序(大的优先)
            shapes.sort((a, b) => b.zIndex() - a.zIndex())

            // 提取重叠目标
            const selected = shapes.find((shape) =>
              // 关键 api
              Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
            )

            // 显示 hover 框
            if (selected) {
              if (selected instanceof Konva.Group) {
                selected.findOne('#hoverRect')?.visible(true)
              }
            }
          }
        }
      },
      mouseleave: () => {
        // 隐藏 hover 框
        for (const shape of this.render.transformer.nodes()) {
          if (shape instanceof Konva.Group) {
            shape.findOne('#hoverRect')?.visible(false)
          }
        }
      }

需要注意的是,hover 优先级是基于节点的 zIndex,所以判断 hover 之前,需要进行一次排序。
判断 hover,这里使用 Konva.Util.haveIntersection,判断两个 rect 是否重叠,鼠标表达为大小为 1 的 rect。
用 find 找到 hover 的目标节点,使用 find 找到第一个即可,第一个就是 zIndex 最大最上层那个。
把 hover 的目标节点内部的 hoverRect 显示出来就行了。
同样的,就可以判断是基于目标节点的右键菜单:
src/Render/draws/ContextmenuDraw.ts:

        if (target instanceof Konva.Transformer) {
          const pos = this.render.stage.getPointerPosition()

          if (pos) {
            // 获取所有图形
            const shapes = target.nodes()
            if (shapes.length > 1) {
              // zIndex 倒序(大的优先)
              shapes.sort((a, b) => b.zIndex() - a.zIndex())

              // 提取重叠目标
              const selected = shapes.find((shape) =>
                // 关键 api
                Konva.Util.haveIntersection({ ...pos, width: 1, height: 1 }, shape.getClientRect())
              )

              // 对齐菜单
              menus.push({
                name: '垂直居中' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.垂直居中, selected)
                }
              })
              menus.push({
                name: '左对齐' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.左对齐, selected)
                }
              })
              menus.push({
                name: '右对齐' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.右对齐, selected)
                }
              })
              menus.push({
                name: '水平居中' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.水平居中, selected)
                }
              })
              menus.push({
                name: '上对齐' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.上对齐, selected)
                }
              })
              menus.push({
                name: '下对齐' + (selected ? '于目标' : ''),
                action: () => {
                  this.render.alignTool.align(Types.AlignType.下对齐, selected)
                }
              })
            }
          }
        }

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

  • 连接线
  • 等等。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

与前端使用 Konva 实现可视化设计器(11)- 对齐效果相似的内容:

前端使用 Konva 实现可视化设计器(11)- 对齐效果

这一章补充一个效果,在多选的情况下,对目标进行对齐。基于多选整体区域对齐的基础上,还支持基于其中一个节点进行对齐。

前端使用 Konva 实现可视化设计器(16)- 旋转对齐、触摸板操作的优化

这一章解决两个缺陷,一是调整一些快捷键,使得 Mac 触摸板可以正常操作;二是修复一个 Issue,使得即使素材节点即使被旋转之后,也能正常触发磁贴对齐效果,有个小坑需要注意。

前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化

本章将处理一些缺陷的同时,实现支持连接点的自定义,一个节点可以定义多个连接点,最终可以满足类似图元接线的效果。

前端使用 Konva 实现可视化设计器(14)- 折线 - 最优路径应用【代码篇】

话接上回[《前端使用 Konva 实现可视化设计器(13)- 折线 - 最优路径应用【思路篇】》](https://www.cnblogs.com/xachary/p/18238704),这一章继续说说相关的代码如何构思的,如何一步步构建数据模型可供 AStar 算法进行路径规划,最终画出节点之间的...

前端使用 Konva 实现可视化设计器(13)- 折线 - 最优路径应用【思路篇】

这一章把直线连接改为折线连接,沿用原来连接点的关系信息。关于折线的计算,使用的是开源的 AStar 算法进行路径规划,启发方式为 曼哈顿距离,且不允许对角线移动。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示

前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线

这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示例地址 相关定义 连接点 记

前端使用 Konva 实现可视化设计器(10)- 对齐线

前端使用 Konva 实现可视化设计器,这次实现对齐线的交互功能,单个、多个、多选都可以对齐,同时还能磁贴。

前端使用 Konva 实现可视化设计器(9)- 另存为SVG

请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue 哟~ github源码 gitee源码 示例地址 另存为SVG 这一章增强了另存为的能力,实现“另存为SVG”,大概是全网唯一的实例分享了吧。 灵感来源:react-konva-custom-context

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

请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了明显的 Bug,可以提 Issue 哟~ 这一章我们实现一个预览框,实时、可交互定位的。 github源码 gitee源码 示例地址 预览框 定位方法 移动画布,将传入 x,y 作为画布中心: // 更新中心位置 updateCenter

前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步

请大家动动小手,给我一个免费的 Star 吧~ 这一章实现导入导出为JSON文件、另存为图片、上一步、下一步。 github源码 gitee源码 示例地址 导出为JSON文件 提取需要导出的内容 getView() { // 复制画布 const copy = this.render.stage.c