首页 > 其他分享 >前端使用 Konva 实现可视化设计器(11)- 对齐效果

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

时间:2024-05-18 21:09:54浏览次数:17  
标签:11 node const render Konva 对齐 AlignType

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

请大家动动小手,给我一个免费的 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源码

示例地址

标签:11,node,const,render,Konva,对齐,AlignType
From: https://www.cnblogs.com/xachary/p/18199767

相关文章

  • 免费解锁Windows 11的HEVC支持:轻松播放4K电影的详细步骤
    Windows11安装完成后,用电影和电视这个应用打开4K或者8K的MP4文件时,提示缺少解码器以下就是本人解决过程第一步:查找HEVC扩展的页面直接浏览器打开:https://apps.microsoft.com/,搜索HEVC扩展,得到以下页面)复制地址栏的网址,如上图红框第二步:获取HEVC扩展的下载链接直接浏览器......
  • 【论文笔记-55~】多语言实体对齐
    2012~2018MultilingualKnowledgeGraphEmbeddingsforCross-lingualKnowledgeAlignment文章核心观点:这篇文章介绍了一种名为MTransE的多语言知识图谱嵌入模型,旨在实现跨语言知识对齐。该模型由知识模型和匹配模型两部分组成,其中知识模型采用TransE对每个语言的实体和关系......
  • 20211314 电子政务及安全研究报告
    任务详情通过微信读书、cnki、密码行业标准化技术委员会网站等途径查找电子政务及安全相关内容,列出你查找的资料综述解电子政务的内容和网络规范的内容综述政务应急平台的内容综述电子政务安全的内容综述电子政务安全管理的内容综述电子政务信息安全等级保护、电子政务认证......
  • HTML 11 - Phrase Tags
     Thephrasetagshavebeendesignedforspecificpurposes,thoughtheyaredisplayedinasimilarwayasotherbasictagslike<b>,<i>,<pre>,and<tt>,asyouhaveseeninthepreviouschapter.Thischapterwilltakeyouthrough......
  • 代码随想录算法训练营第第11天 | 20. 有效的括号 、1047. 删除字符串中的所有相邻重
    今天的题主要是关于栈的,比较简单,一次性过20.有效的括号讲完了栈实现队列,队列实现栈,接下来就是栈的经典应用了。大家先自己思考一下有哪些不匹配的场景,在看视频我讲的都有哪些场景,落实到代码其实就容易很多了。题目链接/文章讲解/视频讲解:https://programmercarl.com/0020.......
  • Codeforces 1113B Sasha and Magnetic Machines 题解
    题目简述有一个长度为$n$的正整数序列。你可以对这个数列进行最多$1$次的如下操作:选择两个数$i$和$j$,其中$1\leqi,j\leqn$并且$i\neqj$,并选择一个可以整除$a_i$的正整数$x$,然后将$a_i$变为$\frac{a_i}{x}$,将$a_j$变为$a_j\cdotx$。问你操作后,该序......
  • Codeforces 1178B WOW Factor
    题目简述给定一个只含$v$和$o$的字符串$s$,求字符串中有多少个$wow$(一个$w$即为连续的两个$v$)。题目分析考虑枚举每一个$o$,设下标为$i$,统计它左边和右边各有多少个$w$,分别设为$a_{i-1}$和$b_{i+1}$,依据乘法原理,将它们乘起来即为答案,累加即可。接下来,考虑怎么处......
  • 「杂题乱刷」AT_abc211_e
    题目链接[ABC211E]RedPolyomino(luogu)[ABC211E]RedPolyomino(at)解题思路从第三个样例可以看出总的方案数一定很少,因此我们可以直接确定第一个被染色的格子后直接向外爆搜,搜到最后可以使用哈希判重,但光凭这样的话\(2\)秒钟肯定跑不过去,因此我们可以在搜索的过程中使用......
  • Oracle11g-EXP-00091错误
    环境说明oracle11gwin10问题情况在终端中exp导出数据库时,遇到报错“EXP-00091”,按照网上教程修改NLS_LANG但是没有效果。最终原因在power中设置环境变量NLS_LANG的方法与CMD不一样。备注记录先通过服务端查询编码集select*fromnls_database_parameterstwheret.pa......
  • hdu1176免费馅饼java
    一个数塔问题,以时间为纵坐标、位置为横坐标创建一个二维数组,然后从下往上相加。状态转移方程:9>=j>=1时dp[i][j]+=max(max(dp[i+1][j],dp[i+1][j+1]),dp[i+1][j-1])  j=0时 dp[i][j]+=max(dp[i+1][j],dp[i+1][j+1]) j=10时dp[i][j]+=......