SVGGeometryElement元素具有方法SVGGeometryElement.getPointAtLength()
,该方法返回沿路径的距离处的DomPoint对象(例如x和y).
我想要的正好相反.给定一个(x,y)坐标,沿这条路径(假设它相交)的距离是多少?似乎我必须测试路径上的所有点,直到它与x,y目标坐标匹配/模糊匹配.有没有更简单的方法?
SVGGeometryElement元素具有方法SVGGeometryElement.getPointAtLength()
,该方法返回沿路径的距离处的DomPoint对象(例如x和y).
我想要的正好相反.给定一个(x,y)坐标,沿这条路径(假设它相交)的距离是多少?似乎我必须测试路径上的所有点,直到它与x,y目标坐标匹配/模糊匹配.有没有更简单的方法?
As commented by Robert Longson:
we may have multiple length results for a point (e.g due to self inflections like in a Moebius/infinity loop)
下面是获得类似lengthAtPoint
功能的sloppy方法:
const svg = document.querySelector("svg");
const pathTmpl = document.getElementById("pathTmpl");
const strokePath = document.getElementById("stroke");
// steps for pathlength lookup
let precision = 500;
let tolerance = 10;
// create length lookup
let t0 = performance.now()
let lengthLookup = getLengthLookup(pathTmpl, precision);
let t1 = performance.now()
let t2 = t1-t0
console.log('lookup calculation:', precision+' sample points', t2, 'ms')
document.addEventListener("click", (e) => {
// cursor point
let pt = new DOMPoint(e.clientX, e.clientY);
// update cursor
let ptSvg = screenToSVG(svg, pt);
circle.setAttribute("cx", ptSvg.x);
circle.setAttribute("cy", ptSvg.y);
// update length
let lengthAtPoint = getLengthAtPoint(lengthLookup, ptSvg);
// demo illustration: change dasharray
if (lengthAtPoint) {
strokePath.setAttribute(
"stroke-dasharray",
`${lengthAtPoint} ${lengthLookup.pathLength}`
);
}
});
function getLengthAtPoint(lengthLookup, pt) {
let lengthAtPoint = 0;
let { lengthArr, yArr, xArr, pathLength } = lengthLookup;
// find length
let found = false;
for (let i = 0; i < yArr.length && !found; i++) {
let x = xArr[i];
let y = yArr[i];
// compare diviations
let diffX = Math.abs(pt.x - x);
let diffY = Math.abs(pt.y - y);
let diff = (diffX + diffY) / 2;
let diffMin = Math.min(diffX, diffY);
let diffMax = Math.max(diffX, diffY);
// add tolerance threshold
let maxDiffRat = 1.5
if (
diff < tolerance ||
(diffX < tolerance && diffY < tolerance * maxDiffRat) ||
(diffY < tolerance && diffX < tolerance * maxDiffRat)
) {
// nearest length with close x/y coordinates
let length = lengthArr[i];
/**
* at this point you can certainly
* find a smarter interpolation to adjust
* the actual path length
*/
lengthAtPoint = (lengthArr[i]) + (diffMax);
// stop loop
found = true;
}
}
return lengthAtPoint;
}
/**
* create lookup containing lengths and
* coordinates at equidistant
*/
function getLengthLookup(path, precision = 100) {
//create pathlength lookup
let pathLength = path.getTotalLength();
let lengthLookup = {
yArr: [],
xArr: [],
lengthArr: [],
pathLength: pathLength
};
// sample point to calculate Y at pathLengths
let step = Math.ceil(pathLength / precision);
for (let l = 0; l < pathLength; l += step) {
//let pt = SVGToScreen(svg, path.getPointAtLength(l));
let pt = path.getPointAtLength(l);
lengthLookup.xArr.push(pt.x);
lengthLookup.yArr.push(pt.y);
lengthLookup.lengthArr.push(l);
}
return lengthLookup;
}
/** Based on @Paul LeBeau's answer
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates#48354404
*/
function SVGToScreen(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(svg.getScreenCTM());
return p;
}
function screenToSVG(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
return p.matrixTransform(svg.getScreenCTM().inverse());
}
svg {
overflow: visible;
}
<h3>Click on path</h3>
<svg width="543" height="7907" viewBox="0 0 543 7907" >
<defs>
<path fill="none" id="pathTmpl" d="M125.5 1v0c0 78.2 63.4 141.5 141.5 141.5h126.9c25.9 0 51.4 6.9 73.9 19.8v0c45.9 26.5 74.2 75.4 74.2 128.4v22.9l-2.2 20.1c-5.7 53.2-40.2 99-89.8 119.1l-25.3 10.2c-5.1 2.1-9.6 5.3-13.1 9.5v0c-5.2 6.1-8.1 13.9-8.1 22v9.7v0c0 12.8-10.4 23.3-23.3 23.3h-368.7c-5.8 0-10.5 4.7-10.5 10.5v0v405.5v0c0 5.8 4.7 10.5 10.5 10.5h378v0c7.7 0 14 6.3 14 14v384.3v762.9c0 34.2-27.8 62-62 62v0c-34.2 0-62 27.7-62 62v180.8c0 7.2-5.8 13-13 13v0h-249.5c-6.6 0-12 5.4-12 12v0v400v0c0 8.8 7.2 16 16 16h245.5v0c7.2 0 13 5.8 13 13v1129.6c0 53.9-43.8 97.7-97.7 97.7v0c-54 0-97.8 43.8-97.8 97.8v133.4v0c0 3.3 2.7 6 6 6h92.5v0c3.3 0 6 2.7 6 6v417.5c0 2.8-2.2 5-5 5v0h-92.5c-3.9 0-7 3.1-7 7v0v331.7c0 77.8 63.1 141 141 141h36.2c57.9 0 104.8 46.9 104.8 104.8v0v104.5v0c0 7.5-6 13.5-13.5 13.5h-240c-8.6 0-15.5 6.9-15.5 15.5v0v376.5v0c0 9.9 8.1 18 18 18h233v0c9.9 0 18 8.1 18 18v549.5c0 9.9-8.1 18-18 18v0h-237c-6.9 0-12.5 5.6-12.5 12.5v0v381v0c0 9.1 7.4 16.5 16.5 16.5h233v0c9.9 0 18 8.1 18 18v681.9c0 53-43 96-96 96v0c-53 0-96 43-96 96v139.6
" />
</path>
</defs>
<use href="#pathTmpl" stroke-width="0.25%" stroke-dasharray="0" stroke="#ccc"></use>
<use id="stroke" href="#pathTmpl" stroke-dasharray="0 100000" stroke-width="0.25%" stroke="red"></use>
<circle id="circle" cy="0" cx="0" r="0.5%" fill="green" fill-opacity="0.5"></circle>
</svg>
getPointAtLength()
isn't per se slow but quite expensive when called hundreds or thousands of times.
We should avoid calling it again and again if we can reuse point coordinates for further processing and also avoid unnecessarily detailed sample point intervals (e.g. iterating by +1 length unit - as a path can easily be > 10 0000 units long).
Therefore, we calculate coordinates at equidistant sample points and save length, x and y to a lookup object.
We can use this lookup later to find a suitable length based on the target x/y coordinates.
We need to define an error tolerance because we most likely don't have the exact coordinates.
In the above example we return an "on-point" length if
一旦在定义的公差内找到长度/坐标匹配,该函数将停止-后续匹配将被忽略.
这是一个任意的容忍门槛.为了获得更好的精度,可以减小公差并增加采样点的数量(当前为500-路径长度~10000个单位)
您也可以将此函数与isPointInStroke()
结合使用,以预先排除偏离路径的坐标.尽管这可能比草率的x/y坐标差异判断更昂贵.
虽然我们可以相当有效地计算许多点坐标部署适当的公式为线/折线/多边形(线性插值- LERP)和二次或三次贝塞尔曲线(德卡斯特劳算法)-我们不能很容易地将这些点与实际长度时,谈到贝塞尔.参见"Finding points on curves in HTML 5 2d Canvas context"和Primer on Bézier Curves §24 Arc length
因此,原生支持的getPointAtlength()
方法已经必须应用一些复杂的计算来获得适当的近似值-原生lengthAtPoint()
方法将是潜在的性能杀手.
getPointAtLength()
for better performanceThere are JS libraries developed mainly to provide getTotalLength()
and getPointAtLength()
functionality in headless or virtual DOM environments such as node.js or react e.g. svg-path-properties.
In fact these "emulations" can often provide a better performance if you need to calculate > 100 points.
svg-path-properties for instance读取重要的路径度量一次
const properties = new path.svgPathProperties("M0,100 Q50,-50 100,100 T200,100");
并根据这些数据生成后续点
const point = properties.getPointAtLength(200);
以这种方式生成多个点要比运行原生getPointAtLength()
快得多--因为后者在我们每次调用它时都会从头开始(解析路径数据、测量段长度等).
上面的例子通过native方法生成500个样本点的长度很容易超过200 ms,而svg-path-properties需要大约5- 10 ms(取决于您的浏览器和设备)
下面的示例基于My own experimental path length script,它与svg-路径-属性有很多相同的概念.
我们可以很容易地通过计算10,000个样本来提高精度,以改善点长度的精度-尽管~2000-5000应该就足够了,而且不必要的高精度也会引入更高的内存使用量.总而言之,我们需要在准确性和性能之间找到平衡.
const svg = document.querySelector("svg");
const pathTmpl = document.getElementById("pathTmpl");
const strokePath = document.getElementById("stroke");
// steps for pathlength lookup
let precision = 10000;
let tolerance = 10;
// get pathLength alternative
t0 = performance.now()
// lookup for path length calculations
let lengthLookup = getPathLengthLookup(pathTmpl.getAttribute('d'))
// lookup for point at length calculations
let pointAtLengthLookup = getPointAtLengthLookup(lengthLookup, precision)
t1 = performance.now()
t2 = t1 - t0
console.log('pathLength alternative:', precision, 'sample points', t2, 'ms')
document.addEventListener("click", (e) => {
// cursor point
let pt = new DOMPoint(e.clientX, e.clientY);
// update cursor
let ptSvg = screenToSVG(svg, pt);
circle.setAttribute("cx", ptSvg.x);
circle.setAttribute("cy", ptSvg.y);
// update length
let lengthAtPoint = getLengthAtPoint(pointAtLengthLookup, ptSvg);
// demo illustration: change dasharray
if (lengthAtPoint) {
strokePath.setAttribute(
"stroke-dasharray",
`${lengthAtPoint} ${lengthLookup.totalLength}`
);
}
});
function getLengthAtPoint(lengthLookup, pt) {
let lengthAtPoint = 0;
let {
lengthArr,
yArr,
xArr,
pathLength
} = lengthLookup;
// find length
let found = false;
for (let i = 0; i < yArr.length && !found; i++) {
let x = xArr[i];
let y = yArr[i];
// compare diviations
let diffX = Math.abs(pt.x - x);
let diffY = Math.abs(pt.y - y);
let diffMin = Math.min(diffX, diffY)
let diffMax = Math.max(diffX, diffY)
let diffTolerance = (tolerance - diffMax);
// simple average diff
let diff = (diffX + diffY) / 2;
// add tolerance threshold
let maxDiffRat = 1.5
if (
diff <= tolerance ||
(diffX <= tolerance && diffY <= tolerance * maxDiffRat) ||
(diffY <= tolerance && diffX <= tolerance * maxDiffRat)
) {
// nearest length with close x/y coordinates
let length = lengthArr[i];
// interpolate length based on length deviation
let lengthPrev = lengthArr[i - 1] ? lengthArr[i - 1] : length;
lengthAtPoint = (lengthArr[i]) + (diffMax)
// stop loop
found = true;
}
}
return lengthAtPoint;
}
/**
* create lookup containing lengths and
* coordinates at equidistant
*/
function getPointAtLengthLookup(lengthLookup, precision = 100) {
//create pathlength lookup
let pathLength = lengthLookup.totalLength;
let lengthAtPointLookup = {
yArr: [],
xArr: [],
lengthArr: [],
pathLength: pathLength
};
// sample point to calculate Y at pathLengths
let step = Math.ceil(pathLength / precision);
for (let l = 0; l < pathLength; l += step) {
//let pt = SVGToScreen(svg, path.getPointAtLength(l));
let pt = lengthLookup.getPointAtLength(l);
lengthAtPointLookup.xArr.push(pt.x);
lengthAtPointLookup.yArr.push(pt.y);
lengthAtPointLookup.lengthArr.push(l);
}
return lengthAtPointLookup;
}
/** Based on @Paul LeBeau's answer
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates#48354404
*/
function SVGToScreen(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(svg.getScreenCTM());
return p;
}
function screenToSVG(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
return p.matrixTransform(svg.getScreenCTM().inverse());
}
svg {
overflow: visible;
}
<script src="https://cdn.jsdelivr.net/npm/svg-getpointatlength@1.0.5/getPointAtLengthLookup.js"></script>
<svg width="543" height="7907" viewBox="0 0 543 7907">
<defs>
<path fill="none" id="pathTmpl" d="M125.5 1v0c0 78.2 63.4 141.5 141.5 141.5h126.9c25.9 0 51.4 6.9 73.9 19.8v0c45.9 26.5 74.2 75.4 74.2 128.4v22.9l-2.2 20.1c-5.7 53.2-40.2 99-89.8 119.1l-25.3 10.2c-5.1 2.1-9.6 5.3-13.1 9.5v0c-5.2 6.1-8.1 13.9-8.1 22v9.7v0c0 12.8-10.4 23.3-23.3 23.3h-368.7c-5.8 0-10.5 4.7-10.5 10.5v0v405.5v0c0 5.8 4.7 10.5 10.5 10.5h378v0c7.7 0 14 6.3 14 14v384.3v762.9c0 34.2-27.8 62-62 62v0c-34.2 0-62 27.7-62 62v180.8c0 7.2-5.8 13-13 13v0h-249.5c-6.6 0-12 5.4-12 12v0v400v0c0 8.8 7.2 16 16 16h245.5v0c7.2 0 13 5.8 13 13v1129.6c0 53.9-43.8 97.7-97.7 97.7v0c-54 0-97.8 43.8-97.8 97.8v133.4v0c0 3.3 2.7 6 6 6h92.5v0c3.3 0 6 2.7 6 6v417.5c0 2.8-2.2 5-5 5v0h-92.5c-3.9 0-7 3.1-7 7v0v331.7c0 77.8 63.1 141 141 141h36.2c57.9 0 104.8 46.9 104.8 104.8v0v104.5v0c0 7.5-6 13.5-13.5 13.5h-240c-8.6 0-15.5 6.9-15.5 15.5v0v376.5v0c0 9.9 8.1 18 18 18h233v0c9.9 0 18 8.1 18 18v549.5c0 9.9-8.1 18-18 18v0h-237c-6.9 0-12.5 5.6-12.5 12.5v0v381v0c0 9.1 7.4 16.5 16.5 16.5h233v0c9.9 0 18 8.1 18 18v681.9c0 53-43 96-96 96v0c-53 0-96 43-96 96v139.6
" />
</path>
</defs>
<use href="#pathTmpl" stroke-width="0.25%" stroke-dasharray="0" stroke="#ccc"></use>
<use id="stroke" href="#pathTmpl" stroke-dasharray="0 100000" stroke-width="0.25%" stroke="red"></use>
<circle id="circle" cy="0" cx="0" r="0.5%" fill="green" fill-opacity="0.5"></circle>
</svg>