如何将三次曲线的2个控制点转换为二次曲线的单个控制点?

时间:2010-01-05 21:19:55

标签: geometry bezier

在网上搜索过后,我看到各种论坛中的各种人都暗示用二次曲线近似一条三次曲线。但我找不到公式。

我想要的是:

输入:startX,startY,control1X,control1Y,control2X,control2Y,endX,endY 输出:startX,startY,controlX,controlY,endX,endY

实际上,由于起点和终点都是一样的,我真正需要的只是......

输入:startX,startY,control1X,control1Y,control2X,control2Y,endX,endY 输出:controlX,controlY

8 个答案:

答案 0 :(得分:7)

如上所述,从4个控制点到3通常是近似值。只有一种情况是精确的 - 当三次贝塞尔曲线实际上是一个度数提升的二次贝塞尔曲线时。

您可以使用度数高程方程来得出近似值。这很简单,结果通常都很好。

让我们调用立方Q0..Q3的控制点和二次P0..P2的控制点。然后对于度数提升,方程是:

Q0 = P0
Q1 = 1/3 P0 + 2/3 P1
Q2 = 2/3 P1 + 1/3 P2
Q3 = P2

在你的情况下,你有Q0..Q3,你正在解决P0..P2。有两种方法可以从上面的等式计算P1:

P1 = 3/2 Q1 - 1/2 Q0
P1 = 3/2 Q2 - 1/2 Q3

如果这是一个度数升高的立方,那么这两个方程式将给出P1的相同答案。由于它可能不是,你最好的选择是平均它们。所以,

P1 = -1/4 Q0 + 3/4 Q1 + 3/4 Q2 - 1/4 Q3

翻译成您的条款:

controlX = -0.25*startX + .75*control1X + .75*control2X -0.25*endX

Y的计算方法类似 - 尺寸是独立的,因此适用于3d(或n-d)。

这将是一个近似值。如果您需要更好的近似,获得它的一种方法是使用deCastlejau算法细分初始立方,然后降低每个段的度数。如果您需要更好的连续性,还有其他近似方法不那么快速和肮脏。

答案 1 :(得分:4)

立方体可以有环和尖点,二次方不能有。这意味着几乎没有简单的解决方案。如果立方体已经是二次方,则存在简单的解。通常,您必须将立方体划分为二次方的零件。你必须决定细分的关键点。

http://fontforge.org/bezier.html#ps2ttf说: “我在网上读到的其他资料建议检查三次样条曲线的拐点(二次样条不能有)并在那里强制中断。在我看来,这实际上会使结果更糟,它使用更多的点而近似不看当忽略拐点时就像它一样接近。所以我忽略它们。“

这是事实,拐点(立方的二阶导数)是不够的。但是如果你考虑局部极值(min,max)这是三次函数的第一个导数,并且强制断开那些,那么子曲线都是二次曲线,可以用二次曲线来表示。

我测试了以下功能,它们按预期工作(找到立方体的所有临界点并将立方体分成向下升高的立方体)。绘制这些子曲线时,曲线与原始立方体完全相同,但由于某种原因,当子曲线绘制为样方时,结果几乎是正确的,但不完全相同。

所以这个答案并不是对问题的严格帮助,但这些函数提供了立方到二次转换的起点。

要查找局部极值和拐点,以下get_t_values_of_critical_points()应提供它们。在

function compare_num(a,b) {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

function find_inflection_points(p1x,p1y,p2x,p2y,p3x,p3y,p4x,p4y)
{
  var ax = -p1x + 3*p2x - 3*p3x + p4x;
  var bx = 3*p1x - 6*p2x + 3*p3x;
  var cx = -3*p1x + 3*p2x;

  var ay = -p1y + 3*p2y - 3*p3y + p4y;
  var by = 3*p1y - 6*p2y + 3*p3y;
  var cy = -3*p1y + 3*p2y;
  var a = 3*(ay*bx-ax*by);
  var b = 3*(ay*cx-ax*cy);
  var c = by*cx-bx*cy;
  var r2 = b*b - 4*a*c;
  var firstIfp = 0;
  var secondIfp = 0;
  if (r2>=0 && a!==0)
  {
    var r = Math.sqrt(r2);
    firstIfp = (-b + r) / (2*a);
    secondIfp = (-b - r) / (2*a);
    if ((firstIfp>0 && firstIfp<1) && (secondIfp>0 && secondIfp<1))
    {
      if (firstIfp>secondIfp)
      {
        var tmp = firstIfp;
        firstIfp = secondIfp;
        secondIfp = tmp;
      }
      if (secondIfp-firstIfp >0.00001)
        return [firstIfp, secondIfp];
      else return [firstIfp];
    }
    else if (firstIfp>0 && firstIfp<1)
      return [firstIfp];
    else if (secondIfp>0 && secondIfp<1)
    {
      firstIfp = secondIfp;
      return [firstIfp];
    }
    return [];
  }
  else return [];
}

function get_t_values_of_critical_points(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
    var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
    b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
    c = p1x - c1x,
    t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a,
    t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a,
    tvalues=[];
    Math.abs(t1) > "1e12" && (t1 = 0.5);
    Math.abs(t2) > "1e12" && (t2 = 0.5);
    if (t1 >= 0 && t1 <= 1 && tvalues.indexOf(t1)==-1) tvalues.push(t1)
    if (t2 >= 0 && t2 <= 1 && tvalues.indexOf(t2)==-1) tvalues.push(t2);

    a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
    b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
    c = p1y - c1y;
    t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
    t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
    Math.abs(t1) > "1e12" && (t1 = 0.5);
    Math.abs(t2) > "1e12" && (t2 = 0.5);
    if (t1 >= 0 && t1 <= 1 && tvalues.indexOf(t1)==-1) tvalues.push(t1);
    if (t2 >= 0 && t2 <= 1 && tvalues.indexOf(t2)==-1) tvalues.push(t2);

    var inflectionpoints = find_inflection_points(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
    if (inflectionpoints[0]) tvalues.push(inflectionpoints[0]);
    if (inflectionpoints[1]) tvalues.push(inflectionpoints[1]);

    tvalues.sort(compare_num);
    return tvalues;
};

当你有那些临界t值(从0-1范围)时,你可以将立方体分成几部分:

function CPoint()
{
  var arg = arguments;
  if (arg.length==1)
  {
    this.X = arg[0].X;
    this.Y = arg[0].Y;
  }
  else if (arg.length==2)
  {
    this.X = arg[0];
    this.Y = arg[1];
  }
}

function subdivide_cubic_to_cubics()
{
    var arg = arguments;
    if (arg.length!=9) return [];
    var m_p1 = {X:arg[0], Y:arg[1]};
  var m_p2 = {X:arg[2], Y:arg[3]};
  var m_p3 = {X:arg[4], Y:arg[5]};
  var m_p4 = {X:arg[6], Y:arg[7]};
  var t = arg[8];
  var p1p = new CPoint(m_p1.X + (m_p2.X - m_p1.X) * t,
                       m_p1.Y + (m_p2.Y - m_p1.Y) * t);
  var p2p = new CPoint(m_p2.X + (m_p3.X - m_p2.X) * t,
                       m_p2.Y + (m_p3.Y - m_p2.Y) * t);
  var p3p = new CPoint(m_p3.X + (m_p4.X - m_p3.X) * t,
                       m_p3.Y + (m_p4.Y - m_p3.Y) * t);
  var p1d = new CPoint(p1p.X + (p2p.X - p1p.X) * t,
                       p1p.Y + (p2p.Y - p1p.Y) * t);
  var p2d = new CPoint(p2p.X + (p3p.X - p2p.X) * t,
                       p2p.Y + (p3p.Y - p2p.Y) * t);
  var p1t = new CPoint(p1d.X + (p2d.X - p1d.X) * t,
                       p1d.Y + (p2d.Y - p1d.Y) * t);
  return [[m_p1.X, m_p1.Y, p1p.X, p1p.Y, p1d.X, p1d.Y, p1t.X, p1t.Y],
          [p1t.X, p1t.Y, p2d.X, p2d.Y, p3p.X, p3p.Y, m_p4.X, m_p4.Y]];
}
上面代码中的

subdivide_cubic_to_cubics()将原始三次曲线除以值t的两个部分。因为get_t_values_of_critical_points()将t值作为按t值排序的数组返回,所以您可以轻松遍历所有t值并获得相应的子曲线。当你有这些划分的曲线时,你必须将第二个子曲线除以下一个t值。

当进行所有分割时,您将拥有所有子曲线的控制点。现在只剩下立方控制点转换为二次方。由于所有子曲线现在都是向下升高的立方体,因此相应的二次控制点很容易计算。二次控制点的第一个和最后一个与第一个和最后一个控制点的立方体(子曲线)相同,并且在P1-P2和P4-P3线交叉的点中找到中间控制点。

答案 2 :(得分:3)

答案 3 :(得分:2)

tfinniga答案的另一个推导:
首先看维基百科Bezier curve 对于二次和三次贝塞尔曲线的公式(也是很好的动画):

Q(t) = (1-t)^2 P0 + 2 (1-t) t Q + t^2 P3
P(t) + (1-t)^3 P0 + 3 (1-t)^2 t P1 + 3 (1-t) t^2 P2 + t^3 P3

要求这些匹配在中间,t = 1/2:

(P0 + 2 Q + P3) / 4 = (P0 + 3 P1 + 3 P2 + P3) / 8  
=> Q = P1 + P2 - (P0 + P1 + P2 + P3) / 4  

(这样写的Q有几何解释:

Pmid = middle of P0 P1 P2 P3  
P12mid = midway between P1 and P2  
draw a line from Pmid to P12mid, and that far again: you're at Q.  

希望这是有道理的 - 请举几个例子。)

答案 4 :(得分:1)

一般来说,你必须使用多个二次曲线 - 很多情况下,三次曲线甚至不能用一个二次曲线模糊地近似。

http://www.timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm(包括互动演示)上,有一篇很好的文章讨论了这个问题以及解决问题的方法。

答案 5 :(得分:1)

我应该注意到Adrian的解决方案对于单立方体很有用,但是当立方体是平滑三次样条的片段时,使用他的中点近似方法会导致节点的节点处的斜率连续性丢失。因此,如果您正在使用字体字形,或者出于任何其他原因,您希望保留曲线的平滑度(这很可能就是这种情况),http://fontforge.org/bezier.html#ps2ttf中描述的方法要好得多。

即使这是一个老问题,很多像我这样的人会在搜索结果中看到它,所以我在这里发布。

答案 6 :(得分:0)

我可能会绘制一系列曲线,而不是尝试使用不同的alg绘制一条曲线。有点像画两个半圈组成一个整圆。

答案 7 :(得分:0)

尝试在Truetype字体转换器中查找开源Postscript字体。我相信他们有。 Postscript使用三次贝塞尔曲线,而Truetype使用二次贝塞尔曲线。祝你好运。