svg & javascript: 检测元素相交

svg & javascript: detect element intersection

我正在处理一个 SVG 图像,它描述了一个网格(所有元素都在 <g id='map'> 上分组),其中有 green/red/yellow 个矩形和一个 "scratch like" 区域(元素在 [=] 上分组12=]) 带有紫色圆圈列表。

https://jsfiddle.net/3xz04ab8/

有没有办法通过 javascript 来检测 <g id='map'>(紫色)组中的哪些元素 below/in 与 <g id='edit'> 组中的元素相同?

找到相交元素的最简单方法是遍历它们并逐一检查交集。 但这不是最优的,因为每次迭代都必须一次又一次地读取和解析 DOM 属性。

由于您知道 map 是静态的并且不会改变,您可以事先收集信息并准备数据以便快速查找。 如果我们假设 map 上的所有矩形都具有相同的大小,我们可以快速计算出矩形与圆面积相交的位置。

由于您的 SVG 太大而无法包含在代码片段中,下面的代码示例仅 JavaScript,带有指向小提琴的额外链接。

简单,次优实施

/**
 * @typedef Area
 * @property {number} x1 X position of top-left
 * @property {number} y1 Y position of top-left
 * @property {number} x2 X position of bottom-right
 * @property {number} y2 Y position of bottom-right
 */

/**
 * Based on 
 * @param {SVGElement} $rect
 * @param {Area}       area
 * @return {boolean}
 */
function areIntersecting ($rect, area) {
  const x1 = parseFloat($rect.getAttribute('x'));
  const y1 = parseFloat($rect.getAttribute('y'));
  const x2 = x1 + parseFloat($rect.getAttribute('width')) + parseFloat($rect.getAttribute('stroke-width'));
  const y2 = y1 + parseFloat($rect.getAttribute('height'));

  return !(x1 > area.x2 ||
          x2 < area.x1 ||
          y1 > area.y2 ||
          y2 < area.y1);
}

/**
 * @param {SVGElement[]} rects
 * @param {SVGElement}   $circle
 * @return {SVGElement[]}
 */
function findIntersectingRects (rects, $circle) {
  let x = parseFloat($circle.getAttribute('cx'));
  let y = parseFloat($circle.getAttribute('cy'));
  let r = parseFloat($circle.getAttribute('r'));
  let box = {
    x1: x - r,
    y1: y - r,
    x2: x + r,
    y2: y + r
  };
  return rects.filter($rect => areIntersecting($rect, box));
}

/*
 * Following code is just for the example.
 */

// Get array of `RECT` elements
const $map = document.getElementById('map');
const rects = Array.from($map.querySelectorAll('rect'));

// Get array of `CIRCLE` elements
const $edit = document.getElementById('edit');
const circles = Array.from($edit.querySelectorAll('circle'));

// Change opacity of `RECT` elements that are
// intersecting with `CIRCLE` elements.
circles.forEach($circle => {
  findIntersectingRects(rects, $circle).forEach($rect => $rect.setAttribute('style', 'fill-opacity: 0.3'))
});

https://jsfiddle.net/subw6reL/ 进行测试。

实施速度稍快

/**
 * @typedef Area
 * @property {number} x1 X position of top-left
 * @property {number} y1 Y position of top-left
 * @property {number} x2 X position of bottom-right
 * @property {number} y2 Y position of bottom-right
 * @property {SVGElement} [$e] optional reference to SVG element
 */

/**
 * Besides properties defined below, grid may contain multiple
 * objects named after X value of area, and those object may contain
 * multiple Areas, named after Y value of those areas.
 *
 * @typedef Grid
 * @property {number} x X position of top-left
 * @property {number} y Y position of top-left
 * @property {number} w Width of each rect in grid
 * @property {number} h Height of each rect in grid
 */

/**
 * @param {Grid}       grid
 * @param {SVGElement} $circle
 * @return {SVGElement[]}
 */
function findIntersectingRects (grid, $circle) {
  let r = parseFloat($circle.getAttribute('r'));
  let x1 = parseFloat($circle.getAttribute('cx')) - r;
  let y1 = parseFloat($circle.getAttribute('cy')) - r;
  let x2 = x1 + r + r;
  let y2 = y1 + r + r;

  let gX = x1 - ((x1 - grid.x) % grid.w);
  let gY = y1 - ((y1 - grid.y) % grid.h);

  var result = [];
  while (gX <= x2) {
    let y = gY;
    let row = grid[gX];
    while (row && y <= y2) {
      if (row[y]) {
        result.push(row[y].$e);
      }
      y += grid.h;
    }
    gX += grid.w;
  }

  return result;
}

/**
 * @param {SVGElement[]} rects
 * @return {Grid}
 */
function loadGrid (rects) {
  const grid = {
    x: Infinity,
    y: Infinity,
    w: Infinity,
    h: Infinity
  };

  rects.forEach($rect => {
    let x = parseFloat($rect.getAttribute('x'));
    let y = parseFloat($rect.getAttribute('y'));
    let w = parseFloat($rect.getAttribute('width')) + parseFloat($rect.getAttribute('stroke-width'));
    let h = parseFloat($rect.getAttribute('height'));

    grid[x] = grid[x] || {};
    grid[x][y] = grid[x][y] || {
      x1: x,
      y1: y,
      x2: x + w,
      y2: y + h,
      $e: $rect
    };

    if (grid.w === Infinity) {
      grid.w = w;
    }
    else if (grid.w !== w) {
      console.error($rect, 'has different width');
    }

    if (grid.h === Infinity) {
      grid.h = h;
    }
    else if (grid.h !== h) {
      console.error($rect, 'has different height');
    }

    if (x < grid.x) {
      grid.x = x;
    }
    if (y < grid.y) {
      grid.y = y;
    }
  });

  return grid;
}

/*
 * Following code is just for the example.
 */

// Get array of `RECT` elements
const $map = document.getElementById('map');
const grid = loadGrid(Array.from($map.querySelectorAll('rect')));

// Get array of `CIRCLE` elements
const $edit = document.getElementById('edit');
const circles = Array.from($edit.querySelectorAll('circle'));

// Change opacity of `RECT` elements that are
// intersecting with `CIRCLE` elements.
circles.forEach($circle => {
  findIntersectingRects(grid, $circle).forEach($rect => $rect.setAttribute('style', 'fill-opacity: 0.3'))
});

https://jsfiddle.net/f2xLq3ka/ 进行测试。

可能进行更多优化

不使用常规 Object 作为 grid,可以使用 Array 通过计算 x 和 y 有点像:arrayGrid[rect.x / grid.w][rect.y / grid.h].

上面的示例代码不能确保值是四舍五入的,因此应该在计算值上使用 Math.floorMath.ceil

如果您不知道 map 个元素是否始终具有相同的大小,您可以在初始化时进行检查,然后准备针对给定情况优化的 findIntersectingRects 函数。

技巧

还有一个技巧是在canvas上绘制网格,每个矩形颜色不同(基于矩形的xy),然后得到像素点的颜色圆圈的 position/area ;)。我怀疑那样会更快,但它在更复杂的情况下很有用(例如,多层地图,具有不规则形状)。