Drawing Gradients - iOS Drawing: Practical UIKit Solutions (2014)

iOS Drawing: Practical UIKit Solutions (2014)

Chapter 6. Drawing Gradients

In iOS, a gradient describes a progression between colors. Used to shade drawings and simulate real-world lighting in computer-generated graphics, gradients are an important component for many drawing tasks and can be leveraged for powerful visual effects. This chapter introduces iOS gradients and demonstrates how to use them to add pizzazz to applications.

Gradients

Gradient progression always involves at least two colors. Colors are associated with starting and ending points that range between 0 and 1. Beyond that, gradients can be as simple or as complex as you’d like to make them. Figure 6-1 demonstrates this range. The top image in Figure 6-1shows the simplest possible gradient. It goes from white (at 0) to black (at 1). The bottom image shows a gradient constructed from 24 individual hues, with the reference colors deposited at equidistant points. This complex gradient goes from red to orange to yellow to green and so forth.

Image

Figure 6-1 A simple white-to-black gradient and a color wheel–inspired gradient.

If you’ve worked with gradients before, you’ll know that you can draw linear and radial output. In case you have not, Figure 6-2 introduces these two styles. On the left, the linear gradient is created by moving from the white (at the bottom) to the black (at the top). Linear gradients draw their colors along an axis that you specify.

Image

Figure 6-2 Linear (left) and radial (right) gradient drawings.

In contrast, radial gradients vary the width of their drawing as they progress from start to end. On the right, the radial gradient starts at the white (in the center) and extends out to the black near the edge. In this example, the radius starts at 0 in the middle of the image and ends at the extent of the right edge. As the radius grows, the color darkens, producing the “sphere” look you see here.

You may not realize that both images in Figure 6-2 use the same source gradient—the one shown at the top of Figure 6-1. Gradients don’t have a shape, a position, or any geometric properties. They simply describe how colors progress. The way a gradient is drawn depends entirely on you and the Core Graphics function calls you use.

Wrapping the CGGradientRef Class

A CGGradientRef is a Core Foundation type that stores an arbitrary number of colors and starting points across a range from 0.0 to 1.0. You build the gradient by passing two arrays to it—a set of colors and their locations, as in this example:

CGGradientRef CGGradientCreateWithColors(
CGColorSpaceRef space,
CFArrayRef colors,
const CGFloat locations[]
);

Before looking any further at the Core Graphics implementation, I want to take a break to introduce an Objective-C workaround that really helps when you use this class.

On the whole, I find it far easier to use gradients through an Objective-C wrapper than to worry about the memory management and mixed C- and Core Foundation–style elements, such as the two arrays used here. As there’s no UIKit-supplied gradient wrapper and no toll-bridged equivalent, I built an Objective-C wrapper. This is where the workaround comes into play.

I’m helped by a little property trick that enables ARC to manage a Core Foundation reference as if it were a normal Cocoa Touch object. Here’s how the http://llvm.org/ website describes this feature:

GCC introduces __attribute__((NSObject)) on structure pointers to mean “this is an object.” This is useful because many low level data structures are declared as opaque structure pointers, e.g. CFStringRef, CFArrayRef, etc.

You use this trick to establish a derived type. Here’s the type definition I use for Quartz gradients:

typedef __attribute__((NSObject)) CGGradientRef GradientObject;

This declaration enables you to establish a Core Foundation–powered class property type outside the bounds of toll-free bridging while using ARC memory management. This is important because, as a rule, Quartz classes aren’t toll-free bridged into UIKit. You use the derived type to build a property using ARC-style strong management:

@property (nonatomic, strong) GradientObject storedGradient;

When the Gradient instance created in Listing 6-1 is released, so is the underlying CGGradientRef. You don’t have to build special dealloc methods to handle the Core Foundation objects. What you get is a class that handles Core Graphics gradients with Objective-C interfaces. You work with NSArrays of UIColor colors and NSNumber locations.


Caution

This attribute approach requires explicit type definitions, as you saw here. Avoid general use with other language features, like __typeof. Consult the LLVM docs for further details and cautions. I feel pretty comfortable using and recommending this approach because Apple engineers introduced me to it.


Listing 6-1 Creating an Objective-C Gradient Class


@interface Gradient ()
@property (nonatomic, strong) GradientObject storedGradient;
@end

@implementation Gradient
- (CGGradientRef) gradient
{
// Expose the internal GradientObject property
// as a CGGradientRef to the outside world on demand
return _storedGradient;
}

// Primary entry point for the class. Construct a gradient
// with the supplied colors and locations
+ (instancetype) gradientWithColors: (NSArray *) colorsArray
locations: (NSArray *) locationArray
{
// Establish color space
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
if (space == NULL)
{
NSLog(@"Error: Unable to create RGB color space");
return nil;
}

// Convert NSNumber *locations array to CGFloat *
CGFloat locations[locationArray.count];
for (int i = 0; i < locationArray.count; i++)
locations[i] = fminf(fmaxf([locationArray[i] floatValue], 0), 1);

// Convert colors array to (id) CGColorRef
NSMutableArray *colorRefArray = [NSMutableArray array];
for (UIColor *color in colorsArray)
[colorRefArray addObject:(id)color.CGColor];

// Build the internal gradient
CGGradientRef gradientRef = CGGradientCreateWithColors(
space, (__bridge CFArrayRef) colorRefArray, locations);
CGColorSpaceRelease(space);
if (gradientRef == NULL)
{
NSLog(@"Error: Unable to construct CGGradientRef");
return nil;
}

// Build the wrapper, store the gradient, and return
Gradient *gradient = [[self alloc] init];
gradient.storedGradient = gradientRef;
CGGradientRelease(gradientRef);
return gradient;
}

+ (instancetype) gradientFrom: (UIColor *) color1
to: (UIColor *) color2
{
return [self
gradientWithColors:@[color1, color2]
locations:@[@(0.0f), @(1.0f)]];
}
@end


Drawing Gradients

Quartz offers two ways to draw gradients: linear and radial. The CGContextDrawLinearGradient() and CGContextDrawRadialGradient() functions paint a gradient between the start and end points you specify. The figures in this section all use an identical purple-to-green gradient, as well as common start and end points. What changes are the functions and parameters used to draw the gradient to the context.

Painting Linear Gradients

Figure 6-3 shows a basic gradient painted by the following linear gradient drawing function:

void CGContextDrawLinearGradient(
CGContextRef context,
CGGradientRef gradient,
CGPoint startPoint,
CGPoint endPoint,
CGGradientDrawingOptions options
);

Image

Figure 6-3 A basic linear gradient.

The green-to-purple gradient is painted from the top left down to the bottom right.

The last parameter of this drawing function is options. You use it to specify whether the gradient should extend beyond its start and end point parameters. You supply either 0 (no options, Figure 6-3) or a mask of kCGGradientDrawsBeforeStartLocation andkCGGradientDrawsAfterEndLocation choices. Figure 6-4 shows these options in use.

Image

Figure 6-4 Using the kCGGradientDrawsBeforeStartLocation (left) and kCGGradientDrawsAfterEndLocation (middle) mask options enables you to continue drawing beyond the start and end point locations. The right image shows both masks used at once, by OR-ing them together.

Painting Radial Gradients

The radial drawing function adds two parameters beyond the linear function. These parameters specify the radius at the start and the end of the drawing. Figure 6-5 shows the green-to-purple gradient drawn with an initial radius of 20 and a final radius of 50. The left version uses no options, so the drawing stops at the start and end circles. The right version continues drawing both before and after the start and end locations. The underlying circles are clipped by the bounds of the drawing rectangle:

void CGContextDrawRadialGradient(
CGContextRef context,
CGGradientRef gradient,
CGPoint startCenter,
CGFloat startRadius,
CGPoint endCenter,
CGFloat endRadius,
CGGradientDrawingOptions options
);

Image

Figure 6-5 The radial gradient on the left is drawn with a starting radius of 20 and an ending radius of 50. The gradient on the right applies flags that continue drawing past the start and end points.

Listing 6-2 shows my Objective-C wrapper for linear and radial gradient drawing operations. These implementations form part of the custom Gradient class defined in Listing 6-1. They establish a simple way to paint the Core Graphics gradients embedded in class instances into the active UIKit drawing context.

Listing 6-2 Drawing Gradients


// Draw a linear gradient between the two points
- (void) drawFrom: (CGPoint) p1
toPoint: (CGPoint) p2 style: (int) mask
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}
CGContextDrawLinearGradient(
context, self.gradient, p1, p2, mask);
}

// Draw a radial gradient between the two points
- (void) drawRadialFrom:(CGPoint) p1
toPoint: (CGPoint) p2 radii: (CGPoint) radii
style: (int) mask
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextDrawRadialGradient(context, self.gradient, p1,
radii.x, p2, radii.y, mask);
}


Building Gradients

Every gradient consists of two collections of values:

• An ordered series of colors

• The locations at which color changes occur

For example, you might define a gradient that proceeds from red to green to blue, at 0.0, 0.5, and 1.0, along the way of the gradient. A gradient interpolates any value between these reference points. One-third of the way along the gradient, at 0.33, the color is approximately 66% of the way from red to green. Or, for example, imagine a simple black-to-white gradient. A medium gray color appears about halfway between the start and end of the drawn gradient.

You can supply any kind of color and location sequence, as long as the colors use either the RGB or grayscale color space. (You can’t draw gradients using pattern colors.) Locations fall between 0.0 and 1.0. If you supply values outside this range, the creation function returns NULL at runtime.

The most commonly used gradients are white-to-black, white-to-clear, and black-to-clear. Because you apply these with varying alpha levels, I find it handy to define the following macro:

#define WHITE_LEVEL(_amt_, _alpha_) \
[UIColor colorWithWhite:(_amt_) alpha:(_alpha_)]

This macro returns a grayscale color at the white and alpha levels you specify. White levels may range from 0 (black) to 1 (white), alpha levels from 0 (clear) to 1 (opaque).

Many developers use a default interpolation between colors to shade their gradients, as in Example 6-1. This example creates a clear-to-black gradient and draws it from points 70% to 100% across the green shape beneath it. You see the result in Figure 6-6, at the top-left corner. Contrast this with the other gradient drawings in Figure 6-6. As you will discover, images were built using gradient easing.

Example 6-1 Drawing the Linear Gradient


Gradient *gradient = [Gradient
gradientFrom:WHITE_LEVEL(0, 0) to:WHITE_LEVEL(0, 1)];

// Calculate the points
CGPoint p1 = RectGetPointAtPercents(path.bounds, 0.7, 0.5);
CGPoint p2 = RectGetPointAtPercents(path.bounds, 1.0, 0.5);

// Draw a green background
[path fill:greenColor];

// Draw the gradient across the green background
[path addClip];
[gradient drawFrom:p1 toPoint:p2];


Image

Figure 6-6 Tweaking your gradients affects drawn output. The top-left image shows standard gradient interpolation. The top-right image demonstrates ease-in interpolation. The bottom-left image uses ease-in-out, and the bottom-right image uses ease-out. Each gradient is drawn onto a solid green rounded-rectangle backdrop.

Easing

Easing functions vary the rate at which gradient shading changes. Depending on the function you select, they provide gentler transitions into and out of the gradient. I am most fond of the ease-in and ease-in-out gradients shown, respectively, at the top right and bottom left of Figure 6-6. As you can see, these two approaches avoid abrupt end transitions. Those harsh transitions are created by perceptual banding, also called illusory mach bands.

Mach bands are an optical illusion first noted by physicist Ernst Mach. Caused by natural pattern processing in our brains, they appear when slightly different shades of gray appear along boundaries. They happen in computer graphics because drawing stops where an algorithm tells it to stop.

In Figure 6-6, you see these effects at the edges of the gradient’s drawing area in the shots at the top left and the bottom right. By using ease-in-out drawing, you stretch the transition between the underlying color and the gradient overlay, avoiding the bands.

Figure 6-7 displays the easing functions for the gradients in Figure 6-6. You see a set of functions: linear (top-left), ease-in (top-right), ease-in-out (bottom left), and ease-out (bottom right). Easing affects the start (for “in”) or end (“out”) of a function to establish more gradual changes. These functions are used with many drawing and animation algorithms.

Image

Figure 6-7 Unlike linear interpolation, where values change equally over time, easing functions vary the rate of change at the start (ease-in) and/or the end (ease-out). They provide more gradual transitions that better reflect natural effects in light and movement.

Listing 6-3 defines a Gradient class method that builds gradients from functions you supply. You pass a block that accepts an input percentage (the time axis) and returns a value (the amount axis) to apply to the start and end color values. The method interpolates the colors and adds the values to the gradient.

The three standard easing functions use two arguments: elapsed time and an exponent. The exponent you pass determines the type of easing produced. For standard cubic easing, you pass 3 as the second parameter, for quadratic easing, 2. Passing 1 produces a linear function without easing.

You may apply any function you like in the interpolation block. The following snippet builds a gradient using in-out cubic easing:

Gradient *gradient = [Gradient gradientUsingInterpolationBlock:
^CGFloat (CGFloat percent) {return EaseInOut(percent, 3);}
between: WHITE_LEVEL(0, 0) and: WHITE_LEVEL(0, 1)];

Listing 6-3 Applying Functions to Create Custom Gradients


typedef CGFloat (^InterpolationBlock)(CGFloat percent);

// Build a custom gradient using the supplied block to
// interpolate between the start and end colors
+ (instancetype) gradientUsingInterpolationBlock:
(InterpolationBlock) block
between: (UIColor *) c1 and: (UIColor *) c2;
{
if (!block)
{
NSLog(@"Error: No interpolation block");
return nil;
}

NSMutableArray *colors = [NSMutableArray array];
NSMutableArray *locations = [NSMutableArray array];
int numberOfSamples = 24;
for (int i = 0; i <= numberOfSamples; i++)
{
CGFloat amt = (CGFloat) i / (CGFloat) numberOfSamples;
CGFloat percentage = fmin(fmax(0.0, block(amt)), 1.0);
[colors addObject:
InterpolateBetweenColors(c1, c2, percentage)];
[locations addObject:@(amt)];
}

return [Gradient gradientWithColors:colors
locations:locations];
}

// Return an interpolated color
UIColor *InterpolateBetweenColors(
UIColor *c1, UIColor *c2, CGFloat amt)
{
CGFloat r1, g1, b1, a1;
CGFloat r2, g2, b2, a2;

if (CGColorGetNumberOfComponents(c1.CGColor) == 4)
[c1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
else
{
[c1 getWhite:&r1 alpha:&a1];
g1 = r1; b1 = r1;
}

if (CGColorGetNumberOfComponents(c2.CGColor) == 4)
[c2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
else
{
[c2 getWhite:&r2 alpha:&a2];
g2 = r2; b2 = r2;
}

CGFloat r = (r2 * amt) + (r1 * (1.0 - amt));
CGFloat g = (g2 * amt) + (g1 * (1.0 - amt));
CGFloat b = (b2 * amt) + (b1 * (1.0 - amt));
CGFloat a = (a2 * amt) + (a1 * (1.0 - amt));
return [UIColor colorWithRed:r green:g blue:b alpha:a];
}

#pragma mark – Easing Functions

// Ease only the beginning
CGFloat EaseIn(CGFloat currentTime, int factor)
{
return powf(currentTime, factor);
}

// Ease only the end
CGFloat EaseOut(CGFloat currentTime, int factor)
{
return 1 - powf((1 - currentTime), factor);
}

// Ease both beginning and end
CGFloat EaseInOut(CGFloat currentTime, int factor)
{
currentTime = currentTime * 2.0;
if (currentTime < 1)
return (0.5 * pow(currentTime, factor));
currentTime -= 2.0;
if (factor % 2)
return 0.5 * (pow(currentTime, factor) + 2.0);
return 0.5 * (2.0 - pow(currentTime, factor));
}


Adding Edge Effects

Radial gradients enable you to draw intriguing edge effects in circles. Consider the effect shown in Figure 6-8. It’s a gradient expressing a sine wave. However, it’s drawn only at the circle’s edge, with the center of the path remaining untouched.

Image

Figure 6-8 A sine-based gradient applied just at the circle’s edge.

Example 6-2 uses a nonintuitive approach to accomplish this effect, demonstrating an interesting way to apply gradients. The sine function is compressed to just the last 25% of the gradient. Because the gradient is drawn radially from the center out, a shading effect appears only at the edge.

Example 6-2 Drawing a Delayed Radial Gradient


InterpolationBlock block = ^CGFloat (CGFloat percent)
{
CGFloat skippingPercent = 0.75;
if (percent < skippingPercent) return 0;
CGFloat scaled = (percent - skippingPercent) *
(1 / (1 - skippingPercent));
return sinf(scaled * M_PI);
};

Gradient *gradient =
[Gradient gradientUsingInterpolationBlock: block
between: WHITE_LEVEL(0, 0) and: WHITE_LEVEL(0, 1)];

CGContextDrawRadialGradient(UIGraphicsGetCurrentContext(),
gradient.gradient, center, 0, center, dx, 0);


You can use this effect to apply easing just at the edges, as shown in Figure 6-9. The interpolation block compresses the easing function, applying it only after a certain percentage has passed—in this case, 50% of the radial distance:

InterpolationBlock block = ^CGFloat (CGFloat percent)
{
CGFloat skippingPercent = 0.5;
if (percent < skippingPercent) return 0;
CGFloat scaled = (percent - skippingPercent) *
(1 / (1 - skippingPercent));
return EaseIn(scaled, 3);
};

Image

Figure 6-9 This clear-to-black radial gradient is drawn starting from about halfway out from the center. The graph of the interpolation function shows that the easing does not begin until half the radial distance has passed, with most of the color being added after about 75% of the radial distance.

The delayed easing appears in the graph on the right in Figure 6-9. The ascent begins after 0.5. As you can see, that ascent is quite gradual. You don’t really see a darkening effect until about 0.75 in.

Basic Easing Background

Say that you’re looking for a nice round button effect. The base radial result shown in Figure 6-2 may be too spherical for you, and the delayed effect in Figure 6-9 may be too flat. The easing function, as it turns out, produces a really nice button base, as shown in Figure 6-10.

Image

Figure 6-10 This ease-in radial function is applied from the center to the edges to provide an easy basis for round buttons.

The interpolation block is simply as follows:

InterpolationBlock block = ^CGFloat (CGFloat percent)
{
return EaseIn(percent, 3);
};

You’ll see this approach used again later in this chapter for building a “glowing” center onto a button base.

State and Transparency Layers

Before continuing further into gradients, this chapter needs to step back and cover an important Quartz drawing feature. This feature is used in the examples that follow in this chapter and deserve an explanation.

If you’ve worked with Photoshop (or similar image-composition and editing apps), you’re probably familiar with layers. Layers encapsulate drawing into distinct individual containers. You stack these layers to build complex drawings, and you apply layer effects to add shadows, highlights, and other ornamentation to the contents of each layer. Importantly, these effects apply to entire layers at a time, regardless of the individual drawing operations that created the layer contents.

Quartz offers a similar feature, called transparency layers. These layers enable you to combine multiple drawing operations into a single buffer. Figure 6-11 demonstrates why you want to use layers in your apps.

Image

Figure 6-11 Using transparency layers ensures that drawing effects are applied to an entire collection of drawing operations at once rather than to individual drawing requests. Clarus the DogCow is sourced from Apple’s TechNote 31.

This drawing was rendered into a context where shadows were enabled. In the top images, the shadows appear under all portions of the drawing, including the “insides” of the DogCow. That’s because this picture was created using three Bezier fill operations:

• The first operation filled the pink DogCow udder. (Clarus purists, please forgive the heresy. I wanted a more complex shape to work with for this example.)

• The second filled the white background within the figure.

• The third drew the spots, the eye, and the outline on top of that background.

The bottom-left image shows the outlines of the Bezier paths used for these drawing tasks. When these paths are performed as three operations, the context applies shadows to each drawing request. To create a single compound drawing, as in the bottom right of Figure 6-11, you use Quartz transparency layers instead. The shadow is applied only at the edges of the compound drawing, not at the edges of the components.

Transparency layers group drawing requests into a distinct buffer, separate from your drawing context. On starting a layer (by calling CGContextBeginTransparencyLayer()), this buffer is initialized with a fully transparent background. Its shadows are disabled, and the global alpha is set to 1. Only after you finish drawing (by calling CGContextEndTransparencyLayer()) are the layer’s contents rendered to the parent context.

Transparency Blocks

As with most other Quartz and UIKit drawing requests, layer declarations quickly become messy: hard to follow, challenging to read, and difficult to maintain. Consider, instead, Example 6-3, which presents the code that created the final DogCow in Figure 6-11. The block passed toPushLayerDraw() ensures that the shadow, which was set before the drawing, applies to the group as a whole.

Example 6-3 Drawing Transparency Layers Using Blocks


SetShadow(shadowColor, CGSizeMake(4, 4), 4);
PushLayerDraw(^{
[udder fill:pinkColor];
[interior fill:whiteColor];
[baseMoof fill:blackColor];
});


Listing 6-4 presents the PushLayerDraw() function. It executes a block of drawing operations within a transparency layer. This approach enables you to group drawings within an easy-to-use block that ensures layer-based rendering.

Listing 6-4 Drawing with Transparency Layers


typedef void (^DrawingStateBlock)();

void PushLayerDraw(DrawingStateBlock block)
{
if (!block) return; // Nothing to do

CGContextRef context =
UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextBeginTransparencyLayer(context, NULL);
block();
CGContextEndTransparencyLayer(context);
}


The virtues of transparency layers are obvious: They enable you to treat drawing operations as groups. The drawback is that they can be memory hogs due to the extra drawing buffer. You mitigate this by clipping your context before working with layers. If you know that your group will be drawing to only a portion of your context, add that clipping before beginning the layer. This forces the layers to draw only to the clipped regions, reducing the buffer sizes and the associated memory overhead.

Be careful, however, with shadows. Add a shadow allowance to your clipping region, as the shadow will be drawn as soon as the transparency layer concludes. As a rule of thumb, you want to allow for the shadow size plus the blur. So for a shadow with an offset of (2, 4) and a blur of 4, add at least (6, 8) points to your clipping region.

State Blocks

Whenever you work with temporary clipping or any other context-specific state, you can make your life a lot easier by using a blocks-based approach, as in Listing 6-5. Similarly to Listing 6-4, this PushDraw() function executes a block between calls that save and restore a context’s state.

Listing 6-5 Drawing with State Blocks


void PushDraw(DrawingStateBlock block)
{
if (!block) return; // Nothing to do

CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}
CGContextSaveGState(context);
block();
CGContextRestoreGState(context);
}


Example 6-4 uses the functions from Listings 6-4 and 6-5 to show the full sequence used to create the final image in Figure 6-11. It performs a context clip, sets the context shadow, and draws all three Bezier paths as a group. After this block executes, the context returns entirely to its predrawing conditions. Neither the clipping nor the shadow state changes persist beyond this example.

Example 6-4 Using State and Transparency Blocks with Clipping


CGRect clipRect = CGRectInset(destinationRect, -8, -8);
PushDraw(^{
// Clip path to bounds union with shadow allowance
// to improve drawing performance
[[UIBezierPath bezierPathWithRect:clipRect] addClip];

// Set shadow
SetShadow(shadowColor, CGSizeMake(4, 4), 4);

// Draw as group
PushLayerDraw(^{
[udder fill:pinkColor];
[interior fill:whiteColor];
[baseMoof fill:blackColor];
});
});


Flipping Gradients

Gradients naturally emulate light. When inverted, they establish visual hollows. These are areas that would be indented in a physical world to catch inverted light patterns. Drawing gradients first one way and then, after insetting, the other builds the effects you see in Figure 6-12.

Image

Figure 6-12 Reversing a gradient creates a 3D inset effect.

Example 6-5 shows the code that built the circular sample on the left. It creates a gradient from light gray to dark gray and draws it first from top to bottom in the larger shape. Then it reverses direction, drawing again in the other direction, using the smaller shape.

Example 6-5 adds a finishing touch in drawing a slight black inner shadow (see Chapter 5) at the top of the smaller shape. This shadow emphasizes the point of differentiation between the two drawings but is otherwise completely optional.

Example 6-5 Drawing Gradients in Opposing Directions


UIBezierPath *outerPath =
[UIBezierPath bezierPathWithOvalInRect:outerRect];
UIBezierPath *innerPath =
[UIBezierPath bezierPathWithOvalInRect:innerRect];

Gradient *gradient =
[Gradient gradientFrom:WHITE_LEVEL(0.66, 1)
to:WHITE_LEVEL(0.33, 1)];

PushDraw(^{
[outerPath addClip];
[gradient drawTopToBottom:outerRect];
});

PushDraw(^{
[innerPath addClip];
[gradient drawBottomToTop:innerRect];
});

DrawInnerShadow(innerPath, WHITE_LEVEL(0.0, 0.5f),
CGSizeMake(0, 2), 2);


Mixing Linear and Radial Gradients

There’s no reason you can’t mix linear and radial effects in your drawings. For example, Example 6-6 draws a blue radial gradient over the base built by Example 6-5. This produces the eye-pleasing glowing button effect you see in Figure 6-13.

Image

Figure 6-13 Combining radial and linear gradients.

Example 6-6 Drawing a Radial Gradient Using Ease-In-Out


CGRect insetRect = CGRectInset(innerRect, 2, 2);
UIBezierPath *bluePath =
[UIBezierPath bezierPathWithOvalInRect:insetRect];

// Produce an ease-in-out gradient, as in Listing 6-5
Gradient *blueGradient = [Gradient
easeInOutGradientBetween:skyColor and:darkSkyColor];

// Draw the radial gradient
CGPoint center = RectGetCenter(insetRect);
CGPoint topright = RectGetTopRight(insetRect);
CGFloat width = PointDistanceFromPoint(center, topright);

PushDraw(^{
[bluePath addClip];
CGContextDrawRadialGradient(UIGraphicsGetCurrentContext(),
blueGradient.gradient, center, 0, center, width, 0);
});


Drawing Gradients on Path Edges

I’m often asked how to work with the edge of a path. Usually this is within the context of testing touches on a Bezier path, but sometimes developers want to know how to add special effects just to a path’s edge. There’s an odd little Core Graphics function calledCGPathCreateCopyByStrokingPath(). It builds a path with a width you specify, formed around the edge of a given Bezier path.

Listing 6-6 uses this function to clip a path around its normal stroke area. You supply the width; it builds and clips the edge path. Once clipped, you can draw a gradient into your context. This produces the effect you see in Figure 6-14. In the left image, the path is filled with the gradient. In the right image, a dashed pattern stroked onto the original path highlights the way the new path was built.

Image

Figure 6-14 Stroking a path with a gradient by using Quartz. Left: the stroked image. Right: the same image showing the original path.

For touches, you use CGPathContainsPoint() to test whether the stroked version of the path contains the touch point.

Listing 6-6 Clipping a Path to Its Stroke


- (void) clipToStroke:(NSUInteger)width
{
CGPathRef pathRef = CGPathCreateCopyByStrokingPath(
self.CGPath, NULL, width, kCGLineCapButt,
kCGLineJoinMiter, 4);
UIBezierPath *clipPath =
[UIBezierPath bezierPathWithCGPath:pathRef];
CGPathRelease(pathRef);
[clipPath addClip];
}


Drawing 3D Letters

Listing 6-7 merges the techniques you just read about to build the 3D letter effects shown in Figure 6-15. So far in this chapter, you’ve read about transparency layers, gradients, and clipping to a path stroke. Alone, each of these tools can be dry and unengaging. Combined, however, they can produce the eye-catching results you see here.

Image

Figure 6-15 Using gradients, transparency layers, and path clipping to create 3D letters.

This drawing consists of letters drawn with a light-to-dark gradient. The path is clipped, and the gradient is drawn top to bottom. Next, a path clipped to the edges adds a reversed dark-to-light gradient trim around each letter. A transparency layer ensures that together, both drawing operations create a single shadow, cast to the bottom right.

Listing 6-7 also adds an inner shadow (see Chapter 5) to the drawing, giving the text a bit of extra shape definition at the bottom of each letter. This produces the extra “height” at the bottom of each letter compared to the gray gradient outline.

Listing 6-7 Drawing Text with a 3D Effect


#define COMPLAIN_AND_BAIL(_COMPLAINT_, _ARG_) \
{NSLog(_COMPLAINT_, _ARG_); return;}

// Brightness scaling
UIColor *ScaleColorBrightness(
UIColor *color, CGFloat amount)
{
if (!color) return [UIColor blackColor];

CGFloat h, s, v, a;
[color getHue:&h saturation:&s brightness:&v alpha:&a];
CGFloat v1 = fmaxf(fminf(v * amount, 1), 0);
return [UIColor colorWithHue:h
saturation:s brightness:v1 alpha:a];
}

void DrawStrokedShadowedShape(UIBezierPath *path,
UIColor *baseColor, CGRect dest)
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (!context)
COMPLAIN_AND_BAIL(@"No context to draw to", nil);

PushDraw(^{
CGContextSetShadow(context, CGSizeMake(4, 4), 4);

PushLayerDraw(^{

// Draw letter gradient (to half brightness)
PushDraw(^{
Gradient *innerGradient =
[Gradient gradientFrom:baseColor
to:ScaleColorBrightness(baseColor, 0.5)];
[path addClip];
[innerGradient drawTopToBottom:path.bounds];
});

// Add the inner shadow with darker color
PushDraw(^{
CGContextSetBlendMode(context, kCGBlendModeMultiply);
DrawInnerShadow(path, ScaleColorBrightness(
baseColor, 0.3), CGSizeMake(0, -2), 2);
});

// Stroke with reversed gray gradient
PushDraw(^{
[path clipToStroke:6];
[path.inverse addClip];
Gradient *grayGradient =
[Gradient gradientFrom:WHITE_LEVEL(0.0, 1)
to:WHITE_LEVEL(0.5, 1)];
[grayGradient drawTopToBottom:dest];
});
});
});
}


Building Indented Graphics

Listing 6-8 applies a different twist on gradients, as shown in Figure 6-16. This function uses a dark-to-light gradient to produce an “indented” path effect. A pair of shadows—a black inner shadow at the top and a white shadow at the bottom—adds to the illusion. Combined with the gradient and a small bevel, they trick your eye into seeing a “cut away” shape.

Image

Figure 6-16 Top: Gradients reinforce the “indented” look of this image. Bottom: The progression of indentation, step by step.

Listing 6-8 Indenting Graphics


void DrawIndentedPath(UIBezierPath *path,
UIColor *primary, CGRect rect)
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (!context) COMPLAIN_AND_BAIL(@"No context to draw to", nil);

// Draw the black inner shadow at the top
PushDraw(^{
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),
kCGBlendModeMultiply);
DrawInnerShadow(path, WHITE_LEVEL(0, 0.4),
CGSizeMake(0, 2), 1);
});

// Draw the white shadow at the bottom
DrawShadow(path, WHITE_LEVEL(1, 0.5), CGSizeMake(0, 2), 1);

// Create a bevel effect
BevelPath(path, WHITE_LEVEL(0, 0.4), 2, 0);

// Draw a gradient from light (bottom) to dark (top)
PushDraw(^{
[path addClip];
CGContextSetAlpha(UIGraphicsGetCurrentContext(), 0.3);

UIColor *secondary = ScaleColorBrightness(primary, 0.3);
Gradient *gradient = [Gradient
gradientFrom:primary to:secondary];
[gradient drawBottomToTop:path.bounds];
});
}


Combining Gradients and Texture

Textures expand the way you color objects, providing shading details for visual interest. Take Figure 6-17, for example. In these images, the kCGBlendModeColor Quartz blend mode enables you to draw gradients over background images. This mode picks up image texture (luminance values) from the destination context while preserving the hue and saturation of the gradient colors.

Image

Figure 6-17 Blending modes combine the colors from a gradient with the textures of an underlying image.

The top two images display a purple gradient drawn from light (on top) to slightly darker (on the bottom, with a 25% reduction in brightness). The top image showcases the original image source (right) together with the gradient overlay (left). The bottom image applies a rainbow gradient.

Listing 6-9 shows the DrawGradientOverTexture() function used to create these images.

Listing 6-9 Transforming Textures with Gradient Overlays


void DrawGradientOverTexture(UIBezierPath *path,
UIImage *texture, Gradient *gradient, CGFloat alpha)
{
if (!path) COMPLAIN_AND_BAIL(
@"Path cannot be nil", nil);
if (!texture) COMPLAIN_AND_BAIL(
@"Texture cannot be nil", nil);
if (!gradient) COMPLAIN_AND_BAIL(
@"Gradient cannot be nil", nil);
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

CGRect rect = path.bounds;
PushDraw(^{
CGContextSetAlpha(context, alpha);
[path addClip];
PushLayerDraw(^{
[texture drawInRect:rect];
CGContextSetBlendMode(
context, kCGBlendModeColor);
[gradient drawTopToBottom:rect];
});
});
}


Adding Noise Texture

The example shown in Figure 6-17 used a kCGBlendModeColor blend mode to add hues onto texture. At times, you’ll want to reverse the process to add texture to color. For this, you use the kCGBlendModeScreen blend mode. Figure 6-18 shows what this looks like. At the top, you see a normal fill, created with a solid purple color. The middle image applies and blends a noise pattern, introducing a subtle texture. The bottom image zooms into the resulting texture, to highlight the underlying variation.

Image

Figure 6-18 Screening enables you to overlay colors with texture.

This noise-based technique has become quite popular in the App Store for texturizing interface colors. Despite its reputation as a “flat” UI, iOS 7 uses subtle textures in many of its apps, such as Notes and Reminders. You may have to look very closely to see this granularity, but it is there. Noise and other textures provide more satisfying backgrounds that feel organic in nature.

Screen blends aren’t limited to noise, of course. Figure 6-19 shows a dot pattern blended into a fill color.

Image

Figure 6-19 A polka dot pattern adds texture to this background.

Listing 6-10 provides two Bezier path methods that enable you to fill a path. The first applies a color using a blend mode you specify, as shown in Figure 6-19. The second fills a path with a color and then screens in a noise pattern, as in Figure 6-18. A trivial noise color generator completes this listing.


Note

If you’re serious about noise, consult the Perlin noise FAQ at http://webstaff.itn.liu.se/~stegu/TNM022-2005/perlinnoiselinks/perlin-noise-math-faq.html. Perlin noise offers a function that generates coherent (that is, smoothly changing) noise content.


Listing 6-10 Drawing Textures onto Colors


// Apply color using the specified blend mode
- (void) fill: (UIColor *) fillColor
withMode: (CGBlendMode) blendMode
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

PushDraw(^{
CGContextSetBlendMode(context, blendMode);
[self fill:fillColor];
});
}

// Screen noise into the fill
- (void) fillWithNoise: (UIColor *) fillColor
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

[self fill:fillColor];
[self fill:[NoiseColor()
colorWithAlphaComponent:0.05f]
withMode:kCGBlendModeScreen];
}

// Generate a noise pattern color
UIColor *NoiseColor()
{
static UIImage *noise = nil;
if (noise)
return [UIColor colorWithPatternImage:noise];

srandom(time(0));

CGSize size = CGSizeMake(128, 128);
UIGraphicsBeginImageContextWithOptions(
size, NO, 0.0);
for (int j = 0; j < size.height; j++)
for (int i = 0; i < size.height; i++)
{
UIBezierPath *path = [UIBezierPath
bezierPathWithRect:CGRectMake(i, j, 1, 1)];
CGFloat level = ((double) random() /
(double) LONG_MAX);
[path fill:[UIColor
colorWithWhite:level alpha:1]];
}
noise = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return [UIColor colorWithPatternImage:noise];
}


Basic Button Gloss

Many iOS developers will continue to use gradients to add a pseudo-gloss to buttons, even in the brave new flat white age of iOS 7. They understand that custom button use doesn’t abrogate the principles of depth and deference.

Glosses create a 3D feel and can be applied to many kinds of views, not just buttons. They play their most important role when working in interfaces created from non-system-supplied items. If your app is primarily based around Apple system controls, go ahead and drop the 3D feel—use borderless buttons and white edge-to-edge design. If not, the techniques you read about here and in the following sections help produce a rich set of alternatives.

Simple glosses consist of linear gradients drawn halfway down the button, followed by a sharp break in color sequence and continued the remaining distance. Figure 6-20 shows a common gloss approach applied to several background colors.

Image

Figure 6-20 Gradient-based linear button gloss.

There are roughly a billion ways to create this kind of highlight gradient, all of which are variations on a fairly similar theme. Listing 6-11 was inspired, long ago, by a Cocoa with Love post by Matt Gallagher (http://www.cocoawithlove.com/). None of the mess you see in my listing is his fault, however, but everything nice about the output is reasonably due to his inspiration.

Listing 6-11 Building a Linear Gloss Gradient Overlay


+ (instancetype) linearGloss:(UIColor *) color
{
CGFloat r, g, b, a;
[color getRed:&r green:&g blue:&b alpha:&a];

// Calculate top gloss as half the core color luminosity
CGFloat l = (0.299f * r + 0.587f * g + 0.114f * b);
CGFloat gloss = pow(l, 0.2) * 0.5;

// Retrieve color values for the bottom gradient
CGFloat h, s, v;
[color getHue:&h saturation:&s brightness:&v alpha:NULL];
s = fminf(s, 0.2f);

// Rotate the color wheel by 0.6 PI. Dark colors
// move toward magenta, light ones toward yellow
CGFloat rHue = ((h < 0.95) && (h > 0.7)) ? 0.67 : 0.17;
CGFloat phi = rHue * M_PI * 2;
CGFloat theta = h * M_PI;

// Interpolate distance to the reference color
CGFloat dTheta = (theta - phi);
while (dTheta < 0) dTheta += M_PI * 2;
while (dTheta > 2 * M_PI) dTheta -= M_PI_2;
CGFloat factor = 0.7 + 0.3 * cosf(dTheta);

// Build highlight colors by interpolating between
// the source color and the reference color
UIColor *c1 = [UIColor colorWithHue:h * factor +
(1 - factor) * rHue saturation:s
brightness:v * factor + (1 - factor) alpha:gloss];
UIColor *c2 = [c1 colorWithAlphaComponent:0];

// Build and return the final gradient
NSArray *colors = @[WHITE_LEVEL(1, gloss),
WHITE_LEVEL(1, 0.2), c2, c1];
NSArray *locations = @[@(0.0), @(0.5), @(0.5), @(1)];
return [Gradient gradientWithColors:colors
locations:locations];
}


Clipped Gloss

Listing 6-12 offers another common take on button gloss. In this approach (see Figure 6-21), you offset the path outline and then cut away a portion of the gradient overlay. The result is a very sharp transition from the overlay to the button contents.

Image

Figure 6-21 Clipped button gloss applied directly at the edges (top) and inset (bottom).

The drawing and clipping take place in a transparency layer. This approach ensures that only the intended overlay survives to deposit its shine onto the original drawing context. The function clears away the remaining material before the transparency layer completes.

Listing 6-12 Building a Clipped Button Overlay


void DrawButtonGloss(UIBezierPath *path)
{
if (!path) COMPLAIN_AND_BAIL(
@"Path cannot be nil", nil);
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

// Create a simple white to clear gradient
Gradient *gradient =
[Gradient gradientFrom:WHITE_LEVEL(1, 1) to:
WHITE_LEVEL(1, 0)];

// Copy and offset the path by 35% vertically
UIBezierPath *offset = [UIBezierPath bezierPath];
[offset appendPath:path];
CGRect bounding = path.calculatedBounds;
OffsetPath(offset, CGSizeMake(0,
bounding.size.height * 0.35));

// Draw from just over the path to its middle
CGPoint p1 = RectGetPointAtPercents(
bounding, 0.5, -0.2);
CGPoint p2 = RectGetPointAtPercents(
bounding, 0.5, 0.5);

PushLayerDraw(^{
PushDraw(^{
// Draw the overlay inside the path bounds
[path addClip];
[gradient drawFrom:p1 toPoint:p2];
});

PushDraw(^{
// And then clear away the offset area
[offset addClip];
CGContextClearRect(context, bounding);
});
});
}


Adding Bottom Glows

Bottom glows create the illusion of ambient light being reflected back to your drawing. Figure 6-22 shows the inner- and outer-glow example from Chapter 5 before and after adding this effect.

Image

Figure 6-22 Adding a bottom glow.

Unlike the glows built in Chapter 5, which were created using Quartz shadows, this example uses an ease-in-out gradient. Listing 6-13 creates a function that clips to the path and paints the gradient from the bottom up. You specify what percentage to rise. Figure 6-22 built its bottom glow using 20% white going 40% up the outer rounded-rectangle path.

Listing 6-13 Adding a Bottom Glow


void DrawBottomGlow(UIBezierPath *path,
UIColor *color, CGFloat percent)
{
if (!path) COMPLAIN_AND_BAIL(
@"Path cannot be nil", nil);
if (!color) COMPLAIN_AND_BAIL(
@"Color cannot be nil", nil);
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

CGRect rect = path.calculatedBounds;
CGPoint h1 = RectGetPointAtPercents(rect, 0.5f, 1.0f);
CGPoint h2 = RectGetPointAtPercents(
rect, 0.5f, 1.0f - percent);

Gradient *gradient = [Gradient
easeInOutGradientBetween:color and:
[color colorWithAlphaComponent:0.0f]];

PushDraw(^{
[path addClip];
[gradient drawFrom:h1 toPoint:h2];
});
}


Building an Elliptical Gradient Overlay

A gradient gloss overlay bounded to an oval path provides another way to introduce light and depth. These are similar to the button glosses you already read about in Listings 6-11 and 6-12. Figure 6-23 shows an example of adding this kind of gloss.

Image

Figure 6-23 Painting an elliptical gloss.

The underlying algorithm is simple, as you see in Listing 6-14: You build an ellipse with the same height as the source path. Stretch its width out to either side. Then move the ellipse up, typically to 0.45 percent from the top edge.

Before drawing, clip the context to the intersection of the original path and the ellipse. Two addClip commands handle this request. To finish, paint a clear-to-white gradient from the top of the path to the bottom of the ellipse.

Listing 6-14 Drawing a Top Shine


void DrawIconTopLight(UIBezierPath *path, CGFloat p)
{
if (!path) COMPLAIN_AND_BAIL(
@"Path cannot be nil", nil);
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(
@"No context to draw into", nil);

// Establish an ellipse and move it up to cover just
// p percent of the parent path
CGFloat percent = 1.0f - p;
CGRect rect = path.bounds;
CGRect offset = rect;
offset.origin.y -= percent * offset.size.height;
offset = CGRectInset(offset,
-offset.size.width * 0.3f, 0);

// Build an oval path
UIBezierPath *ovalPath = [UIBezierPath
bezierPathWithOvalInRect:offset];
Gradient *gradient = [Gradient
gradientFrom:WHITE_LEVEL(1, 0.0)
to:WHITE_LEVEL(1, 0.5)];;

PushDraw(^{
[path addClip];
[ovalPath addClip];

// Draw gradient
CGPoint p1 = RectGetPointAtPercents(rect, 0.5, 0.0);
CGPoint p2 = RectGetPointAtPercents(
ovalPath.bounds, 0.5, 1.0);
[gradient drawFrom:p1 toPoint:p2];
});
}


Summary

This chapter introduces gradients, demonstrating powerful ways you can use them in your iOS drawing tasks. Here are some final thoughts about gradients:

• Gradients add a pseudo-dimension to interface objects. They enable you to mimic the effect of light and distance in your code. You’ll find a wealth of Photoshop tutorials around the Web that show you how to build 3D effects into 2D drawing. I highly encourage you to seek these out and follow their steps using Quartz and UIKit implementations.

• This chapter uses linear and radial gradients, with and without Quartz shadows, to create effects. Always be careful to balance computational overhead against the visual beauty you intend to create.

• iOS 7 does not mandate “flat design” in your interfaces. Of course, if you build an application primarily around system-supplied components, try to match your app to the Apple design aesthetics. Otherwise, let your design imagination act as your compass to create apps centered on deference, clarity, and depth.