The Language of Geometry - iOS Drawing: Practical UIKit Solutions (2014)

iOS Drawing: Practical UIKit Solutions (2014)

Chapter 2. The Language of Geometry

Drawing and geometry are inextricably linked. In order to express a drawing operation to the compiler, you must describe it with geometric descriptions that iOS can interpret on your behalf. This chapter reviews basics you’ll need to get started. It begins with the point-pixel dichotomy, continues by diving into core structures, and then moves to UIKit objects. You’ll learn what these items are and the roles they play in drawing.

Points Versus Pixels

In iOS, points specify locations on the screen and in drawings. They provide a unit of measure that describes positions and extents for drawing operations. Points have no fixed relationship to physical-world measurements or specific screen hardware. They enable you to refer to positions independently of the device being used.

Points are not pixels. Pixels are addressable screen components and are tied directly to specific device hardware. Each pixel can individually be set to some brightness and color value. A point, in contrast, refers to a logical coordinate space.

For example, all members of the iPhone family present a display that is 320 points wide in portrait orientation. Those points correspond to 320 pixels wide on older units and 640 pixels wide on newer Retina-based models. A unified coordinate system applies across all iPhone devices, regardless of whether you’re using newer Retina models or older, lower pixel-density units.

As Figure 2-1 shows, the position (160.0, 240.0) sits in the center of the 3.5-inch iPhone or iPod touch screens in portrait orientation, regardless of pixel density. That same point rests a bit above center on 4-inch Retina-based iPhones and iPod touches. The natural center for these newer devices is (160.0, 284.0).

Image

Figure 2-1 The logical point (160.0, 240.0) displayed on a variety of target devices. Top row: Original iPhone, 3.5-inch Retina iPhone, 4-inch Retina iPhone. Middle row: Original iPhone, 3.5-inch Retina iPhone, 4-inch Retina iPhone. Bottom row: Portrait (Retina) iPad, landscape (Retina) iPad.

In landscape orientation, the same point lies toward the bottom-left corner of iPhone screens. On the larger iPad screen, this appears towards the top-left corner of the screen.

Scale

The UIScreen class provides a property called scale. This property expresses the relationship between a display’s pixel density and its point system. A screen’s scale is used to convert from the logical coordinate space of the view system measured in points to the physical pixel coordinates. Retina displays use a scale of 2.0, and non-Retina displays use a scale of 1.0. You can test for a Retina device by checking the main screen’s scale:

- (BOOL) hasRetinaDisplay
{
return ([UIScreen mainScreen].scale == 2.0f);
}

The main screen always refers to the device’s onboard display. Other screens may be attached over AirPlay and via Apple’s connector cables. Each screen provides an availableModes property. This supplies an array of resolution objects, ordered from lowest to highest resolution.

Many screens support multiple modes. For example, a VGA display might offer as many as a half dozen or more different resolutions. The number of supported resolutions varies by hardware. There will always be at least one resolution available, but you should be able to offer choices to users when there are more.

The UIScreen class also offers two useful display-size properties. The bounds property returns the screen’s bounding rectangle, measured in points. This gives you the full size of the screen, regardless of any onscreen elements like status bars, navigation bars, or tab bars. TheapplicationFrame property is also measured in points. It excludes the status bar if it is visible, providing the frame for your application’s initial window size.

iOS Devices

Table 2-1 summarizes the iOS families of devices, listing the addressable coordinate space for each family. While there are five distinct display families to work with, you encounter only three logical spaces at this time. (Apple may introduce new geometries in the future.) This breaks down design to three groups:

3.5-inch iPhone-like devices (320.0 by 480.0 points)

4-inch iPhone-like devices (320.0 by 568.0 points)

iPads (768.0 by 1024.0 points)

Image

Table 2-1 iOS Devices and Their Logical Coordinate Spaces

View Coordinates

The numbers you supply to drawing routines are often tightly tied to a view you’re drawing to, especially when working within drawRect: methods. Every view’s native coordinate system starts at its top-left corner.

In iOS 7 and later, a view controller’s origin may or may not start below a navigation bar depending on how you’ve set the controller’s edgesForExtendedLayout property. By default, views now stretch under bars, offering an edge-to-edge UI experience.

Frames and Bounds

Views live in (at least) two worlds. Each view’s frame is defined in the coordinate system of its parent. It specifies a rectangle marking the view’s location and size within that parent. A view’s bounds is defined in its own coordinate system, so its origin defaults to (0, 0). (This can change when you display just a portion of the view, such as when working with scroll views.) A view’s bounds and frame are tightly coupled. Changing a view’s frame size affects its bounds and vice versa.

Consider Figure 2-2. The gray view’s origin starts at position (80, 100). It extends 200 points horizontally and 150 points vertically. Its frame in its parent’s coordinate system is {80, 100, 200, 150}. In its own coordinate system, its bounds are {0, 0, 200, 150}. The extent stays the same; the relative origin changes.

Image

Image

Figure 2-2 The position of any point depends on the coordinate system it’s participating in.

A point’s location is contingent on the coordinate system it’s being defined for. In Figure 2-2, a circle surrounds a point in the gray view. In the gray view’s coordinate system, this point appears at position (66, 86)—that is, 66 points down and 86 points to the right of the view’s origin.

In the parent’s coordinate system, this point corresponds to position (146, 186). That is 146 points down and 186 points to the right of the parent’s origin, which is the top left of the white backsplash. To convert from the view’s coordinate system to the parent’s coordinate system, you add the view’s origin (80, 100) to the point. The result is (66 + 80, 100 + 86), or (146, 186).


Note

A view frame is calculated from its bounds, its center, and any transform applied to the view. It expresses the smallest rectangle that contains the view within the parent.


Converting Between Coordinate Systems

The iOS SDK offers several methods to move between coordinate systems. For example, you might want to convert the position of a point from a view’s coordinate system to its parent’s coordinate system to determine where a drawn point falls within the parent view. Here’s how you do that:

CGPoint convertedPoint =
[outerView convertPoint:samplePoint fromView:grayView];

You call the conversion method on any view instance. Specify whether you want to convert a point into another coordinate system (toView:) or from another coordinate system (fromView:), as was done in this example.

The views involved must live within the same UIWindow, or the math will not make sense. The views do not have to have any particular relationship, however. They can be siblings, parent/child, ancestor/child, or whatever. The methods return a point with respect to the origin you specify.

Conversion math applies to rectangles as well as points. To convert a rectangle from a view into another coordinate system, use convertRect:fromView:. To convert back, use convertRect:toView:.

Key Structures

iOS drawing uses four key structures to define geometric primitives: points, sizes, rectangles, and transforms. These structures all use a common unit, the logical point. Points are defined using CGFloat values. These are type defined as float on iOS and double on OS X.

Unlike pixels, which are inherently integers, points are not tied to device hardware. Their values refer to mathematical coordinates, offering subpixel accuracy. The iOS drawing system handles the math on your behalf.

The four primitives you work with are as follows:

Image CGPoint—Points structures are made up of an x and a y coordinate. They define logical positions.

Image CGSize—Size structures have a width and a height. They establish extent.

Image CGRect—Rectangles include both an origin, defined by a point, and a size.

Image CGAffineTransform—Affine transform structures describe changes applied to a geometric item—specifically, how an item is placed, scaled, and rotated. They store the a, b, c, d, tx, and ty values in a matrix that defines a particular transform.

The next sections introduce these items in more depth. You need to have a working knowledge of these geometric basics before you dive into the specifics of drawing.

Points

The CGPoint structure stores a logical position, which you define by assigning values to the x and y fields. A convenience function CGPointMake() builds a point structure from two parameters that you pass:

struct CGPoint {
CGFloat x;
CGFloat y;
};

These values may store any floating-point number. Negative values are just as valid as positive ones.

Sizes

The CGSize structure stores two extents, width and height. Use CGSizeMake() to build sizes. Heights and widths may be negative as well as positive, although that’s not usual or common in day-to-day practice:

struct CGSize {
CGFloat width;
CGFloat height;
};

Rectangles

The CGRect structure is made up of two substructures: CGPoint, which defines the rectangle’s origin, and CGSize, which defines its extent. The CGRectMake() function takes four arguments and returns a populated rect structure. In order, the arguments are x-origin, y-origin, width, and height. For example, CGRectMake(0, 0, 50, 100):

struct CGRect {
CGPoint origin;
CGSize size;
};

Call CGRectStandardize() to convert a rectangle with negative extents to an equivalent version with a positive width and height.

Transforms

Transforms represent one of the most powerful aspects of iOS geometry. They allow points in one coordinate system to transform into another coordinate system. They allow you to scale, rotate, mirror, and translate elements of your drawings, while preserving linearity and relative proportions. You encounter drawing transforms primarily when you adjust a drawing context, as you saw in Chapter 1, or when you manipulate paths (shapes), as you read about in Chapter 5.

Widely used in both 2D and 3D graphics, transforms provide a complex mechanism for many geometry solutions. The Core Graphics version (CGAffineTransform) uses a 3-by-3 matrix, making it a 2D-only solution. 3D transforms, which use a 4-by-4 matrix, are the default for Core Animation layers. Quartz transforms enable you to scale, translate, and rotate geometry.

Every transform is represented by an underlying transformation matrix, which is set up as follows:

Image

This matrix corresponds to a simple C structure:

struct CGAffineTransform {
CGFloat a;
CGFloat b;
CGFloat c;
CGFloat d;
CGFloat tx;
CGFloat ty;
};

Creating Transforms

Unlike other Core Graphics structures, you rarely access an affine transform’s fields directly. Most people won’t ever use or need the CGAffineTransformMake() constructor function, which takes each of the six components as parameters.

Instead, the typical entry point for creating transforms is either CGAffineTransformMakeScale(), CGAffineTransformMakeRotation(), or CGAffineTransformMakeTranslation(). These functions build scaling, rotation, and translation (position offset) matrices from the parameters you supply. Using these enables you to discuss the operations you want to apply in the terms you want to apply them. Need to rotate an object? Specify the number of degrees. Want to move an object? State the offset in points. Each function creates a transform that, when applied, produces the operation you specify.

Layering Transforms

Related functions enable you to layer one transform onto another, building complexity. Unlike the first set of functions, these take a transform as a parameter. They modify that transform to layer another operation on top. Unlike the “make” functions, these are always applied in sequence, and the result of each function is passed to the next. For example, you might create a transform that rotates and scales an object around its center. The following snippet demonstrates how you might do that:

CGAffineTransform t = CGAffineTransformIdentity;
t = CGAffineTransformTranslate(t, -center.x, -center.y);
t = CGAffineTransformRotate(t, M_PI_4);
t = CGAffineTransformScale(t, 1.5, 1.5);
t = CGAffineTransformTranslate(t, center.x, center.y);

This begins with the identity transform. The identity transform is the affine equivalent of the number 0 in addition or the number 1 in multiplication. When applied to any geometric object, it returns the identical presentation you started with. Using it here ensures a consistent starting point for later operations.

Because transforms are applied from their origin, and not their center, you then translate the center to the origin. Scale and rotation transforms always relate to (0, 0). If you want to relate them to another point, such as the center of a view or a path, you should always move that point to (0, 0). There, you perform the rotation and scaling before resetting the origin back to its initial position. This entire set of operations is stored in the single, final transform, t.

Exposing Transforms

The UIKit framework defines a variety of helper functions specific to graphics and drawing operations. These include several affine-specific functions. You can print out a view’s transform via UIKit’s NSStringFromCGAffineTransform() function. Its inverse isCGAffineTransformFromString(). Here’s what the values look like when logged for a transform that’s scaled by a factor of 1.5 and rotated by 45 degrees:

2013-03-31 09:43:20.837 HelloWorld[41450:c07]
[1.06066, 1.06066, -1.06066, 1.06066, 0, 0]

This particular transform skips any center reorientation, so you’re only looking at these two operations.

These raw numbers aren’t especially meaningful. Specifically, this representation does not tell you directly exactly how much the transform scales or rotates. Fortunately, there’s an easy way around this, a way of transforming unintuitive parameters into more helpful representations. Listing 2-1 shows how. It calculates x- and y-scale values as well as rotation, returning these values from the components of the transform structure.

There’s never any need to calculate the translation (position offset) values. These values are stored for you directly in the tx and ty fields, essentially in “plain text.”

Listing 2-1 Extracting Scale and Rotation from Transforms


// Extract the x scale from transform
CGFloat TransformGetXScale(CGAffineTransform t)
{
return sqrt(t.a * t.a + t.c * t.c);
}

// Extract the y scale from transform
CGFloat TransformGetYScale(CGAffineTransform t)
{
return sqrt(t.b * t.b + t.d * t.d);
}

// Extract the rotation in radians
CGFloat TransformGetRotation(CGAffineTransform t)
{
return atan2f(t.b, t.a);
}


Predefined Constants

Every Core Graphics structure has predefined constants. The most common ones you’ll encounter when working in Quartz are as follows:

Image Geometric zeros—These constants provide defaults with zero values. CGPointZero is a point constant that refers to the location (0, 0). The CGSizeZero constant references an extent whose width and height are 0. CGRectZero is equivalent to CGRectMake(0,0,0,0), with a zero origin and size.

Image Geometric identity—CGAffineTransformIdentity supplies a constant identity transform. When applied to any geometric element, this transform returns the same element you started with.

Image Geometric infinity—CGRectInfinite is a rectangle with infinite extent. The width and heights are set to CGFLOAT_MAX, the highest possible floating-point value. The origin is set to the most negative number possible (-CGFLOAT_MAX/2).

Image Geometric nulls—CGRectNull is distinct from CGRectZero in that it has no position. CGRectZero’s position is (0, 0). CGRectNull’s position is (INFINITY, INFINITY). You encounter this rectangle whenever you request the intersection of disjoint rectangles.

The union of any rectangle with CGRectNull always returns the original rect. For example, the union of CGRectMake(10, 10, 10, 10) with CGRectNull is {{10, 10}, {10, 10}}. Contrast this with the union with CGRectZero, which is {{0, 0}, {20, 20}} instead. The origin is pulled toward (0, 0), doubling the rectangle’s size and changing its position.

Conversion to Objects

Because geometric items are structures and not objects, integrating them into standard Objective-C can be challenging. You cannot add a size or a rectangle to an NSArray or a dictionary. You can’t set a default value to a point or a transform. Because of this, Core Graphics and Core Foundation provide functions and classes to convert and encapsulate structures within objects. The most common object solutions for geometric structures are strings, dictionaries, and values.

Strings

You morph structures to string representation and back by using a handful of conversion functions, which are listed in Table 2-2. These convenience functions enable you to log information in human-readable format or store structures to files using a well-defined and easy-to-recover pattern. A converted point or size (for example, NSStringFromCGPoint(CGPointMake(5, 2))) looks like this: {5, 2}.

Image

Table 2-2 String Utility Functions

Strings are most useful for logging information to the debugging console.

Dictionaries

Dictionaries provide another way to transform geometry structures to and from objects for storage and logging. As Table 2-3 reveals, these are limited to points, sizes, and rectangles. What’s more, they return Core Foundation CFDictionaryRef instances, not NSDictionary items. You need to bridge the results.

Image

Table 2-3 Dictionary Utility Functions

Dictionaries are most useful for storing structures to user defaults.

The following code converts a rectangle to a Cocoa Touch dictionary representation and then back to a CGRect, logging the results along the way:

// Convert CGRect to NSDictionary representation
CGRect testRect = CGRectMake(1, 2, 3, 4);
NSDictionary *dict = (__bridge_transfer NSDictionary *)
CGRectCreateDictionaryRepresentation(testRect);
NSLog(@"Dictionary: %@", dict);

// Convert NSDictionary representation to CGRect
CGRect outputRect;
BOOL success = CGRectMakeWithDictionaryRepresentation(
(__bridge CFDictionaryRef) dict, &outputRect);
if (success)
NSLog(@"Rect: %@", NSStringFromCGRect(outputRect));

Here is the output this code produces. In it you see the dictionary representation for the test rectangle and the restored rectangle after being converted back to a CGRect:

2013-04-02 08:23:07.323 HelloWorld[62600:c07] Dictionary: {
Height = 4;
Width = 3;
X = 1;
Y = 2;
}
2013-04-02 08:23:07.323 HelloWorld[62600:c07]
Rect: {{1, 2}, {3, 4}}


Note

The CGRectMakeWithDictionaryRepresentation() function has a known bug that makes some values created on OS X unreadable on iOS.


Values

The NSValue class provides a container for C data items. It can hold scalar types (like integers and floats), pointers, and structures. UIKit extends normal NSValue behavior to encapsulate Core Graphics primitives. When a scalar type is placed into a value, you can add geometric primitives to Objective-C collections and treat them like any other object.

Values are most useful for adding structures to arrays and dictionaries for in-app algorithms.

Image

Table 2-4 Value Methods


Note

NSCoder provides UIKit-specific geometric encoding and decoding methods for CGPoint, CGSize, CGRect, and CGAffineTransform. These methods enable you to store and recover these geometric structures, associating them with a key you specify.


Geometry Tests

Core Graphics offers a basic set of geometry testing functions, which you see listed in Table 2-5. These items enable you to compare items against each other and check for special conditions that you may encounter. These functions are named to be self-explanatory.

Image

Table 2-5 Checking Geometry

Other Essential Functions

Here are a few final functions you’ll want to know about:

Image CGRectInset(rect, xinset, yinset)—This function enables you to create a smaller or larger rectangle that’s centered on the same point as the source rectangle. Use a positive inset for smaller rectangles, negative for larger ones. This function is particularly useful for moving drawings and subimages away from view edges to provide whitespace breathing room.

Image CGRectOffset(rect, xoffset, yoffset)—This function returns a rectangle that’s offset from the original rectangle by an x and y amount you specify. Offsets are handy for moving frames around the screen and for creating easy drop-shadow effects.

Image CGRectGetMidX(rect) and CGRectGetMidY(rect )—These functions recover the x and y coordinates in the center of a rectangle. These functions make it very convenient to recover the midpoints of bounds and frames. Related functions return minX, maxX, minY, maxY, width, and height. Midpoints help center items in a drawing.

Image CGRectUnion(rect1, rect2)—This function returns the smallest rectangle that entirely contains both source rectangles. This function helps you establish the minimum bounding box for distinct elements you’re drawing. The union of drawing paths lets you frame items together and build backsplashes for just those items.

Image CGRectIntersection(rect1, rect2)—This function returns the intersection of two rectangles or, if the two do not intersect, CGRectNull. Intersections help calculate rectangle insets when working with Auto Layout. The intersections between a path’s bounds and an image frame can define the intrinsic content used for alignment.

Image CGRectIntegral(rect)—This function converts the source rectangle to integers. The origin values are rounded down from any fractional values to whole integers. The size is rounded upward. You are guaranteed that the new rectangle fully contains the original rectangle. Integral rectangles speed up and clarify your drawing. Views drawn exactly on pixel boundaries require less antialiasing and result in less blurry output.

Image CGRectStandardize(rect)—This function returns an equivalent rectangle with positive height and width values. Standardized rectangles simplify your math when you’re performing interactive drawing, specifically when users may interactively move left and up instead of right and down.

Image CGRectDivide(rect, &sliceRect, &remainderRect, amount, edge)—This function is the most complicated of these Core Graphics functions, but it is also among the most useful. Division enables you to iteratively slice a rectangle into portions, so you can subdivide the drawing area.

Using CGRectDivide()

The CGRectDivide() function is terrifically handy. It provides a really simple way to divide and subdivide a rectangle into sections. At each step, you specify how many to slice away and which side to slice it away from. You can cut from any edge, namely CGRectMinXEdge,CGRectMinYEdge, CGRectMaxXEdge, and CGRectMaxYEdge.

A series of these calls built the image in Figure 2-3. Listing 2-2 shows how this was done. The code slices off the left edge of the rectangle and then divides the remaining portion into two vertical halves. Removing two equal sections from the left and the right further decomposes the bottom section.

Image

Figure 2-3 You can subdivide rectangles by iteratively slicing off sections.

Listing 2-2 Creating a Series of Rectangle Divisions


UIBezierPath *path;
CGRect remainder;
CGRect slice;

// Slice a section off the left and color it orange
CGRectDivide(rect, &slice, &remainder, 80, CGRectMinXEdge);
[[UIColor orangeColor] set];
path = [UIBezierPath bezierPathWithRect:slice];
[path fill];

// Slice the other portion in half horizontally
rect = remainder;
CGRectDivide(rect, &slice, &remainder,
remainder.size.height / 2, CGRectMinYEdge);

// Tint the sliced portion purple
[[UIColor purpleColor] set];
path = [UIBezierPath bezierPathWithRect:slice];
[path fill];

// Slice a 20-point segment from the bottom left.
// Draw it in gray
rect = remainder;
CGRectDivide(rect, &slice, &remainder, 20, CGRectMinXEdge);
[[UIColor grayColor] set];
path = [UIBezierPath bezierPathWithRect:slice];
[path fill];

// And another 20-point segment from the bottom right.
// Draw it in gray
rect = remainder;
CGRectDivide(rect, &slice, &remainder, 20, CGRectMaxXEdge);
// Use same color on the right
path = [UIBezierPath bezierPathWithRect:slice];
[path fill];

// Fill the rest in brown
[[UIColor brownColor] set];
path = [UIBezierPath bezierPathWithRect:remainder];
[path fill];


Rectangle Utilities

You use the CGRectMake() function to build frames, bounds, and other rectangle arguments. It accepts four floating-point arguments: x, y, width, and height. This is one of the most important functions you use in Quartz drawing.

There are often times when you’ll want to construct a rectangle from the things you usually work with: points and size. Although you can use component fields to retrieve arguments, you may find it helpful to have a simpler function on hand—one that is specific to these structures. Listing 2-3 builds a rectangle from point and size structures.

Listing 2-3 Building Rectangles from Points and Sizes


CGRect RectMakeRect(CGPoint origin, CGSize size)
{
return (CGRect){.origin = origin, .size = size};
}


Quartz, surprisingly, doesn’t supply a built-in routine to retrieve a rectangle’s center. Although you can easily pull out x and y midpoints by using Core Graphics functions, it’s handy to implement a function that returns a point directly from the rect. Listing 2-4 defines that function. It’s one that I find myself using extensively in drawing work.

Listing 2-4 Retrieving a Rectangle Center


CGPoint RectGetCenter(CGRect rect)
{
return CGPointMake(CGRectGetMidX(rect),
CGRectGetMidY(rect));
}


Building a rectangle around a center is another common challenge. For example, you might want to center text or place a shape around a point. Listing 2-5 implements this functionality. You supply a center and a size. It returns a rectangle that describes your target placement.

Listing 2-5 Creating a Rectangle Around a Target Point


CGRect RectAroundCenter(CGPoint center, CGSize size)
{
CGFloat halfWidth = size.width / 2.0f;
CGFloat halfHeight = size.height / 2.0f;

return CGRectMake(center.x - halfWidth,
center.y - halfHeight, size.width, size.height);
}


Listing 2-6 uses Listings 2-4 and 2-5 to draw a string centered in a given rectangle. It calculates the size of the string (using iOS 7 APIs) and the rectangle center and builds a target around that center. Figure 2-4 shows the result.

Listing 2-6 Centering a String


NSString *string = @"Hello World";
UIFont *font =
[UIFont fontWithName:@"HelveticaNeue" size:48];

// Calculate string size
CGSize stringSize =
[string sizeWithAttributes:{NSFontAttributeName:font}];

// Find the target rectangle
CGRect target =
RectAroundCenter(RectGetCenter(grayRectangle), stringSize);

// Draw the string
[greenColor set];
[string drawInRect:target withFont:font];


Image

Figure 2-4 Drawing a string into the center of a rectangle. The dashed line indicates the outline of the target rectangle.

Another way to approach the same centering problem is shown in Listing 2-7. It takes an existing rectangle and centers it within another rectangle rather than working with a size and a target point. You might use this function when working with a Bezier path whose bounds property returns a bounding rectangle. You can center that path in a rectangle by calling RectCenteredInRect().

Listing 2-7 Centering a Rectangle


CGRect RectCenteredInRect(CGRect rect, CGRect mainRect)
{
CGFloat dx = CGRectGetMidX(mainRect) - CGRectGetMidX(rect);
CGFloat dy = CGRectGetMidY(mainRect) - CGRectGetMidY(rect);
return CGRectOffset(rect, dx, dy);
}


Fitting and Filling

Often you need to resize a drawing to fit into a smaller or larger space than its natural size. To accomplish this, you calculate a target for that drawing, whether you’re working with paths, images, or context drawing functions. There are four basic approaches you take to accomplish this: centering, fitting, filling, and squeezing.

If you’ve worked with view content modes, these approaches may sound familiar. When drawing, you apply the same strategies that UIKit uses for filling views with content. The corresponding content modes are center, scale aspect fit, scale aspect fill, and scale to fill.

Centering

Centering places a rectangle at its natural scale, directly in the center of the destination. Material larger than the pixel display area is cropped. Material smaller than that area is matted. (Leaving extra area on all four sides of the drawing is called windowboxing.) Figure 2-5 shows two sources, one larger than the destination and the other smaller.

Image

Figure 2-5 Centering a rectangle leaves its original size unchanged. Accordingly, cropping and padding may or may not occur.

In each case, the items are centered using RectAroundCenter() from Listing 2-5. Since the outer figure is naturally larger, it would be cropped when drawn to the gray rectangle. The smaller inner figure would be matted (padded) on all sides with extra space.

Fitting

When you fit items into a target, you retain the original proportions of the source rectangle and every part of the source material remains visible. Depending on the original aspect ratio, the results are either letterboxed or pillarboxed, with some extra area needed to mat the image.

In the physical world, matting refers to a backdrop or border used in framing pictures. The backdrop or border adds space between the art and the physical frame. Here, I use the term matting to mean the extra space between the edges of the drawing area and the extent of the destination rectangle.

Figure 2-6 shows items fit into target destinations. The gray background represents the destination rectangle. The light purple background shows the fitting rectangle calculated by Listing 2-8. Unless the aspects match exactly, the drawing area will be centered, leaving extra space on the top and bottom or on the sides.

Image

Figure 2-6 Fitting into the destination preserves the original aspect but may leave letterbox (top) or pillarbox (bottom) extra spaces.

Listing 2-8 Calculating a Destination by Fitting to a Rectangle


// Multiply the size components by the factor
CGSize SizeScaleByFactor(CGSize aSize, CGFloat factor)
{
return CGSizeMake(aSize.width * factor,
aSize.height * factor);
}

// Calculate scale for fitting a size to a destination
CGFloat AspectScaleFit(CGSize sourceSize, CGRect destRect)
{
CGSize destSize = destRect.size;
CGFloat scaleW = destSize.width / sourceSize.width;
CGFloat scaleH = destSize.height / sourceSize.height;
return MIN(scaleW, scaleH);
}

// Return a rect fitting a source to a destination
CGRect RectByFittingInRect(CGRect sourceRect,
CGRect destinationRect)
{
CGFloat aspect =
AspectScaleFit(sourceRect.size, destinationRect);
CGSize targetSize =
SizeScaleByFactor(sourceRect.size, aspect);
return RectAroundCenter(
RectGetCenter(destinationRect), targetSize);
}


Filling

Filling, which is shown in Figure 2-7, ensures that every pixel of the destination space corresponds to the drawing area within the source image. Not to be confused with the drawing “fill” operation (which fills a path with a specific color or pattern), this approach avoids the excess pixel areas you saw in Figure 2-6.

Image

Figure 2-7 Filling a destination ensures that the target rectangle covers every pixel.

To accomplish this, filling grows or shrinks the source rectangle to exactly cover the destination. It crops any elements that fall outside the destination, either to the top and bottom, or to the left and right. The resulting target rectangle is centered, but only one dimension can be fully displayed.

Listing 2-9 calculates the target rectangle, and Figure 2-7 shows the results. In the left image, the ears and feet would be cropped. In the right image, the nose and tail would be cropped. Only images that exactly match the target’s aspect remain uncropped. Filling trades off cropping against fitting’s letterboxing.

Listing 2-9 Calculating a Destination by Filling a Rectangle


// Calculate scale for filling a destination
CGFloat AspectScaleFill(CGSize sourceSize, CGRect destRect)
{
CGSize destSize = destRect.size;
CGFloat scaleW = destSize.width / sourceSize.width;
CGFloat scaleH = destSize.height / sourceSize.height;
return MAX(scaleW, scaleH);
}

// Return a rect that fills the destination
CGRect RectByFillingRect(CGRect sourceRect, CGRect destinationRect)
{
CGFloat aspect = AspectScaleFill(sourceRect.size, destinationRect);
CGSize targetSize = SizeScaleByFactor(sourceRect.size, aspect);
return RectAroundCenter(RectGetCenter(destinationRect), targetSize);
}


Squeezing

Squeezing adjusts the source’s aspect to fit all available space within the destination. All destination pixels correspond to material from the source rectangle. As Figure 2-8 shows, the material resizes along one dimension to accommodate. Here, the bunny squeezes horizontally to match the destination rectangle.

Image

Figure 2-8 Squeezing a source rectangle ignores its original aspect ratio, drawing to the destination’s aspect.

Unlike with the previous fitting styles, with squeezing, you don’t have to perform any calculations. Just draw the source material to the destination rectangle and let Quartz do the rest of the work.

Summary

This chapter introduces basic terminology, data types, and manipulation functions for Core Graphics drawing. You read about the difference between points and pixels, explored common data types, and learned ways to calculate drawing positions. Here are a few final thoughts to carry on with you as you leave this chapter.

Image Always write your code with an awareness of screen scale. Relatively modern devices like the iPad 2 and the first-generation iPad mini do not use Retina displays. Focusing on logical space (points) instead of device space (pixels) enables you to build flexible code that supports upgrades and new device geometries.

Image A lot of drawing is relative, especially when you’re working with touches on the screen. Learn to migrate your points from one view coordinate system to another, so your drawing code can use the locations you intend instead of the point locations you inherit.

Image Affine transforms are often thought of as the “other” Core Graphics struct, but they play just as important a role as their more common cousins—the points, sizes, and rectangles. Transforms are wicked powerful and can simplify drawing tasks in many ways. If you can spare the time, you’ll find it’s a great investment of your effort to learn about transforms and the matrix math behind them. Khan Academy (www.khanacademy.org) offers excellent tutorials on this subject.

Image A robust library of routine geometric functions can travel with you from one project to the next. Basic geometry never goes out of style, and the same kinds of tasks occur over and over again. Knowing how to center rectangles within one another and how to calculate fitting and filling destinations are skills that will save you time and effort in the long run.