如何通过坐标获取SVG内部子元素?并实现路径限制的元素移动功能
搞定SVG坐标元素获取+圆形路径移动限制
嘿,我来帮你解决这两个问题!
为啥document.elementsFromPoint拿不到SVG内部元素?
document.elementsFromPoint(x, y)认的是浏览器窗口的屏幕坐标(从视口左上角算起),但你传入的cx、cy是SVG自己的内部坐标(也就是用户坐标系),这俩不是一个体系,所以自然匹配不到内部的circle、path啦。
解决的核心就是坐标转换——要么把SVG内部坐标转成屏幕坐标,要么直接用SVG自带的API来判断点是否属于某个元素,后者其实更高效。
修改代码实现圆形只能在绿色路径上移动
我们可以用SVG路径的isPointInStroke()方法来判断新位置是否在路径的描边范围内(因为你的路径是用stroke画的,fill是透明的),同时修复坐标转换和键盘焦点的问题。下面是修改后的完整代码:
<html> <head> <script src="https://d3js.org/d3.v7.min.js"></script> </head> <body> <script> function onClick() { alert('You have clicked the circle.') } function onKeyPress(event) { switch (event.keyCode) { case 37: moveLeft(); break; case 38: moveUp(); break; case 39: moveRight(); break; case 40: moveDown(); break; default: } } // 工具函数:判断点是否在路径的描边上 function isPointOnPath(pathElement, x, y) { const svg = pathElement.ownerSVGElement; // 创建SVG内部坐标点 const point = svg.createSVGPoint(); point.x = x; point.y = y; // 把SVG用户坐标转换成路径的局部坐标 const ctm = pathElement.getScreenCTM().inverse(); const transformedPoint = point.matrixTransform(ctm); // 检查点是否在路径的描边范围内 return pathElement.isPointInStroke(transformedPoint); } function moveDown() { console.log('down'); const path = document.getElementById('path'); const robot = d3.select('#robot'); const cx = Number(robot.attr('cx')); const cy = Number(robot.attr('cy')); const newCy = cy + 10; // 先检查新位置是否合法,再移动 if (isPointOnPath(path, cx, newCy)) { robot.attr('cy', newCy); } } function moveUp() { console.log('up'); const path = document.getElementById('path'); const robot = d3.select('#robot'); const cx = Number(robot.attr('cx')); const cy = Number(robot.attr('cy')); const newCy = cy - 10; if (isPointOnPath(path, cx, newCy)) { robot.attr('cy', newCy); } } function moveLeft() { console.log('left'); const path = document.getElementById('path'); const robot = d3.select('#robot'); const cx = Number(robot.attr('cx')); const cy = Number(robot.attr('cy')); const newCx = cx - 10; if (isPointOnPath(path, newCx, cy)) { robot.attr('cx', newCx); } } function moveRight() { console.log('right'); const path = document.getElementById('path'); const robot = d3.select('#robot'); const cx = Number(robot.attr('cx')); const cy = Number(robot.attr('cy')); const newCx = cx + 10; if (isPointOnPath(path, newCx, cy)) { robot.attr('cx', newCx); } } function onLoad() { console.log('onload'); this.addEventListener('keydown', event => onKeyPress(event)); // 给SVG加焦点,不然可能监听不到键盘事件 this.focus(); } </script> <svg width='500px' height='500px' focusable onload="onLoad()"> <text x='0' y='20' fill='blue'>Hello world from within svg! Press arrow keys to move the circle:</text> <path id="path" d="M100 100 L 100 200 L 200 200" stroke='green' stroke-width="10" fill="transparent"/> <circle id="robot" cx="100" cy="100" r="5" fill='red' onclick="onClick()" /> </svg> </body> </html>
关键修改点说明:
- 路径判断逻辑:新增的
isPointOnPath函数专门处理坐标转换和点校验,用isPointInStroke而不是isPointInFill,因为你的路径是靠stroke显示的,fill是透明的。 - 键盘事件修复:在
onLoad里给SVG加了focus(),确保SVG能接收到键盘事件(之前可能因为没焦点导致按键没反应)。 - 路径描边宽度:给路径加了
stroke-width="10",这样圆形移动时更容易“踩”在路径上,你可以根据需求调整这个值。 - 统一移动规则:所有方向的移动都先检查新位置是否在路径上,只有合法才允许移动。
补充:如果一定要用elementsFromPoint获取SVG子元素
要是你坚持想用elementsFromPoint,可以先把SVG内部坐标转换成屏幕坐标再调用:
// 工具函数:把SVG内部坐标转成屏幕坐标 function svgToScreen(svgElement, x, y) { const point = svgElement.createSVGPoint(); point.x = x; point.y = y; const screenPoint = point.matrixTransform(svgElement.getScreenCTM()); return { x: screenPoint.x, y: screenPoint.y }; } // 使用示例 const svg = document.querySelector('svg'); const screenPos = svgToScreen(svg, cx, newCy); const elements = document.elementsFromPoint(screenPos.x, screenPos.y); // 检查路径是否在返回的元素列表里 if (elements.includes(document.getElementById('path'))) { robot.attr('cy', newCy); }
不过这种方法不如直接用isPointInStroke高效,因为elementsFromPoint会返回所有叠在该点的元素,还要额外过滤判断。
内容的提问来源于stack exchange,提问作者Stefan
相关产品推荐
相关产品推荐

