二次贝塞尔曲线:计算点

63
我希望能够计算二次曲线上的一个点,并将其与HTML5的画布元素一起使用。
当我在JavaScript中使用 quadraticCurveTo() 函数时,我有一个源点,一个目标点和一个控制点。
如果我只知道这三个点,如何在创建的二次曲线上计算在 t=0.5 时的一个点呢?
5个回答

132
使用二次Bézier公式,例如在维基百科页面Bézier Curves上找到的公式:

quadratic Bezier formula

伪代码如下:

t = 0.5; // given example value
x = (1 - t) * (1 - t) * p[0].x + 2 * (1 - t) * t * p[1].x + t * t * p[2].x;
y = (1 - t) * (1 - t) * p[0].y + 2 * (1 - t) * t * p[1].y + t * t * p[2].y;

p[0] 是起始点,p[1] 是控制点,p[2] 是结束点。 t 是参数,它从0到1变化。


4
在这种情况下,点的乘法(加法)意味着你要对每个分量进行乘法(加法)运算。也就是说,3P = [3 * P.x,3 * P.y]P1 + P2 = [P1.x + P2.x,P1.y + P2.y]。最后,平方某物,就是将其自身与自身相乘:x²=x * x。最后一部分“t ∈ [1,0]”表示t应该介于0和1之间。 - Markus Jarderot
8
所以,这意味着:点的X坐标 = (1-t)^2 * P0.x + 2 * (1-t) * t * P1.x + t^2 * P2.x; 点的Y坐标 = (1-t)^2 * P0.y + 2 * (1-t) * t * P1.y + t^2 * P2.y;测试过并且有效!=) 谢谢! - Christian Engel
4
我认为你应该使用一些代码(或伪代码)来回答这个编程问题,而不是数学符号。 - user336063
1
什么是t?什么是p0、p1和p2? - openfrog
4
@openfrog,t表示相对于起始点和终点的点的位置,是假定起始点和终点总和为1的情况下的百分比,因此t通常是一个分数。p0是您的起始点,p1是控制/锚点,p2是您的终点。 - Razgriz
@Razgriz 那是答案中关键缺失的部分,谢谢。 - mojuba

37

如果有人需要立方形式:

        //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3

        x = (1-t)*(1-t)*(1-t)*p0x + 3*(1-t)*(1-t)*t*p1x + 3*(1-t)*t*t*p2x + t*t*t*p3x;
        y = (1-t)*(1-t)*(1-t)*p0y + 3*(1-t)*(1-t)*t*p1y + 3*(1-t)*t*t*p2y + t*t*t*p3y;

10
我创建了这个演示:
// x = a * (1-t)³ + b * 3 * (1-t)²t + c * 3 * (1-t)t² + d * t³
//------------------------------------------------------------
// x = a - 3at + 3at² - at³ 
//       + 3bt - 6bt² + 3bt³
//             + 3ct² - 3ct³
//                    + dt³
//--------------------------------
// x = - at³  + 3bt³ - 3ct³ + dt³
//     + 3at² - 6bt² + 3ct²
//     - 3at + 3bt
//     + a
//--------------------------------
// 0 = t³ (-a+3b-3c+d) +  => A
//     t² (3a-6b+3c)   +  => B
//     t  (-3a+3b)     +  => c
//     a - x              => D
//--------------------------------

var A = d - 3*c + 3*b - a,
    B = 3*c - 6*b + 3*a,
    C = 3*b - 3*a,
    D = a-x;

// So we need to solve At³ + Bt² + Ct + D = 0 

这里有完整的示例

可能会对某些人有帮助。


你的 JSFiddle 示例实际上没有显示 x 的 y 值。但我还是尝试了一下。它起作用了。转换为 Swift:https://gist.github.com/eonist/f5bb11533ee52ce24bad3ee47044239a 谢谢! - Sentry.co
@GitSyncApp 这是因为 cubic 函数。它返回了 3 个答案,而我只使用了第一个答案。请参考 http://www.1728.org/cubic.htm。 - talkhabi
是的,我知道。但那正是我所需要的。在三次贝塞尔曲线图上找到x对应的y值。我的意思是你的fiddle在x轴上有点缩放。可能是浏览器的问题 ¯_(ツ)_/¯ 不过还是很棒的。赞! - Sentry.co

2
我修改了talkhabis的答案(立方曲线),以便曲线显示正确的坐标。(无法评论)Y坐标需要更改(-p[].y+150)。(为此新建一个变量可能是更好、更有效的解决方案,但你明白我的意思)"最初的回答"
// Apply points to SVG and create the curve and controllers :

var path  =  document.getElementById('path'),
    ctrl1 =  document.getElementById('ctrl1'),
    ctrl2 =  document.getElementById('ctrl2'),
    D = 'M ' + p0.x + ' ' + (-p0.y+150) +
    'C ' + c0.x + ' ' + (-c0.y+150) +', ' + c1.x + ' ' + (-c1.y+150) + ', ' + p1.x + ' ' + (-p1.y+150);

path.setAttribute('d',D);
ctrl1.setAttribute('d','M'+p0.x+','+(-p0.y+150)+'L'+c0.x+','+(-c0.y+150));
ctrl2.setAttribute('d','M'+p1.x+','+(-p1.y+150)+'L'+c1.x+','+(-c1.y+150));

// Lets test the "Bezier Function" 

var t = 0, point = document.getElementById('point');

setInterval(function(){

  var p = Bezier(p0,c0,c1,p1,t);
  point.setAttribute('cx',p.x);
  point.setAttribute('cy',-p.y+150);

  t += 0.01;
  if(t>=1) t=0;

},50);


// OK ... Now tring to get "y" on cruve based on mouse "x" : 

var svg = document.getElementById('svg'),
    point2 = document.getElementById('point2');

svg.onmousemove = function(e){

    var x = (e.pageX - 50)/2,  
        y = (e.pageY - 50)/2;
   // "-50" because of "50px margin" on the left side 
   // and "/2" because the svg width is 300 units and 600 px => 300 = 600/2    

  // Get the x,y by mouse x
  var p = YBX(p0,c0,c1,p1,x); 

  point2.setAttribute('cx',p.x);
  point2.setAttribute('cy',-p.y+150);  
} 

我还创建了一些C代码来测试三次曲线的结果。只需在主函数中输入X和Y坐标即可。

http://jsfiddle.net/u214gco8/1/

最初的回答

#include <stdio.h>
#include <stdlib.h> 
#include <math.h> 

void bezierCurve(int x[] , int y[]) 
{ 
    double xu = 0.0 , yu = 0.0 , u = 0.0 ; 
    int i = 0 ; 
    for(u = 0.0 ; u <= 1.0 ; u += 0.05) 
    { 
        xu = pow(1-u,3)*x[0]+3*u*pow(1-u,2)*x[1]+3*pow(u,2)*(1-u)*x[2] 
             +pow(u,3)*x[3]; 
        yu = pow(1-u,3)*y[0]+3*u*pow(1-u,2)*y[1]+3*pow(u,2)*(1-u)*y[2] 
            +pow(u,3)*y[3]; 
        printf("X: %i   Y: %i \n" , (int)xu , (int)yu) ; 
    } 
} 

int main(void) {
    int x[] = {0,75,50,300};
    int y[] = {0,2,140,100};
    bezierCurve(x,y);
    return 0;
}

https://ideone.com/glLXcB


调整Y轴150的原因是什么?这是一个“固定”的调整吗,还是在不同大小的曲线/容器中会有所变化? - phun-ky

1
只是提醒一下:如果您使用此处介绍的常规公式,则不要期望 t = 0.5 返回曲线长度一半的点。在大多数情况下,它不会返回该点。
有关更多信息,请参见“§23-以固定距离间隔追踪曲线”下的此处此处

曲线的长度很难测量。在t=0.5时,如果假设控制点是随机的,你通常会在中心位置。但请注意,它与大多数速度不同的曲线一样存在问题。通常需要测量曲线的部分并使用二分查找找到中心位。这并不是非常常见的需求。但是,值得理解的是,如果您在t = .1增量处找到所有点,则它们的长度不相等。--虽然这与问题无关,但与曲线的性质有很大关系。 - Tatarize
1
@Tatarize:大部分是正确的,正如提供的链接所解释的那样。一个非常典型的场景可能是相机或网格沿着一条带有恒定速度的路径移动... 最终可能会使用从曲线计算出的折线,并使用二进制搜索... - A.J.Bauer

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