18

Searching the web for about 4 hours not getting an answer so:
How to draw a shadow on a path which has transparency?

- (void)drawRect:(CGRect)rect
{
    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(c, 2);
    CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
    CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
    CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.8] CGColor]);

    // Sample Path
    CGContextMoveToPoint(c, 20.0, 10.0);
    CGContextAddLineToPoint(c, 100.0, 40.0);
    CGContextAddLineToPoint(c, 40.0, 70.0);
    CGContextClosePath(c);

    CGContextDrawPath(c, kCGPathFillStroke);
}

The first thing I notice, the shadow is only around the stroke. But that isn't the problem so far. The shadow behind the path/rect is still visible, which means: the shadow color is effecting the fill color of my path. The fill color should be white but instead its grey. How to solve this issue?

STW
  • 44,917
  • 17
  • 105
  • 161
  • 1
    If @DavidRönnqvist's answer is what you wanted, then I'm confused. This is going to have a trompe-l'oeil-ish effect if drawn over a non-solid background. (i.e. you'll see the background through what's drawn, but it won't be darkened consistently with the shadow that appears outside the path.) If you're only concerned about drawing over solid backgrounds, you should consider pre-multiplying the "effective" color, and not doing this song and dance with the clipping. If somehow this *is* the exact effect you want, hey, that's your prerogative, but it's gonna look inconsistent in some situations. – ipmcc Jan 04 '13 at 03:27
  • actually I don't know what I was searching for as I asked the question (bounty is only to have an answer). I think the problem was: the shadow color was only drawn below the stroke and not below the fill. Can you include your answer too, please? –  Jan 04 '13 at 10:27

4 Answers4

18

You will have to clip the context and draw twice.

First you create a reference to your path since you will have to use it a few times and save your graphics context so you can come back to it.

Then you clip the graphics context to a only draw outside of your path. This is done by adding your path to the path that covers the entire view. Once you have clipped you draw your path with the shadow so that it's draw on the outside.

Next you restore the graphics context to how it was before you clipped and draw your path again without the shadow.

It's going to look like this on an orange background (white background wasn't very visible)

The described drawing on a orange background

The code to do the above drawing is this:

- (void)drawRect:(CGRect)rect
{
    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(c, 2);
    CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
    CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.5] CGColor]);

    // Sample Path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, 20.0, 10.0);
    CGPathAddLineToPoint(path, NULL, 40.0, 70.0); 
    CGPathAddLineToPoint(path, NULL, 100.0, 40.0);
    CGPathCloseSubpath(path);

    // Save the state so we can undo the shadow and clipping later
    CGContextSaveGState(c);
    { // Only for readability (so we know what are inside the save/restore scope
        CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
        CGFloat width = CGRectGetWidth(self.frame);
        CGFloat height = CGRectGetHeight(self.frame);

        // Create a mask that covers the entire frame
        CGContextMoveToPoint(c, 0, 0);
        CGContextAddLineToPoint(c, width, 0);
        CGContextAddLineToPoint(c, width, height);
        CGContextAddLineToPoint(c, 0, height);
        CGContextClosePath(c);

        // Add the path (which by even-odd rule will remove it)
        CGContextAddPath(c, path);

        // Clip to that path (drawing will only happen outside our path)
        CGContextClip(c);

        // Now draw the path in the clipped context
        CGContextAddPath(c, path);
        CGContextDrawPath(c, kCGPathFillStroke);
    }
    CGContextRestoreGState(c); // Go back to before the clipping and before the shadow

    // Draw the path without the shadow to get the transparent fill
    CGContextAddPath(c, path);
    CGContextDrawPath(c, kCGPathFillStroke);
}

If you want the entire shadow to be as strong and don't want the transparency of the fill color to make the shadow weaker then you can use a fully opaque color when filling the first time. It's going to get clipped so it won't be visible inside the path anyway. It will only affect the shadow.

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • thanks, works like a charm. I will award the bounty at the end of the period. let's see if someone else got an idea ;) –  Jan 01 '13 at 16:04
  • I can't seem to make this one work if I create the initial path using `CGPathAddRoundedRect`. Quite confused here. – Berk Jul 30 '14 at 19:14
  • 2
    This example actually uses the nonzero winding number rule to clip out the path, not the even-odd rule as the comment states. To get this to work with a path that was oriented the other direction, I had to use use `CGContextEOClip` instead. More info on the two rules at [Filling a Path](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_paths/dq_paths.html#//apple_ref/doc/uid/TP30001066-CH211-TPXREF106) in the Quartz 2D Programming Guide. – Andrew Watt Jan 19 '16 at 21:31
13

Per your request in comments, here's a more in-depth exploration. Consider the following screenshot (StackOverflow shrinks it for me -- it helps to look at it full size.):

5 Different ways of drawing over 3 different kinds of backgrounds

What you're seeing here is 5 different drawing approaches (top to bottom) over three different backgrounds (left to right). (I've also dropped the fill alpha from 0.8 to 0.5 to make the effects easier to see.) The three different drawing approaches are (top to bottom):

  1. Just the stroke, not the fill, with a shadow
  2. The way you posted in the code in your original question
  3. The stroke and fill, with no shadow applied
  4. Just the shadow, by itself
  5. The way @DavidRönnqvist proposed in his answer.

The three different backgrounds should be self explanatory.

You said in your original question:

The first thing I notice, the shadow is only around the stroke.

This is why I included the first drawing approach. That's what it really looks like when there is just the stroke, with no fill, and (therefore) only the stroke is being shadowed.

Then, you said:

But that isn't the problem so far. The shadow behind the path/rect is still visible, which means: the shadow color is effecting the fill color of my path. The fill color should be white but instead its grey.

Your original code is the next version (#2). What you're seeing there is that the shadow for the stroke is darker than the shadow for the fill. This is because the stroke color's alpha is 1.0 and the fill's alpha is less than 1.0. This might be easier to see in version #4 which is just the shadow -- it's darker around the edge where the stroke is. Version #3 shows the drawing without a shadow. See you you can see the red and the image semi-obsurced in the fill of the shape? So in your original drawing you're seeing the object's own shadow through the object itself.

If that's not making sense, try thinking of a piece of glass that's got a tint to it (if you're into photography, think of a Neutral Density Filter). If you hold that glass between a light source and another surface, and then peek from the side and look just at the lower surface, you know that the semi-transparent glass is going to cast some shadow, but not as dark a shadow as something completely opaque (like a piece of cardboard). This is what you're seeing -- you're looking through the object at it's shadow.

Version #5 is @DavidRönnqvist's approach. The eye-fooling effect I was talking about in my comment is easiest to appreciate (for me, anyway) by looking at the shapes drawn over the image background. What it ends up looking like (in version #5) is that the shape is a bordered, copied, portion of the image that's been overlaid with a semi-transparent white mask of some sort. If you look back at version #3, it's clear, in the absence of the shadow, what's going on: you're looking through the semi-transparent shape at the image beneath. Then if you look at version #4, it's also clear that you have a shadow being cast by an object that's behind your eye/camera. From there, I would argue that that's also clear when looking at version #2 over the image what's going on (even if it's less clear over a solid color). At first glance, my eye/brain doesn't know what it's looking at in version #5 -- there's a moment of "visual dissonance" before I establish the mental model of "copied, masked, portion of the image floating above the original image."

So if that effect (#5) was what you were going for, then David's solution will work great. I just wanted to point out that it's sort of a non-intuitive effect.

Hope this is helpful. I've put the complete sample project I used to generate this screenshot on GitHub.

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • Ahh this makes sense. The things which bugs me most is the drop shadow of the upper strokes. Now I see it's valid logic but as an UI object it's horrible :D. Back then I was working on an `UIAlertView` like control and the shadow shouldn't be there. btw, you can have a look here https://github.com/relikd/OGActionChooser . To all other: please upvote this great explanation ;D –  Jan 04 '13 at 15:47
2
CGFloat lineWidth = 2.0f;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextSetLineWidth(c, lineWidth);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathStroke);
CGContextRestoreGState(c);
someRect.origin.x += lineWidth/2;
someRect.origin.y += lineWidth/2;
someRect.size.width -= lineWidth;
someRect.size.height -= lineWidth;
CGContextClearRect(c, someRect);
CGContextSetFillColorWithColor(c, [[[UIColor whiteColor] colorWithAlphaComponent:0.8] CGColor]);
CGContextAddRect(c, someRect);  
CGContextDrawPath(c, kCGPathFill);
Felix
  • 35,354
  • 13
  • 96
  • 143
  • yes probably. but this will only work with a `CGRect` and I want to use it with a custom path. Didn't mentioned that? sorry –  Jul 15 '11 at 16:55
1
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowColor: [NSColor blackColor]];
[shadow setShadowOffset: NSMakeSize(2.1, -3.1)];
[shadow setShadowBlurRadius: 5];

NSBezierPath* bezierPath = [NSBezierPath bezierPath];
[bezierPath moveToPoint: NSMakePoint(12.5, 6.5)];
[bezierPath curveToPoint: NSMakePoint(52.5, 8.5) controlPoint1: NSMakePoint(40.5, 13.5) controlPoint2: NSMakePoint(52.5, 8.5)];
[bezierPath lineToPoint: NSMakePoint(115.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(150.5, 6.5)];
[bezierPath lineToPoint: NSMakePoint(201.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(222.5, 8.5)];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] setStroke];
[bezierPath setLineWidth: 1];
[bezierPath stroke];
[NSGraphicsContext restoreGraphicsState];
modusCell
  • 13,151
  • 9
  • 53
  • 80
  • I don't think you got my question right. But nonetheless thanks for trying ^^. @David's answer works properly –  Jan 01 '13 at 16:02