I am adding a shadow to a CALayer but I would like this shadow to only be on the left and right side of the view. Does anyone know if this is possible?
Currently, I am adding the shadow like this:
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
view.layer.shadowOpacity = 1.0f;
view.layer.shadowRadius = 4.0f;
view.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
You set the shadow path to the bounds of the view. Try getting that rect, and expanding it using CGRectInset (with negative x value, and 0 y value). This will give you a shadow path that expands beyond the left and right sides of the layer. You may need to tinker with the radius or inset the y value slightly to prevent the shadow appearing above and below.
Related
I have a SKShapeNode, and a label under one generic SKNode. If I try to scale that node, its position changes for no reason!. If I try to scale only the nodes it contains under it, it scales properly but not from the center, so it grows out to the up and right.
What am I doing wrong?
First before scale
After scale
Notice its position moves up. Can't figure out why, does it even if I do it as an action or not. Also, the position is not changing based on log, but it appears the frame got bigger (though that is expected)
Steps to recreate issue:
set height and width
#h = 50
#w = 256
create rect with height and width
CGRectMake(0, 0, #w, #h)
get path for rect
UIBezierPath.bezierPathWithRect(rect).CGPath
create skshape node with path, set position
antialiased: false,
lineWidth: 1.0,
strokeColor: SKColor.blackColor,
fillColor: SKColor.whiteColor
Create sklabel, vertical center alignment, set position
create generic SKNode, no properties, add skshapenode and sklabel to it as children
Scale the generic SKNode.
I had a similar problem: I created an SKShapeNode.ellipseInRect which look perfectly fine when I didn't need to scale it. Yet as soon as I tried to scale it down to fit the text it was behind, it would randomly shrink towards the bottom left of the screen!
I did a little debugging, and the number that surprised me was that the position was set to (0, 0) even though the ellipse was in the top left corner of the screen! I then realized that by initializing with a rect the "anchor point" (even though SKShapeNodes don't technically have them) was at (0, 0) too. This behavior would explain my shrinking towards that point and your growing away from that point.
What solved this problem for me was instead of putting my position in the rect, set the position afterwards. Then it scaled around my node's center-point and not the screen origin. Hope that helps!
Try this
SKLabelNode *labNode = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
labNode.fontSize = 12.0f;
labNode.fontColor = [SKColor blackColor];
labNode.text = #"SMASH";
SKSpriteNode *topWithText = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:CGSizeMake(100, 30)];
topWithText.position = CGPointMake(100, 110);
[topWithText addChild:labNode];
SKSpriteNode *bottom = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(100, 10)];
bottom.position = CGPointMake(100, 100);
[self addChild:topWithText];
[self addChild:bottom];
[topWithText runAction:[SKAction sequence:#[
[SKAction waitForDuration:3.0],
[SKAction scaleBy:3.0 duration:3]
]]];
With the SKShapeNode, this is impossible.
I solved it by adding a move Action to center the node manually.
You need to apply this to the y and x coordinates
Like so:
I know that this will not work with NSShadow, drawing it in drawRect: will work just fine.
You can do this and many other kinds of shadows using Core Animations layers and the shadowPath property. The shadow that you are describing can be make with an elliptical shadow path.
The code to produce this shadow is below. You can tweak the size of the ellipse to have a rounder shape of the shadow. You can also tweak the position, opacity, color and blur radius using the shadow properties on the layer.
self.wantsLayer = YES;
NSView *viewWithRoundShadow = [[NSView alloc] initWithFrame:NSMakeRect(30, 30, 200, 100)];
[self addSubview:viewWithRoundShadow];
CALayer *backingLayer = viewWithRoundShadow.layer;
backingLayer.backgroundColor = [NSColor orangeColor].CGColor;
// Configure shadow
backingLayer.shadowColor = [NSColor blackColor].CGColor;
backingLayer.shadowOffset = CGSizeMake(0, -1.);
backingLayer.shadowRadius = 5.0;
backingLayer.shadowOpacity = 0.75;
CGRect shadowRect = backingLayer.bounds;
CGFloat shadowRectHeight = 25.;
shadowRect.size.height = shadowRectHeight;
// make narrow
shadowRect = CGRectInset(shadowRect, 5, 0);
backingLayer.shadowPath = CGPathCreateWithEllipseInRect(shadowRect, NULL);
Just to show some examples of other shadows than can be created using the same technique; a path like this
will produce a shadow like this
It's far from perfect but I think it does draw the sort of shadow you are looking for. Bear in mind that I have left a plain linear gradient in place from a total black to a clear color. Being so dark, this will not give you a super-realistic shadow unless you tweak the values a bit. You may want to play with the gradient by adding more locations with different alpha values to get whatever stepping you like. Some experimentation is probably required but the values are all there to play with.
As per your suggestion it's a drawRect:(CGRect)rect thing. Just create a custom view and only override it:
- (void)drawRect:(CGRect)rect {
// Get the context
CGContextRef context = UIGraphicsGetCurrentContext();
// Setup the gradient locations. We just want 0 and 1 as in the start and end of the gradient.
CGFloat locations[2] = { 0.0, 1.0 };
// Setup the two colors for the locations. A plain black and a plain black with alpha 0.0 ;-)
CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 1.0f, // Start color
0.0f, 0.0f, 0.0f, 0.0f }; // End color
// Build the gradient
CGGradientRef gradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(),
colors,
locations,
2);
// Load a transformation matrix that will squash the gradient in the current context
CGContextScaleCTM(context,1.0f,0.1f);
// Draw the gradient
CGContextDrawRadialGradient(context, // The context
gradient, // The gradient
CGPointMake(self.bounds.size.width/2,0.0f), // Starting point
0.0f, // Starting redius
CGPointMake(self.bounds.size.width/2,0.0f), // Ending point
self.bounds.size.width/2, // Ending radius
kCGGradientDrawsBeforeStartLocation); // Options
// Release it an pray that everything was well written
CGGradientRelease(gradient);
}
This is how it looks like on my screen...
I simply placed an image just over the shadow but you can easily merge the shadow with an image if you subclass UIImageView and override it's drawRect method.
As you can see, what I did was to simply setup a circular gradient but I loaded a scaling matrix to squash it before drawing it to the context.
If you plan to do anything else in that method, remember that you have the matrix in place and everything you do will be deformed by it. You may want to save the the CTM with CGContextSaveGState() before loading the matrix and then restore the original state with CGContextRestoreGState()
Hope this was what you where looking for.
Cheers.
I could explain how to do this in code, or explain how to use a tool which generate this code for you. I choose the latter.
Using PaintCode (free demo available, 1 hour limit per session).
Draw an oval
Draw a Rectangle which intersects with the bottom of the oval.
CMD click both the rectangle and the oval, in the "Objects" list in the top left corner.
Press the Intersect button in the Toolbar.
Select the Bezier from the Objects list.
Set its Stroke to "No Stroke"
Click the Gradient button (located on the left, below the Selection Inspector)
Press the "+" button
Change the gradient color to light grey.
From the Selection inspector, change the Fill Style to "Gradient"
Select Gradient: Linear
adjust the gradient till you are satisfied.
- (void)viewDidLoad
{
UIImage *natureImage = [UIImage imageNamed:#"nature.jpg"];
CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 200, 200);
layer.position = CGPointMake(380, 200);
layer.contents = (id)natureImage.CGImage;
layer.shadowOffset = CGSizeMake(0,2);
layer.shadowOpacity = 0.70;
layer.shadowPath = (layer.shadowPath) ? nil : [self bezierPathWithCurvedShadowForRect:layer.bounds].CGPath;
[self.view.layer addSublayer:layer];
}
- (UIBezierPath*)bezierPathWithCurvedShadowForRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint topLeft = rect.origin;
CGPoint bottomLeft = CGPointMake(0.0, CGRectGetHeight(rect) + offset);
CGPoint bottomMiddle = CGPointMake(CGRectGetWidth(rect)/2, CGRectGetHeight(rect) - curve);
CGPoint bottomRight = CGPointMake(CGRectGetWidth(rect), CGRectGetHeight(rect) + offset);
CGPoint topRight = CGPointMake(CGRectGetWidth(rect), 0.0);
[path moveToPoint:topLeft];
[path addLineToPoint:bottomLeft];
[path addQuadCurveToPoint:bottomRight controlPoint:bottomMiddle];
[path addLineToPoint:topRight];
[path addLineToPoint:topLeft];
[path closePath];
return path;
}
Hope this will help you.
OK, so I needed a rounded triangle. So what I did was use a technique similar to what I've used in other vector drawing programs. Draw the triangle and use the stroke to create the rounded corners. Worked like a charm too, until I needed to reduce the alpha of the color used to fill and stroke the UIBezierPath. For some reason I keep getting this inset outline that isn't the same color as the Fill and Stroke. Somehow the alpha value isn't being respected. Maybe I'm overlooking something silly here, but try as I might I can't get the triangle all one color with a alpha value lower than 1. This is what I get:
And heres the simple code:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(63.5, 10.5)];
[path addLineToPoint: CGPointMake(4.72, 119.5)];
[path addLineToPoint: CGPointMake(122.28, 119.5)];
[path addLineToPoint: CGPointMake(63.5, 10.5)];
[path closePath];
path.miterLimit = 7;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
path.lineWidth = 8;
UIColor *whiteAlph5 = [UIColor colorWithWhite:0.6 alpha:0.5];
[whiteAlph5 setFill];
[whiteAlph5 setStroke];
[path fill];
[path stroke];
}
I can't understand why the line would be anything other than the "whiteAlpha5" if that's the only color I've set for both fill and stroke. I suppose I can just draw the rounded triangle out adding the curves to to corners, but I'm just curious as to why this happens.Thanks in advance...
If you must have the stroke, alter your call to [UIBezierPath stroke] like so:
[path fill];
[path strokeWithBlendMode:kCGBlendModeCopy alpha:1.0];
This should achieve the effect you want (I think - haven't been able to test it)
This is a bit of a guess, but I think you're seeing here is essentially two layers of semitransparent white, one drawn on top of the other. When the triangle is just filled in, it would be what you're expecting. When you stroke, it's drawing the same colour - but it's adding it on top of the existing colour, not replacing it, which is the effect you might expect if you've done this before in paint programs or similar. Thus, where the stroke and fill overlap, you're getting a stronger white than you're after. Just using fill by itself could solve this, but might not get the rounded effect you're after.
If you need a visual demonstration of what I mean, you can do this in Photoshop. Create a new image with a black background and create a new layer above it, set to 50% opacity. Draw a white square on it (which will look grey due to the opacity). Then, without changing layers, draw a line through it. You won't see the line, because it's replacing the existing colour - this is what you expected to happen with your code. Then, add another layer above it, also set to 50% opacity. Draw a line on this layer, through the square. You'll see the line as a brighter grey. This is additive, the white overlapping on both layers - the effect that your code is creating.
The line is because your stroke and your fill are drawing to the same pixels. Since both the stroke and the fill are partially transparent, the colors accumulate.
One way to fix this is to just create a path that outlines your rounded triangle, and fill it without stroking it.
Here's the interface for a category that creates a path outlining a rounded polygon:
#interface UIBezierPath (MyRoundedPolygon)
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset;
#end
Here's the implementation:
#implementation UIBezierPath (MyRoundedPolygon)
static CGPoint vertexForPolygon(int sides, CGPoint center, CGFloat circumradius, CGFloat index) {
CGFloat angle = index * 2 * M_PI / sides;
return CGPointMake(center.x + circumradius * cosf(angle),
center.y + circumradius * sinf(angle));
}
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset
{
CGFloat circumradius = vertexRadius + cornerRadius;
CGPoint veryLastVertex = vertexForPolygon(sides, center, circumradius, rotationOffset - 1);
CGPoint currentVertex = vertexForPolygon(sides, center, circumradius, rotationOffset);
CGMutablePathRef cgpath = CGPathCreateMutable();
CGPathMoveToPoint(cgpath, NULL, (veryLastVertex.x + currentVertex.x) / 2,
(veryLastVertex.y + currentVertex.y) / 2);
for (CGFloat i = 0; i < sides; ++i) {
CGPoint nextVertex = vertexForPolygon(sides, center, circumradius,
i + 1 + rotationOffset);
CGPathAddArcToPoint(cgpath, NULL, currentVertex.x, currentVertex.y,
nextVertex.x, nextVertex.y, cornerRadius);
currentVertex = nextVertex;
}
CGPathCloseSubpath(cgpath);
UIBezierPath *path = [self bezierPathWithCGPath:cgpath];
CGPathRelease(cgpath);
return path;
}
#end
Here's how you use it:
#implementation MyView
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
UIBezierPath *path = [UIBezierPath my_roundedPolygonWithSides:3 center:center
vertexRadius:70 cornerRadius:8 rotationOffset:0.25];
[[UIColor colorWithWhite:0.6 alpha:0.5] setFill];
[path fill];
}
#end
And here's the result:
Note that setting rotationOffset to 0.25 rotated the triangle one quarter turn. Setting it to zero will give you a right-pointing triangle.
I'm trying just to create a simple gradient with a label on it that I can overlay on top of a MKMapView. I looked on SO and saw I could use CAGradientLayer rather than override drawRect in UIView. I thought I would give it a shot. In my viewDidLoad, I do this:
CGRect frame = CGRectMake(self.mapView.frame.origin.x, self.mapView.frame.origin.y, 320.0, 44.0);
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = frame;
gradientLayer.backgroundColor = (__bridge CGColorRef)((__bridge id)([UIColor clearColor].CGColor));
[gradientLayer setCornerRadius:12.0];
[gradientLayer setOpacity:0.5];
gradientLayer.colors = #[(id)[UIColor blackColor].CGColor, (id)[UIColor whiteColor].CGColor];
[self.view.layer addSublayer:gradientLayer];
It's on the right path, except the upper left corner and upper right corner have this white underneath. Is there a reason for this? I thought it might be because I didn't set the backgroundColor of the layer or something since that has happened to me on UIViews before, but it doesn't seem like it.
Thanks!
The corners don't have white underneath. They have map underneath. Here are the four corners, zoomed 600%:
When you set the corner radius of a layer, you're clipping that layer. Anything under the layer will show through where you've clipped the rounded layer away.
In your case, you've clipped your gradient layer's corners off, so the map layer underneath shows through without being blended with the gradient.
You need to clip your map layer's corners also. The easiest way is to set the same corner radius on your map layer.
R u include this header files...
#import <QuartzCore/QuartzCore.h>
#import "QuartzCore/CALayer.h"
then use radius.
gradientLayer.layer.cornerRadius = 7.0;
Add QuartzCore.framework
#import <QuartzCore/QuartzCore.h>
yourView.layer.cornerRadius = 12.0f;
I'm building a UIBezierPath as follows:
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat yOrigin = 2;
for (int i = 0; i < lines; ++i) {
[path moveToPoint:CGPointMake(0, yOrigin)];
[path addLineToPoint:CGPointMake(width, yOrigin)];
yOrigin += distance;
}
[path closePath];
I then put it in a CAShapeLayer:
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
[shapeLayer setFrame:aFrame];
[shapeLayer setPath:[bezierPath CGPath]];
[shapeLayer setStrokeColor:[[UIColor blackColor] CGColor]];
[shapeLayer setMasksToBounds:YES];
However, when added to my scroll view, here is the result:
Why does it do this? The first four lines are gray, almost! It looks like an aliasing problem, but the path is so straightforward.
Yes this is an antialiasing problem.
The path you use has a width of 1 pt. In the above example the upper three instances of the CAShapeLayer are positioned exactly centered on a whole point value eg. 5.0, where the lower instance is positioned between two whole points eg. 14.5.
When the system renders your Layers to the screens pixels it antialiases the first three lines, drawing both touched pixels with 50% transparency. The lower line perfectly sit on a pixel and is therefore drawn in perfect black.
I hope a little ASCII Art helps understanding.
pt upper lines lower line
5.0|--------- -------------------------------
| pixel -------------------------------------- perfectly aligned center 5.5
6.0|--------- line between pixels centered on 6.0 -------------------------------
| pixel --------------------------------------
7.0|---------
I got the same situation: vertical and horizontal black lines become grey.
It look like UiView 'snap' coordinates on pixels, not CALayer.
My solution to this problem is to add 0.5 on coodinates.
CGRect bounds_on_pixels(const CGRect& bounds) {
const float f = 0.5f;
return CGRectMake(bounds.origin.x+f, bounds.origin.y+f, bounds.size.width-1.f, bounds.size.height-1.f);
}
Note: -1.f on size is facultative.
Example of usage with a CAShapeLayer and a UIBezierPath
CAShapeLayer* shape_layer = [CAShapeLayer layer];
// initialize shape_layer...
shape_layer.path =
[
UIBezierPath bezierPathWithRoundedRect:bounds_on_pixels(self.view.bounds)
byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
cornerRadii:CGSizeMake(10.0, 10.0)
].CGPath;
// add shape_layer: [self.view.layer insertSublayer:shape_layer atIndex:0];
#tonklon : thx for the idea !