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


(资料图片)

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

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

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

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 个点)的距离,并在很接近时吸附过去。结尾

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

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

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

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

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

推荐内容