哪些是使用 8 条三次贝塞尔曲线创建(近似)圆的控制点
Which are the control points to create (approximate) a circle using 8 cubic bezier curves
处所
我有一个圆圈,它可以动画化为由 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
的缩写
'sql'在sqlx
、sqly
、sqlMidx
、sqlMidY
、sqlMaxx
&sqlMaxy
代表'squareLeft', 圆的边界矩形。这些都是 CGFloats.
addRelativeCurveToPoint
用于定义相对于start/end点的控制点。 (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) 不同的符号组合
使用四段,使用三次贝塞尔曲线的圆近似值不能比 @fang 在评论中给你的 0.55228[...] 值更圆:它只是数学上唯一的值三次贝塞尔曲线最接近圆形。在无限精度表示中,它实际上是您从中获得的值:
4 angle 4 sqrt(2) - 1
k = - * tan(-------) = - * tan(pi/8) = 4 * -----------
3 2 3 3
并且是 0.5522847498307933984022516322796[...]。这为您提供了 4 段的最佳近似值,因此如果您需要使用 8 段,我们需要一个不同的值,这意味着我们需要使用为我们提供 k 的推导pi/2(四分之一圆)的角度,看看它为我们提供了 pi/4(八分之一圆)的结果。所以:我们将角度 pi/4
插入到此 Primer on Bezier Curve's section on approximating circles with cubic curves 中概述的函数中,我们得到:
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)
这些坐标用于覆盖四分之一圆的演示如下:http://jsbin.com/ridedahixu/edit?html,output
其余为(+,-),(-,+),(-,-)象限中明显的对称
这些是最佳可能的近似值,所以:如果bezierPathWithOvalInRect(...)
做其他事情,它不如正确我们几十年前就锻炼过了 =)
OP 的解决方案。
基于迈克的回答
- (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(恕我直言,这是完美的)。
观察:
此代码适用于 iOS,其中坐标 (0,0) 位于左上角,坐标 (320,548) 位于右下角。对于 MacOS (0,0) 位于左下方;
画图从9点开始顺时针走;
处所
我有一个圆圈,它可以动画化为由 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
'sql'在sqlx
、sqly
、sqlMidx
、sqlMidY
、sqlMaxx
&sqlMaxy
代表'squareLeft', 圆的边界矩形。这些都是 CGFloats.
addRelativeCurveToPoint
用于定义相对于start/end点的控制点。 (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) 不同的符号组合
使用四段,使用三次贝塞尔曲线的圆近似值不能比 @fang 在评论中给你的 0.55228[...] 值更圆:它只是数学上唯一的值三次贝塞尔曲线最接近圆形。在无限精度表示中,它实际上是您从中获得的值:
4 angle 4 sqrt(2) - 1
k = - * tan(-------) = - * tan(pi/8) = 4 * -----------
3 2 3 3
并且是 0.5522847498307933984022516322796[...]。这为您提供了 4 段的最佳近似值,因此如果您需要使用 8 段,我们需要一个不同的值,这意味着我们需要使用为我们提供 k 的推导pi/2(四分之一圆)的角度,看看它为我们提供了 pi/4(八分之一圆)的结果。所以:我们将角度 pi/4
插入到此 Primer on Bezier Curve's section on approximating circles with cubic curves 中概述的函数中,我们得到:
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)
这些坐标用于覆盖四分之一圆的演示如下:http://jsbin.com/ridedahixu/edit?html,output
其余为(+,-),(-,+),(-,-)象限中明显的对称
这些是最佳可能的近似值,所以:如果bezierPathWithOvalInRect(...)
做其他事情,它不如正确我们几十年前就锻炼过了 =)
OP 的解决方案。
基于迈克的回答
- (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(恕我直言,这是完美的)。
观察:
此代码适用于 iOS,其中坐标 (0,0) 位于左上角,坐标 (320,548) 位于右下角。对于 MacOS (0,0) 位于左下方;
画图从9点开始顺时针走;