self.myPath=[UIBezierPath bezierPathWithArcCenter:center
radius:200
startAngle:0
endAngle:180
clockwise:YES];
(This much I was able to get up running with some web searching).
I have this path. Now I want to fill the reverse of this path, so leaving this portion and filling everything else. How can I finish the coding? I don't have much info on this.
The problem
The area it is showing after using Cemal Answer previously it only showed a circle with red stroke.
Edit
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
self.punchedOutPath =
[UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 50, 400, 400)];
self.fillColor = [UIColor redColor];
self.alpha = 0.8;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[[self fillColor] set];
UIRectFill(rect);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[[self punchedOutPath] fill];
CGContextSetBlendMode(ctx, kCGBlendModeNormal);
}
Use bezierPathByReversingPath. From the docs (iOS 6.0+ only):
Creates and returns a new bezier path object with the reversed contents of the current path.
so to reverse your path, you'd just:
UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:center
radius:200
startAngle:0
endAngle:180
clockwise:YES];
self.myPath = [aPath bezierPathByReversingPath];
Here's an alternative that doesn't require reversing the path at all.
You have a portion of a view you essentially want to "clip out":
Let's say you want the white area to be [UIColor whiteColor] with 75% alpha. Here's how you do it quickly:
You create a new UIView subclass.
This view has two properties:
#property (retain) UIColor *fillColor;
#property (retain) UIBezierPath *punchedOutPath;
You override its -drawRect: method to do this:
- (void)drawRect:(CGRect)rect {
[[self fillColor] set];
UIRectFill(rect);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[[self punchedOutPath] fill];
CGContextSetBlendMode(ctx, kCGBlendModeNormal);
}
There's a caveat here: The fillColor of the view must not include the alpha component. So in your case, you'd want that to just be [UIColor whiteColor]. You then apply the alpha bit yourself by calling [myView setAlpha:0.75].
What's going on here: This is using a blend mode called "Destination Out". Mathematically it's defined as R = D*(1 - Sa), but in layman's terms it means "Destination image wherever destination image is opaque but source image is transparent, and transparent elsewhere."
So it's going to use the destination (i.e., what's already in the context) wherever the new stuff is transparent (i.e. outside of the bezier path), and then where the bezier path would be opaque, that stuff is going to become transparent. However, the destination stuff must already be opaque. If it's not opaque, the blending doesn't do what you want. This is why you have to provide an opaque UIColor and then do any transparency you want with the view directly.
I ran this myself, with these circumstances:
the window has a [UIColor greenColor] background
the fillColor is white
the punchedOutPath is a oval that's inset 10 points from the edges of the view.
the view has an alpha of 0.75
With the code above, I get this:
The interior is pure green, and the outside has the semi-transparent overlay.
Update
If your covering is an image, then you'll need to create a new image. But the principle is the same:
UIImage* ImageByPunchingPathOutOfImage(UIImage *image, UIBezierPath *path) {
UIGraphicsBeginImageContextWithOptions([image size], YES, [image scale]);
[image drawAtPoint:CGPointZero];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[path fill];
UIImage *final = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return final;
}
You would then take the result of this function and put it into a UIImageView.
You can put this into a single screen app into the view controller: It will make a yellow background view and a blue layer on top of it that has an oval region cut out by a mask.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// create a yellow background
UIView *bg = [[UIView alloc] initWithFrame:self.view.bounds];
bg.backgroundColor = [UIColor yellowColor];
[self.view addSubview:bg];
// create the mask that will be applied to the layer on top of the
// yellow background
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.frame = self.view.frame;
// create the paths that define the mask
UIBezierPath *maskLayerPath = [UIBezierPath bezierPath];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(self.view.bounds, 20, 20)]];
// here you can play around with paths :)
// [maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{80, 80}, {140, 190}}]];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{100, 100}, {100, 150}}]];
maskLayer.path = maskLayerPath.CGPath;
// create the layer on top of the yellow background
CALayer *imageLayer = [CALayer layer];
imageLayer.frame = self.view.layer.bounds;
imageLayer.backgroundColor = [[UIColor blueColor] CGColor];
// apply the mask to the layer
imageLayer.mask = maskLayer;
[self.view.layer addSublayer:imageLayer];
}
this might answer this question as well: UIBezierPath Subtract Path
I have two solution for you.
Draw this path on a CALayer. And use that CALayer as a mask layer for you actual CALayer.
Draw a rectangle with the sizes of you frame before adding arc.
UIBezierPath *path = [UIBezierPath bezierPathWithRect:view.frame];
[path addArcWithCenter:center
radius:200
startAngle:0
endAngle:2*M_PI
clockwise:YES];
I would use second solution. :)
Related
In my app , I made a see through UIView by subclassing simple UIView's. However, If I try to do the same using UIVisualEffectView, I am not able to do it.
Here is what I am able to do using normal UIView:
When I use the UIVisualEffectView in place of green UIView,I cannot see the see through UIView , even though see through UIView is added to the UIVisualEffectView as subview.
Code:
- (void)drawRect:(CGRect)rect { //this is same for the UIVIew and for the UIVisualEffectView
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
// Clear any existing drawing on this view
// Remove this if the hole never changes on redraws of the UIView
CGContextClearRect(context, self.bounds);
// Create a path around the entire view
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:self.bounds];
// Your transparent window. This is for reference, but set this either as a property of the class or some other way
CGRect transparentFrame;
// Add the transparent window
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:transparentFrame cornerRadius:5.0f];
[clipPath appendPath:path];
// NOTE: If you want to add more holes, simply create another UIBezierPath and call [clipPath appendPath:anotherPath];
// This sets the algorithm used to determine what gets filled and what doesn't
clipPath.usesEvenOddFillRule = YES;
// Add the clipping to the graphics context
[clipPath addClip];
// set your color
UIColor *tintColor = [UIColor greenColor];
// (optional) set transparency alpha
CGContextSetAlpha(context, 0.7f);
// tell the color to be a fill color
[tintColor setFill];
// fill the path
[clipPath fill];
}
Question: Why this didn't work with UIVisualEffectView ?
Add following global variables in your ViewController.h file-
CAShapeLayer *fillLayer;
UIVisualEffectView *overlayView;
Add following methods in your ViewController.m file-
-(void)addOverlay:(CGRect)rect{
float x = rect.origin.x;
float y = rect.origin.y;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) cornerRadius:0];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(x, y, rect.size.width, rect.size.height) cornerRadius:5];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
[self removeOverlay];
overlayView = [[UIVisualEffectView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height+64)];
overlayView.backgroundColor = [UIColor clearColor];
[self.view addSubview:overlayView];
fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor colorWithRed:78/255.0 green:103/255.0 blue:135/255.0 alpha:1.0].CGColor;
fillLayer.opacity = 0.85;
[[UIApplication sharedApplication].keyWindow.layer addSublayer:fillLayer];
}
-(void)removeOverlay{
[overlayView removeFromSuperview];
[fillLayer removeFromSuperlayer];
}
and call it as -
[self addOverlay:rect];
I have a UIView I'm trying to put together which will be layered above another view. This new view needs to have a fully transparent hole in it. I've attached a screenshot of what it is I'm trying to accomplish (checkerboard pattern is the underlying UIView that will be adding this new UIView as a sublayer, red is a UIImage).
I have the following code that will render the black background with the hole in the center:
- (void)
drawRect:(CGRect)rect
{
CGRect boxRect = CGRectMake(_location.x - (kPointRadius/2), _location.y - (kPointRadius/2), kPointRadius, kPointRadius);
UIBezierPath *path = [UIBezierPath
bezierPathWithRoundedRect:CGRectMake(0, 0, self.layer.frame.size.width, self.layer.frame.size.height)
cornerRadius:0];
UIBezierPath *circlePath = [UIBezierPath
bezierPathWithRoundedRect:boxRect
cornerRadius:kPointRadius];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.borderWidth = 5.0;
fillLayer.borderColor = [UIColor redColor].CGColor;
fillLayer.opacity = 0.7;
[self.layer addSublayer:fillLayer];
}
However, when I add an image to that and use [[UIImage imageNamed:#"testimg" drawAtPoint:CGPointMake(x, y)]; to add the image, the image covers the hole.
Can anyone point me in the right direction here? I'm stumped.
EDIT: I'm now able to get it almost there. I can get everything I need EXCEPT the image layered on top of the black, 90% opaque background is also at 90% opacity.
CGRect boxRect = CGRectMake(
_location.x - (kPointRadius/2),
_location.y - (kPointRadius/2),
kPointRadius,
kPointRadius);
UIBezierPath *path = [UIBezierPath
bezierPathWithRoundedRect:CGRectMake(0, 0, self.layer.frame.size.width, self.layer.frame.size.height) cornerRadius:0];
UIBezierPath *circlePath = [UIBezierPath
bezierPathWithRoundedRect:boxRect
cornerRadius:kPointRadius];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
UIImage *image = [UIImage imageNamed:#"testimg"];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(13, 57, 350, 230)];
imageView.image = image;
CGRect r = CGRectMake(self.layer.frame.origin.x, self.layer.frame.origin.y, self.layer.frame.size.width, self.layer.frame.size.height);
UIGraphicsBeginImageContextWithOptions(r.size, NO, 0);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, CGPathCreateCopy(path.CGPath));
CGContextEOClip(c);
CGContextFillRect(c, r);
UIImage* maskim = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer* mask = [CALayer layer];
mask.frame = r;
mask.contents = (id)maskim.CGImage;
imageView.layer.mask = mask;
self.layer.mask = mask;
self.layer.backgroundColor = [UIColor blackColor].CGColor;
self.layer.opacity = 0.8;
[self.layer addSublayer:imageView.layer];
EDIT: I'm now able to get it almost there. I can get everything I need EXCEPT the image layered on top of the black, 90% opaque background is also at 90% opacity.
Instead of setting its opacity, you could set the layer's background color to [UIColor colorWithWhite:0.0 alpha:0.9].
I am trying to create a custom view to draw a music CD with the album artwork on the CD.
Currently I use core graphics to draw two circular disks inside the drawRect method. The inner disk's colour is set to the background colour of the view to make it look like the inner circle is a hollow circle.
My problem is that I can't draw the UIImage inside the the circular bezier path to get the result that I need.
Here's my drawRect code:
// Draw a CD like view using three drawing operations:
// 1st draw the main disk
// 2nd draw the image if it is set
// 3rd draw the inner disk over the image so that it looks like it is a hollow disk with the image painted on the disk.
// Note:) This view expects its aspect ratio to be set as 1:1
- (void)drawRect:(CGRect)rect
{
// Draw the main Circular CD disk
UIBezierPath* outerRing = [UIBezierPath bezierPathWithOvalInRect:self.bounds];
[self.mainColor setFill];
[outerRing fill];
[self.outerRingColor setStroke];
[outerRing stroke];
// Draw the album image if it is set
if(self.artwork){
[self.artwork drawInRect:self.bounds blendMode:kCGBlendModeNormal alpha:1.0];
}
// now draw another smaller disk inside
// this will be a percentage smaller than the whole disk's bounds and will be centered inside it
CGFloat sidePaddingCoord = ((1.0f - INNER_DISK_SIZE_PERCENTAGE) / 2) * self.bounds.size.height;
CGFloat side = INNER_DISK_SIZE_PERCENTAGE * self.bounds.size.width;
UIBezierPath* innerRing = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(sidePaddingCoord, sidePaddingCoord, side, side)];
[self.innerRingColor setFill];
[innerRing fill];
[innerRing stroke];
}
This fills the image as a square. I want the image to be clipped inside the outerRing bezier path so that it looks like a music CD.
I'm sure there is a way to use core graphics to achieve something else besides using the image's drawInRect method. Is there any way to 'clip' the image inside the circular bezier path or only draw inside the bezier path?
I've read really great posts by rob mayoff, UIBezierPath Subtract Path, iOS UIImage clip to paths, many thanks.
Here is an adoption of his code. Keep your creation code of outerRing and innerRing, add them to an array named paths.
[self.paths addObject:outerRing];
[self.paths addObject:innerRing];
Then use this help method.
- (UIImage *)maskedImage
{
CGRect rect = CGRectZero;
rect.size = self.originalImage.size;
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0);
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:self.paths[1]];
clipPath.usesEvenOddFillRule = YES;
CGContextSaveGState(UIGraphicsGetCurrentContext()); {
[clipPath addClip];
[[UIColor orangeColor] setFill];
[self.paths[0] fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIImage *mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0); {
CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, mask.CGImage);
[self.originalImage drawAtPoint:CGPointZero];
}
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return maskedImage;
}
In drawRect,
UIImage *maskedImage = [self maskedImage];
[maskedImage drawInRect:self.bounds blendMode:kCGBlendModeNormal alpha:1.0];
Try below code:
UIImageView *userImageView= [[UIImageView alloc] initWithFrame:CGRectMake(85, 55.0, 90, 90)];
userImageView.layer.cornerRadius = 90/2;
userImageView.clipsToBounds = YES;
userImageView.layer.borderWidth = 1.0f;
userImageView.layer.borderColor = [UIColor blueColor];
[self.view addSubview:userImageView]; //or add it to the view you want
UIImage *userImage = [UIImage imageNamed:#"<image you want to set>"];
userImageView= [[UIImageView alloc] initWithFrame:CGRectMake(90, 60.0, 80, 80)];
userImageView.image = userImage;
userImageView.layer.cornerRadius = 80/2;
userImageView.clipsToBounds = YES;
userImageView.layer.borderWidth = 1.0f;
userImageView.layer.borderColor = [UIColor blueColor;
[self.view addSubview:userImageView]; //or add it to the view you want
I have a UIView that draws different shapes. I can make a hole in my image to have it transparent, working fine, but the hole is only square,
//hole
CGRect holeRectValue = CGRectMake(300, 40, 80, 100);
CGRect holeRectIntersection = CGRectIntersection( holeRectValue, rect );
[[UIColor clearColor] setFill];
UIRectFill(holeRectIntersection);
Now I need to make the hole in the image but not as a rect, as the shape of my drawn figure,
Here is the code:
- (id)initWithFrame:(CGRect)frame forNode0:(CGPoint)node0 forNode1:(CGPoint)node1 fornode2:(CGPoint)node2 fornode3:(CGPoint)node3 fornode4:(CGPoint)node4 fornode5:(CGPoint)node5 fornode6:(CGPoint)node6 fornode7:(CGPoint)node7 fornode8:(CGPoint)node8 fornode9:(CGPoint)node9
{
self = [super initWithFrame:frame];
if (self) {
self.opaque = NO;
self.node0Pos = node0;
self.node1Pos = node1;
self.node2Pos = node2;
self.node3Pos = node3;
self.node4Pos = node4;
self.node5Pos = node5;
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
//bgnd
UIGraphicsBeginImageContext(self.frame.size);
[[UIImage imageNamed:#"cat.jpeg"] drawInRect:self.bounds];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[[UIColor colorWithPatternImage:image] setFill];
// Drawing code
UIRectFill(rect);
// Drawing code
//1. begin new path
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath(context);
//2. move to initial starting point
CGContextMoveToPoint(context, self.node0Pos.x, self.node0Pos.y);
//3. add lines defining shape
CGContextAddLineToPoint(context, self.node1Pos.x, self.node1Pos.y);
CGContextAddLineToPoint(context, self.node2Pos.x, self.node2Pos.y);
CGContextAddLineToPoint(context, self.node3Pos.x, self.node3Pos.y);
CGContextAddLineToPoint(context, self.node4Pos.x, self.node4Pos.y);
CGContextAddLineToPoint(context, self.node5Pos.x, self.node5Pos.y);
//4. optionally close path
CGContextClosePath(context);
CGColorRef color;
//5. draw path
color = [UIColor colorWithRed:1 green:0 blue:228/255.0f alpha:0.5].CGColor;
//CGContextSetFillColor(context, color);
CGContextSetFillColorWithColor(context, color);
CGContextDrawPath(context, kCGPathFill);
//
//hole
CGRect holeRectValue = CGRectMake(300, 40, 80, 100);
CGRect holeRectIntersection = CGRectIntersection( holeRectValue, rect );
[[UIColor clearColor] setFill];
UIRectFill(holeRectIntersection);
}
So how do I make the "hole" with my actual context path?
P.S. I was doing some masking, but it leaves a white line around the hole, so I need to avoid this.
[[UIColor clearColor] setFill];
UIRectFill(holeRectIntersection);
This doesn’t do anything—drawing with a clear color is effectively a no-op. What you most likely want to do is add the rectangle you’re trying to cut out as part of the path you’re creating, i.e. inserting a call to CGContextAddRect before your CGContextClosePath. See Filling a Path in the Quartz 2D Programming Guide.
I believe what you're looking for is the CALayer.mask property. To create a "hole", you would generate a CALayer object with an alpha channel in the shape of the hole you want to make, and then apply it to the view you want to punch the hole in.
In semi-pseudocode:
CALayer *holeMask;
UIView *myView;
//
// Build the holeMask object via whatever means,
// and set myView to the target view that you want
// to punch the hole in...
//
myView.layer.mask = holeMask
I am using the code below to draw an arc into the drawLayer method of a custom CALayer class but nothing is displayed:
(void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
float r = self.bounds.size.width/2;
CGContextClearRect(ctx, self.bounds); // clear layer
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
//CGContextFillRect(ctx, layer.bounds);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:r startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
CGContextAddPath(ctx, path.CGPath);
CGContextStrokePath(ctx);
}
Note that if I uncomment the CGContextFillRect(ctx, layer.bounds) line, a rectangle is properly rendered.
The problem I can see is that you don't set a stroke color (but a fill color)
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
When you configure a fill color it is used to fill the path. Same for a stroke color.
If you want to only want to either stroke or fill the path then you should use either CGContextStrokePath or CGContextFillPath but if you want to do both then you should use CGContextDrawPath with kCGPathFillStroke as the "drawing mode".
Thanks to both of you. I finally decided to draw the arc outside the drawLayer method using the following code:
- (id) initWithLayer:(id)layer withRadius:(float)radius
{
self = [super initWithLayer:layer];
// ...
self.strokeColor = [UIColor whiteColor].CGColor;
self.lineWidth = 10.0;
return self;
}
- (void) update:(float)progress
{
// ....
UIBezierPath *arcPath = [UIBezierPath bezierPath];
[arcPath addArcWithCenter:CGPointMake(r, r) radius:r - self.lineWidth/2 startAngle:startAngle endAngle:nextAngle clockwise:YES];
self.path = arcPath.CGPath;
}
It looks like all the arc drawing methods for both CGPath and UIBezierPath have a bug on 64 bit devices where they only work if the clockwise parameter is set to YES. I notice that your non-working code shows clockwise:NO and the working code has clockwise:YES.