martes, 31 de enero de 2012

Nice emboss effect using Core Graphics

I'm working on an enterprise data visualization app. Most of the data is shown using circles whose attributes change according to the data they represent, with varying color and size.
The graphic designer made a great work giving those circles nice adornments that make them look nicer than a simple plain color circle. One of that adornments is the Photoshop "bevel and emboss" layer style as shown here
The easy way in the short term, but painful in the long run, is to use a transparent PNG with the adornments and resize accordingly. But ask yourself: what if Apple releases a Retina Display iPad? Not sure when it will happen, but they will do it.
One way to go may be CoreImage, buy it seems like using a sledgehammer to crack a nut. Core Graphics looks like a better tool for this.
Basically, we have four different adornments: a color gradient, the black shadow, the shine and a small border. Gradient and border are trivial, but shadow and shine may not.

Black shadow
Shadows are not difficult to get using CoreGraphics. Simply configure your CGContext using CGContextSetShadow() and fill a path to obtain an outer shadow for your filled path. But here we need an inner shadow, and there is no CoreGraphics to get it. How to draw an inner shadow with CoreGraphics? Simply make an "inverse" of your path using Even-Odd path compositing APIs with the help of an outer CGRect. Once you have your "inverse" path, configure shadow and fill your path; to avoid the outer rect being filled, simply clip the context to the original circle.

Shine
Shine is easier. Simply realize that it's a white outer shadow of a smaller circle. By default, CoreGraphics shadows are black, but you can change shadow color with CGContextSetShadowWithColor(). Don't forget to make Even-Odd clipping to avoid the circle dropping the shadow from being draw.

Code
And finally: this is our code and an example output:

- (void)drawRect:(CGRect)rect
{
// Drawing code
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);

[[UIColor clearColor] setFill];
CGContextFillRect(currentContext, rect);

CGFloat diameter=MIN(rect.size.height, rect.size.width);
CGFloat borderWidth=1;
CGMutablePathRef circle=CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(rect), CGRectGetMidY(rect), (diameter/2.0)-borderWidth, M_PI, -M_PI, NO);
//percentValue goes from 0 to 1 and defines the circle main color from red (0) to green (1)
CGColorRef color1=[UIColor colorWithHue:_percentValue*(1.0/3.0) saturation:0.9 brightness:0.8 alpha:1].CGColor;
CGColorRef color2=[UIColor colorWithHue:_percentValue*(1.0/3.0) saturation:0.7 brightness:0.6 alpha:1].CGColor;
CGGradientRef gradient;
CGFloat locations[2] = { 0.0, 1.0 };
NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];

gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);

CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
//fill the circle with gradient
CGContextAddPath(currentContext, circle);
CGContextSaveGState(currentContext);
CGContextClip(currentContext);
CGContextDrawLinearGradient(currentContext, gradient, topCenter, midCenter, 0);

//inner shadow to simulate emboss
CGMutablePathRef innerShadowPath=CGPathCreateMutable();
CGPathAddRect(innerShadowPath, NULL, CGRectInset(rect, -100, -100));
CGPathAddEllipseInRect(innerShadowPath, NULL, CGRectInset(rect, borderWidth-1, borderWidth-1));
CGContextSetShadow(currentContext, CGSizeMake(-4, -4), 3);
[[UIColor whiteColor] setFill];
CGContextAddPath(currentContext, innerShadowPath);
CGContextEOFillPath(currentContext);
CGPathRelease(innerShadowPath);

// white shine
CGMutablePathRef whiteShinePath=CGPathCreateMutable();
CGPathAddEllipseInRect(whiteShinePath, NULL, CGRectInset(rect, borderWidth+5, borderWidth+5));
CGContextSetShadowWithColor(currentContext, CGSizeMake(-3, -3), 2, [UIColor colorWithWhite:1 alpha:0.4].CGColor);

CGMutablePathRef innerClippingPath=CGPathCreateMutable();
CGPathAddRect(innerClippingPath, NULL, CGRectInset(rect, -100, -100));
CGPathAddEllipseInRect(innerClippingPath, NULL, CGRectInset(rect, borderWidth+4, borderWidth+4));
CGContextAddPath(currentContext, innerClippingPath);
CGContextEOClip(currentContext);

CGContextAddPath(currentContext, whiteShinePath);
CGContextFillPath(currentContext);
CGPathRelease(innerClippingPath);
CGPathRelease(whiteShinePath);
CGMutablePathRef circleBorder=CGPathCreateMutable();
CGPathAddArc(circleBorder, NULL, CGRectGetMidX(rect), CGRectGetMidY(rect), (diameter-(borderWidth*2))/2.0, M_PI, -M_PI, NO);
[[UIColor colorWithWhite:0.2 alpha:1] setStroke];
CGContextSetLineWidth(currentContext, borderWidth);
CGContextAddPath(currentContext, circleBorder);
CGContextStrokePath(currentContext);
CGPathRelease(circleBorder);
CGContextRestoreGState(currentContext);

CGGradientRelease(gradient);
CFRelease(circle);
}

And the result, circles with different sizes and colors and a Core Graphics generated emboss effect:
Note that the main outer shadow is not generated with the published Core Graphics code. It's a CoreAnimation shadow.

No hay comentarios:

Publicar un comentario