前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >吸附设计:学会正确地贴贴

吸附设计:学会正确地贴贴

作者头像
前端西瓜哥
发布2024-07-12 16:27:59
630
发布2024-07-12 16:27:59
举报
文章被收录于专栏:前端西瓜哥的前端文章

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

本文将介绍图形编辑器中吸附系统中,各种吸附类型的吸附逻辑和算法实现,让大家对吸附有一个概念。

吸附类型常见的有这么几种:

  1. 网格吸附;
  2. 极轴追踪;
  3. 参考线吸附;
  4. 正交;
  5. 对象吸附;

下面我们来具体看看吧。

网格吸附(Grid Snap)

首先是网格吸附。

所谓网格,指的是在图形所在的场景世界上,以原点出发按照特定的 x 和 y 间隔绘制出一条条直线,所构成的网格。我们把两条直线的交点叫做网格点。

网格吸附就是 让目标点吸附到最近的网格点上

特殊的,如果 x 和 y 间隔为 1,那就变成了像素网格吸附了

吸附算法很简单,找到距离目标点的 x 最近的两个网格点的 x 值:space * nspace * (n+1),取其中最近的。y 同理。

代码语言:javascript
复制
// 计算网格吸附点
const getGridSnapPt(point: IPoint, snapSpacing: IPoint) {
  return {
    x: getClosestNum(point.x, snapSpacing.x),
    y: getClosestNum(point.y, snapSpacing.y),
  };
}

// 找出离 value 最近的 space 的倍数值
const getClosestNum = (value: number, space: number) => {
  const n = Math.floor(value / space);
  const left = space * n;
  const right = space * (n + 1);
  return value - left <= right - value ? left : right;
};

更详细的说明,可以看我的这篇文章:

图形编辑器开发:网格与网格吸附

极轴追踪(Polar Tracking)

极轴追踪,就是以某个参照点为极坐标原点构造极坐标系,并指定特定的增量角度,绘制多条直线,然后找到目标点到其中距离最近的直线,对其作投影作为吸附点。

吸附实现需要用到 点到直线的投影(最近点) 算法。我们先计算目标点投影到所有直线的位置,然后计算目标点到投影点的距离,取其中最近的直线的投影点作为吸附点

代码语言:javascript
复制
// -- 极轴追踪 --
// 求目标点 p,以 center 为极坐标原点,增量角为 180 / count 构造的直线最近的投影点
// count 的 4 代表角度:0, 45, 90, 135, 150...
const getPolarTrackSnapPt = (center: IPoint, p: IPoint, count = 4) => {
  let closestPt: IPoint = { x: 0, y: 0 };
  let closestDist = Infinity;
  for (let i = 1; i <= count; i++) {
    const rad = (Math.PI / count) * i;
    const pt = {
      x: center.x + Math.cos(rad),
      y: center.y + Math.sin(rad),
    };
    // 基于增量角,得到直线 [center, pt], 然后 p 到直线的投影点
    const { point } = closestPtOnLine(center, pt, p);
    const dist = distance(point, p);
    if (dist === 0) {
      return point;
    }
    if (dist < closestDist) {
      closestDist = dist;
      closestPt = point;
    }
  }
  return closestPt;
};

求点到直线投影的算法实现如下。

代码语言:javascript
复制
// 求点 p 到直线 p1-p2 的投影点(最近点)
const closestPtOnLine = (
  p1: IPoint,
  p2: IPoint,
  p: IPoint,
) => {
  if (p1.x === p2.x && p1.y === p2.y) {
    return {
      t: 0,
      point: { x: p1.x, y: p1.y },
    };
  }
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  let t = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);
  const closestPt = {
    x: p1.x + t * dx,
    y: p1.y + t * dy,
  };
  return {
    t,
    point: closestPt,
  };
};

算法的详解可以看我的这篇文章:

平面几何算法:求点到直线和圆的最近点

和网格吸附不同,极轴追踪下,可以强制吸附,也可以不强制吸附

如果不要求强制吸附,通常我们会规定一个阈值(比如 4px)。当目标点距离吸附点小于这个值,才应用吸附,使用吸附点;否则不做吸附。

需要注意,阈值指的是在视口坐标系下的距离,计算要考虑视口的 zoom。

代码语言:javascript
复制
const viewportPolarTrackTol = 4;

const snapPt = getPolarTrackSnapPt(lastPt, mousePt);

const resPt =
  distance(mousePt, snapPt) < viewportPolarTrackTol / zoom ? snapPt : mousePt;

AutoCAD 中开启极轴追踪,不要求强制吸附。

Figma 用钢笔工具绘制时,按住 Shift 会 强制做极轴追踪吸附

参考线吸附(Reference Line)

参考线指的是一些水平或垂直线。然后我们要让目标点和其中最近的水平线和垂直线贴合。

通常我们可以通过标尺可以拖出来这种参考线,比如 Figma 是这样的。

参考线有是可见的,也有不可见的,比如我们可以将视口范围内图形的 AABB 包围盒的 4 条边以及经过包围盒中心的垂直水平两条线,延申为 6 条参考线,以实现灵活地对齐功能。

Figma 中点吸附到图形参照线的效果:

参考线通常有多条,图形很多的情况下,上百条也是有可能的,所以可以在合适的时机(比如移动图形前)做一下缓存。

以 x 值吸附为例,对所有垂直线(垂直线表达为 x = b)的 x 值去重然后排序,然后缓存下来。接着通过二分查找找到里最近值,这个值就是吸附后的 x 值。y 同理,不赘述。

代码语言:javascript
复制
// 求参考吸附点
const getRefLineSnapPt = (point: IPoint, sortedXs: number[], sortedYs: number[]) {
  return {
    x: getClosestValInSortedArr(sortedXs, point.x),
    y: getClosestValInSortedArr(sortedYs, point.y),
  };
}

找到排序数组中最近值的算法实现。

代码语言:javascript
复制
// 找到排序数组中的最近值
const getClosestValInSortedArr = (
  sortedArr: number[],
  target: number,
) => {
  if (sortedArr.length === 0) {
    throw new Error('sortedArr can not be empty');
  }
  if (sortedArr.length === 1) {
    return sortedArr[0];
  }

  let left = 0;
  let right = sortedArr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (sortedArr[mid] === target) {
      return sortedArr[mid];
    } else if (sortedArr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  // 靠边的情况
  if (left >= sortedArr.length) {
    return sortedArr[right];
  }
  if (right < 0) {
    return sortedArr[left];
  }

  // 找到左右两边的值后,再取其中较近的
  return Math.abs(sortedArr[right] - target) <=
    Math.abs(sortedArr[left] - target)
    ? sortedArr[right]
    : sortedArr[left];
};

同样,参考线吸附也有一个最短距离阈值,小于该阈值才做吸附。

如果是对被移动的图形要做参考线吸附,又会麻烦一点。

我们会取被移动图形的 4 个顶点和中心点都作为目标点,先找到它们各自距离最近的参考线吸附点,再取这些其中 x 值最小的,计算出相对水平位移 dx,应用到图形上。y 方向同理。

正交锁定(Orthogonal Locking)

正交是线性代数的概念:若内积空间中两向量的内积为0,则称它们是正交的。简单理解就是这两向量是垂直关系。

在图形编辑器,正交锁定指的就是强制目标点只能在参照点的水平或垂直方向上

效果等价 增量角为 90 且要求强制吸附的极轴追踪

所以正交锁定的吸附算法实现,可以直接套用极轴追踪吸附算法。

代码语言:javascript
复制
const getOrthSnapPt = (center: IPoint, p: IPoint) => {
  // 2 对应增量角为 90,对应角度依次为:0, 90, 180, ...
  return getPolarTrackSnapPt(center, p, 2);
};

对象吸附(Object Snap)

对象吸附,指的是吸附到图形的一些设定好的吸附点上,吸附不是强制的。

我们根据需要配置图形的可吸附点,比如图形的端点、中点、最近点(线上的某一点)。

吸附算法为:先判断目标点是否在图形的包围盒内,然后再计算目标点到所有吸附点的距离,取其中距离最短的,然后和上面的极轴吸附一样,看距离是否小于某个阈值。

如果是,使用吸附点;如果不是,还使用原来的点。

吸附之间的冲突

不同的吸附类型如果做叠加,在某些场景下可能会发生冲突,需要选择合适的策略去处理的。

我们来看看几个场景。

1、像素网格吸附和参考线吸附同时开启

像素网格吸附(间隔为 1 的网格吸附)要求点强制吸附在像素网格上,即 x 和 y 的值是整数。

但是参考线可能是小数,如果吸附到参考线上,就对不上像素网格点了。

Figma 的做法是,像素网格吸附优先,参考线做让步。

具体做法是 调整参照图形的 bbox,让它所有点位置都修正为整数。(被移动图形的点也会修正为整数)

2、正交和极轴追踪同时开启。

前面说了正交是特殊的极轴追踪,增量角为 90 度,且强制吸附。极轴追踪增量角的设置可不一定是 90,而且可能也不强制吸附。

二者完全冲突无法同时存在,解决方式是同时只能启用其中一个。

3、网格吸附和正交同时开启

如果我在一个非网格点绘制了第一个点(参照点),然后开启网格吸附和正交,绘制第二个点(目标点)。

如果应用正交,因为要求目标点垂直或垂直于参照点,这样会导致点无法落在网格点上。二者无法同时满足。

最后方案是,先计算网格吸附后,然后对这个网格吸附点再做正交吸附

4、网格吸附和对象吸附同时开启

同上,先求网格吸附点,然后再对这个网格吸附点做对象吸附。

结尾

今天就简单介绍介绍吸附系统,主要围绕吸附是什么,和一些算法实现,希望对你有所帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端西瓜哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 网格吸附(Grid Snap)
  • 极轴追踪(Polar Tracking)
  • 参考线吸附(Reference Line)
  • 正交锁定(Orthogonal Locking)
  • 对象吸附(Object Snap)
  • 吸附之间的冲突
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档