尝试使用4个点找到贝塞尔曲线的长度

3

我找到了大约1000个关于这个问题的答案,但是没有一个能够帮助我,因为我使用的是四个控制点来控制曲线。

说到这里,我偶然发现了这个页面:这里

double BezierArcLength(point2d p1, point2d p2, point2d p3, point2d p4)
{
    point2d k1, k2, k3, k4;

    k1 = -p1 + 3*(p2 - p3) + p4;
    k2 = 3*(p1 + p3) - 6*p2;
    k3 = 3*(p2 - p1);
    k4 = p1;

    q1 = 9.0*(sqr(k1.x) + sqr(k1.y));
    q2 = 12.0*(k1.x*k2.x + k1.y*k2.y);
    q3 = 3.0*(k1.x*k3.x + k1.y*k3.y) + 4.0*(sqr(k2.x) + sqr(k2.y));
    q4 = 4.0*(k2.x*k3.x + k2.y*k3.y);
    q5 = sqr(k3.x) + sqr(k3.y);

    double result = Simpson(balf, 0, 1, 1024, 0.001);
    return result;
}

看起来这似乎是完美的解决方案,但开头部分对我来说非常令人困惑。
k1 = -p1 + 3*(p2 - p3) + p4;
k2 = 3*(p1 + p3) - 6*p2;
k3 = 3*(p2 - p1);
k4 = p1;

我该如何在二维对象上执行加、减和乘法等操作(我假定point2d是一个对象结构,类似于{x: 0, y: 0})?我感到很白痴,但这是唯一阻碍我实现这个怪物的东西。
顺便说一下,我正在使用这个方程来规范化游戏中曲线遍历时实体的速度。如果你知道更好的方法,请告诉我。

2
问题:我到底该如何在二维对象上执行加、减和乘法等操作呢?答案:您需要逐个坐标(x 或 y)进行操作。 - geowar
3个回答

17

如何以均匀速度遍历你的三次贝塞尔曲线

要在三次贝塞尔曲线上获得等长度的线段(即等弧长线段),并没有一个简单的公式。需要计算曲线上许多点,然后使用插值来“调整”每个点成为大致等距离的点。

我可以帮助你接近目标,而无需获得数学博士学位。

首先,使用常见的公式从t=0到t=1计算曲线上的x/y点,其中t=0表示曲线的起点,t=1表示曲线的终点。这是常见的公式:

// calc the x/y point at t interval
// t=0 at startPt, t=1 at endPt
var x=CubicN(t,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(t,startPt.y,controlPt1.y,controlPt2.y,endPt.y);

// cubic helper formula at t interval
function CubicN(t, a,b,c,d) {
    var t2 = t * t;
    var t3 = t2 * t;
    return a + (-a * 3 + t * (3 * a - a * t)) * t
    + (3 * b + t * (-6 * b + b * 3 * t)) * t
    + (c * 3 - c * 3 * t) * t2
    + d * t3;
}

如果你计算足够的间隔,比如100个间隔(每次循环t += .01),那么你将得到曲线非常好的近似值。这意味着,如果你用线连接这100个点,结果看起来会非常像一个三次贝塞尔曲线。
但是你还没有完成!
上面计算出的x/y点序列在弧距上不均匀。一些相邻的点彼此靠近,而另一些相邻的点则相距较远。
为了计算均匀分布的点:
1. 用线连接所有的点(创建一个折线)。 2. 计算该折线的总长度(T)。 3. 将(T)除以所需的均匀段数,得到均匀段长度(SL)。 4. 最后,从起点到终点遍历折线,计算距离前一个点(SL)距离的每个点。
结果:你可以使用这些等距点来遍历曲线。
额外的优化:这应该会在贝塞尔路径上产生视觉上平滑的移动。但如果你想要更加平滑,只需计算超过100个点即可--更多的点==更平滑。

只是为了尝试并尝试扩展第4点,以帮助其他像我一样遇到困难的人。在我的情况下,我取了10个样本,代表我的各种间隔(在上面的示例中表示为“t”)。一旦我得到了“SL”,我没有将每个样本除以10,而是将其除以其间隔平均长度的结果。方程式类似于来自间隔(t)的单个样本/(间隔长度/所有间隔的平均长度)。您可能还需要调整您的十次幂,但我不确定这是否肯定或仅适用于我的情况。 - atomictom

1
一个二维对象,或者说是一个Point2D,就是一个向量,而在数学中向量算术是被定义清楚的。例如:
          k*(x,y) = (k*x, k*y)
           -(x,y) = (-1)*(x,y)
(x1,y1) + (x2,y2) = (x1+x2, y1+y2)

那些就是计算 k1k2k3k4 所需的所有公式。

那不是TS所问的。 - Artem Volkhin
1
好的,这是他问题的一部门:“我该如何在二维对象上执行像加、减和乘法之类的操作呢?” - Octopus

0
如果您无法使用浏览器方法 getTotalLength(),您可以使用基于Snap.svg的bezlen()函数的辅助方法来近似计算三次贝塞尔曲线的长度。

/**
 * Based on snap.svg bezlen() function
 * https://github.com/adobe-webplatform/Snap.svg/blob/master/dist/snap.svg.js#L5786
 */
function cubicBezierLength(p0, cp1, cp2, p, t = 1) {
  if (t === 0) {
    return 0;
  }
  const base3 = (t, p1, p2, p3, p4) => {
    let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
      t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
    return t * t2 - 3 * p1 + 3 * p2;
  };
  t = t > 1 ? 1 : t < 0 ? 0 : t;
  let t2 = t / 2;
  let Tvalues =  [-.1252, .1252, -.3678, .3678, -.5873, .5873, -.7699, .7699, -.9041, .9041, -.9816, .9816];
  let Cvalues = [0.2491, 0.2491, 0.2335, 0.2335, 0.2032, 0.2032, 0.1601, 0.1601, 0.1069, 0.1069, 0.0472, 0.0472];
  
  
  let n = Tvalues.length;
  let sum = 0;
  for (let i = 0; i < n; i++) {
    let ct = t2 * Tvalues[i] + t2,
      xbase = base3(ct, p0.x, cp1.x, cp2.x, p.x),
      ybase = base3(ct, p0.y, cp1.y, cp2.y, p.y),
      comb = xbase * xbase + ybase * ybase;
    sum += Cvalues[i] * Math.sqrt(comb);
  }
  return t2 * sum;
}
<svg id="svg" viewBox="0 0 300  300 " style="border:1px solid #ccc">
  <path id="path" d="" stroke="#999" fill="none" stroke-width="1%"></path>
</svg>

<script>  
window.addEventListener('DOMContentLoaded', e=>{

//example points
let p0 = { x: 250, y: 40 },
  cp1 = { x: 0, y: 240 },
  cp2 = { x: 0, y: 250 },
  p = { x: 260, y: 240 };

// render example to svg
path.setAttribute(
  "d",
  `M${p0.x} ${p0.y} C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y}  ${p.x} ${p.y}`
);

// native getTotalLength()
let t0 = performance.now();
let length = path.getTotalLength();
let end0 = +(performance.now() - t0).toFixed(5) + " ms";

// calculated with helper
let t1 = performance.now();
let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);
let end1 = +(performance.now() - t1).toFixed(5) + " ms";

console.log('native: ', length, end0, 'calculated: ', lengthCalc, end1)
console.log("diff:", length - lengthCalc)
})
</script>

偏差从getTotalLength():-0.015894306215841425
(以blink/chromium为单位测量)

此辅助工具基于使用n=12查找的Legendre-Gauss积分法进行评估。
另请参阅“贝塞尔曲线入门”§24弧长

您还可以通过使用更精确的横坐标/权重查找数组,如n=24,来提高准确性。

/**
 * Based on snap.svg bezlen() function
 * https://github.com/adobe-webplatform/Snap.svg/blob/master/dist/snap.svg.js#L5786
 */
function cubicBezierLength(p0, cp1, cp2, p, t = 1) {
  if (t === 0) {
    return 0;
  }
  const base3 = (t, p1, p2, p3, p4) => {
    let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
      t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
    return t * t2 - 3 * p1 + 3 * p2;
  };
  t = t > 1 ? 1 : t < 0 ? 0 : t;
  let t2 = t / 2;
  let Tvalues =  [
        -0.0640568928626056260850430826247450385909,
        0.0640568928626056260850430826247450385909,
        -0.1911188674736163091586398207570696318404,
        0.1911188674736163091586398207570696318404,
        -0.3150426796961633743867932913198102407864,
        0.3150426796961633743867932913198102407864,
        -0.4337935076260451384870842319133497124524,
        0.4337935076260451384870842319133497124524,
        -0.5454214713888395356583756172183723700107,
        0.5454214713888395356583756172183723700107,
        -0.6480936519369755692524957869107476266696,
        0.6480936519369755692524957869107476266696,
        -0.7401241915785543642438281030999784255232,
        0.7401241915785543642438281030999784255232,
        -0.8200019859739029219539498726697452080761,
        0.8200019859739029219539498726697452080761,
        -0.8864155270044010342131543419821967550873,
        0.8864155270044010342131543419821967550873,
        -0.9382745520027327585236490017087214496548,
        0.9382745520027327585236490017087214496548,
        -0.9747285559713094981983919930081690617411,
        0.9747285559713094981983919930081690617411,
        -0.9951872199970213601799974097007368118745,
        0.9951872199970213601799974097007368118745
    ];
  let Cvalues = [
        0.1279381953467521569740561652246953718517,
        0.1279381953467521569740561652246953718517,
        0.1258374563468282961213753825111836887264,
        0.1258374563468282961213753825111836887264,
        0.1216704729278033912044631534762624256070,
        0.1216704729278033912044631534762624256070,
        0.1155056680537256013533444839067835598622,
        0.1155056680537256013533444839067835598622,
        0.1074442701159656347825773424466062227946,
        0.1074442701159656347825773424466062227946,
        0.0976186521041138882698806644642471544279,
        0.0976186521041138882698806644642471544279,
        0.0861901615319532759171852029837426671850,
        0.0861901615319532759171852029837426671850,
        0.0733464814110803057340336152531165181193,
        0.0733464814110803057340336152531165181193,
        0.0592985849154367807463677585001085845412,
        0.0592985849154367807463677585001085845412,
        0.0442774388174198061686027482113382288593,
        0.0442774388174198061686027482113382288593,
        0.0285313886289336631813078159518782864491,
        0.0285313886289336631813078159518782864491,
        0.0123412297999871995468056670700372915759,
        0.0123412297999871995468056670700372915759
    ];
  
  
  let n = Tvalues.length;
  let sum = 0;
  for (let i = 0; i < n; i++) {
    let ct = t2 * Tvalues[i] + t2,
      xbase = base3(ct, p0.x, cp1.x, cp2.x, p.x),
      ybase = base3(ct, p0.y, cp1.y, cp2.y, p.y),
      comb = xbase * xbase + ybase * ybase;
    sum += Cvalues[i] * Math.sqrt(comb);
  }
  return t2 * sum;
}
<svg id="svg" viewBox="0 0 300  300 " style="border:1px solid #ccc">
  <path id="path" d="" stroke="#999" fill="none" stroke-width="1%"></path>
</svg>

<script>  
window.addEventListener('DOMContentLoaded', e=>{

//example points
let p0 = { x: 250, y: 40 },
  cp1 = { x: 0, y: 240 },
  cp2 = { x: 0, y: 250 },
  p = { x: 260, y: 240 };

// render example to svg
path.setAttribute(
  "d",
  `M${p0.x} ${p0.y} C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y}  ${p.x} ${p.y}`
);

// native getTotalLength()
let t0 = performance.now();
let length = path.getTotalLength();
let end0 = +(performance.now() - t0).toFixed(5) + " ms";

// calculated with helper
let t1 = performance.now();
let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);
let end1 = +(performance.now() - t1).toFixed(5) + " ms";

console.log('native: ', length, end0, 'calculated: ', lengthCalc, end1)
console.log("diff:", length - lengthCalc)
})
</script>

偏离getTotalLength():0.0005884069474291209 (以blink/chromium为单位测量)
模拟pointAtLength() 这个辅助函数还可以近似计算曲线在特定t值处的长度。
不幸的是,我们无法将t值处的长度转换为实际的路径段长度 - 换句话说:t=0.25处的长度并不等于总路径长度(t=1)的四分之一。
但是我们可以通过创建一个(在t处的长度)查找表,在t值之间进行插值,得到一个可接受的近似值。

/**
 * calculate point at length values
 * based on interpolated length at t values
 */
function getPointAtLengthLookup(lengthLookup, length) {
  let foundT = false;
  let pt;
  let {
    points,
    lengths
  } = lengthLookup;
  let [p0, cp1, cp2, p] = [points[0], points[1], points[2], points[3]];
  let tStep = 1 / (lengths.length - 1);
  let lengthTotal = lengths[lengths.length - 1];

  // first point
  if (length === 0) {
    return p0;
  }

  // last point
  if (length.toFixed(3) === lengthTotal.toFixed(3)) {
    return p;
  }

  for (let i = 0; i < lengths.length && !foundT; i++) {
    let lengthAtT = lengths[i];
    let lengthAtTPrev = i > 0 ? lengths[i - 1] : lengths[i];
    // found length at t range
    if (lengthAtT > length) {
      let t = tStep * i;
      // length between previous and current t
      let tSegLength = lengthAtT - lengthAtTPrev;
      // difference between length at t and exact length
      let diffLength = lengthAtT - length;
      // ratio between segment length and difference
      let tScale = (1 / tSegLength) * diffLength;
      let newT = t - tStep * tScale;

      foundT = true;
      pt = getPointAtCubicSegmentT(p0, cp1, cp2, p, newT);
    }
  }
  return pt;
}


/**
 * create length at t lookup
 */
function getPathLengthLookup(points, tDivisions = 10) {
  let lengthLookup = {
    points: points,
    lengths: [],
    diviations: []
  };
  let pL = points.length;
  let p0 = points[0],
    cp1 = points[1],
    cp2,
    p = points[pL - 1];
  if (points.length === 4) {
    cp2 = points[2];
  }
  let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);

  for (let i = 0; i < tDivisions; i++) {
    let t = (1 / tDivisions) * i;
    let len = cubicBezierLength(p0, cp1, cp2, p, t);
    lengthLookup.lengths.push(len);
  }

  lengthLookup.lengths.push(lengthCalc);
  lengthLookup.diviations.push(0);
  return lengthLookup;
}

/**
 * Based on snap.svg bezlen() function
 * https://github.com/adobe-webplatform/Snap.svg/blob/master/dist/snap.svg.js#L5786
 */
function cubicBezierLength(p0, cp1, cp2, p, t = 1) {
  if (t === 0) {
    return 0;
  }
  const base3 = (t, p1, p2, p3, p4) => {
    let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
      t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
    return t * t2 - 3 * p1 + 3 * p2;
  };
  t = t > 1 ? 1 : t < 0 ? 0 : t;
  let t2 = t / 2;
  let Tvalues = [-0.1252,
    0.1252, -0.3678,
    0.3678, -0.5873,
    0.5873, -0.7699,
    0.7699, -0.9041,
    0.9041, -0.9816,
    0.9816
  ];
  let Cvalues = [
    0.2491,
    0.2491,
    0.2335,
    0.2335,
    0.2032,
    0.2032,
    0.1601,
    0.1601,
    0.1069,
    0.1069,
    0.0472,
    0.0472
  ];


  let n = Tvalues.length;
  let sum = 0;
  for (let i = 0; i < n; i++) {
    let ct = t2 * Tvalues[i] + t2,
      xbase = base3(ct, p0.x, cp1.x, cp2.x, p.x),
      ybase = base3(ct, p0.y, cp1.y, cp2.y, p.y),
      comb = xbase * xbase + ybase * ybase;
    sum += Cvalues[i] * Math.sqrt(comb);
  }
  return t2 * sum;
}

function quadraticBezierLength(p0, cp1, p, t = 1) {
  if (t === 0) {
    return 0;
  }

  const interpolate = (p1, p2, t) => {
    let pt = {
      x: (p2.x - p1.x) * t + p1.x,
      y: (p2.y - p1.y) * t + p1.y
    };
    return pt;
  };
  const getLineLength = (p1, p2) => {
    return Math.sqrt(
      (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
    );
  };

  // is flat/linear
  let l1 = getLineLength(p0, cp1) + getLineLength(cp1, p);
  let l2 = getLineLength(p0, p);
  if (l1 === l2) {
    let m1 = interpolate(p0, cp1, t);
    let m2 = interpolate(cp1, p, t);
    p = interpolate(m1, m2, t);
    let lengthL;
    lengthL = Math.sqrt(
      (p.x - p0.x) * (p.x - p0.x) + (p.y - p0.y) * (p.y - p0.y)
    );
    return lengthL;
  }

  let a, b, c, d, e, e1, d1, v1x, v1y;

  v1x = cp1.x * 2;
  v1y = cp1.y * 2;
  d = p0.x - v1x + p.x;
  d1 = p0.y - v1y + p.y;
  e = v1x - 2 * p0.x;
  e1 = v1y - 2 * p0.y;
  a = 4 * (d * d + d1 * d1);
  b = 4 * (d * e + d1 * e1);
  c = e * e + e1 * e1;

  const bt = b / (2 * a),
    ct = c / a,
    ut = t + bt,
    k = ct - bt ** 2;

  return (
    (Math.sqrt(a) / 2) *
    (ut * Math.sqrt(ut ** 2 + k) -
      bt * Math.sqrt(bt ** 2 + k) +
      k *
      Math.log((ut + Math.sqrt(ut ** 2 + k)) / (bt + Math.sqrt(bt ** 2 + k))))
  );
}

function getLineLength(p1, p2) {
  return Math.sqrt(
    (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
  );
}

/**
 * Linear  interpolation (LERP) helper
 */
function interpolatedPoint(p1, p2, t = 0.5) {
  //t: 0.5 - point in the middle
  if (Array.isArray(p1)) {
    p1.x = p1[0];
    p1.y = p1[1];
  }
  if (Array.isArray(p2)) {
    p2.x = p2[0];
    p2.y = p2[1];
  }
  let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
  return {
    x: x,
    y: y
  };
}

/**
 * calculate single points on segments
 */
function getPointAtCubicSegmentT(p0, cp1, cp2, p, t = 0.5) {
  let t1 = 1 - t;
  return {
    x: t1 ** 3 * p0.x +
      3 * t1 ** 2 * t * cp1.x +
      3 * t1 * t ** 2 * cp2.x +
      t ** 3 * p.x,
    y: t1 ** 3 * p0.y +
      3 * t1 ** 2 * t * cp1.y +
      3 * t1 * t ** 2 * cp2.y +
      t ** 3 * p.y
  };
}



function renderPoint(
  svg,
  coords,
  fill = "red",
  r = "2",
  opacity = "1",
  id = "",
  className = ""
) {
  //console.log(coords);
  if (Array.isArray(coords)) {
    coords = {
      x: coords[0],
      y: coords[1]
    };
  }

  let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
  <title>${coords.x} ${coords.y}</title></circle>`;
  svg.insertAdjacentHTML("beforeend", marker);
}
<svg id="svg" viewBox="0 0 300  300 " style="border:1px solid #ccc">
  <path id="path" d="" stroke="#999" fill="none" stroke-width="1%"></path>
  <g id="segs"></g>
</svg>

<script>
  window.addEventListener('DOMContentLoaded', e => {
    let p0 = {
        x: 150,
        y: 50
      },
      cp1 = {
        x: 400,
        y: 100
      },
      cp2 = {
        x: -200,
        y: 200
      },
      p = {
        x: 150,
        y: 250
      };

    path.setAttribute(
      "d",
      `M${p0.x} ${p0.y} C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y}  ${p.x} ${p.y}`
    );


    let length = path.getTotalLength();
    let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);


    /**
     * get lookup
     */
    let tDivision = 10;
    let lengthLookup = getPathLengthLookup([p0, cp1, cp2, p], tDivision);
    let {
      points,
      lengths
    } = lengthLookup;

    // points on path
    let steps = 10;
    let ptsN = [];
    let ptsC = [];

    //calculate points with native `getPointAtLength()`
    for (let i = 0; i <= steps; i++) {
      //native
      let pt0 = path.getPointAtLength((lengthCalc / steps) * i);
      renderPoint(segs, pt0, "black", "1.2%");

      // approximated
      let totalLength = lengths[lengths.length - 1];
      let pt = getPointAtLengthLookup(lengthLookup, (totalLength / steps) * i);
      renderPoint(segs, pt, "red", "1.2%");

    }
  })
</script>

(黑色圆圈:原生点的长度;红色圆圈:插值点) 如您所见,10个步骤(0, 0.1 – 0.9, 1)并不十分准确。
我们可以通过增加在搜索中计算t时的长度数量来提高准确性,例如通过存储t为1/36的长度。 这当然会花费更多时间来进行更好的搜索,这会影响性能。但是当我们需要计算许多点时,这个劣势会得到平衡。

/**
 * calculate point at length values
 * based on interpolated length at t values
 */
function getPointAtLengthLookup(lengthLookup, length) {
  let foundT = false;
  let pt;
  let {
    points,
    lengths
  } = lengthLookup;
  let [p0, cp1, cp2, p] = [points[0], points[1], points[2], points[3]];
  let tStep = 1 / (lengths.length - 1);
  let lengthTotal = lengths[lengths.length - 1];

  // first point
  if (length === 0) {
    return p0;
  }

  // last point
  if (length.toFixed(3) === lengthTotal.toFixed(3)) {
    return p;
  }

  for (let i = 0; i < lengths.length && !foundT; i++) {
    let lengthAtT = lengths[i];
    let lengthAtTPrev = i > 0 ? lengths[i - 1] : lengths[i];
    // found length at t range
    if (lengthAtT > length) {
      let t = tStep * i;
      // length between previous and current t
      let tSegLength = lengthAtT - lengthAtTPrev;
      // difference between length at t and exact length
      let diffLength = lengthAtT - length;
      // ratio between segment length and difference
      let tScale = (1 / tSegLength) * diffLength;
      let newT = t - tStep * tScale;

      foundT = true;
      pt = getPointAtCubicSegmentT(p0, cp1, cp2, p, newT);
    }
  }
  return pt;
}


/**
 * create length at t lookup
 */
function getPathLengthLookup(points, tDivisions = 10) {
  let lengthLookup = {
    points: points,
    lengths: [],
    diviations: []
  };
  let pL = points.length;
  let p0 = points[0],
    cp1 = points[1],
    cp2,
    p = points[pL - 1];
  if (points.length === 4) {
    cp2 = points[2];
  }
  let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);

  for (let i = 0; i < tDivisions; i++) {
    let t = (1 / tDivisions) * i;
    let len = cubicBezierLength(p0, cp1, cp2, p, t);
    lengthLookup.lengths.push(len);
  }

  lengthLookup.lengths.push(lengthCalc);
  lengthLookup.diviations.push(0);
  return lengthLookup;
}

/**
 * Based on snap.svg bezlen() function
 * https://github.com/adobe-webplatform/Snap.svg/blob/master/dist/snap.svg.js#L5786
 */
function cubicBezierLength(p0, cp1, cp2, p, t = 1) {
  if (t === 0) {
    return 0;
  }
  const base3 = (t, p1, p2, p3, p4) => {
    let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
      t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
    return t * t2 - 3 * p1 + 3 * p2;
  };
  t = t > 1 ? 1 : t < 0 ? 0 : t;
  let t2 = t / 2;
  let Tvalues = [-0.1252,
    0.1252, -0.3678,
    0.3678, -0.5873,
    0.5873, -0.7699,
    0.7699, -0.9041,
    0.9041, -0.9816,
    0.9816
  ];
  let Cvalues = [
    0.2491,
    0.2491,
    0.2335,
    0.2335,
    0.2032,
    0.2032,
    0.1601,
    0.1601,
    0.1069,
    0.1069,
    0.0472,
    0.0472
  ];


  let n = Tvalues.length;
  let sum = 0;
  for (let i = 0; i < n; i++) {
    let ct = t2 * Tvalues[i] + t2,
      xbase = base3(ct, p0.x, cp1.x, cp2.x, p.x),
      ybase = base3(ct, p0.y, cp1.y, cp2.y, p.y),
      comb = xbase * xbase + ybase * ybase;
    sum += Cvalues[i] * Math.sqrt(comb);
  }
  return t2 * sum;
}

function quadraticBezierLength(p0, cp1, p, t = 1) {
  if (t === 0) {
    return 0;
  }

  const interpolate = (p1, p2, t) => {
    let pt = {
      x: (p2.x - p1.x) * t + p1.x,
      y: (p2.y - p1.y) * t + p1.y
    };
    return pt;
  };
  const getLineLength = (p1, p2) => {
    return Math.sqrt(
      (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
    );
  };

  // is flat/linear
  let l1 = getLineLength(p0, cp1) + getLineLength(cp1, p);
  let l2 = getLineLength(p0, p);
  if (l1 === l2) {
    let m1 = interpolate(p0, cp1, t);
    let m2 = interpolate(cp1, p, t);
    p = interpolate(m1, m2, t);
    let lengthL;
    lengthL = Math.sqrt(
      (p.x - p0.x) * (p.x - p0.x) + (p.y - p0.y) * (p.y - p0.y)
    );
    return lengthL;
  }

  let a, b, c, d, e, e1, d1, v1x, v1y;

  v1x = cp1.x * 2;
  v1y = cp1.y * 2;
  d = p0.x - v1x + p.x;
  d1 = p0.y - v1y + p.y;
  e = v1x - 2 * p0.x;
  e1 = v1y - 2 * p0.y;
  a = 4 * (d * d + d1 * d1);
  b = 4 * (d * e + d1 * e1);
  c = e * e + e1 * e1;

  const bt = b / (2 * a),
    ct = c / a,
    ut = t + bt,
    k = ct - bt ** 2;

  return (
    (Math.sqrt(a) / 2) *
    (ut * Math.sqrt(ut ** 2 + k) -
      bt * Math.sqrt(bt ** 2 + k) +
      k *
      Math.log((ut + Math.sqrt(ut ** 2 + k)) / (bt + Math.sqrt(bt ** 2 + k))))
  );
}

function getLineLength(p1, p2) {
  return Math.sqrt(
    (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
  );
}

/**
 * Linear  interpolation (LERP) helper
 */
function interpolatedPoint(p1, p2, t = 0.5) {
  //t: 0.5 - point in the middle
  if (Array.isArray(p1)) {
    p1.x = p1[0];
    p1.y = p1[1];
  }
  if (Array.isArray(p2)) {
    p2.x = p2[0];
    p2.y = p2[1];
  }
  let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
  return {
    x: x,
    y: y
  };
}

/**
 * calculate single points on segments
 */
function getPointAtCubicSegmentT(p0, cp1, cp2, p, t = 0.5) {
  let t1 = 1 - t;
  return {
    x: t1 ** 3 * p0.x +
      3 * t1 ** 2 * t * cp1.x +
      3 * t1 * t ** 2 * cp2.x +
      t ** 3 * p.x,
    y: t1 ** 3 * p0.y +
      3 * t1 ** 2 * t * cp1.y +
      3 * t1 * t ** 2 * cp2.y +
      t ** 3 * p.y
  };
}



function renderPoint(
  svg,
  coords,
  fill = "red",
  r = "2",
  opacity = "1",
  id = "",
  className = ""
) {
  //console.log(coords);
  if (Array.isArray(coords)) {
    coords = {
      x: coords[0],
      y: coords[1]
    };
  }

  let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
  <title>${coords.x} ${coords.y}</title></circle>`;
  svg.insertAdjacentHTML("beforeend", marker);
}
<p>
  <label>t division <span id="spanT"></span><input class="inputs" id="inpT" type="range" value="36" min="5" max="72" step="1"></label>
  <label>points on path <span id="spanPoints"></span><input class="inputs" id="inpPoints" type="range" value="10" min="5" max="500" step="1"></label>
</p>

<p><strong>Performance:</strong> native: <span id="perfN"></span> calculated:<span id="perfC"></span></p>


<svg id="svg" viewBox="0 0 300  300 " style="border:1px solid #ccc">
  <path id="path" d="" stroke="#999" fill="none" stroke-width="1%"></path>
  <g id="segs"></g>
</svg>

<script>
  window.addEventListener('DOMContentLoaded', e => {
    let p0 = {
        x: 150,
        y: 50
      },
      cp1 = {
        x: 400,
        y: 100
      },
      cp2 = {
        x: -200,
        y: 200
      },
      p = {
        x: 150,
        y: 250
      };
    
    path.setAttribute(
      "d",
      `M${p0.x} ${p0.y} C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y}  ${p.x} ${p.y}`
    );
    let length = path.getTotalLength();
    let lengthCalc = cubicBezierLength(p0, cp1, cp2, p, 1);
    
    
    let inputs = document.querySelectorAll('.inputs')
    inputs.forEach(input=>{
      input.addEventListener('input', e=>{
         renderPointsatLength(p0, cp1, cp2, p)
      });
    })

    renderPointsatLength(p0, cp1, cp2, p)
    function renderPointsatLength(p0, cp1, cp2, p) {
      let tDivision = +inpT.value;
      spanT.textContent = tDivision
      segs.innerHTML = '';
      /**
       * get lookup
       */
      let lengthLookup = getPathLengthLookup([p0, cp1, cp2, p], tDivision);
      let {
        points,
        lengths
      } = lengthLookup;
      
      let steps = +inpPoints.value;
      spanPoints.textContent = steps;
      let ptsN = [];
      let ptsC = [];
      
      //calculate points with native `getPointAtLength()`
      let t0 = performance.now();
      for (let i = 0; i <= steps; i++) {
        let pt = path.getPointAtLength((lengthCalc / steps) * i);
        ptsN.push(pt);
      }
      let end0 = +(performance.now() - t0).toFixed(8) + " ms";
      perfN.textContent = end0

      
      
      //calculate points with helper
      let t1 = performance.now();
      for (let i = 0; i <= steps; i++) {
        let totalLength = lengths[lengths.length - 1];
        let pt = getPointAtLengthLookup(lengthLookup, (totalLength / steps) * i);
        ptsC.push(pt);
      }
      let end1 = +(performance.now() - t1).toFixed(8) + " ms";
      perfC.textContent = end1

      //console.log('native: ', end0, 'calculated: ', end1)
      //render points
      ptsN.forEach(pt => {
        renderPoint(segs, pt, "black", "1.2%");
      })
      ptsC.forEach(pt => {
        renderPoint(segs, pt, "red", "1.2%");
      })
    }
  })
</script>

性能与准确性

实际上,一个粗糙但快速的近似可能是一个比更准确但显著更昂贵的计算更好的选择。
过于简化的基准测试结果绝不代表真实情况。
然而,通过getPointAtLength()计算大量(例如>50个)给定长度上的“在路径上”的点是非常昂贵的 -
超过10,000个甚至会明显减慢页面渲染速度。

相关文章

"贝塞尔曲线弧长"
"计算三次贝塞尔曲线的弧长,为什么不起作用?"


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接