December 96 - QuickDraw GX Line Layout:
QuickDraw GX Line Layout: Bending the Rules
Daniel I. Lipton
Many high-end drawing applications provide the user with a way to
draw text along an arbitrary path. Some of them even provide the
means to edit this text in place. The methods discussed here will
illustrate how to do both in a QuickDraw GX-based application. In
fact, I'll show you how QuickDraw GX enables your application to
provide better line layout capabilities than those currently
available in mainstream graphics arts applications.
One of the things that makes QuickDraw GX interesting is its amazing typography --
whatever else has been said about GX, its type capabilities are universally
acknowledged as the best. Besides aesthetics, a distinguishing attribute of QuickDraw
GX typography is not what is graphically possible, but the ease with which visual
content can be created. A user can create documents that are kerned or tracked or have
swashes, ligatures, and so on without QuickDraw GX, but achieving these effects
requires a great deal of manual labor, with lots of Shift-Option-Command key
combinations and switching to special fonts -- assuming the developer has made the
effort to provide such a level of functionality. With QuickDraw GX, these effects are
features designed into the font itself, so all the user needs to do is type and the text
comes out beautifully. For different effects, the user just picks font features from a
dialog or palette. The time saved by the user can be 90% over a non-GX interface! It
simply has to be experienced to be appreciated.
All of this great typography is easy for the user to use because it's easy for the
developer to implement. How many pre-GX drawing and illustration packages support
the same level of typographic quality as their pre-GX publishing package
counterparts? Not many, and of those that do, how much more memory do those
applications require than they would without the typography code? Thanks to
QuickDraw GX, we're beginning to see drawing and illustration programs that
incorporate high-end typographic features and page layout programs that incorporate
high-end graphics features -- all with lower overall memory requirements (when
compared with traditional non-GX applications) because the code is in the system and
is shared between applications.
Let me get to the real topic of this article. A favorite feature in illustration and page
layout programs is to enable the user to lay a line of text along an arbitrary path, be it
straight or curved. My goal here is to provide the developer with a way to do this,
using QuickDraw GX, that's familiar to those who have been using GX and enticing to
those who haven't tried it yet. If you've written code to handle text on a path in a
non-GX application, consider the amount of code needed to do the editing on the screen
and the amount of code you had to write to make it print on a PostScript printer. Then
compare it to what's presented here -- I think the QuickDraw GX advantage will be
clear!
I'll assume you have some knowledge of typography and a working knowledge of
QuickDraw GX throughout my discussion. If you need to brush up on GX, there are the
Inside Macintosh: QuickDraw GX books; the Programmer's Overview andTypography volumes are most relevant here.
Accompanying this article on this issue's CD and develop's Web site is a library of code
called CurveLayout which very closely parallels the QuickDraw GX line layout
mechanism but with support for curved layouts. The functions of this library will be
described later.
SIMPLE INTERFACE TO DRAW TEXT ON A PATH
It turns out that QuickDraw GX provides an extremely simple method for drawing text
along a curve, since it lets us make any shape a dash outline for any other shape -- to
dash a shape means to draw another shape in a repeating pattern along the perimeter of
the first shape (see Figure 1). The PostScript language, by comparison, allow only
simple dashing.
Figure 1. Dashing a shape
Since QuickDraw GX text is a shape, you can draw text along a path by using dashing, as
shown in Listing 1.
______________________________
Listing 1. Drawing text on a curve using dashing
void PutTextOnCurve(gxShape myPath)
gxDashRecord aDash;
gxShape textShape = GXNewText(5, "Hello", nil);
// Call primitive shape to convert text to glyph.
GXPrimitiveShape(textShape); // All dashes must be primitive.
aDash.dash = textShape;
aDash.attributes = gxBreakDash;// Dash each letter separately.
aDash.advance = 0; // 0 advance means single repeat.
GXSetShapeDash(myPath, &aDash);
GXDisposeShape(textShape); // Dash is now sole owner of it.
GXSetShapeFill(myPath, gxFrameFill);
// Dash only for framed shapes.
} // PutTextOnCurve
______________________________
Listing 1 demonstrates just how simple it can be to put text on a path in QuickDraw GX.
This method would also work with layout shapes in addition to text shapes (although
there's a bug in GX 1.1.3 and earlier that may cause a crash if you do try to dash with a
layout). While dashing may be a simple interface for drawing text along a curve, it
isn't an efficient solution for editing because of the overhead incurred by constantly
rebuilding the dash every time the text changes. Additionally, the dashing solution puts
you at the mercy of the algorithms used for dashing -- they weren't specifically
designed for text or layout manipulation.
BEHIND CURVELAYOUT
The CurveLayout library discussed here provides the illusion of a new shape type for
QuickDraw GX which I affectionately call the "curve layout." I wanted to continue the
object-oriented philosophy of GX by providing an API for drawing and editing text
along a curved path. The idea is to provide an efficient mechanism for adding curve
layouts to your application without forcing you to learn too much new stuff. While the
article will go into great detail concerning the algorithms used, you need not
understand them to incorporate curve layouts into your application using the
CurveLayout library.
THE GLYPH SHAPE
Before we go too much further, it's worth discussing some things about our friend the
glyph shape. There are three different kinds of shapes for drawing text: the text shape,
the glyph shape, and the layout shape. Text shapes are the most familiar to those of us
who have used QuickDraw or PostScript. When a text shape is drawn, its appearance is
the same as the result of the DrawString call in QuickDraw or the show operator in
PostScript: simply the text itself.
The relationship between glyph and layout shapes can be compared to programming.
The layout shape is like a high-level language such as LISP -- a very high-level,
powerful way of drawing text that produces beautiful results, but it makes specific
manipulations difficult. The glyph shape is more like assembly language -- you can
control every aspect of how the text draws, but even simple things require a good deal
of programming effort. Nevertheless, it's the direct control possible with the glyph
shape that enabled me to write CurveLayout.
A glyph shape allows the specification of individual positions and tangent vectors (and
even typographic style) of every typographic element drawn in a shape.
Figure 2 illustrates the power of the glyph shape for our purposes. At left is a typical
straight piece of text. The arrows represent the tangent vectors stored in the glyph
shape and the dots at the ends of the arrow lines show the positions. (Note that both
here and in Figure 3, the tangent vectors are drawn as normals -- rotated 90 degrees
counterclockwise from their actual positions -- since otherwise they would run
together and make a mess of the figures.) Beside that is the same text with different
tangents and positions showing how these attributes might be manipulated to draw text
along a path. The tangent vector specifies the scale and rotation of the glyph and is
stored in the glyph shape as a point. The x and y values of this point are used to
construct the mapping shown at the right in the figure. This mapping is applied to the
glyph to reposition it as desired.
Figure 2. Glyph shape tangents, positions, and mapping matrix
But wait -- while glyph shapes give us the control we need to flow our glyphs along a
path (by setting positions and tangents for each glyph in the shape), they're still like
assembly language, and we don't want to position every glyph ourselves if we don't need
to. The good news is that QuickDraw GX provides a "compiler" to convert layout shapes
to glyph shapes, so we can use the high-level language (layout shape) instead. Our
compiler is the routine GXPrimitiveShape, which allows us to deal mostly in the
beautiful world of layouts and then convert them into glyph shapes that can have their
positions and tangents modified to flow along a path. Layout shapes also provide us with
the high-level abilities required for interactive editing (more on this later).
Now we've seen how we can convert a line layout shape into a glyph shape, and how the
glyph shape can have its positions and tangents modified so that the glyphs draw along a
path, but how do we generate those positions and tangents?
POINTS ALONG A PATH
QuickDraw GX can evaluate points along paths. GXShapeLengthToPoint accepts a
distance and a polygon or path and returns the point along that polygon or path that's
the specified distance along its perimeter. In addition to the point, the tangent vector at
that point (slope of the path or polygon) is returned, which is exactly what we need!
With this information, we can lay glyph shapes along a path by simply modifying the
tangents and positions appropriately.
The x coordinate of each glyph's position represents the current linear offset of the
glyph in the layout (as if it were along a straight line). A glyph's position can be
determined with the GXGetGlyphMetrics function and can be used as the length along the
path for GXShapeLengthToPoint. The returned point and tangent can then be inserted
into the glyph shape. The results of this technique are almost what we want, but
consider the example shown in Figure 3.
Figure 3. Glyphs positioned on a curve using x position as length along curve
In Figure 3, the arrows show the positions and tangents of glyphs that have been
rotated around their positions, which are typically on the bottom left (at least for
Roman fonts) rather than around their centers. Because of this, the text doesn't look
quite right -- some of the wider glyphs even seem to leap off the curve! We can do
better than this. We can use the horizontal centers of the glyphs (that is, the center of
the glyph along its baseline) as the input rather than their positions. Unfortunately,
the point we put back in the glyph shape must be for the position of the glyph, not its
center, so we have a little work to do translating back and forth.
Given a glyph shape, the function GXGetGlyphMetrics gave us the position information
that we needed to compute the points and tangents for our first attempt. This function
can also be used to obtain the bounding box of a glyph, and from the bounding box we
can determine the horizontal center points of each glyph.
In Figure 4, we see the glyphs for the word "Pig." The dots represent the glyph
positions obtained from GXGetGlyphMetrics and the starbursts represent the
horizontal center of each glyph along the x axis, determined by the bounding boxes,
which I've bisected for clarity. (Note that the position of a glyph doesn't necessarily
fall on the left edge of the glyph's bounding box.)
Figure 4. Glyph shape positions and horizontal centers
Listing 2 shows the loop for repositioning glyphs from CurveLayout. It illustrates how
to compute the new glyph position given the location and tangent returned from
GXShapeLengthToPoint, using the horizontal center to adjust the input length rather
than merely using the glyph's x position. Before looping through the glyphs, we get
some necessary information about the glyph shapes: the tangents (the glyph shape may
have rotated glyphs to begin with, and we'd like to preserve them), the positions, and
the bounding boxes. Then, for each glyph we do the following:
1. Compute the horizontal center of the glyph's bounding box.
2. Compute a vector that describes the glyph's position relative to the
horizontal center of the bounding box (we'll use this in step 4).
3. Find out the position and tangent on the curve using the horizontal center
of the glyph as the length along the curve.
4. Compute the new glyph position. Since earlier we described the glyph
position as a vector relative to the horizontal bounding box center, and the
point returned from GXShapeLengthToPoint is on the curve at the place where
we want the horizontal bounding box center, we can compute the new glyph
position as the vector we saved translated to the new position and rotated by
the angle described by the tangent.
5. Compute the new tangent. This is the composition of the glyph's original
tangent vector and the one from the path.
______________________________
Listing 2. The CurveLayout glyph loop
// Get the positions, tangents, and bounding boxes of all glyphs.
GXGetGlyphs(theGlyphs, nil, nil, nil, nil, tangents, nil, nil, nil);
GXGetGlyphMetrics(theGlyphs, positions, glyphBoxes, nil);
// For each glyph, move its position to the correct place on the
// curve.
for (idx = 0; idx < glyphCount; ++idx) {
// Compute glyph's horizontal center.
pointToMapOnCurve.x = glyphBoxes[idx].left +
(glyphBoxes[idx].right - glyphBoxes[idx].left) / 2;
pointToMapOnCurve.y = 0;
// Compute new glyph position relative to horizontal center.
relativePosition.x = positions[idx].x - pointToMapOnCurve.x;
relativePosition.y = positions[idx].y - pointToMapOnCurve.y;
// Find the new location and tangent for horizontal center.
GXShapeLengthToPoint(thePath, 0, pointToMapOnCurve.x, &newPoint,
&newTangent);
// New position will be the glyph's position relative to the
// horizontal center rotated by the tangent. First rotate.
ResetMapping(&aMapping);
aMapping.map[0][0] = newTangent.x;
aMapping.map[1][0] = -newTangent.y;
aMapping.map[0][1] = newTangent.y;
aMapping.map[1][1] = newTangent.x;
MapPoints(&aMapping, 1, &relativePosition);
// Now position this relative to the new point.