javascript - 是否可以检查内联 SVG 中各种形状之间的共享边界?
问题描述
我有一个地图的内联 SVG,其中每个区域都被绘制为一个单独的形状。我想做的是获取与该形状共享边界的所有邻居的列表。
这是我目前拥有的一个jsfiddle:
https://jsfiddle.net/kzscxj30/
$( ".map" ).click(function( event ) {
var $mapTarget = $( event.target );
if ($mapTarget.is( "path" )) {
$( ".region" ).attr("class", "region");
$mapTarget.attr("class", "region active");
} else {
return;
}
})
这个想法是,一旦单击了一个形状,它就会为该形状添加一个“活动”类,并为任何接触的形状添加一个“邻居”类。例如,Region1 是 Region2 和 Region3 的邻居,因此单击 Region1 应该将类“neighbor”添加到 Region2 和 Region3。Region2 和 Region3 彼此不是邻居,因此单击其中一个只会将邻居类添加到 Region1。Region4 不是任何其他形状的邻居,因此它不会将邻居类应用于任何东西。
我考虑过比较每个形状的坐标以找到匹配项,但我意识到可以有两个具有共享边框但没有共享坐标的形状。
这可能更像是一个不知道如何表达我正在寻找的东西的问题!
解决方案
有(据我所知),没有内置的方法可以做到这一点......
现在,您可以自己设置一个算法来进行检查,但它可能会有点计算密集型。
作为第一步,您将只检查路径的边界框,消除所有不会以任何方式到达路径的边界框:
function intersectBoxes(bb1, bb2, padding = 0) {
// half padding (we'll add it in every direction of our rects)
const pad = padding / 2;
// helper function to get clean labels
const getCorners = (bb) => ({
left: bb.x - pad,
top: bb.y - pad,
right: bb.x + bb.width + pad,
bottom: bb.x + bb.width + pad,
});
const r1 = getCorners(bb1);
const r2 = getCorners(bb2);
// check intersection
return r1.left <= r2.right &&
r1.right >= r2.left &&
r1.top <= r2.bottom &&
r1.bottom >= r2.top;
}
// usage
intersectBoxes(path1.getBBox(), path2.getBBox(), 2);
// @return Boolean
完成此操作后,您可以开始更重的检查。SVG2 引入了一个新的isPointInStroke方法,但目前只有少数浏览器支持。所以你可能需要 polyfill 它。我没有找到,所以我很快制作了一个猴子补丁,而不是使用 2DContext 等效项。
在这个方法的帮助下,我们只需要沿着它的笔划抓取我们的一条路径的 x, y 坐标并重复调用这个方法。
function slowHitCheck(p1, p2) {
// we will walk along the smallest of both paths
const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
const longest = smallest === p1 ? p2 : p1;
const length = smallest.getTotalLength();
let pos = 0;
while(pos < length) {
const pt = smallest.getPointAtLength(pos);
if(longest.isPointInStroke(pt)) return true;
pos += stroke_width;
}
return false;
}
$(".map").click(function(event) {
var $mapTarget = $(event.target);
if ($mapTarget.is("path")) {
$(".region").attr("class", "region");
const neighbors = getNeighbors($mapTarget[0], $('#Map')[0]);
neighbors.forEach(node => {
node.classList.add('neighbour');
})
$mapTarget.addClass("active");
} else {
return;
}
})
function getNeighbors(target, root, stroke_width = 1) {
const targetBB = target.getBBox();
return [...root.querySelectorAll('path')]
.filter(path =>
path !== target && // not the target
// fast check BBoxes
intersectBoxes(path.getBBox(), targetBB, stroke_width / 2) &&
// walk the path
slowHitCheck(path, target, stroke_width)
);
}
function intersectBoxes(bb1, bb2, padding) {
const pad = padding / 2;
const getCorners = (bb) => ({
left: bb.x - pad,
top: bb.y - pad,
right: bb.x + bb.width + pad,
bottom: bb.x + bb.width + pad,
});
const r1 = getCorners(bb1);
const r2 = getCorners(bb2);
return r1.left <= r2.right &&
r1.right >= r2.left &&
r1.top <= r2.bottom &&
r1.bottom >= r2.top;
}
function slowHitCheck(p1, p2, stroke_width) {
const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
const longest = smallest === p1 ? p2 : p1;
const length = smallest.getTotalLength();
let pos = 0;
while (pos < length) {
const pt = smallest.getPointAtLength(pos);
if (longest.isPointInStroke(pt)) return true;
pos += stroke_width;
}
return false;
}
/* Half related code below:
* Monkey Patches SVGGeometryElement's isPointInStroke
* and is isPointInFill.
* You can check the revision history
* for a simpler version that only worked for SVGPathElements
*/
// Beware untested code below
// There may very well be a lot of cases where it will not work at all
if (window.SVGGeometryElement && !window.SVGGeometryElement.prototype.isPointInStroke) {
monkeyPatchSVGIsPointIn();
}
function monkeyPatchSVGIsPointIn() {
const ctx = get2DContext(0, 0);
const default_ctx = get2DContext(0, 0);
Object.defineProperty(SVGGeometryElement.prototype, 'isPointInStroke', {
value: function isPointInStroke(point) {
returnIfAbrupt(point);
const path = generatePath2DFromSVGElement(this);
setUpContextToSVGElement(ctx, this);
ctx.stroke(path);
return ctx.isPointInStroke(path, point.x, point.y);
}
});
Object.defineProperty(SVGGeometryElement.prototype, 'isPointInFill', {
value: function isPointInFill(point) {
returnIfAbrupt(point);
const path = generatePath2DFromSVGElement(this);
setUpContextToSVGElement(ctx, this);
ctx.fill(path, this.getAttribute('fill-rule') || "nonzero")
return ctx.isPointInPath(path, point.x, point.y, this.getAttribute('fill-rule') || 'nonzero');
}
});
function returnIfAbrupt(svgPoint) {
if (svgPoint instanceof SVGPoint === false) {
throw new TypeError("Failed to execute 'isPointInStroke' on 'SVGGeometryElement':" +
"parameter 1 is not of type 'SVGPoint'.")
}
}
function generatePath2DFromSVGElement(el) {
const def = el instanceof SVGPathElement ?
el.getAttribute('d') :
(el instanceof SVGPolygonElement ||
el instanceof SVGPolylineElement) ?
("M" + el.getAttribute('points').split(' ').filter(Boolean).join('L')) :
"";
const path = new Path2D(def);
if (!def) {
if (el instanceof SVGLineElement) {
path.lineTo(el.getAttribute('x1'), el.getAttribute('y1'))
path.lineTo(el.getAttribute('x2'), el.getAttribute('y2'))
}
if (el instanceof SVGRectElement) {
path.rect(el.getAttribute('x'), el.getAttribute('y'), el.getAttribute('width'), el.getAttribute('height'));
} else if (el instanceof SVGCircleElement) {
path.arc(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('r'), Math.PI * 2, 0);
} else if (el instanceof SVGEllipseElement) {
path.ellipse(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('rx'), el.getAttribute('ry'), 0, Math.PI * 2, 0);
}
}
return path;
}
function setUpContextToSVGElement(ctx, svgEl) {
const default_ctx = get2DContext();
const dict = {
"stroke-width": "lineWidth",
"stroke-linecap": "lineCap",
"stroke-linejoin": "lineJoin",
"stroke-miterlimit": "miterLimit",
"stroke-dashoffset": "lineDashOffset"
};
for (const [key, value] of Object.entries(dict)) {
ctx[value] = svgEl.getAttribute(key) || default_ctx[value];
}
ctx.setLineDash((svgEl.getAttribute("stroke-dasharray") || "").split(' '));
}
function get2DContext(width = 0, height = 0) {
return Object.assign(
document.createElement("canvas"),
{ width, height }
).getContext('2d');
}
}
body {
background: lightblue;
}
.region {
fill: green;
stroke: white;
stroke-width: 0.5;
}
.region:hover {
fill: lightgreen;
}
.active {
fill: red;
}
.neighbour {
fill: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg id="Map" class="map" viewBox="0 0 125 75">
<g>
<path id="Region01" class="region" d="M0,0 L 0,50 L 50,50Z"/>
<path id="Region02" class="region" d="M25,25 L 75,25 L 75,75Z"/>
<path id="Region03" class="region" d="M0,50 L 25,50 L 25,75 L 0,75Z"/>
<path id="Region04" class="region" d="M100,0 L 125,0 L 125,25 L 100,25Z"/>
</g>
</svg>
推荐阅读
- php - 多张图片上传PHP限制
- javascript - 有没有办法通过 JS/CSS display none 属性禁用自动完成功能?
- python - Django不会将表单数据保存到postgres数据库
- c - 没有收到来自父进程的信号
- r - base R — 如何检测 var-args (...) 是否分配了键名(命名参数)?
- javascript - RegExp:如何只找到一个匹配项(或不匹配模式)
- pandas - 如何按熊猫每个月份的单位价格分组?
- python - 使用熊猫数据框中的分类数据重命名变量
- macos - 如果内容视图具有点击手势动作,SwiftUI ForEach 的 onMove() 修饰符将停止工作
- node.js - 使用 express 插入文档时出现 postgresql 错误