Cocoa 哪些是使用8条三次贝塞尔曲线创建(近似)圆的控制点

Cocoa 哪些是使用8条三次贝塞尔曲线创建(近似)圆的控制点,cocoa,math,geometry,uibezierpath,bezier,Cocoa,Math,Geometry,Uibezierpath,Bezier,场所 我有一个圆,它的动画变成了一个由8条贝塞尔曲线组成的形状。为了使过渡平滑,我需要圆也由8个三次贝塞尔曲线组成。 以下是我目前掌握的情况: 代码 - (UIBezierPath*)pathBubbleLeft { UIBezierPath *path = [UIBezierPath new]; [path moveToPoint:p(sqlx, sqlMidy)]; CGFloat r = sqlW/2; CGFloat sin45 = 0.7071 * r

场所

我有一个圆,它的动画变成了一个由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;
}
路径从左侧开始,顺时针方向(从pi到pi/2、0、3pi/4、pi)

point
vector
是CGPointMake和CGVectorMake的缩写

sqlx
sqly
sqlMidx
sqlMidY
sqlMaxx
&
sqlMaxy
中的“sql”代表“squareLeft”,即圆的边界矩形。这些都是CGfloat

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个零长度插入,底部有2个(
[path addLineToPoint:path.currentPoint];


从左到中的过渡正常,但从中到右的过渡异常

当圆由四条贝塞尔曲线近似时,第一个控制点(在(1,0)起点之后)具有坐标(1,0.552)。对于8曲线情况,由于Bezies细分规则,它将为(1,0.276)


因此,您的控制向量是(0,0.276),(0.276,0),(0.195,0.195)使用不同的符号组合使用四段,使用三次贝塞尔曲线的圆近似不能比使用0.55228[…]的圆更圆@fang在评论中给出的值:它只是一条三次贝塞尔曲线最接近圆的数学唯一值。在无限精度表示中,它实际上是您从中获得的值:

     4        angle       4                     sqrt(2) - 1
k =  - * tan(-------)  =  - * tan(pi/8)  =  4 * -----------
     3          2         3                          3
是0.5522847498307933984022516322796[…]。这将得到4个线段的最佳近似值,因此如果需要使用8个线段,我们需要一个不同的值,这意味着我们需要使用为π/2(四分之一圆)的角度提供k的导数,看看它为π/4(八分之一圆)提供了什么。因此:我们将角度
pi/4
插入本节中概述的函数中,得到:

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...)
这将是段1,然后其余的段通过对称简单地导出,段2为:

s  = (0.7071...,   0.7071...)
c1 = (0.51957...,  0.894643...)
c2 = (0.265216..., 1)
e  = (0,           1)
此处展示了叠加在四分之一圆上的这些坐标:

其余的是(+、-)、(、+)和(-,-)象限中明显的对称性

这些是最好的近似值,因此:如果
bezierPathWithOvalInRect(…)
做了其他事情,那么它的正确性就不如我们几十年前算出的值=)

OP的解决方案

根据迈克的回答

结果: 浅灰色是pathBubbleLeft,红色笔划是bezierPathWithOvalInRect(我认为这是完美的)

意见:

此代码适用于iOS,其坐标(0,0)为左上角,(320548)为右下角。对于MacOS(0,0)为左下角


图纸从9点开始,顺时针方向绘制

如果需要更精确的值,则为0.552284749。使用0.552284749进行了测试。圆更圆,但仍然不能完全匹配bezierPathWithOvalInRect(红色笔划)。在接受答案之前,我会把它再放几天,也许还有其他方法。对于我所需要的(动画被缩放为~40x40px的边界矩形),在当前形式下已经足够了,但我仍然好奇:)。Thanx@MBo&fang快速回复@fang I在上面发布了一个测试结果,但看起来每个评论只能标记一个用户:|根据接受的答案,该值将为0.26521648983954。。。而不是此处所述的0.276。我用Illustrator做了一个测试,0.265的值给出了非常精确的结果。@Marco Luglio我的值是粗略划分的Pi/2弧的一半,Mike值是真实的Pi/4弧,所以他的值应该更精确。这是一个简洁的答案。我正在用最后的代码更新这个问题,为懒惰的人。干杯根据,这不是最好的近似;它可以提高约28%,这是一个。。。不满意的页面。为什么我们没有得到真实值,而只是一个近似值?如果这是您的页面,您可以为该值添加真正的符号函数吗?我已将您的解决方案移至社区wiki答案。
- (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;
}