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.floor
和 Math.ceil
。
如果您不知道 map
个元素是否始终具有相同的大小,您可以在初始化时进行检查,然后准备针对给定情况优化的 findIntersectingRects
函数。
技巧
还有一个技巧是在canvas上绘制网格,每个矩形颜色不同(基于矩形的x
和y
),然后得到像素点的颜色圆圈的 position/area ;)。我怀疑那样会更快,但它在更复杂的情况下很有用(例如,多层地图,具有不规则形状)。
我正在处理一个 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.floor
和 Math.ceil
。
如果您不知道 map
个元素是否始终具有相同的大小,您可以在初始化时进行检查,然后准备针对给定情况优化的 findIntersectingRects
函数。
技巧
还有一个技巧是在canvas上绘制网格,每个矩形颜色不同(基于矩形的x
和y
),然后得到像素点的颜色圆圈的 position/area ;)。我怀疑那样会更快,但它在更复杂的情况下很有用(例如,多层地图,具有不规则形状)。