图形编辑器开发:最基础但却复杂的选择工具

news2025/2/12 21:51:27

大家好,我是前端西瓜哥。

对于一个图形设计软件,它最基础的工具是什么?选择工具

但这个选择工具,却是相当的复杂。这次我来和各位,细说细说选择工具的一些弯弯道道。

我正在开发的图形设计工具的:

https://github.com/F-star/suika

线上体验:

https://blog.fstars.wang/app/suika/

单选

最基本的,要做到单个图形的选中。

光标停留在图形上方,按下鼠标左键,这个图形就被选中了。这就是一个简单的选中了单个图形的场景。

注意必须是 mousedown,不是 click。后面会说为什么。

在代码层,我们会使用 “图形拾取” 算法确定光标落在哪个图形的点击区域上,注意考虑隐藏、锁定、组的情况。

如果你对图形拾取的细节感兴趣,可以看我的这篇文章:

《如何在 Canvas 上实现图形拾取?》

隐藏和锁定的图形会被忽略,如果点的是组下的一个元素,要将整个组的所有元素都选中。

清空 被选中图形集合(暂且叫做 selectSet),然后把这个图形添加进去。

selectSet.clear()
selectSet.add(targetEl)

选中集合保存的是被选中的图形,可以保存 id,也可以是图形对象。

在渲染层,会对被选中的图形进行轮廓高亮,让用户有感知。

此外还会有一个 矩形选中框,上面还会有控制点,让用户可以缩放和旋转图形。

选中框是图形的包围盒,通常是 带旋转的 OBB 包围盒

如果点击到空白区域,要将 selectSet 清空。

在这里插入图片描述

多选

有时候我们希望选中出多个图形。

通常的做法是,按住 Shift 键,然后点击一个图形。

同时也要 支持取消选中:原来被选中的一个图形,我按住 Shift 再

代码的核心逻辑是:

如果这个图形不在 selectSet 中,将其加入;如果这个图形在 selectSet,将其移除

if (event.shiftKey) {
  if (selectSet.has(targetEl)) {
    selectSet.delete(targetEl)
  } else {
    selectSet.add(targetEl)
  }
}

多个图形被选中了,除了给它们高亮轮廓线,我们还需要用一个更大的矩形选中框包裹所有被选中图形

一个小点:如果是取消选中的逻辑,需要鼠标释放后才更新 selectSet。因为要防止和后面会说的按住 Shift 水平垂直拖拽冲突。

在这里插入图片描述

框选

框选,提供了 一次性选中大量特定区域内图形 的能力。

在空白区域按下鼠标拖拽,然后释放,可以构造出一个矩形,这个矩形我们称为 “选区”。

在这里插入图片描述

选区矩形会和图形进行碰撞检测判断,决定将哪些图形是被框选中的。

碰撞检测有三种方案:

  1. 选区矩形和选中图形的包围盒属于 包含(contain)关系
  2. 选区矩形和选中图形的包围盒属于 相交(intersect)关系
  3. 不使用包围盒,精准判断是否有真正的 像素上的相交

个人比较推荐相交的判断方案,figma 也选择了该方案。

如果你对碰撞检测的细节感兴趣,可以看我之前写的文章:

《图形编辑器——矩形选区是如何实现选中多个图形的?》

《几何算法:矩形碰撞和包含检测算法》

框选可以和多选结合。即你可以按住 Shift 键,然后去框选。

它的效果是和按住 Shift 一个个去选中图形的效果是一样的。

核心代码实现:

if (!event.shiftKey) {
  selectSet.clear();
}

for (const el of elementsInScence) {
  // 判断是否碰撞,这个方法
  if (isRectIntersect(selectionBox, el)) {
    // 普通框选
    if (!event.shiftKey) {
      selectSet.add(el);
    }
    // 连续和框选的组合
    else {
      if (selectSet.has(el)) {
        selectSet.delete(el);
      } else {
        selectSet.add(el);
      }
    }
  }
}

移动

选择工具,主要是用来选择,选中后一个很普遍的操作是:移动选中元素

所以这也是它有时候也被叫做 移动工具 的原因。

移动的交互过程:

  1. 光标停留在已经被选中的图形上,按下鼠标不放;
  2. 然后拖拽鼠标,被选中图形跟随光标移动;
  3. 释放鼠标,表示移动到目标位置,移动结束。

在这里插入图片描述

代码核心实现:

  1. 移动前此时记录图形的位置,和起始位置;
  2. 拖拽时计算相对位移,更新图形的位置;
  3. 释放时重置状态,以及记录到历史记录中。
// 图形移动前位置
let elStartCoords = [];
// 鼠标按下事件的光标位置,计算偏移量时作为基准
let startCoord = { x: undefined, y: undefined };

const onStart = (e) => {
  // 记录初始坐标
  elStartCoords = elements.map((el) => ({ x: el.x, y: el.y }));
  startCoord.x = e.clientX;
  startCoord.y = e.clientY;
};

const onDrag = (e) => {
  // 计算偏移量,更新坐标
  const dx = e.clientX - startCoord.x;
  const dy = e.clientY - startCoord.y;
  elements.forEach((el, i) => {
    el.x = elStartCoords[i].x + dx;
    el.y = elStartCoords[i].y + dy;
  });
};

const onEnd = () => {
  // 重置状态
  elStartCoords = [];
  startCoord = { x: undefined, y: undefined };
};

按住 Shift 键的垂直水平移动

假设我们做好了几个对齐的图形,当我们移动其中一个图形的时候,希望能够保持原来的对齐。

这时候,限制移动为水平或垂直方向就很有用。

通常通过在拖拽时按住 Shift 来开启这个能力。

在这里插入图片描述

要点:

  1. 拖拽的中途从没按住 Shift 到按住,要立即响应,代码实现上要补一个键盘事件监听,而不是靠鼠标移动事件,因为你不移动鼠标,被选中元素就不会更新。
  2. 比较 dx 和 dy 的大小。dx 大,水平移动;dy 大,垂直移动。这样图形就能尽量靠近十字线(水平线+垂直线)

对齐到像素网格

对齐到网格,开启后,让图形在移动的时候,让图片尽量贴到网格线上。

在这里插入图片描述

做法是将一个或多个图形的包围盒(AABB)的左上角坐标,进行取余,得到一个落在网格线上的位置,用这位置去更新选中图形。

扩展能力:控制点

选中图形,是为了对它们进行操作。

这些 操作的实现,要通过控制点来落地

常见的有:

  • 缩放控制点,在图形选中框的 4 个角上;

  • 旋转控制点,拖拽它设置图形的旋转,旋转控制点;

  • 给图形设置渐变填充色,需要指定两种颜色的颜色和位置,需要的 渐变色控制点

下面是 figma 的缩放和旋转演示,我开发的编辑器还没实现完整。

在这里插入图片描述

此外,不同图形绘制工具可能会有它们独有的操作方式,这些都需要你根据图形的特性去设计。

看看 Figma 对不同图形的特殊控制点逻辑。

在这里插入图片描述

所以选择工具模块在设计上,要提供 注册各种类型图形控制点逻辑 的能力。

在 “图形拾取” 时,要把控制点也考虑进来,光标是否点在控制点上。

如果点在控制点上,拖拽逻辑就要走控制点的逻辑,不再走选择工具的基础逻辑。

其他

还有一些可考虑实现的增强能力:

  • 双击,进入编辑模式,进行一些更复杂的操作,比如可以变成贝塞尔曲线操作任意点。
  • 移动时,用线条显示和其他图形的点(比如中点、选中框角落的 4 个点)的距离,并在很接近时吸附过去。

结尾

总结一下,选择工具,是一款图形设计软件最基础的功能。

它的作用是选中的图形,对它们进行操作,目的是 更新指定图形属性

最基础的操作是移动,接着是通过控制点实现的增强操作。

控制点操作的两个基本能力是旋转和缩放。然后我们会根据不同类型的图形,去实现不同的控制点逻辑。

说是工具的一种,但它其实的定位更多是底层的基础建设。

我是前端西瓜哥,欢迎关注我,学习更多图形开发知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/639713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

复习并发编程的基础知识(二)

线程的状态6种状态及生命周期 1.new 2.Runnable(Ready和Running) 3.Blocked 4.Waiting 5.Timed_Waiting 6.Terminated 线程同步 同步:一些敏感的数据(比如共享的需要修改的资源)不允许被多个线程同时访问&#…

centos7 gitlab安装配置

gitlab概述 GitLab是一个基于Web的Git存储库管理和代码协作平台。它提供了一套完整的工具和功能,使团队能够更高效地进行代码版本控制、协作开发和持续集成/持续部署(CI/CD)。 以下是GitLab的主要功能和概述: 版本控制系统&…

使用布隆过滤器的flink十亿级数据实时过滤实践一

1项目背景 1.1 需求 实时推荐项目需求如下:根据用户实时行为(如关注,播放,收藏)推荐该UP主(关注的up主,播放视频发布up主,收藏up主)或其相似UP主的作品,UP主及相似UP主下的作品是提前离线召回…

react---生命周期

目录 1.新旧生命周期对比 2.常用的3个钩子函数 3.生命周期(旧) 4.生命周期(新) React 中为我们提供了一些生命周期钩子函数,让我们能在 React 执行的重要阶段,在钩子函数中做一些事情。 1.新旧生命周期…

数据安全--17--数据安全管理之数据传输

本博客地址:https://security.blog.csdn.net/article/details/131061729 一、数据传输概述 数据传输有两个主体,一个是数据发送方,另一个是数据接收方。数据在通过不可信或者较低安全性的网络进行传输时,容易发生数据被窃取、伪…

Mybatis源码学习之全局配置文件和映射文件解析

全局配置文件和映射文件解析 全局配置文件解析 public static void main(String[] args) throws IOException {// 读取配置文件InputStream is Resources.getResourceAsStream("org/apache/ibatis/builder/MapperConfig1.xml");// 创建SqlSessionFactory工厂SqlSes…

JDK11 官网下载(提供网盘下载资源)

目录 引言一、Oracle(甲骨文)二、JDK11下载1.JDK11下载入口2.JDK版本说明3.JDK11下载前说明4.JDK11下载 三、网盘下载1.资源提供说明2.资源列表清单(持续更新中...)3.获取方式 总结 引言 我们要学习 Java 语言去开发 Java 程序&a…

k8s 基本架构

k8s 中支持的 node 数 和 pod 数 k8s 也是逐步发展过来的,来看看以前和现在支持的 node 数 和 pod 数对比 node 即 节点 , 早期的 k8s 版本能够支持 100 台节点,现在 k8s 可以支持到 2000 台了 pod 数,早期的版本可以支持 1000 …

Android 自定义View 之 Dialog弹窗

Dialog弹窗 前言正文一、弹窗视图帮助类二、弹窗控制类三、监听接口四、样式五、简易弹窗六、常规使用七、简易使用八、源码 前言 在日常开发中用到弹窗是比较多的,常用于提示作用,比如错误操作提示,余额不足提示,退出登录提示等&…

linux 内核版本和发行版本

当要明确自己的Linux系统的版本号时,大多数情况是用命令确定Linux内核版本的。不过这个还是要与CentOS的版本号(就是你使用的Linux系统的发行版本)区分开来,这两个不是一个东西。 一、发行版本号 比如当时安装CentOS时&#x…

【Python】集合 set ① ( 集合定义 | 集合特点 | 代码示例 - 集合定义 )

文章目录 一、集合特点二、集合定义三、代码示例 - 集合定义 一、集合特点 在之前 的博客中 介绍了 列表 / 元组 / 字符串 数据容器 , 列表 支持 定义后 , 增加元素 / 修改元素 / 删除元素 , 并且 列表中可以存储 重复 / 有序 的元素 ;元组 定义后 不能 进行 增加元素 / 修改元…

(转载)有导师学习神经网络的回归拟合(matlab实现)

神经网络的学习规则又称神经网络的训练算法,用来计算更新神经网络的权值和阈值。学习规则有两大类别:有导师学习和无导师学习。在有导师学习中,需要为学习规则提供一系列正确的网络输入/输出对(即训练样本),当网络输入时,将网络输…

对于Promise的理解

1.什么是回调地狱 多层异步函数的嵌套叫做回调地狱 代码1: setTimeout(() > {console.log(吃饭);setTimeout(() > {console.log(睡觉);setTimeout(() > {console.log(打豆豆);}, 1000);}, 2000);}, 3000); 代码2: 通过id获取用户名,通过用户名获取邮箱…

如何自动识别快递单号和批量查询的方法

最近有很多朋友问我,有没有办法批量查询快递单号,查询该快递单号的所有物流发货信息?今天小编就来分享一个实用的查询技巧,教大家轻松查询多个快递单号,还可以一键保存查询数据,一起来看看吧。 首先今天我们…

PoseNet深度网络进行6D位姿估计的训练,python3实现

0.相关github网址 原版github代码-caffe实现tensorflow实现,相关版本较低,python2,本文根据此代码迁移到python3上面。pytorch实现,但将骨干模型从goglenet改成了resnet,实验效果得到提升,但没公布预训练权…

快递单号一键批量查询的具体操作方法和步骤

最近做电商的朋友对一个话题很感兴趣:如何批量查询快递单号?今天,小编给你安利一款软件:固乔快递查询助手,支持大量快递单号的批量查询。下面我们来看看批量查询的具体操作方法。 小伙伴们需要先在“固乔科技”的官网上…

session与cookie的来源与区别

目录 1.什么是HTTP? 2.cookie 3.session 4.cookie和session的区别 如果你对于session 和cookie 只有一点模糊理解,那么此文章能帮你更深入理解session和cookie ,依旧和上篇文章一样,我们采用问题的方式来一步步探索&#xff0…

第七章 测试

文章目录 第七章 测试7.1 编码7.1.1 选择程序设计语言1. 计算机程序设计语言基本上可以分为汇编语言和高级语言2. 从应用特点看,高级语言可分为基础语言、结构化语言、专用语言 7.1.2 编码风格 7.2 软件测试基础7.2.1 软件测试的目标7.2.2 软件测试准则7.2.3 测试方…

JVM基础面试题及原理讲解

基本问题 介绍下 Java 内存区域(运行时数据区)Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)对象的访问定位的两种方式(句柄和直接指针两种方式) 拓展问题 Strin…

Flutter Widget 生命周期 key探究

Widget 在Flutter中,一切皆是Widget(组件),Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,它只是描述显示元素的一个配置数据。 …