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 calculationsperceptualGlossFractionForColor()
, which scales the highlight intensity for a given base colorperceptualCausticColorForColor()
, which calculates a caustic color for a given base colorcalc_glossy_color()
, a callback used by the gradient rendering code to compute colors for points on the gradientdrawGlossyRect:withColor:inContext:
, the top-level class method ofUIButton
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(¶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);
}
Examples
Here 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 incurrentImageContextWithWidth: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 byUIGraphicsGetImageFromCurrentImageContext
. - 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.