2D - 元素命中

162 阅读3分钟

背景

在2d编辑器中,判断元素之间是否有交集是很重要的功能要求,比如

  • 点与点
  • 点与线
  • 点与面
  • 线与线
  • 线与面
  • 面与面

这些实现在fabric和konva中不一样。

点与点

很简单,忽略

点与面

fabric

根据元素的层级,由上到下遍历,先进行元素的包围盒(矩形)与点的位置进行判断,根据盒子的左上角和右下角与点(x,y)进行比较即可判断点是否在包围盒内。

  • 如果元素是矩形,那么即可判断点是否在元素内
  • 如果元素不是矩形,那么就单独将元素绘制到一个离屏的canvas上,然后判断canvas上点(x,y)的像素值(通过ctx.getImageData(x, y, 1, 1).data)
    • 如果像素透明,则点不在元素内
    • 否则,点在元素内

konva

由于knova的图层设计,konva会复制canvas在一个离屏canvas上,在离屏canvas上,给每个元素独特的颜色值,然后直接判断离屏canvas上点(x,y)的像素值,根据键值对索引,即可判断命中了哪个元素。

点与线

这点实现上和点与面的实现逻辑基本一致

线与线

两个框架都没有判断线与线是否有交集的api,需要我们自己去实现。

曲线可以拆分成多个直线,我们直接学两直线判断是否有交集即可。

我们先通过两者的包围盒子是否相交来快速排除。

然后利用向量叉积法来判断

function segmentsIntersect(p1, p2, p3, p4) {
  // 线段1:从p1到p2,线段2:从p3到p4
  
  // 判断p3和p4是否在线段p1-p2的两侧
  function direction(a, b, c) {
    return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
  }
  
  const d1 = direction(p1, p2, p3);
  const d2 = direction(p1, p2, p4);
  const d3 = direction(p3, p4, p1);
  const d4 = direction(p3, p4, p2);
  
  // 如果p3和p4在p1-p2的两侧,且p1和p2在p3-p4的两侧,则线段相交
  if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && 
      ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
    return true;
  
  // 处理共线和端点重合的情况
  if (d1 === 0 && onSegment(p1, p3, p2)) return true;
  if (d2 === 0 && onSegment(p1, p4, p2)) return true;
  if (d3 === 0 && onSegment(p3, p1, p4)) return true;
  if (d4 === 0 && onSegment(p3, p2, p4)) return true;
  
  return false;
}

// 判断点c是否在线段a-b上
function onSegment(a, c, b) {
  return (c.x <= Math.max(a.x, b.x) && c.x >= Math.min(a.x, b.x) &&
          c.y <= Math.max(a.y, b.y) && c.y >= Math.min(a.y, b.y));
}

线与面

这会比较复杂,曲线与面的交集可以转换成多个直线和面的交集。

先进行两者的包围盒子是否相交。

直线和圆、矩形是否有交集的判断很简单。

直线和多边形的判断就需要直线和多边形的每一条边进行交集判断了。

面与面

先进行两者的包围盒子是否相交。

圆与圆、矩形与矩形的判断比较简单

多边形和多边形的判断比较复杂

  • 分离轴定理
  • 克利平-霍奇曼算法
  • 取A多边形的所有边和另一个多边形矩形交集判断