Shiny Red Buttons (6.2)

Last week we began to look in detail at the backgrounds of some of the iPhone’s built-in shiny buttons. We saw that those backgrounds were really simulated 3-D grooves in the control’s parent’s surface, and that they were comprised of two elements: A “moat”, making up the bulk of the background, and a 1-pixel “edge” at the perimeter of the control. We also saw how to render a moat, so, this week, we’ll look at rendering an edge, and at compositing a complete button image.

Specular

The edge appears deceptively simple; at first, it might look like a simple gray border. In fact, it appears to be rendered with a gradient, intended to simulate the interaction of a light source with a sharp bend in a shiny surface. This isn’t too much of a problem, as we’ve rendered a lot of gradients in this project.

Toroid

The other major characteristic of the edge is its shape; it’s hollow. This isn’t a particularly good fit for the simple clipping paths we’ve been using; we would either have to cook up something new, or accept the inefficiency of redrawing the bulk of the pixels in the button 3 times. (Once for the edge gradient, once for the moat gradient, and once for the button gradient.) Until I see a convincing reason to feel otherwise, I’m feeling a lot of love for the simplicity of inefficiency.

El-Hacko

If we (a.) once again ignore the problem of dynamically determining the gradient’s begin and end colors (and just hard-code something that looks decent) and (b.) aren’t too picky about the shape of the gradient, we can reuse the drawGrooveRect function from last week, generalizing it slightly:

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 fromIntensity:(CGFloat)from toIntensity:(CGFloat)to 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] = from;
	params.color[1] = from;
	params.color[2] = from;
	params.color[3] = 1;
	// Set the 'caustic' color
	params.caustic[0] = to;
	params.caustic[1] = to;
	params.caustic[2] = to;
	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);
}

Test Code

If we add this function to the Glossy category of UIButton that we defined earlier, we can generate some test images with the following code. (Again, this test code is designed to be added to a view controller of your own devising):

- (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 color:(UIColor*)color
{
	CGContextRef context = [self currentImageContextWithWidth:width height:height];

	// Draw edge gradient
	[UIButton setPathToRoundedRect:CGRectMake(0, 0, width, height) forInset:0 inContext:context];
	CGContextClip(context);
	[UIButton drawGrooveRect:CGRectMake(0, 0, width, height) fromIntensity:61.0/255.0 toIntensity:82.0/255.0 inContext:context];

	// Draw moat gradient
	[UIButton setPathToRoundedRect:CGRectMake(1, 1, width-2, height-2) forInset:1 inContext:context];
	CGContextClip(context);
	[UIButton drawGrooveRect:CGRectMake(1, 1, width-2, height-2) fromIntensity:0.0 toIntensity:34.0/255.0 inContext:context];

	// Draw button image
	[UIButton setPathToRoundedRect:CGRectMake(4, 4, width-8, height-8) forInset:4 inContext:context];
	CGContextClip(context);
	[UIButton drawGlossyRect:CGRectMake(4, 4, width-8, height-8) withColor:color inContext:context];

	// Build and return image
	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 color:[UIColor colorWithRed:0.50 green:0.05 blue:0.05 alpha:1]].center = CGPointMake(160, 54);
	[self testImageWithWidth:310 height:54 color:[UIColor colorWithRed:0.05 green:0.50 blue:0.05 alpha:1]].center = CGPointMake(160, 162);
	[self testImageWithWidth:310 height:54 color:[UIColor colorWithRed:0.05 green:0.05 blue:0.50 alpha:1]].center = CGPointMake(160, 270);
}


The results, I hope you’ll agree, are creditable.

Next week we’ll look at generating the colors used in the backgrounds dynamically, and rolling the whole endeavour into the Glossy category of UIButton.

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

Comments are closed.