Shiny Red Buttons (6.1)

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(&params, 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 and params.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

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.