使用8个三次贝塞尔曲线创建(近似)圆形的控制点是什么?

4

前提条件

我有一个动画圆,它会变成由8个贝塞尔曲线组成的形状。为了使过渡更加平滑,我需要将圆也由8个立方贝塞尔曲线组成。 这是我目前所拥有的:

代码

- (UIBezierPath*)pathBubbleLeft {
    UIBezierPath *path = [UIBezierPath new];
    [path moveToPoint:p(sqlx, sqlMidy)];

    CGFloat r = sqlW/2;
    CGFloat sin45 = 0.7071 * r;
    CGFloat cos45 = 0.7071 * r;

    [path addRelativeCurveToPoint:point(sqlMidx - cos45, sqlMidy - sin45) control1:vector(0, 0.4) control2:vector(0.5, 0.8)];
    [path addRelativeCurveToPoint:point(sqlMidx, sqly) control1:vector(0.2, 0.5) control2:vector(0.4, 1)];

    [path addRelativeCurveToPoint:point(sqlMidx + cos45, sqlMidy - sin45) control1:vector(0.6, 0) control2:vector(0.8, 0.5)];
    [path addRelativeCurveToPoint:point(sqlMaxx, sqlMidy) control1:vector(0.5, 0.2) control2:vector(1, 0.5)];

    [path addRelativeCurveToPoint:point(sqlMidx + cos45, sqlMidy + sin45) control1:vector(0, 0.4) control2:vector(0.5, 0.8)];
    [path addRelativeCurveToPoint:point(sqlMidx, sqlMaxy) control1:vector(0.2, 0.5) control2:vector(0.4, 1)];

    [path addRelativeCurveToPoint:point(sqlMidx - cos45, sqlMidy + sin45) control1:vector(0.6, 0) control2:vector(0.8, 0.5)];
    [path addRelativeCurveToPoint:point(sqlx, sqlMidy) control1:vector(0.5, 0.2) control2:vector(1, 0.5)];

    return path;
}

路径从左侧开始,顺时针方向绕圆周运动(从π到π / 2,0,3π / 4,π)。

pointvector是对CGPointMakeCGVectorMake的简写。

'sql'在sqlxsqlysqlMidxsqlMidYsqlMaxxsqlMaxy中代表'squareLeft',即圆形的边界矩形。这些都是CGFloats。

addRelativeCurveToPoint用于相对于起始/结束点定义控制点。(0,0)为起点,(1,1)为终点。更容易阅读。

- (void)addRelativeCurveToPoint:(CGPoint)endPoint control1:(CGVector)controlPoint1 control2:(CGVector)controlPoint2 {
    CGPoint start = self.currentPoint;
    CGPoint end = endPoint;
    CGFloat x1 = start.x + controlPoint1.dx*(end.x - start.x);
    CGFloat x2 = start.x + controlPoint2.dx*(end.x - start.x);
    CGFloat y1 = start.y + controlPoint1.dy*(end.y - start.y);
    CGFloat y2 = start.y + controlPoint2.dy*(end.y - start.y);
    [self addCurveToPoint:endPoint controlPoint1:CGPointMake(x1, y1) controlPoint2:CGPointMake(x2, y2)];
}

迄今为止的结果如下:静态图片。红色圆周略微波动,需要修复。
下图中,左边的圆使用了上述代码,右边的圆由4个曲线组成,在顶部和底部插入2个零长度插入 ([path addLineToPoint:path.currentPoint];)。 输入图像描述 从左边到中间花生状的过渡是可以的,但从中间到右边很奇怪。

我将您的解决方案移动到社区维基答案。 - Cœur
3个回答

4

使用四个线段,使用三次贝塞尔曲线逼近圆的过程中,最接近圆的值为0.55228[...],这是@fang在评论中提供给您的数学上最优的值。在无限精度表示中,实际上是通过以下方式得到的:

     4        angle       4                     sqrt(2) - 1
k =  - * tan(-------)  =  - * tan(pi/8)  =  4 * -----------
     3          2         3                          3

这将获得最佳的4段近似值,因此如果您需要使用8个段,则需要不同的值,这意味着我们需要使用导出值来获取角度为pi / 2(四分之一圆)的k,并查看它对于pi / 4(八分之一圆)给出了什么结果。因此:我们将角度 pi / 4 插入到Bezier Curve入门中概述的函数中,在用三次曲线逼近圆部分中,我们得到:

start = {
  x: 1,
  y: 0
}

c1 = {
  x: 1,
  y: 4/3 * tan(pi/16)
}

c2 = {
  x: cos(pi/4) + 4/3 * tan(pi/16) * sin(pi/4)
  y: sin(pi/4) - 4/3 * tan(pi/16) * cos(pi/4)
}

e = {
  x: cos(pi/4)
  y: sin(pi/4)
}

这将给我们提供这些(非常有用的)近似坐标:

s  = (1,           0)
c1 = (1,           0.265216...)
c2 = (0.894643..., 0.51957...)
e  = (0.7071...,   0.7071...)

那就是第一段,其余的部分只需要对称处理即可获得其他段落。第二段是:
s  = (0.7071...,   0.7071...)
c1 = (0.51957...,  0.894643...)
c2 = (0.265216..., 1)
e  = (0,           1)

这里有一个使用叠加了四分之一圆的坐标演示: http://jsbin.com/ridedahixu/edit?html,output 其余的对称性在(+,-),(-,+)和(-,-)象限中都是显而易见的。
这些是最好的近似值,因此:如果bezierPathWithOvalInRect(...)做了其他事情,则比我们几十年前计算出的值更不正确 =)

这是一个很好的答案。我将更新问题并附上最终代码,方便懒人。干杯! - codrut
根据这篇文章,这并不是最佳的近似方法;它可以通过改进来提高约28%。 - Ky -
这是一个不尽如人意的页面。为什么我们没有得到真实的值,而只有一个近似值?如果这是您的页面,您能否添加该值的真实符号函数? - Mike 'Pomax' Kamermans

1
当圆被四条贝塞尔曲线近似时,第一个控制点(在(1,0)起始点后)的坐标为(1,0.552)。对于8曲线情况,由于贝塞尔细分规则,它将变为(1,0.276)。
因此,您的控制向量是(0,0.276), (0.276,0), (0.195,0.195),具有不同的符号组合。

1
如果您需要更精确的值,它是0.552284749。 - fang
进行了一个测试,使用0.552284749 http://i.imgur.com/A8T3oaB.png。圆形更加圆润,但仍然不完全匹配bezierPathWithOvalInRect(红色描边)。在接受答案之前,我会再等几天,也许还有其他方法。对于我所需的内容(动画被缩小到约40x40像素的边界矩形),当前形式已经足够了,但我仍然很好奇。谢谢@MBo和fang的快速回复! - codrut
@fang 我上面发布了一个测试结果,但似乎每个评论只能标记一个用户 :| - codrut
根据被接受的答案,该值应为0.26521648983954...而不是此处所述的0.276。我已经使用Illustrator进行了测试,0.265的值给出非常精确的结果。 - Marco Luglio
@Marco Luglio 我的值是根据大约分割一半的 Pi/2 弧线计算的,而 Mike 的值则是真正的 Pi/4 弧线,因此他的值应该更精确。 - MBo

0

楼主的解决方案。

基于Mike的答案。

- (CGRect)sql {
    return CGRectMake(0, 0, self.frameHeight, self.frameHeight);
}

CGPoint point(CGFloat x, CGFloat y) {return CGPointMake(x, y);}

#define sqlx (self.sql.origin.x)
#define sqly (self.sql.origin.y)
#define sqlMaxx (self.sql.origin.x + self.sql.size.width)
#define sqlMidx (self.sql.origin.x + self.sql.size.width/2)
#define sqlMaxy (self.sql.origin.y + self.sql.size.height)
#define sqlMidy (self.sql.origin.y + self.sql.size.height/2)
#define sqlW (self.sql.size.width)
#define sqlH (self.sql.size.height)


- (UIBezierPath*)pathBubbleLeft {
    UIBezierPath *path = [UIBezierPath new];
    [path moveToPoint:p(sqlx, sqlMidy)];

    CGFloat r = sqlW/2;
    CGFloat sin45 = sin(M_PI_4)*r;
    CGFloat cos45 = cos(M_PI_4)*r;

    CGFloat magic1 = (cos(M_PI_4) + 4/3.0 * tan(M_PI/16.0) * sin(M_PI_4))*r;
    CGFloat magic2 = (sin(M_PI_4) - 4/3.0 * tan(M_PI/16.0) * cos(M_PI_4))*r;
    CGFloat magic3 = 4/3.0 * tan(M_PI/16.0)*r;

    [path addCurveToPoint:point(sqlMidx - cos45, sqlMidy - sin45)
            controlPoint1:point(sqlx, sqlMidy - magic3)
            controlPoint2:point(sqlMidx - magic1, sqlMidy - magic2)];

    [path addCurveToPoint:point(sqlMidx, sqly)
            controlPoint1:point(sqlMidx - magic2 , sqlMidy - magic1)
            controlPoint2:point(sqlMidx - magic3, sqly)];

    [path addCurveToPoint:point(sqlMidx + cos45, sqlMidy - sin45)
            controlPoint1:point(sqlMidx + magic3, sqly)
            controlPoint2:point(sqlMidx + magic2 , sqlMidy - magic1)];

    [path addCurveToPoint:point(sqlMaxx, sqlMidy)
            controlPoint1:point(sqlMidx + magic1 , sqlMidy - magic2)
            controlPoint2:point(sqlMaxx, sqlMidy - magic3)];

    [path addCurveToPoint:point(sqlMidx + cos45, sqlMidy + sin45)
            controlPoint1:point(sqlMaxx, sqlMidy + magic3)
            controlPoint2:point(sqlMidx + magic1 , sqlMidy + magic2)];

    [path addCurveToPoint:point(sqlMidx, sqlMaxy)
            controlPoint1:point(sqlMidx + magic2 , sqlMidy + magic1)
            controlPoint2:point(sqlMidx + magic3, sqlMaxy)];

    [path addCurveToPoint:point(sqlMidx - cos45, sqlMidy + sin45)
            controlPoint1:point(sqlMidx - magic3 , sqlMaxy)
            controlPoint2:point(sqlMidx - magic2 , sqlMidy + magic1)];

    [path addCurveToPoint:point(sqlx, sqlMidy)
            controlPoint1:point(sqlMidx - magic1 , sqlMidy + magic2)
            controlPoint2:point(sqlx, sqlMidy + magic3)];

    return path;
}

结果: 浅灰色是pathBubbleLeft,红色描边是bezierPathWithOvalInRect(我认为这非常完美)。

enter image description here

观察:
此代码适用于iOS,其中坐标(0,0)为左上角,(320,548)为右下角。对于MacOS,(0,0)为左下角;
绘图从9点钟开始,顺时针进行;

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