开发者

CoreGraphics Quartz drawing shadow on transparent alpha path

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, [[U开发者_如何学GoIColor 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?


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)

CoreGraphics Quartz drawing shadow on transparent alpha path

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.


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

CoreGraphics Quartz drawing shadow on transparent alpha path

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.


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);


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];
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜