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

问题描述 投票:0回答:3

我找到了这个问题的大约 1000 个答案,但没有一个是我可以使用的,因为我在曲线中使用了 4 个控制点。

也就是说,我偶然发现了这个人在这里

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}
这样的对象结构)?我觉得自己很白痴,但这是唯一阻止我真正实现这个怪物的原因。

FWIW,我使用这个方程来标准化实体在游戏中穿过曲线时的速度。如果您知道更好的方法来完成此操作,我洗耳恭听。

javascript math canvas bezier
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 点以上即可——更多点 == 更平滑。


1
投票

二维对象,或 Point2D,只是一个向量,而向量算术在数学中有明确的定义。例如:

          k*(x,y) = (k*x, k*y)
           -(x,y) = (-1)*(x,y)
(x1,y1) + (x2,y2) = (x1+x2, y1+y2)

这些是您需要计算

k1
k2
k3
k4

所需的所有公式

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
(以眨眼/铬为单位测量)

此助手基于使用 n=12 查找的勒让德-高斯求积求值。
另请参阅“贝塞尔曲线入门”§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
(以眨眼/铬为单位测量)

© www.soinside.com 2019 - 2024. All rights reserved.