即使在 3 像素的距离内也能检测 SVG 线上的点击



以下是我检测SVG行点击的方法:

window.onmousedown = (e) => {
if (e.target.tagName == 'line') {
alert();  // do something with e.target
}
}
svg line:hover { cursor: pointer; }
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<line x1="320" y1="160" x2="140" y2="00" stroke="black" stroke-width="2"></line>
<line x1="140" y1="00" x2="180" y2="360" stroke="black" stroke-width="2"></line>
<line x1="180" y1="360" x2="400" y2="260" stroke="black" stroke-width="2"></line>
<line x1="00" y1="140" x2="280" y2="60" stroke="black" stroke-width="2"></line>
</svg>

它仅在鼠标光标精确在线上时才有效,这并不容易,因此这是一个糟糕的用户体验。

如何检测来自 Javascript 的 SVG 行的点击,即使不是完全在线上,而是在 <= 3 像素的距离上?

有点棘手的解决方案,但可以完成工作:

window.onmousedown = (e) => {
if (e.target.classList.contains('line')) {
console.log(e.target.href);
}
}
svg .line:hover {
cursor: pointer;
}
.line {
stroke: black;
stroke-width: 2px;
}
.line.stroke {
stroke: transparent;
stroke-width: 6px;
}
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<defs>
<line id="line1" x1="320" y1="160" x2="140" y2="00"></line>
<line id="line2" x1="140" y1="00" x2="180" y2="360"></line>
<line id="line3" x1="180" y1="360" x2="400" y2="260"></line>
<line id="line4" x1="00" y1="140" x2="280" y2="60"></line>
</defs>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line1" class="line stroke"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line1" class="line"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line2" class="line stroke"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line2" class="line"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line3" class="line stroke"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line3" class="line"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line4" class="line stroke"></use>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#line4" class="line"></use>
</svg>

一个只有一个<line>和一些JavaScript的解决方案会很有趣。

我们可以使用现有的 Web APIdocument.elementFromPoint(x, y).它返回给定点的最顶层元素。
表单用户单击点我们可以沿着每个轴移动并使用该方法找到第一个<line>元素。当我们得到一条线或达到最大搜索距离时,我们会停止搜索。

在下面的演示中,没有创建额外的元素。变量proximity控制与线的最大距离,以考虑将其用于选择。
奖励功能:突出显示离鼠标指针最近的行。因此,用户可以轻松单击所需的行,而无需任何麻烦。

const proximity = 8;
const directions = [
[0, 0],
[0, 1], [0, -1],
[1, 1], [-1, -1],
[1, 0], [-1, 0],
[-1, 1], [1, -1]
];
// tracks nearest line
let currentLine = null;
// highlight nearest line to mouse pointer
container.onmousemove = (e) => {
let line = getNearestLine(e.clientX, e.clientY);
if (line) {
if (currentLine !== line)
currentLine?.classList.remove('highlight');
currentLine = line;
currentLine.classList.add('highlight');
container.classList.add('pointer');
} else {
currentLine?.classList.remove('highlight');
currentLine = null;
container.classList.remove('pointer')
}
}
container.onclick = (e) => {
// we already know in 'onmousemove' which line is the nearest
// so no need to figure it out again.
log.textContent = currentLine ? currentLine.textContent : '';
}
// find a nearest line within 'proximity'
function getNearestLine(x, y) {
// move along each axis and see if we land on a line
for (let i = 1; i <= proximity; i++) {
for (let j = 0; j < directions.length; j++) {
const xx = x + directions[j][0] * i;
const yy = y + directions[j][1] * i;
const element = document.elementFromPoint(xx, yy);
if (element?.tagName == 'line')
return element;
};
}
return null;
}
svg {
background-color: wheat;
}
.pointer {
cursor: pointer;
}
.highlight {
filter: drop-shadow(0 0 4px black);
}
#log {
user-select: none;
}
<p>Clicked on: <span id="log"></span></p>
<svg id='container' width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<line x1="320" y1="160" x2="140" y2="00" stroke="red" stroke-width="2">1</line>
<line x1="140" y1="00" x2="180" y2="360" stroke="green" stroke-width="2">2</line>
<line x1="18" y1="60" x2="400" y2="60" stroke="orange" stroke-width="2">3</line>
<line x1="00" y1="140" x2="280" y2="60" stroke="blue" stroke-width="2">4</line>
</svg>

这只是一个演示代码,您可以摆脱不需要的东西。如果您不希望手在接近时显示,请删除onmousemove并将逻辑移动到onclick方法。
只有filter: drop-shadow(...)才能突出显示非方形形状。否则,您可以更改线宽或颜色等。

只是做数学...

这可能是矫枉过正,但是这3个像素的精确性困扰着我,所以这里有一个"关于数学的全部"解决方案。

getLinesInRange(point, minDist,svg)将返回minDist范围内的所有行。它当前正在将类应用于具有mousemove的范围中的所有行。单击显示范围内所有线的数组,按距离排序,其中最近的线排在最前面。

需要注意的是,这在执行任何内部缩放或偏移定位的 svg 中不起作用。

更新:现在不关心任何SVG突变,如缩放和偏移。

更新 2速度问题已经提出,所以我决定演示它实际计算的速度。计算机擅长的一件事是处理数字。唯一真正的减速是当它对 150+ 行应用投影时,但是,这是渲染的限制,而不是数学的限制,只需稍作修改,您就可以将效果仅应用于最近的行。现在,您最多可以添加 1000 行进行测试。

//Distance Calculations
const disToLine = (p, a, b) => {
let sqr = (n) => n * n,
disSqr = (a, b) => sqr(a.x - b.x) + sqr(a.y - b.y),
lSqr = disSqr(a, b);
if (!lSqr) return disSqr(p, a);
let t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / lSqr;
t = Math.max(0, Math.min(1, t));
return Math.sqrt(
disSqr(p, { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) })
);
};
//Calculates the absolute coordinates of a line
const calculateAbsoluteCords = (line) => {
let getSlope = ([p1, p2]) => (p1.y - p2.y) / (p1.x - p2.x),
rec = line.getBoundingClientRect(),
coords = [
{ x: +line.getAttribute("x1"), y: +line.getAttribute("y1") },
{ x: +line.getAttribute("x2"), y: +line.getAttribute("y2") }];
if (getSlope(coords) <= 0)
coords = [ 
{ x: rec.x, y: rec.y + rec.height },
{ x: rec.x + rec.width, y: rec.y }
];
else
coords = [
{ x: rec.x, y: rec.y },
{ x: rec.x + rec.width, y: rec.y + rec.height }
];
return coords;
};
//gets all lines in range of a given point
const getLinesInRange = (point, minimumDistance, svg) => {
let linesInRange = [],
lines = svg.querySelectorAll("line");
lines.forEach(line => {
let [p1, p2] = calculateAbsoluteCords(line),
dis = disToLine(point, p1, p2);
if (dis <= minimumDistance) {
line.classList.add("closeTo");
linesInRange.push({ dis: dis, line: line });
} else line.classList.remove("closeTo");
});
return linesInRange.sort((a,b) => a.dis > b.dis ? 1 : -1).map(l => l.line);
};
let minDist = 3, el = {};
['mouseRange', 'rangeDisplay', 'mouseRangeDisplay', 'numberOfLines', 'numberInRange', 'numberOfLinesDisplay', 'clicked', 'svgContainer']
.forEach(l => {el[l] = document.getElementById(l); })
el.svgContainer.addEventListener("mousemove", (e) => {
el.numberInRange.textContent = getLinesInRange({ x: e.clientX, y: e.clientY }, minDist, el.svgContainer).length;
});
el.svgContainer.addEventListener("click", (e) => {
let lines = getLinesInRange({ x: e.clientX, y: e.clientY }, minDist, el.svgContainer);
el.clicked.textContent = lines.map((l) => l.getAttribute("stroke")).join(', ');
});
el.mouseRange.addEventListener("input", () => {
minDist = parseInt(el.mouseRange.value);
el.mouseRangeDisplay.textContent = minDist;
});
el.numberOfLines.addEventListener("input", () => {
let numOfLines = parseInt(el.numberOfLines.value);
el.numberOfLinesDisplay.textContent = numOfLines;
generateLines(numOfLines);
});
let generateLines = (total) => {
let lineCount = el.svgContainer.querySelectorAll('line').length;
if(lineCount > total) {
let lines = el.svgContainer.querySelectorAll(`line:nth-last-child(-n+${lineCount-total})`);
lines.forEach(l => l.remove());
}
for(let i=lineCount; i<total; i++) {
var newLine = document.createElementNS('http://www.w3.org/2000/svg','line')
newLine.setAttribute('id','line2');
['x1','y1','x2','y2'].map(attr => newLine.setAttribute(attr,Math.floor(Math.random()*500)));
newLine.setAttribute("stroke", '#' + Math.floor(Math.random()*16777215).toString(16));
el.svgContainer.appendChild(newLine);
}
}
generateLines(10);
.closeTo {
filter: drop-shadow(0 0 3px rgba(0,0,0,1));
}
Range: <input type="range" min="1" max="50" id="mouseRange" value="3" /><span id="mouseRangeDisplay">3</span>
#Lines: <input type="range" min="0" max="1000" id="numberOfLines" value="10" step="10" /><span id="numberOfLinesDisplay">10</span>
In Range: <span id="numberInRange">3</span>
<br/>
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svgContainer" style="width:500px;height:500px;background:#F1F1F1;">
</svg><br/>
Clicked: <span id="clicked"></span>

使用多个元素

通常,您可以使用 svg 组('g'元素),并包含两个元素,一个较大且不透明度为 0 或笔触/填充为transparent

document.querySelectorAll('g.clickable').forEach(node => node.addEventListener('click', function() {
alert();
}))
svg .clickable:hover { cursor: pointer; }
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<g class="clickable">
<line x1="320" y1="160" x2="140" y2="0" stroke="black" stroke-width="2"></line>
<line x1="320" y1="160" x2="140" y2="0" stroke="transparent" stroke-width="16" opacity="0"></line>
</g>
</svg>

自动执行此操作

使用两个具有相同坐标的元素有点多余。在实践中,您可能希望基于动态数据构造元素(尤其是在执行数据驱动图形时),或者可以通过编程方式遍历所有现有行,然后用组元素替换它们。

我将展示第二个,因为这就是问题似乎要问的问题:

var svgNS = 'http://www.w3.org/2000/svg';
document.querySelectorAll('svg line').forEach(function (node) {
if (svg.parentNode.classList.contains('clickable-line')) {
return;
}
var g = document.createElementNS(svgNS, 'g');
g.classList.add('clickable-line');
var displayLine = node.cloneNode();
var transparentLine = node.cloneNode();
g.appendChild(displayLine);
g.appendChild(transparentLine);
transparentLine.setAttributeNS(null, 'stroke-width', '20');
transparentLine.setAttributeNS(null, 'opacity', '0');

g.addEventListener('click', function () {
// do something with `node` or `g`
alert();
});
node.parentNode.replaceChild(g, node);
});
svg .clickable-line:hover {
cursor: pointer
}
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<line x1="320" y1="160" x2="140" y2="0" stroke="black" stroke-width="2"></line>
<line x1="140" y1="0" x2="180" y2="360" stroke="black" stroke-width="2"></line>
</svg>

将其

包装在本机Web组件(JSWC)<svg-lines>

中在所有浏览器中都受支持。因此,您可以在任何地方重复使用它

  • 从其他答案中获取最佳部分

<svg-lines margin="30">
<svg>
<style> line { stroke-width:2 }  </style>
<line x1="320" y1="160" x2="140" y2="00" stroke="red"  >1</line>
<line x1="140" y1="0"  x2="180" y2="360" stroke="green" >2</line>
<line x1="18"  y1="60"  x2="400" y2="60" stroke="orange">3</line>
<line x1="00"  y1="140" x2="280" y2="60" stroke="blue"  >4</line>
</svg>
</svg-lines>
<script>
customElements.define("svg-lines", class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // wait till lightDOM is parsed
this.querySelector("svg")
.append(Object.assign(
document.createElement("style"), {
innerHTML: `.hover { filter:drop-shadow(0 0 4px black) }
.hoverline {stroke-width:${this.getAttribute("margin")||20}; 
opacity:0; cursor:pointer }`
}),
...[...this.querySelector("svg")
.querySelectorAll("[stroke]")
].map((el) => {
let hover = el.cloneNode();
hover.classList.add("hoverline");
hover.onmouseenter = () => el.classList.add("hover");
hover.onmouseout = () => el.classList.remove("hover");
hover.onclick = () => alert("clicked line#" + el.innerHTML);
return hover;
}));
})
}
})
</script>

制作该行的两个副本,将它们组合在一起,并在CSS中增加第二行的笔触宽度 还设置笔触:透明以隐藏第二行,现在您将获得更宽的可点击区域。我希望你发现这是最好的方法。

document.querySelectorAll('#svg g').forEach((item) => {
item.addEventListener('click', (e) => {
const index = Array.from(item.parentNode.children).indexOf((item))
console.log(index+1);
})
})
g{
cursor: pointer;
}
line{
stroke: black;
stroke-width: 2px;
}
line:nth-child(2) {
stroke-width: 1em;
stroke: transparent;
}
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" id="svg">
<g>
<line x1="320" y1="160" x2="140" y2="00"></line>
<line x1="320" y1="160" x2="140" y2="00"></line>
</g>
<g>
<line x1="140" y1="00" x2="180" y2="360"></line>
<line x1="140" y1="00" x2="180" y2="360"></line>
</g>
<g>
<line x1="00" y1="140" x2="280" y2="60"></line>
<line x1="00" y1="140" x2="280" y2="60"></line>
</g>
</svg>

最新更新