How can I draw a curved shadow?
Asked Answered
L

4

5

Like so:

enter image description here

I know that this will not work with NSShadow, drawing it in drawRect: will work just fine.

Lucarne answered 7/9, 2013 at 18:41 Comment(8)
To what are you applying the shadow?Bigod
@Pétur No nothing, I would want to draw it using NSGradient or something similar. Do you understand what I mean?Lucarne
Are you aiming for a drop shadow of some kind?Gyrfalcon
@MikeD I wrote a control a few days ago: github.com/iluuu1994/ITShadowScrollView It's working fine, only the shadow is not looking very nice, that's what I wanna achieve with this.Lucarne
So this is not for an iOS project?Gyrfalcon
@MikeD No, but the code for drawing is mostly the same. So I figured I could adapt it it necessary.Lucarne
@NSAddict: Hey, probably your question can be best addressed if you add few more inputs in your question above. :) Also, please add that github link above if it is related.Tactical
@Tactical My question was answered perfectly, see belowLucarne
G
6

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.

enter image description here

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

enter image description here

will produce a shadow like this

enter image description here

Greenhouse answered 10/9, 2013 at 8:6 Comment(1)
This one is great! Let me try it out tonight and I'll get back to youLucarne
I
2

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...

Resulting shadow

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.

Iberian answered 10/9, 2013 at 0:1 Comment(4)
Ah I forgot to mention that, since in my example I'm squashing everything with a 0.1f index it means that you will get a shadow that's one tenth of the height of the view. If you do set this up, make your view tall enough to see the shadow. Depending on where you want to actually place such code, you would have to put some multipliers around to have the shadow more similar to the actual view size. For the sake of simplicity I skipped this part.Iberian
You would also need to clip the gradient if you plan on drawing it far from the top margin as I did here.Iberian
This could be useful, thank you! I'll try this out when I have some time and I'll get back to youLucarne
Ps. for Mac OSX simply get the graphics context using CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];, the rest will work just fineLucarne
A
1

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.

Image

Using PaintCode (free demo available, 1 hour limit per session).

  1. Draw an oval
  2. Draw a Rectangle which intersects with the bottom of the oval.
  3. CMD click both the rectangle and the oval, in the "Objects" list in the top left corner.
  4. Press the Intersect button in the Toolbar.
  5. Select the Bezier from the Objects list.
  6. Set its Stroke to "No Stroke"
  7. Click the Gradient button (located on the left, below the Selection Inspector)
  8. Press the "+" button
  9. Change the gradient color to light grey.
  10. From the Selection inspector, change the Fill Style to "Gradient"
  11. Select Gradient: Linear

adjust the gradient till you are satisfied.

Ashcan answered 9/9, 2013 at 20:45 Comment(1)
I appreciate your effort, but this is far from what I need. The shadow I need doesn't only progress from left to right, but also fades from top to bottom. This is not possible with NSGradient, which is why I'm looking for a different approach. I bought PaintCode a while ago, it doesn't offer any other similar feature either.Lucarne
A
1
- (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.

Austrasia answered 10/9, 2013 at 8:18 Comment(1)
Did you see the osx tag? I'm not saying that iOS code can't be translated to OSX but you should be the one doing that work.Leasia

© 2022 - 2024 — McMap. All rights reserved.