Circular CAGradientLayer Mask - ios

I have a UIView that programmatically draws a "sunburst" pattern using UIBezierPath. Now I would like to extend this by masking the edges with a gradient -- effectively making each "burst" fade from opaque to transparent. I'm think this could be done with a CAGradientLayer mask but I'm not sure how to make it circular.
This is what I'm trying -- it masks the view but the gradient is linear:
CAGradientLayer *l = [CAGradientLayer layer];
l.frame = CGRectMake(0.0f, 0.0f, rect.size.width, rect.size.height);
l.cornerRadius = rect.size.width/2.0f;
l.masksToBounds = YES;
l.colors = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor, (id)[UIColor clearColor].CGColor, nil];
self.layer.mask = l;
I'm open to not using CAGradientLayer if anyone knows any other ways to mask a view with a circle with blurred edges.
SOLUTION
Thanks to matt's insight, I ended up drawing a mask view using CGContextDrawRadialGradient, rendering that as a UIImage, and using that as a mask layer. If anyone is interested in this process, it is being used in this test project.

An obvious approach is to draw this effect manually. Start very small and draw the sunburst larger and larger and (at the same time) less and less opaque.
On the other hand, it might be sufficient to make a radial gradient and use it as a mask (vignette effect). Core Graphics will draw the radial gradient for you:
https://developer.apple.com/library/ios/#documentation/graphicsimaging/reference/CGContext/Reference/reference.html#//apple_ref/c/func/CGContextDrawRadialGradient
I'm also very fond of CIFilters for adding touches like this: you should look through the catalogue and see what strikes your fancy:
https://developer.apple.com/library/ios/#documentation/graphicsimaging/reference/CoreImageFilterReference/Reference/reference.html
CIFilter actually gives you a sunburst transition that might suit your purposes, especially if combined with masking and compositing; here's a discussion of the sunburst (used in an animation), from my book:
http://www.apeth.com/iOSBook/ch17.html#_cifilter_transitions

CAGradientLayer draws linear gradients.
You could try to set a shadow and see if that suits your needs:
l.shadowColor = [UIColor blackColor];
l.shadowRadius = rect.size.width / 2;

You can do this by creating a CALayer instance and give it a delegate that implements drawLayer:inContext: and draws a radial gradient in the implementation of that method. Then use that CALayer as your mask.

Related

Draw gradient below a CAShapeLayer graph

I'm drawing a graph using a CGPath applied to a CAShapeLayer. The graph itself is drawn just fine, but I want to add a gradient underneath it afterwards. My problem is that the path is closed with a straight line going from the last point to the first point (see below) – this would make a gradient fill look totally ridiculous.
As far as I can see, the only way to circumvent this issue is to draw two additional lines: one from the last point of the graph to the bottom-right corner, and from there, another one to the bottom-left corner. This would close the path off nicely, but it would add a bottom line to the graph, which I don't want.
If I were using CGContext, I could easily solve this by changing the stroke color to transparent for the last two lines. However, with the code below, I don't see how that would be possible.
CGMutablePathRef graphPath = CGPathCreateMutable();
for (NSUInteger i = 0; i < self.coordinates.count; i++) {
CGPoint coordinate = [self.coordinates[i] CGPointValue];
if (!i) {
CGPathMoveToPoint(graphPath, NULL, coordinate.x, coordinate.y);
} else {
CGPathAddLineToPoint(graphPath, NULL, coordinate.x, coordinate.y);
}
}
CAShapeLayer *graphLayer = [CAShapeLayer new];
graphLayer.path = graphPath;
graphLayer.strokeColor = [UIColor whiteColor].CGColor;
graphLayer.fillColor = [UIColor redColor].CGColor;
[self.layer addSublayer:graphLayer];
I hope you guys can help me out!
Update: You suggest that I could create a CAGradientLayer, and then apply the graph layer as its mask. I don't see how that would work, though, when the graph/path looks the way it does. I have replaced the image above with another graph that hopefully illustrates the problem better (note that I've given the CAShapeLayer a red fill). As I see it, if I were to apply above layer as the mask of a CAGradientLayer, some of the gradient would lie above the graph, some it below. What I want is for all of the gradient to be placed right beneath the graph.
Maybe I'm not understanding the problem correctly, but if you're looking to add a consistent gradient, couldn't you create a gradient layer and then make your graphLayer be that layer's mask?
Figure out whatever the min max bounds of your coordinates, create a CAGradientLayer that size, configure it however you might like and then, apply your graphLayer as it's mask. Then add the new CAGradientLayer to your self.layer.
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
// ... Configure the gradientLayer colors / locations / size / etc...
gradientLayer.mask = graphLayer;
[self.layer addSubLayer:gradientLayer];
This doesn't take into account the stroke, but it shouldn't be difficult to apply that as well if that's important.
I solved the problem by creating two separate paths: One for the graph (as shown in my original post), and one that starts in the lower-right corner, moves in a straight line to the lower-left corner, and from there follows the same path as the graph. By doing so, the path gets closed off nicely, since the graph ends at the same x-coordinate as where the path started.
From there, I applied the second path to a CAShapeLayer, and then used this layer as the mask of a gradient layer.

Can I add a custom line cap to a UIBezierPath?

I'm drawing an arc by creating a CAShapeLayer and giving it a Bezier path like so:
self.arcLayer = [CAShapeLayer layer];
UIBezierPath *remainingLayerPath = [UIBezierPath bezierPathWithArcCenter:self.center
radius:100
startAngle:DEGREES_TO_RADIANS(135)
endAngle:DEGREES_TO_RADIANS(45)
clockwise:YES];
self.arcLayer.path = remainingLayerPath.CGPath;
self.arcLayer.position = CGPointMake(0,0);
self.arcLayer.fillColor = [UIColor clearColor].CGColor;
self.arcLayer.strokeColor = [UIColor blueColor].CGColor;
self.arcLayer.lineWidth = 15;
This all works well, and I can easily animate the arc from one side to the other. As it stands, this gives a very squared edge to the ends of my lines. Can I round the edges of these line caps with a custom radius, like 3 (one third the line width)? I have played with the lineCap property, but the only real options seem to be completely squared or rounded with a larger corner radius than I want. I also tried the cornerRadius property on the layer, but it didn't seem to have any effect (I assume because the line caps are not treated as actual layer corners).
I can only think of two real options and I'm not excited about either of them. I can come up with a completely custom Bezier path tracing the outside of the arc, complete with my custom rounded edges. I'm concerned however about being able to animate the arc in the same fashion (right now I'm just animating the stroke from 0 to 1). The other option is to leave the end caps square and mask the corners, but my understanding is that masking is relatively expensive, and I'm planning on doing some fairly intensive animations with this view.
Any suggestions would be helpful. Thanks in advance.
I ended up solving this by creating two completely separate layers, one for the left end cap and one for the right end cap. Here's the right end cap example:
self.rightEndCapLayer = [CAShapeLayer layer];
CGRect rightCapRect = CGRectMake(remainingLayerPath.currentPoint.x, remainingLayerPath.currentPoint.y, 0, 0);
rightCapRect = CGRectInset(rightCapRect, self.arcWidth / -2, -1 * endCapRadius);
self.rightEndCapLayer.frame = rightCapRect;
self.rightEndCapLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.rightEndCapLayer.bounds
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(endCapRadius, endCapRadius)].CGPath;
self.rightEndCapLayer.fillColor = self.remainingColor.CGColor;
// Rotate the end cap
self.rightEndCapLayer.anchorPoint = CGPointMake(.5, 0);
self.rightEndCapLayer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(45), 0.0, 0.0, 1.0);
[self.layer addSublayer:self.rightEndCapLayer];
Using the bezier path's current point saves from doing a lot of math to calculate where the end point should appear. Moving the anchoring point also allows the layers to not overlap, which is important if your arc is at all transparent.
This still isn't entirely ideal, as animations have to be chained through multiple layers. It's better than the alternatives I could come up with though.

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?

iOS: drop shadow with sharp edges

I'm trying to add some shadows to one of my views and what I would like to achieve is drawing shadows only on one side and let them have sharp edges. No I've tried quite a few methods without any luck (using the shadow related properties of the view's CALayer + UIBezierPaths). However, iOS is always rendering a shadow with soft edges like this:
But what I really want to acchieve is something like this (without round corners and sharp edges on the sides except one):
Is there any elegant way to do this or will I have to draw the shadow myself using CoreGraphics?
PS: I forgot to mention, my view should actually be a custom UIButton, so overriding drawRect: would be a pain here
I've experienced a mask removing the shadow from view...so maybe you can try that.
CALayer *mask = [[[CALayer alloc] init] autorelease];
mask.backgroundColor = [UIColor blackColor].CGColor;
mask.frame = CGRectMake(0.0, 0.0, yellowView.bounds.size.width + shadowWidth, yellowView.bounds.size.height);
yellowView.layer.mask = mask;
I think what you want to be changing is the shadowRadius value - set that to zero and you should get the sharp edge you're looking for:
myView.layer.shadowRadius = 0;

Making a CALayer transparent

Consider the following...
Say I have two CALayer's, one on top of the other. Each layer is the size of the entire iPad screen, the top layer obscures the bottom layer.
Is there some way to mark a portion of the top layer as being "transparent", so that the same section of the bottom layer shows through the transparent portion? In other words, is there a way to "cut out" a portion of the top layer to reveal the bottom layer underneath?
The CALayer mask property. You'll need to subclass CALayer to drawToContext: opaque black over the entire bounds, then do a CGContextClear(ctx, <your see-through box>);
Then create an instance of the layer, give it the same frame as your top-layer bounds, and set it to the mask property.
Yes, you can do it by making different alpha values for each layer,
basically the inner layer(super) one should have at least an alpha value of 0.7 and the outer layer(subLayer) should have less alpha value than its parent, lets say 0.3
Then the outer layer should reveal the inner layer.
But if you wanna do some better revelation, you could draw the outer layer, by setting radial gradient on it.
This is my sample code, but it I haven't drawn the radial gradient for the outer layer.
//
CALayer *innnerLayer = [CALayer layer];
innnerLayer.borderColor = [UIColor greenColor].CGColor;
innnerLayer.borderWidth = 0.8f;
innnerLayer.backgroundColor = [UIColor colorWithWhite:0. alpha:0.5].CGColor;
innnerLayer.frame = CGRectMake(70.0, 150.0f, 100.0f, 100.0f);
CALayer *outLayer = [CALayer layer];
outLayer.frame = innnerLayer.bounds;
outLayer.backgroundColor = [UIColor colorWithWhite:0.f alpha:0.3f].CGColor;
// add outer layer to inner layer
[innnerLayer addSublayer:outLayer];
// add the inner layer to main view
[self.view.layer addSublayer:innnerLayer];
// Experiment with different alpha values, but the outerAlpha
Are you using CALayer as a sublayer of your UIView? You have to set the backgroundColor of your UIView to clear like this:
self.backgroundColor = [UIColor clearColor];
Just setting backgroundColor property to NULL helped me.

Resources