可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm trying to find a way to draw a iOS 7-style icon 'squircle' shape programmatically, using core graphics. I'm not asking how to draw a rounded rectangle. A squircle is a superellipse:
which is slightly different than a regular rounded rectangle:
It's exact formula is readily available. However, I can't figure out how to draw this using, for example, a CGPath, let alone fill it, and be able to resize it rather easily. All this while being entirely exact with the formula.
回答1:
Quote from Wikipedia: Superellipse
For n = 1/2, in particular, each of the four arcs is a Quadratic Bézier curve defined by the two axes; as a result, each arc is a segment of a parabola.
So why not try to approximate Squircle using Bezier curves? Both curves (Bezier and Squircle) are defined by the parametric equations.
UIBezierPath Class have method: addCurveToPoint:controlPoint1:controlPoint2:
Appends a cubic Bézier curve to the receiver’s path.
NOTE: Use of the addQuadCurveToPoint:controlPoint:
method gives worse results - tested.
I used this method and that's what happened as a result:
red line
- rounded rectangle, blue line
- rectangle from fours Bezier curves
If this result is interested - drawing code below.
NOTE: To achieve a more exact match Bezier curve can be required to change the coordinates of the four corner points
(now they correspond to the angles of the rectangle in which is inscribed the figure).
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//set rect size for draw
float rectSize = 275.;
CGRect rectangle = CGRectMake(CGRectGetMidX(rect) - rectSize/2, CGRectGetMidY(rect) - rectSize/2, rectSize, rectSize);
//Rounded rectangle
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
UIBezierPath* roundedPath = [UIBezierPath bezierPathWithRoundedRect:rectangle cornerRadius:rectSize/4.7];
[roundedPath stroke];
//Rectangle from Fours Bezier Curves
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
UIBezierPath *bezierCurvePath = [UIBezierPath bezierPath];
//set coner points
CGPoint topLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMinY(rectangle));
CGPoint topRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMinY(rectangle));
CGPoint botLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMaxY(rectangle));
CGPoint botRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMaxY(rectangle));
//set start-end points
CGPoint midRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMidY(rectangle));
CGPoint botMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMaxY(rectangle));
CGPoint topMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMinY(rectangle));
CGPoint midLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMidY(rectangle));
//Four Bezier Curve
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topLPoint controlPoint2:topLPoint];
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botLPoint controlPoint2:botLPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topRPoint controlPoint2:topRPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botRPoint controlPoint2:botRPoint];
[bezierCurvePath stroke];
CGContextRestoreGState(context);
回答2:
in iOS 13/ Xcode 11 you can now use CALayerCornerCurve
Example
yourLayer.cornerCurver = CALayerCornerCurve.continuous
source: https://developer.apple.com/documentation/quartzcore/calayercornercurve
回答3:
A filled version of the accepted answer, also ported to Swift:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
let rect = self.bounds
let rectSize: CGFloat = rect.width
let rectangle = CGRect(x: rect.midX - rectSize / 2, y: rect.midY - rectSize / 2, width: rectSize, height: rectSize)
let topLPoint = CGPoint(x: rectangle.minX, y: rectangle.minY)
let topRPoint = CGPoint(x: rectangle.maxX, y: rectangle.minY)
let botLPoint = CGPoint(x: rectangle.minX, y: rectangle.maxY)
let botRPoint = CGPoint(x: rectangle.maxX, y: rectangle.maxY)
let midRPoint = CGPoint(x: rectangle.maxX, y: rectangle.midY)
let botMPoint = CGPoint(x: rectangle.midX, y: rectangle.maxY)
let topMPoint = CGPoint(x: rectangle.midX, y: rectangle.minY)
let midLPoint = CGPoint(x: rectangle.minX, y: rectangle.midY)
let bezierCurvePath = UIBezierPath()
bezierCurvePath.move(to: midLPoint)
bezierCurvePath.addCurve(to: topMPoint, controlPoint1: topLPoint, controlPoint2: topLPoint)
bezierCurvePath.addCurve(to: midRPoint, controlPoint1: topRPoint, controlPoint2: topRPoint)
bezierCurvePath.addCurve(to: botMPoint, controlPoint1: botRPoint, controlPoint2: botRPoint)
bezierCurvePath.addCurve(to: midLPoint, controlPoint1: botLPoint, controlPoint2: botLPoint)
context.setFillColor(UIColor.lightGray.cgColor)
bezierCurvePath.fill()
context.restoreGState()
}
perfect for use in a UIView subclass.
回答4:
Building on top of Ruslan's and Sunkas' answers above, I created a path that joins superelliptic "corners" with straight line segments; i.e. a superelliptic analog of the regular, rounded rectangle (like the mask seen around the edges of the iPhone X Simulator):
extension UIBezierPath {
static func superellipse(in rect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
// (Corner radius can't exceed half of the shorter side; correct if
// necessary:)
let minSide = min(rect.width, rect.height)
let radius = min(cornerRadius, minSide/2)
let topLeft = CGPoint(x: rect.minX, y: rect.minY)
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
// The two points of the segment along the top side (clockwise):
let p0 = CGPoint(x: rect.minX + radius, y: rect.minY)
let p1 = CGPoint(x: rect.maxX - radius, y: rect.minY)
// The two points of the segment along the right side (clockwise):
let p2 = CGPoint(x: rect.maxX, y: rect.minY + radius)
let p3 = CGPoint(x: rect.maxX, y: rect.maxY - radius)
// The two points of the segment along the bottom side (clockwise):
let p4 = CGPoint(x: rect.maxX - radius, y: rect.maxY)
let p5 = CGPoint(x: rect.minX + radius, y: rect.maxY)
// The two points of the segment along the left side (clockwise):
let p6 = CGPoint(x: rect.minX, y: rect.maxY - radius)
let p7 = CGPoint(x: rect.minX, y: rect.minY + radius)
let path = UIBezierPath()
path.move(to: p0)
path.addLine(to: p1)
path.addCurve(to: p2, controlPoint1: topRight, controlPoint2: topRight)
path.addLine(to: p3)
path.addCurve(to: p4, controlPoint1: bottomRight, controlPoint2: bottomRight)
path.addLine(to: p5)
path.addCurve(to: p6, controlPoint1: bottomLeft, controlPoint2: bottomLeft)
path.addLine(to: p7)
path.addCurve(to: p0, controlPoint1: topLeft, controlPoint2: topLeft)
return path
}
}
The points p0
through p7
in the code can be visualized in the following diagram:
If you pass a rectangle that is actually a square, and the corner radius is equal to or greater than half the side length, the straight line segments collapse (p0
"merges" with p1
, p2
with p3
, etc.) and you get the standard superellipse.
回答5:
This isn't a great answer since it doesn't get to the heart of what you're asking which is how to draw a superellipse** programmatically, but you can:
- Download the SVG for the iOS7 icon shape here: http://dribbble.com/shots/1127699-iOS-7-icon-shape-PSD
- Import it to your Xcode project
- Add PocketSVG to your project: https://github.com/arielelkin/PocketSVG
Load the SVG, and convert it to a UIBezierPath, from there you can scale and transform however you like:
PocketSVG *myVectorDrawing = [[PocketSVG alloc] initFromSVGFileNamed:@"iOS_7_icon_shape"];
UIBezierPath *myBezierPath = myVectorDrawing.bezier;
// Apply your transforms here:
[myBezierPath applyTransform:CGAffineTransformMakeScale(2.5, 2.5)];
[myBezierPath applyTransform:CGAffineTransformMakeTranslation(10, 50)];
CAShapeLayer *myShapeLayer = [CAShapeLayer layer];
myShapeLayer.path = myBezierPath.CGPath;
myShapeLayer.strokeColor = [[UIColor redColor] CGColor];
myShapeLayer.lineWidth = 2;
myShapeLayer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:myShapeLayer];
** It's maybe worth noting that the shape might not be an exact superellipse anyway: http://i.imgur.com/l0ljVRo.png
回答6:
This would be pretty easy to do in OpenGL ES with a shader. Just draw a quad and pass in the x and y as vertex attributes. In the fragment shader, plug x and y into the equation. If the result <= 1, then the fragment is inside the shape. If I can get some free time I might try this and post it here.
If you want to use CGPath, I think the key is to parameterize x and y in terms of t, which goes from 0 to 2π. Then evaluate x and y at regular intervals. I'll try to figure this out in my free time, too, but my math is kind of rusty.
BTW, I'm certain that Apple is not using this formula. See the link that @millimoose posted: http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon