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)

Example1: lengthAtPoint via lookup

下面是获得类似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>

它是如何工作的

1.测量路径并将数据保存到查找

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.

2.耐受性阈值

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

  • XTarget/xLookup和yTarget/yLookup差异均小于公差=10个用户单位
  • OR x Target/x Lookup Diff低于阈值,而y Target/y Lookup Diff容差较小*2

一旦在定义的公差内找到长度/坐标匹配,该函数将停止-后续匹配将被忽略.

这是一个任意的容忍门槛.为了获得更好的精度,可以减小公差并增加采样点的数量(当前为500-路径长度~10000个单位)

您也可以将此函数与isPointInStroke()结合使用,以预先排除偏离路径的坐标.尽管这可能比草率的x/y坐标差异判断更昂贵.

Main challenge: point-to-length relation

虽然我们可以相当有效地计算许多点坐标部署适当的公式为线/折线/多边形(线性插值- LERP)和二次或三次贝塞尔曲线(德卡斯特劳算法)-我们不能很容易地将这些点与实际长度时,谈到贝塞尔.参见"Finding points on curves in HTML 5 2d Canvas context"Primer on Bézier Curves §24 Arc length

因此,原生支持的getPointAtlength()方法已经必须应用一些复杂的计算来获得适当的近似值-原生lengthAtPoint()方法将是潜在的性能杀手.


Alternatives to native getPointAtLength() for better performance

There 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(取决于您的浏览器和设备)

Example 2: pointAtlength() altenative

下面的示例基于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>

Alternative path length libraries

Javascript相关问答推荐

如何在不分配整个数组的情况下修改包含数组的行为主体?

使用JavaScript单击上一个或下一个特定按钮创建卡滑动器以滑动单个卡

如何分配类型脚本中具有不同/额外参数的函数类型

Snowflake JavaScript存储过程返回成功,尽管预期失败

cypress中e2e测试上的Click()事件在Switch Element Plus组件上使用时不起作用

如何使用TypeScrip设置唯一属性?

同一类的所有div';S的模式窗口

Rxjs流中生成IMMER不能在对象上操作

是否可以在Photoshop CC中zoom 路径项?

与svg相反;S getPointAtLength(D)-我想要getLengthAtPoint(x,y)

打字脚本中方括号符号属性访问和拾取实用程序的区别

使用线性插值法旋转直线以查看鼠标会导致 skip

JWT Cookie安全性

MongoDB通过数字或字符串过滤列表

在Puppeteer中使用promise进行日志(log)记录时出现TargetCloseError

有没有办法在R中创建一张具有多个色标的热图?

如何检测当前是否没有按下键盘上的键?

无法向甜甜圈图表上的ChartJSImage添加可见标签

ReactJS Sweep Line:优化SciChartJS性能,重用wasmContext进行多图表渲染

与find()方法一起使用时,Mongoose中的$or运算符没有提供所有必需的数据