大家好,我是前端西瓜哥。
对于一个图形设计软件,它最基础的工具是什么?选择工具。
但这个选择工具,却是相当的复杂。这次我来和各位,细说细说选择工具的一些弯弯道道。
我正在开发的图形设计工具:
https://github.com/F-star/suika
线上体验:
https://blog.fstars.wang/app/suika/
单选
最基本的,要做到单个图形的选中。
光标停留在图形上方,按下鼠标左键,这个图形就被选中了。这就是一个简单的选中了单个图形的场景。
注意必须是 mousedown,不是 click。后面会说为什么。
在代码层,我们会使用 “图形拾取” 算法确定光标落在哪个图形的点击区域上,注意考虑隐藏、锁定、组的情况。
隐藏和锁定的图形会被忽略,如果点的是组下的一个元素,要将整个组的所有元素都选中。
清空 被选中图形集合(暂且叫做 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 个点)的距离,并在很接近时吸附过去。
结尾
总结一下,选择工具,是一款图形设计软件最基础的功能。
它的作用是选中的图形,对它们进行操作,目的是 更新指定图形属性。
最基础的操作是移动,接着是通过控制点实现的增强操作。
控制点操作的两个基本能力是旋转和缩放。然后我们会根据不同类型的图形,去实现不同的控制点逻辑。
说是工具的一种,但它其实的定位更多是底层的基础建设。