The premises
I have a circle that animates into a shape made out of 8 bezier curves. For the transition to be smooth, I need the circle to also be made out of 8 cubic bezier curves. Here's what I have so far:
The code
- (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;
}
the path starts from left and goes clockwise (from pi to pi/2, 0, 3pi/4, pi)
point
and vector
are shorts for CGPointMake and CGVectorMake
'sql' in sqlx
, sqly
, sqlMidx
, sqlMidY
, sqlMaxx
& sqlMaxy
stands for 'squareLeft', the bounding rect of the circle. These are all CGFloats.
addRelativeCurveToPoint
is used to define the control points relatively to the start/end points. (0,0) is start, (1,1) is end. Easier to read.
- (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)];
}
The red circle is a bit wavy. That's what I'd like to fix.
Below, the left circle uses the above code, the right circle is made out of 4 curves with 2 zero length insertions at the top and 2 at the bottom ([path addLineToPoint:path.currentPoint];
).
The transition is ok from the left one to mid-peanut but weird from mid to the right
Using four segments, a circle approximation using cubic Bezier curves cannot get rounder than what it is with the 0.55228[...] value that @fang gave you in a comment: it is simply the mathematically only value at which a cubic Bezier curve best approximates a circle. In infinite precision representation, it's actually the value that you get from:
4 angle 4 sqrt(2) - 1
k = - * tan(-------) = - * tan(pi/8) = 4 * -----------
3 2 3 3
and is 0.5522847498307933984022516322796[...]. This gets you the best possible approximation with 4 segments, so if you need to use 8 segments, we need a different value, which means we need to use the derivation that gives us k for an angle of pi/2 (a quarter circle), and see what it gives us for pi/4 (an eighth circle). So: we plug the angle pi/4
into the functions outlined in this Primer on Bezier Curve's section on approximating circles with cubic curves, and we get:
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)
}
which gives us these (entirely useful) approximated coordinates:
s = (1, 0)
c1 = (1, 0.265216...)
c2 = (0.894643..., 0.51957...)
e = (0.7071..., 0.7071...)
That'll be segment 1, and then the rest of the segments are simply derived through symmetry, with segment 2 being:
s = (0.7071..., 0.7071...)
c1 = (0.51957..., 0.894643...)
c2 = (0.265216..., 1)
e = (0, 1)
A demonstration of these coordinates used overlayed a quarter circle is here: http://jsbin.com/ridedahixu/edit?html,output
And the rest are the obvious symmetries in the (+,-), (-,+), and (-,-) quadrants.
These are the best possible approximations, so: if bezierPathWithOvalInRect(...)
does something else, it's being less correct than values we worked out decades ago =)