图形编辑器开发:最基础但却复杂的选择工具
大家好,我是前端西瓜哥。
对于一个图形设计软件,它最基础的工具是什么?选择工具。
但这个选择工具,却是相当的复杂。这次我来和各位,细说细说选择工具的一些弯弯道道。
我正在开发的图形设计工具的:
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 水平垂直拖拽冲突。

框选
框选,提供了 一次性选中大量特定区域内图形 的能力。
在空白区域按下鼠标拖拽,然后释放,可以构造出一个矩形,这个矩形我们称为 “选区”。

选区矩形会和图形进行碰撞检测判断,决定将哪些图形是被框选中的。
碰撞检测有三种方案:
- 选区矩形和选中图形的包围盒属于 包含(contain)关系;
- 选区矩形和选中图形的包围盒属于 相交(intersect)关系;
- 不使用包围盒,精准判断是否有真正的 像素上的相交;
个人比较推荐相交的判断方案,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);}}}
}
移动
选择工具,主要是用来选择,选中后一个很普遍的操作是:移动选中元素。
所以这也是它有时候也被叫做 移动工具 的原因。
移动的交互过程:
- 光标停留在已经被选中的图形上,按下鼠标不放;
- 然后拖拽鼠标,被选中图形跟随光标移动;
- 释放鼠标,表示移动到目标位置,移动结束。

代码核心实现:
- 移动前此时记录图形的位置,和起始位置;
- 拖拽时计算相对位移,更新图形的位置;
- 释放时重置状态,以及记录到历史记录中。
// 图形移动前位置
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 来开启这个能力。

要点:
- 拖拽的中途从没按住 Shift 到按住,要立即响应,代码实现上要补一个键盘事件监听,而不是靠鼠标移动事件,因为你不移动鼠标,被选中元素就不会更新。
- 比较 dx 和 dy 的大小。dx 大,水平移动;dy 大,垂直移动。这样图形就能尽量靠近十字线(水平线+垂直线)
对齐到像素网格
对齐到网格,开启后,让图形在移动的时候,让图片尽量贴到网格线上。

做法是将一个或多个图形的包围盒(AABB)的左上角坐标,进行取余,得到一个落在网格线上的位置,用这位置去更新选中图形。
扩展能力:控制点
选中图形,是为了对它们进行操作。
这些 操作的实现,要通过控制点来落地。
常见的有:
-
缩放控制点,在图形选中框的 4 个角上;
-
旋转控制点,拖拽它设置图形的旋转,旋转控制点;
-
给图形设置渐变填充色,需要指定两种颜色的颜色和位置,需要的 渐变色控制点;
下面是 figma 的缩放和旋转演示,我开发的编辑器还没实现完整。

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

所以选择工具模块在设计上,要提供 注册各种类型图形控制点逻辑 的能力。
在 “图形拾取” 时,要把控制点也考虑进来,光标是否点在控制点上。
如果点在控制点上,拖拽逻辑就要走控制点的逻辑,不再走选择工具的基础逻辑。
其他
还有一些可考虑实现的增强能力:
- 双击,进入编辑模式,进行一些更复杂的操作,比如可以变成贝塞尔曲线操作任意点。
- 移动时,用线条显示和其他图形的点(比如中点、选中框角落的 4 个点)的距离,并在很接近时吸附过去。
结尾
总结一下,选择工具,是一款图形设计软件最基础的功能。
它的作用是选中的图形,对它们进行操作,目的是 更新指定图形属性。
最基础的操作是移动,接着是通过控制点实现的增强操作。
控制点操作的两个基本能力是旋转和缩放。然后我们会根据不同类型的图形,去实现不同的控制点逻辑。
说是工具的一种,但它其实的定位更多是底层的基础建设。
我是前端西瓜哥,欢迎关注我,学习更多图形开发知识。
相关文章:
图形编辑器开发:最基础但却复杂的选择工具
大家好,我是前端西瓜哥。 对于一个图形设计软件,它最基础的工具是什么?选择工具。 但这个选择工具,却是相当的复杂。这次我来和各位,细说细说选择工具的一些弯弯道道。 我正在开发的图形设计工具的: http…...
apk签名-signapk.jar
如果做平台app开发,需要签platform签名,除了通过adroid.bp或者android.mk的方式使用AOSP整个大工程中签名外,还可以直接通过signapk.jar的方式进行签名,效率更高更快捷简便。 首先我们来回顾下AOSP平台签名的办法。 Android.mk 使…...
【100个高大尚求职简历】简历模板+修改教程+行业分类简历模板 (涵盖各种行业) (简历模板+编辑指导+修改教程)
文章目录 1 简历预览2 简历下载 很多人说自己明明投了很多公司的简历,但是都没有得到面试邀请的机会。自己工作履历挺好的,但是为什么投自己感兴趣公司的简历,都没有面试邀请的机会。反而是那些自己没有投递的公司,经常给自己打电…...
Nginx平滑升级版本或添加模块
文章目录 一、Nginx 平滑升级二、升级失败 回滚操作三、遇到问题 一、Nginx 平滑升级 一般有两种情况下需要升级 nginx,一种是确实要升级 nginx 的版本,另一种是要为 nginx 添加新的模块。 Nginx平滑升级其原理简单概括: (1&am…...
高阶复杂网络重建:从时间序列中重建高阶网络
论文链接:https://www.nature.com/articles/s41467-022-30706-9 一、为什么要研究高阶网络? 复杂网络跟我们生活息息相关,例如社交网络的信息传播,疾病的感染扩散和基因调控网络的相互作用等。越来越多的研究突破了传统网络中两…...
Day05 03-MySQL主从-主主原理与搭建详解
文章目录 第十六章 MySQL的系统架构(主从架构)16.1 MySQL集群架构的介绍16.1.1 主从架构介绍16.1.2 主从复制的原理 16.2 MySQL主从复制的实现16.2.1 环境说明16.2.2 主库配置16.2.3 从库配置16.2.4 主从复制测试 16.3 MySQL主主复制的实现16.3.1 主主复…...
STL之vector
目录 vector模拟实现一. vector的基本框架二. 常用方法及实现1.初始化和清理a. 默认构造函数b. 析构函数 2. 迭代器a. beginb. end 3.数据访问a. sizeb. capacityc. operator[]d. frontc. back 4.增删查改操作a. reserveb. resizec. insertd. push_backe. erasef. pop_back 5.构…...
2020年CSP-J认证 CCF非专业级别软件能力认证第一轮真题-单项选择题解析
2020 CCF认证第一轮(CSP-J)真题 一、单项选择题 (共15题,每2分,共30分;每题有且有一个正确选项) 1、在内存储器中每个存储单元都被赋予一个唯一的序号,称为 A、下标 B、序号 C、地址 D、编号 答案:C…...
vscode Delete `␍⏎·····`
在公司拉取代码报错 Delete ␍⏎,首先问题的关键是换行导致,相信你看别的博客也知道为什么了,但是我使用别的博客的解决办法,没搞定,无论是配置 auto 还是命令行执行,都不行 下面介绍我的解决办法 我使用…...
读书笔记-《ON JAVA 中文版》-摘要16[第十六章 代码校验]
文章目录 第十六章 代码校验1. 测试1.1 单元测试1.2 JUnit1.3 测试覆盖率的幻觉 2. 前置条件2.1 断言(Assertions)2.2 Java 断言语法2.3 Guava 断言2.4 使用断言进行契约式设计2.4.1 检查指令2.4.2 前置条件2.4.3 后置条件2.4.4 不变性2.4.5 放松 DbC 检…...
SQL Server:打造高效数据管理系统的利器
使用SQL Server进行数据管理 简介 SQL Server是由Microsoft开发的一款关系型数据库管理系统,它可以用于存储和管理大量结构化数据。本篇博客将介绍如何使用SQL Server进行数据管理。 数据库连接 在开始使用SQL Server之前,需要先建立与数据库的连接。…...
代码随想录二刷day20 | 二叉树之 654.最大二叉树 617.合并二叉树 700.二叉搜索树中的搜索 98.验证二叉搜索树
day20 654.最大二叉树617.合并二叉树700.二叉搜索树中的搜索98.验证二叉搜索树 654.最大二叉树 题目链接 解题思路: 本题属于构造二叉树,需要使用前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 确定递归函数的参数…...
python基础知识(十三):numpy库的基本用法
目录 1. numpy的介绍2. numpy库产生矩阵2.1 numpy将列表转换成矩阵2.2 numpy创建矩阵 3. numpy的基础运算4. numpy的基础运算25. 索引 1. numpy的介绍 numpy库是numpy是python中基于数组对象的科学计算库。 2. numpy库产生矩阵 2.1 numpy将列表转换成矩阵 import numpy as …...
【SA8295P 源码分析】16 - TouchScreen Panel (TP)线程函数 tp_recv_thread() 源码分析
【【SA8295P 源码分析】16 - TouchScreen Panel (TP)线程函数 tp_recv_thread 源码分析 一、TP 线程函数:tp_recv_thread()二、处理&上报 坐标数据 cypress_read_touch_data()系列文章汇总见:《【SA8295P 源码分析】00 - 系列文章链接汇总》 本文链接:《【SA8295P 源码…...
Python3数据分析与挖掘建模(13)复合分析-因子关分析与小结
1.因子分析 1.1 探索性因子分析 探索性因子分析(Exploratory Factor Analysis,EFA)是一种统计方法,用于分析观测变量之间的潜在结构和关联性。它旨在确定多个观测变量是否可以归结为较少数量的潜在因子,从而帮助简化…...
【stable diffusion】图片批量自动打标签、标签批量修改(BLIP、wd14)用于训练SD或者LORA模型
参考: B站教学视频【:AI绘画】新手向!Lora训练!训练集准备、tag心得、批量编辑、正则化准备】官方教程:https://github.com/darkstorm2150/sd-scripts/blob/main/docs/train_README-en.md#automatic-captioning 一、…...
TCP可靠数据传输
TCP的可靠数据传输 1.TCP保证可靠数据传输的方法 TCP主要提供了检验和、序号/确认号、超时重传、最大报文段长度、流量控制等方法实现了可靠数据传输。 检验和 通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢失该TC…...
Python 私有变量和私有方法介绍
Python 私有变量和私有方法介绍 关于 Python 私有变量和私有方法,通常情况下,开发者可以在方法或属性名称前加上单下划线(_),以表示该方法或属性仅供内部使用,但这只是一种约定,并没有强制执行禁…...
Kotlin Lambda表达式和匿名函数的组合简直太强了
Kotlin Lambda表达式和匿名函数的组合简直太强了 简介 首先,在 Kotlin 中,函数是“第一公民”(First Class Citizen)。因此,它们可以被分配为变量的值,作为其他函数的参数传递或者函数的返回值。同样&…...
uniapp 小程序 获取手机号---通过前段获取
<template><!-- 获取手机号,登录内容 --><view><!-- 首先需要先登录获取code码,然后才可以获取用户唯一标识openid以及会话密钥及用于解密获取手机的加密信息 --><view click"login">登录</view><view…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
