svg & javascript: обнаружение пересечения элементов - PullRequest
1 голос
/ 15 октября 2019

Я работаю над SVG-изображением, описывающим сетку (все элементы сгруппированы по <g id='map'>) с зелеными / красными / желтыми прямоугольниками и областью «как царапина» (с элементами, сгруппированными по <g id='edit'>) со спискомкруга, заполненного фиолетовым.

https://jsfiddle.net/3xz04ab8/

Есть ли способ, с помощью javascript, определить, какие элементы из группы <g id='map'> (в фиолетовом) находятся ниже / общие с элементами<g id='edit'> один?

1 Ответ

2 голосов
/ 16 октября 2019

Самый простой способ найти пересекающиеся элементы - это перебирать их и проверять пересечение по одному. Но это не оптимально, поскольку каждая итерация должна будет снова и снова считывать и анализировать атрибуты 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 https://stackoverflow.com/a/2752387/6352710
 * @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/.

Aчуть более быстрая реализация

/**
 * @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.floor и Math.ceil следует использовать для вычисляемых значений.

Если вы не знаете, будут ли элементы map всегда иметь одинаковый размер, вы можете проверить это при инициализации, а затем подготовить функцию findIntersectingRects, оптимизированную для данной ситуации.

Трюки

Есть и хитростьw сетка на холсте, каждый прямоугольник разного цвета (на основе прямоугольников x и y), а затем получить цвет пикселя в позиции / области круга;). Я сомневаюсь, что это будет быстрее, но может быть полезно в немного более сложных ситуациях (например, многослойная карта с неправильными формами).

...