Shiny Red Buttons (3)

Today, we’ll actually get around to the business of drawing some shiny pictures. We’ll be using Matt Gallagher’s gloss gradients code as a base, with some minor adjustments to allow for porting to the iPhone, and for personal taste. We’ll combine this code with Friday’s rounded rect clipping code to generate a rough draft of our shiny buttons. Let’s get started.

HSV

Gallagher’s code makes use of the Hue-Saturation-Value (aka Hue-Saturation-Brightness) color space to compute certain effects. Unfortunately, there doesn’t seem to be a built-in RGB-to-HSV converter on the iPhone. Fortunately, it’s pretty easy to write one:

static void rgb_to_hsv(const float* inputComponents, float* outputComponents)
{
	// Unpack r,g,b for conciseness
	double r = inputComponents[0];
	double g = inputComponents[1];
	double b = inputComponents[2];

	// Rather tediously, find the min and max values, and the max component
	char max_elt = 'r';
	double max_val=r, min_val=r;
	if (g > max_val)
	{
		max_val = g;
		max_elt = 'g';
	}
	if (b > max_val)
	{
		max_val = b;
		max_elt = 'b';
	}
	if (g < min_val) min_val = g;
	if (b < min_val) min_val = b;

	// Cached
	double max_minus_min = max_val - min_val;

	// Calculate h as a degree (0 - 360) measurement
	double h = 0;
	switch (max_elt)
	{
		case 'r':
			h = !max_minus_min?0:60*(g-b)/max_minus_min + 360;
			if (h >= 360) h -= 360;
			break;
		case 'g':
			h = !max_minus_min?0:60*(b-r)/max_minus_min + 120;
			break;
		case 'b':
		default:
			h = !max_minus_min?0:60*(r-g)/max_minus_min + 240;
			break;
	}

	// Normalize h
	h /= 360;

	// Calculate s
	double s = 0;
	if (max_val) s = max_minus_min/max_val;

	// Store HSV triple; v is just the max
	outputComponents[0] = h;
	outputComponents[1] = s;
	outputComponents[2] = max_val;
}

Code

It seems a little pointless to re-explain the following code, as Gallagher does a great job of covering it in his original post. Therefore, I’m simply going to provide a quick overview, and then present the code itself.

The code comes in 5 parts:

  • GlossyParams, a configuration structure for gradient calculations
  • perceptualGlossFractionForColor(), which scales the highlight intensity for a given base color
  • perceptualCausticColorForColor(), which calculates a caustic color for a given base color
  • calc_glossy_color(), a callback used by the gradient rendering code to compute colors for points on the gradient
  • drawGlossyRect:withColor:inContext:, the top-level class method of UIButton which supervises the rendering of a glossy gradient

GlossyParams

typedef struct
{
	float color[4];
	float caustic[4];
	float expCoefficient;
	float expOffset;
	float expScale;
	float initialWhite;
	float finalWhite;
} GlossyParams;

perceptualGlossFractionForColor()

static float perceptualGlossFractionForColor(float* inputComponents)
{
    static const float REFLECTION_SCALE_NUMBER	= 0.2;
    static const float NTSC_RED_FRACTION	= 0.299;
    static const float NTSC_GREEN_FRACTION	= 0.587;
    static const float NTSC_BLUE_FRACTION	= 0.114;

    float glossScale =	NTSC_RED_FRACTION * inputComponents[0] +
			NTSC_GREEN_FRACTION * inputComponents[1] +
			NTSC_BLUE_FRACTION * inputComponents[2];

    return pow(glossScale, REFLECTION_SCALE_NUMBER);
}

perceptualCausticColorForColor()

static void perceptualCausticColorForColor(float* inputComponents, float* outputComponents)
{
    static const float CAUSTIC_FRACTION			= 0.60;
    static const float COSINE_ANGLE_SCALE		= 1.4;
    static const float MIN_RED_THRESHOLD		= 0.95;
    static const float MAX_BLUE_THRESHOLD		= 0.7;
    static const float GRAYSCALE_CAUSTIC_SATURATION	= 0.2;

    float temp[3];

    rgb_to_hsv(inputComponents, temp);
    float hue=temp[0], saturation=temp[1], brightness=temp[2];

    rgb_to_hsv(CGColorGetComponents([[UIColor yellowColor] CGColor]), temp);
    float targetHue=temp[0], targetSaturation=temp[1], targetBrightness=temp[2];

    if (saturation < 1e-3)
    {
        hue = targetHue;
        saturation = GRAYSCALE_CAUSTIC_SATURATION;
    }

    if (hue > MIN_RED_THRESHOLD)
    {
        hue -= 1.0;
    }
    else if (hue > MAX_BLUE_THRESHOLD)
    {
	rgb_to_hsv(CGColorGetComponents([[UIColor magentaColor] CGColor]), temp);
	targetHue=temp[0], targetSaturation=temp[1], targetBrightness=temp[2];
    }

    float scaledCaustic = CAUSTIC_FRACTION * 0.5 * (1.0 + cos(COSINE_ANGLE_SCALE * M_PI * (hue - targetHue)));
    UIColor* caustic = [UIColor colorWithHue:hue * (1.0 - scaledCaustic) + targetHue * scaledCaustic
				  saturation:saturation
				  brightness:brightness * (1.0 - scaledCaustic) + targetBrightness * scaledCaustic
				       alpha:inputComponents[3]];

    const CGFloat* causticComponents = CGColorGetComponents([caustic CGColor]);
    for (int j = 3; j >= 0; j−−) outputComponents[j] = causticComponents[j];
}

calc_glossy_color()

static void calc_glossy_color(void* info, const float* in, float* out)
{
    GlossyParams*	params		= (GlossyParams*) info;
    float		progress	= *in;

    if (progress < 0.5)
    {
        progress = progress * 2.0;

        progress = 1.0 - params->expScale * (expf(progress * -params->expCoefficient) - params->expOffset);

        float currentWhite = progress * (params->finalWhite - params->initialWhite) + params->initialWhite;

        out[0] = params->color[0] * (1.0 - currentWhite) + currentWhite;
        out[1] = params->color[1] * (1.0 - currentWhite) + currentWhite;
        out[2] = params->color[2] * (1.0 - currentWhite) + currentWhite;
        out[3] = params->color[3] * (1.0 - currentWhite) + currentWhite;
    }
    else
    {
        progress = (progress - 0.5) * 2.0;

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

drawGlossyRect:withColor:inContext:

+ (void)drawGlossyRect:(CGRect)rect withColor:(UIColor*)color inContext:(CGContextRef)context
{
	static const float EXP_COEFFICIENT	= 1.2;
	static const float REFLECTION_MAX	= 0.60;
	static const float REFLECTION_MIN	= 0.20;

	static const CGFloat normalizedRanges[8] = {0, 1, 0, 1, 0, 1, 0, 1};
	static const CGFunctionCallbacks callbacks = {0, calc_glossy_color, NULL};

	// Prepare gradient configuration struct
	GlossyParams params;
	// Set the base color
	const CGFloat* colorComponents = CGColorGetComponents([color CGColor]);
	for (int j = 3; j >= 0; j−−) params.color[j] = colorComponents[j];
	// Set the caustic color
	perceptualCausticColorForColor(params.color, params.caustic);
	// Set the exponent curve parameters
	params.expCoefficient	= EXP_COEFFICIENT;
	params.expOffset	= expf(-params.expCoefficient);
	params.expScale		= 1.0/(1.0 - params.expOffset);
	// Set the highlight intensities
	float glossScale	= perceptualGlossFractionForColor(params.color);
	params.initialWhite	= glossScale * REFLECTION_MAX;
	params.finalWhite	= glossScale * REFLECTION_MIN;

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

Examples

Test resultsHere is some test code; it’s designed to be added to a View Controller (in my case, a View Controller that was pushed onto a Navigation Controller’s stack, but probably any VC will do). Naturally, it assumes that both the code presented above and discussed last Friday has been added to a Glossy category of UIButton. It uses this code to draw three large, primary-colored buttons, which you can see by clicking on the image to the left.

The test code is supposed to be pretty self-explanatory, but there are one or two things that aren’t as clear as I’d like:

  • The CGContext*CTM stuff in currentImageContextWithWidth:height: is there to translate between Quartz 2D’s co-ordinate system (origin in the lower left) and Cocoa’s (origin in the upper left). It’s effect is to cancel out the vertical image flip performed by UIGraphicsGetImageFromCurrentImageContext.
  • The colors set in currentImageContextWithWidth:height: aren’t used in this test code.
- (UIButton*)buttonWithWidth:(NSUInteger)width height:(NSUInteger)height
{
	// Create button (in UL corner of client area)
	UIButton* button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
	button.frame = CGRectMake(0, 0, width, height);

	// Add to TL view, and return
	[self.view addSubview:button];
	return button;
}
- (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:159.0/255 green:159.0/255 blue:159.0/255 alpha:1] CGColor]);

	// Return context
	return context;
}
- (UIImage*)imageFromCurrentContext
{
	UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
	return image;
}
- (UIButton*)shinyButtonWithWidth:(NSUInteger)width height:(NSUInteger)height color:(UIColor*)color
{
	UIButton*	button;
	CGContextRef	context;

	// Create drawing context for an inset rounded image
	// Dimensions are reduced by 2, to allow for a 1-pixel surrounding border
	context = [self currentImageContextWithWidth:width-2 height:height-2];
	// Add clipping path
	// * Runs around the perimeter of the included area
	// * Dimensions are *not* (further) reduced, as path is a zero-thickness boundary
	// * A path is created "forInset" 1:
	//   . When one rounded corner is placed inside another, the interior
	//     corner must have its radius reduced for a proper appearance
	[UIButton setPathToRoundedRect:CGRectMake(0, 0, width-2, height-2) forInset:1 inContext:context];
	CGContextClip(context);
	// Fill image
	[UIButton drawGlossyRect:CGRectMake(0, 0, width-2, height-2) withColor:color inContext:context];

	// Create, configure, and return button
	button = [self buttonWithWidth:width height:height];
	[button setImage:[self imageFromCurrentContext] forState:UIControlStateNormal];
	return button;
}

// 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 whiteColor];

	// Create buttons
	[self shinyButtonWithWidth:310 height:54 color:[UIColor colorWithRed:.65 green:.05 blue:.05 alpha:1]].center = CGPointMake(160, 54);
	[self shinyButtonWithWidth:310 height:54 color:[UIColor colorWithRed:.05 green:.65 blue:.05 alpha:1]].center = CGPointMake(160, 162);
	[self shinyButtonWithWidth:310 height:54 color:[UIColor colorWithRed:.05 green:.05 blue:.65 alpha:1]].center = CGPointMake(160, 270);
}

Upcoming

Tomorrow, we’ll take a look at integrating labels with this code, and packaging the result into a coherent UIButton category that can be used to easily generate shiny buttons. On Thursday, we’ll look at the many configurable parameters in the gloss gradient code, and try to adjust some of them to achieve a slightly different button appearance. (I’d like the gloss highlights to ‘pop’ a but more, and feel the caustics are overstated.) On Friday, we’ll consider button backgrounds a little more carefully than we have heretofore.

Wednesday, of course, is Book Club, when we’ll cover Chapter 6 of Men Against Fire: Fire as the Cure.

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.