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

Bas*_*asj 9 javascript mouse svg hover

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

window.onmousedown = (e) => {
    if (e.target.tagName == 'line') {
        alert();  // do something with e.target
    }
}
Run Code Online (Sandbox Code Playgroud)
svg line:hover { cursor: pointer; }
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

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

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

syd*_*uki 10

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

window.onmousedown = (e) => {
    if (e.target.classList.contains('line')) {
        console.log(e.target.href);
    }
}
Run Code Online (Sandbox Code Playgroud)
svg .line:hover {
  cursor: pointer;
}
.line {
  stroke: black;
  stroke-width: 2px;
}
.line.stroke {
  stroke: transparent;
  stroke-width: 6px;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


onk*_*kar 8

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

我们可以使用现有的Web API document.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;
}
Run Code Online (Sandbox Code Playgroud)
svg {
  background-color: wheat;
}

.pointer {
  cursor: pointer;
}

.highlight {
  filter: drop-shadow(0 0 4px black);
}

#log {
  user-select: none;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

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


Pel*_*lay 5

只要做数学...

这可能有点过分了,但是这 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);
Run Code Online (Sandbox Code Playgroud)
.closeTo {
  filter: drop-shadow(0 0 3px rgba(0,0,0,1));
}
Run Code Online (Sandbox Code Playgroud)
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>
Run Code Online (Sandbox Code Playgroud)