Inverse CGPath in Rect - ios

I'm looking to inverse a CGPath (relative to a Rect). By this, I mean I have defined a CGPath and I want to get a CGPath that outlines all other areas in the rect.
I've been experimenting different ways and been googling for a couple days now with no luck. I simply just cannot work it out!
Consider the following rect with defined CGPath (where green is the defined CGPath filled):
Now, with what I want (the inversed CGPath) would produce the following:
Specifically, this is for usage with an SKCropNode and SKPhysicsBody in the SpriteKit framework, however i'm producing the polygon for the mask node using CGPaths (and storing the path in an ivar).
Is inversing a CGPath possible, or some other way? If so, how?
Thanks in advance.

I copied the code from the answer #DavidRonnqvist linked to, but changed it to retrieve the UIBezierPath from a CGPath, which is what you need:
CGPath filledPath = ...; //This is the inner filled shape
CAShapeLayer *invertedLayer = [CAShapeLayer layer];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithCGPath:filledPath];
[invertedLayer setPath:[maskPath CGPath]];
[invertedLayer setFillRule:kCAFillRuleEvenOdd];
[invertedLayer setFillColor:[[UIColor colorOfYourChoice] CGColor]];
I didn't test this but in theory it should work.

Code sample, using UIBezierPath.reversing():
let cgPath: CGPath = ...
let size = cgPath.boundingBox.size
let inverted = UIBezierPath(rect: CGRect(origin: .zero, size: size))
inverted.append(UIBezierPath(cgPath: cgPath).reversing())
//draw result
let image = UIGraphicsImageRenderer(size: size).image { context in
context.cgContext.setFillColor(UIColor.red.cgColor)
context.cgContext.addPath(inverted.cgPath)
context.cgContext.fillPath()
}

Related

"Behind the scenes" difference between cornerRadius and bezierPathWithRoundedRect

Premise: I am rounding the edges of a view with a solid background (to make something like an oval). There are many ways to do this, but the two most common I've see recommended are:
1) set the cornerRadius property on the view layer and bind the mask layer to the view layer bounds (through clipsToBounds or masksToBounds) (https://developer.apple.com/reference/quartzcore/calayer/1410818-cornerradius)
ex:
view.layer.cornerRadius = 2
view.layer.maskToBounds = true
2) create a Bezier Path with [bezierPathWithRoundedRect:byRoundingCorners:cornerRadii] and use it to create a mask layer which you set to the view's layer's mask. (https://developer.apple.com/reference/uikit/uibezierpath/1624368-bezierpathwithroundedrect)
ex:
UIBezierPath* bezierPath = [UIBezierPath bezierPathWithRoundedRect: view.bounds
byRoundingCorners:UIRectCornerAllCorners
cornerRadii: CGSizeMake(2, 2)];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.frame = view.bounds;
maskLayer.path = bezierPath.CGPath;
view.layer.mask = maskLayer;
Question: Both ways work well for me and I see no significant performance of one over the other (admittedly, I'm not using it heavily). In which situations should I use a Bezier path over setting the corner radius (assuming the corner radii are equal)? Do these two methods to creating rounded edges do the same thing behind the scenes? Are there certain platforms that won't support both solutions?
Best Regards

Change fill color of a part of a combined path with animation?

In simple words only part of my icon must change color at some event.
I drawing an icon with curves inside an UIView. The drawing boilds
down to combining several paths together like this (swift code):
let combinedPath = CGPathCreateMutableCopy(bezierPath.CGPath)
CGPathAddPath(combinedPath, nil, bezier2Path.CGPath);
CGPathAddPath(combinedPath, nil, bezier3Path.CGPath);
Then I'm adding the combined path to the UIView like this (obj-c, sorry for
the mix):
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = self.bounds;
shapeLayer.path = combinedPath;
self.layer.mask = shapeLayer;
And now at some point in time I want to change the fill color of
only one bezierPath with animation. How can I do this?
You can clean the current mask, and redraw any of the bezierpath with stroke/fill color and combine them together, and set it again. And for drawing animation, you can refer to How to Animate CoreGraphics Drawing of Shape Using CAKeyframeAnimation

How to create a CGPath which exactly match a CALayer bounds when the layer rotate or scale?

We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results

Why the circle path of the cashapelayer is not so round?

The doucumet said that the CAShapLayer is anti-aliased ,why got this result.
I also draw a circle with the CGContext in the drawRect method,It's very perfect.
UIBezierPath *path = [UIBezierPath bezierPath];
[path appendPath:[UIBezierPath bezierPathWithOvalInRect:CGRectMake(150, 300, 136, 136)]];
self.circleLayer = [CAShapeLayer layer];
self.circleLayer.path = path.CGPath;
self.circleLayer.frame = self.bounds;
self.circleLayer.fillColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6].CGColor;
[self.layer addSublayer:self.circleLayer];
According to the UIBezierPath docs, bezierPathWithOvalInRect: "creates a closed subpath that approximates the oval using a sequence of Bézier curves," so it may not draw a perfect circle.
If you use a UIBezierPath to draw a circle will not a perfect circle because its an approximation from Bézier curves.
You can draw a perfect circle from a square UIView. For example:
myView.layer.cornerRadius = myView.frame.size.width/2;
In this topic explain three methods to draw a circle -> How to draw a smooth circle with CAShapeLayer and UIBezierPath?
iOS 12:
I had the same issue on iPhone XS and XS Max. I don't know why, but on iPhone 8 there was no issue. The circle was perfectly round.
To solve the issue on XS and XS Max I set the flatness property of UIBezierPath to 0.0 and made sure that the center of the rect used for UIBezierPath is either an integer number or a floating number of e.g. 25.5. The center of my rect was y=51.166666 and this caused the issue that the oval was not round.

Why is making two corners round slower than make all corners round?

Edit: Language updated to improve readability.
I made an image view with 2 rounded corners like this:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.photoImageView.bounds byRoundingCorners:UIRectCornerBottomLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(10, 10)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
self.photoImageView.layer.mask = maskLayer;
But it is slower than making all the corners round using this code.
self.photoImageView.layer.cornerRadius = 10;
Would anyone know what why and how I can improve my '2 corner' code please?
Your code is adding another stage to the drawing. Normally, the background is drawn (with the given cornerRadius) directly to the target, but with a mask specified, it is drawn to a temporary surface, then copied to the target using the mask.
There isn't any built-in functionality for only rounding some background corners in the standard CALayer object.
I do wonder how slow "slower" really is; is this premature optimisation?

Resources