简体   繁体   中英

svg & javascript: detect element intersection

I'm working on an SVG image describing a grid (all elements are grouped on <g id='map'> ) with green/red/yellow rectangles and a "scratch like" area (with elements grouped on <g id='edit'> ) with a list of circle filled in violet.

https://jsfiddle.net/3xz04ab8/

Is there a way, with javascript, to detect which elements of <g id='map'> (in violet) group are below/in common with the elements of <g id='edit'> one?

Easiest way to find intersecting elements is to iterate over them and check intersection one by one. But that's not optimal, as each iteration will have to read and parse DOM attributes again and again.

Since you know that the map is static and will not change, you can gather info beforehand and prepare data for quick lookups. If we assume that all rects on the map are of the same size, we can make quick calculation to get positions of rects intersecting with area of a circle.

Since your SVG is too big for including in code snippets, code samples below are JavaScript-only, with additional links to fiddles.

Easy, sub-optimal implementation

/**
 * @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'))
});

Test it at https://jsfiddle.net/subw6reL/ .

A bit faster implementation

/**
 * @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'))
});

Test it at https://jsfiddle.net/f2xLq3ka/ .

More optimizations possible

Instead of using regular Object for a grid , one could use Array by calculating x and y somewhat like: arrayGrid[rect.x / grid.w][rect.y / grid.h] .

Example code above does not make sure that values are rounded, so Math.floor and Math.ceil should be used on calculated values.

If you don't know if map elements will always be of the same size, you could check that at initialization and then prepare findIntersectingRects function optimized for given situation.

Tricks

There's also a trick to draw the grid on canvas, each rectangle with different color (based on rectangle's x and y ), and then get the color of pixel at the circle's position/area;). I doubt that would be faster, but it can be useful in a bit more complicated situations (multi-layered map, with irregular shapes, for example).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM