Editorial note: This is the first of several posts covering shiny button backgrounds. I’m breaking the topic up in order to keep the posts shorter. Also, today’s post is up a little late, due to another project. My apologies.
If you look carefully at the glossy buttons used in native iPhone apps (e.g. the Stopwatch Clock app, or the “slide to unlock” control) you’ll see that they have a rather complex border. They appear to sit in a “well” comprised of a several pixel wide “moat”, surrounded by a one pixel wide “edge”. Both the moat and the edge have gradients applied to them.
Moat
The pixels immediately surrounding native shiny buttons (most obviously in the “slide to unlock” control) create the illusion of a “groove” in the underlying surface with a gradient running (in these examples) from black to dark gray. Sidestepping (for now) the question of how to pick the initial and final colors of this gradient, we find that it can be reproduced with the following code:
static void calc_groove_color(void* info, const float* in, float* out)
{
GlossyParams* params = (GlossyParams*) info;
float progress = *in;
progress = params->expScale * (expf((1.0 - progress) * -params->expCoefficient) - params->expOffset);
out[0] = params->color[0] * (1.0 - progress) + params->caustic[0] * progress;
out[1] = params->color[1] * (1.0 - progress) + params->caustic[1] * progress;
out[2] = params->color[2] * (1.0 - progress) + params->caustic[2] * progress;
out[3] = params->color[3] * (1.0 - progress) + params->caustic[3] * progress;
}
+ (void)drawGrooveRect:(CGRect)rect inContext:(CGContextRef)context
{
static const float EXP_COEFFICIENT = 2.0;
static const CGFloat normalizedRanges[8] = {0, 1, 0, 1, 0, 1, 0, 1};
static const CGFunctionCallbacks callbacks = {0, calc_groove_color, NULL};
// Prepare gradient configuration struct
GlossyParams params;
// Set the base color
params.color[0] = 0;
params.color[1] = 0;
params.color[2] = 0;
params.color[3] = 1;
// Set the 'caustic' color
params.caustic[0] = 34.0/255.0;
params.caustic[1] = 34.0/255.0;
params.caustic[2] = 34.0/255.0;
params.caustic[3] = 1;
// Set the exponent curve parameters
params.expCoefficient = EXP_COEFFICIENT;
params.expOffset = expf(-params.expCoefficient);
params.expScale = 1.0/(1.0 - params.expOffset);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFunctionRef function = CGFunctionCreate(¶ms, 1, normalizedRanges, 4, normalizedRanges, &callbacks);
CGPoint sp = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGPoint ep = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGShadingRef shader = CGShadingCreateAxial(colorSpace, sp, ep, function, NO, NO);
CGFunctionRelease(function);
CGColorSpaceRelease(colorSpace);
CGContextDrawShading(context, shader);
CGShadingRelease(shader);
}
Please note that:
calc_groove_color()
is directly adapted from the caustic portion of the shiny button code.EXP_COEFFICIENT
was experimentally selected to match the ‘shape’ of observed gradients; as it increases, the rate of change of the gradient increases near the bottom, relative to the top and middle.- The
params.color
andparams.caustic
values were experimentally selected to match the colors of observed gradients.
Test Code
The following code, if added to a UIViewController
, and assuming the preceding code is available in a Glossy
category of UIButton
, will draw some test wells with simple borders:
- (CGContextRef)currentImageContextWithWidth:(NSUInteger)width height:(NSUInteger)height
{
// Create and get a pointer to context
UIGraphicsBeginImageContext(CGSizeMake(width, height));
CGContextRef context = UIGraphicsGetCurrentContext();
// Convert co-ordinate system to Cocoa’s (origin in UL, not LL)
CGContextTranslateCTM(context, 0, height);
CGContextConcatCTM(context, CGAffineTransformMakeScale(1, -1));
// Set fill and stroke colors
CGContextSetFillColorWithColor(context, [[UIColor colorWithRed:0.65 green:0.85 blue:0.85 alpha:1] CGColor]);
CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:70.0/255 green:70.0/255 blue:70.0/255 alpha:1] CGColor]);
// Return context
return context;
}
- (UIImage*)imageFromCurrentContext
{
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- (UIImageView*)testImageWithWidth:(NSUInteger)width height:(NSUInteger)height
{
CGContextRef context = [self currentImageContextWithWidth:width height:height];
[UIButton setPathToRoundedRect:CGRectMake(0.5, 0.5, width-1, height-1) forInset:0 inContext:context];
CGContextStrokePath(context);
[UIButton setPathToRoundedRect:CGRectMake(1, 1, width-2, height-2) forInset:1 inContext:context];
CGContextClip(context);
[UIButton drawGrooveRect:CGRectMake(1, 1, width-2, height-2) inContext:context];
UIImageView* imageView = [[[UIImageView alloc] initWithImage:[self imageFromCurrentContext]] autorelease];
[self.view addSubview:imageView];
return imageView;
}
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView
{
// TL view
self.view = [[UIView alloc] initWithFrame:CGRectZero];
self.view.backgroundColor = [UIColor blackColor];
// Create buttons
[self testImageWithWidth:310 height:54].center = CGPointMake(160, 54);
[self testImageWithWidth:310 height:54].center = CGPointMake(160, 162);
[self testImageWithWidth:310 height:54].center = CGPointMake(160, 270);
}
Please note that this code uses the setPathToRoundedRect:forInset:inContext:
function, which was previously added to the Glossy
category.
Next Up: The Edge
Pingback: Things that were not immediately obvious to me » Blog Archive » Shiny Red Buttons (6.2)