Drawing a text along a path in QuartzCore
Say I have an array of points that form a line and a text. How can I go about drawing the t开发者_如何学Cext along this line in
- (void)drawRect:(CGRect)rect
of a UIView?
I am able to draw the path without a problem. Is there a standard method that I overlooked or a framework that would allow me to draw the text along that path? Ideally I would like to do this using only QuartzCore/CoreGraphics.
I tried calculating the width of each character and rotating every character. This kind of works, but I was wondering if there is a more elegant method.
I believe you can do this in Mac OS X, but the closest you'll come on the iPhone is CGContextShowGlyphsWithAdvances and this wont even rotate.
It shouldn't be too hard to use a loop and draw each character using something like the following. This is adapted from Apple's documentation and not tested, so beware:
CGContextSelectFont(myContext, "Helvetica", 12, kCGEncodingMacRoman);
CGContextSetCharacterSpacing(myContext, 10);
CGContextSetTextDrawingMode(myContext, kCGTextFillStroke);
CGContextSetRGBFillColor(myContext, 0, 0, 0, 1);
CGContextSetRGBStrokeColor(myContext, 0, 0, 0, 1);
NSUInteger charIndex = 0;
for(NSString *myChar in arrayOfChars) {
char *cString = [myChar UTF8String];
CGPoint charOrigin = originForPositionAlongPath(charIndex, myPath);
CGFloat slope = slopeForPositionAlongPath(charIndex, myPath);
CGContextSetTextMatrix(myContext, CGAffineTransformMakeRotation(slope));
CGContextShowTextAtPoint(myContext, charOrigin.x, charOrigin.y, cString, 1);
}
Edit: Here's an idea of the PositionAlongPath functions. Again, they aren't tested, but should be close. originAlong... returns (-1, -1) if you run out of path.
CGPoint originForPositionAlongPath(int index, NSArray *path) {
CGFloat charWidth = 12.0;
CGFloat widthToGo = charWidth * index;
NSInteger i = 0;
CGPoint position = [path objectAtIndex:i];
while(widthToGo >= 0) {
//out of path, return invalid point
if(i >= [path count]) {
return CGPointMake(-1, -1);
}
CGPoint next = [path objectAtIndex:i+1];
CGFloat xDiff = next.x - position.x;
CGFloat yDiff = next.y - position.y;
CGFloat distToNext = sqrt(xDiff*xDiff + yDiff*yDiff);
CGFloat fracToGo = widthToGo/distToNext
//point is before next point in path, interpolate the answer
if(fracToGo < 1) {
return CGPointMake(position.x+(xDiff*fracToGo), position.y+(yDiff*fracToGo));
}
//advance to next point on the path
widthToGo -= distToNext;
position = next;
++i;
}
}
CGFloat slopeForPositionAlongPath(int index, NSArray *path) {
CGPoint begin = originForPositionAlongPath(index, path);
CGPoint end = originForPositionAlongPath(index+1, path);
CGFloat xDiff = end.x - begin.x;
CGFloat yDiff = end.y - begin.y;
return atan(yDiff/xDiff);
}
This is probably irrelevant but you can text along a path with SVG (http://www.w3.org/TR/SVG/text.html#TextOnAPath), and the iPhoneOS supports it.
Above example unfortunately didn't work as expected. I have now finally found the correct way to paint a text along the path.
Here we go:
You cannot take this code 1:1 as its just excerpted from my application, but i will make things clear with some comments.
// MODIFIED ORIGIN FUNCTION
CGPoint originForPositionAlongPath(float *l, float nextW, int index, NSArray *path) {
CGFloat widthToGo = *l + nextW;
NSInteger i = 0;
CGPoint position = [[path objectAtIndex:i] CGPointValue];
while(widthToGo >= 0) {
//out of path, return invalid point
if(i+1 >= [path count]) {
return CGPointMake(-1, -1);
}
CGPoint next = [[path objectAtIndex:i+1] CGPointValue];
CGFloat xDiff = next.x - position.x;
CGFloat yDiff = next.y - position.y;
CGFloat distToNext = sqrt(xDiff*xDiff + yDiff*yDiff);
CGFloat fracToGo = widthToGo/distToNext;
//point is before next point in path, interpolate the answer
if(fracToGo < 1) {
return CGPointMake(position.x+(xDiff*fracToGo), position.y+(yDiff*fracToGo));
}
//advance to next point on the path
widthToGo -= distToNext;
position = next;
++i;
}
}
// MODIFIED SLOPE FUNCTION
CGFloat slopeForPositionAlongPath(float* l, float nextW, int index, NSArray *path) {
CGPoint begin = originForPositionAlongPath(l, 0, index, path);
CGPoint end = originForPositionAlongPath(l, nextW, index+1, path);
CGFloat xDiff = end.x - begin.x;
CGFloat yDiff = end.y - begin.y;
return atan(yDiff/xDiff);
}
// IMPORTANT: CHARACTER WIDTHS FOR HELVETICA, ABOVE EXAMPLE USES FIXED WIDTHS WHICH IS NOT ACCURATE
float arraychars[] = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0.278, 0.333, 0.474, 0.556, 0.556, 0.889, 0.722, 0.278,
0.333, 0.333, 0.389, 0.584, 0.278, 0.584, 0.278, 0.278,
0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.556,
0.556, 0.556, 0.333, 0.333, 0.584, 0.584, 0.584, 0.611,
0.975, 0.722, 0.722, 0.722, 0.722, 0.667, 0.611, 0.778,
0.722, 0.278, 0.556, 0.722, 0.611, 0.833, 0.722, 0.778,
0.667, 0.778, 0.722, 0.667, 0.611, 0.722, 0.667, 0.944,
0.667, 0.667, 0.611, 0.333, 0.278, 0.333, 0.584, 0.556,
0.278, 0.556, 0.611, 0.556, 0.611, 0.556, 0.333, 0.611,
0.611, 0.278, 0.278, 0.556, 0.278, 0.889, 0.611, 0.611,
0.611, 0.611, 0.389, 0.556, 0.333, 0.611, 0.556, 0.778,
0.556, 0.556, 0.5, 0.389, 0.28, 0.389, 0.584, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0.278, 0.333, 0.333, 0.333, 0.333, 0.333, 0.333, 0.333,
0.333, 0, 0.333, 0.333, 0, 0.333, 0.333, 0.333,
0.278, 0.333, 0.556, 0.556, 0.556, 0.556, 0.28, 0.556,
0.333, 0.737, 0.37, 0.556, 0.584, 0.333, 0.737, 0.333,
0.4, 0.584, 0.333, 0.333, 0.333, 0.611, 0.556, 0.278,
0.333, 0.333, 0.365, 0.556, 0.834, 0.834, 0.834, 0.611,
0.722, 0.722, 0.722, 0.722, 0.722, 0.722, 1, 0.722,
0.667, 0.667, 0.667, 0.667, 0.278, 0.278, 0.278, 0.278,
0.722, 0.722, 0.778, 0.778, 0.778, 0.778, 0.778, 0.584,
0.778, 0.722, 0.722, 0.722, 0.722, 0.667, 0.667, 0.611,
0.556, 0.556, 0.556, 0.556, 0.556, 0.556, 0.889, 0.556,
0.556, 0.556, 0.556, 0.556, 0.278, 0.278, 0.278, 0.278,
0.611, 0.611, 0.611, 0.611, 0.611, 0.611, 0.611, 0.584,
0.611, 0.611, 0.611, 0.611, 0.611, 0.556, 0.611, 0.556,
};
void o_DrawContourLabel(void *myObjectInstance, TransBuffer* transBuffer, MapPainterIphone* mp,const Projection& projection,
const MapParameter& parameter,
const LabelStyle& style,
const std::string& text,
size_t transStart, size_t transEnd){
// HERE WE Initialize the font etc.
CGContextSelectFont(context, "Helvetica-Bold", 10, kCGEncodingMacRoman);
CGContextSetCharacterSpacing(context, 0);
CGContextSetTextDrawingMode(context, kCGTextFillStroke);
CGContextSetLineWidth(context, 3.0);
CGContextSetRGBFillColor(context, style.GetTextR(), style.GetTextG(), style.GetTextB(), style.GetTextA());
CGContextSetRGBStrokeColor(context, 1, 1, 1, 1);
// Here we prepare a NSArray holding all waypoints of our path.
// I fill it from a "transBuffer" but you may fill it with whatever you want
NSMutableArray* path = [[NSMutableArray alloc] init];
if (transBuffer->buffer[transStart].x<transBuffer->buffer[transEnd].x) {
for (size_t j=transStart; j<=transEnd; j++) {
if (j==transStart) {
[path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[j].x,transBuffer->buffer[j].y)]];
}
else {
[path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[j].x,transBuffer->buffer[j].y)]];
}
}
}
else {
for (size_t j=0; j<=transEnd-transStart; j++) {
size_t idx=transEnd-j;
if (j==0) {
[path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[idx].x,transBuffer->buffer[idx].y)]];
}
else {
[path addObject:[NSValue valueWithCGPoint:CGPointMake(transBuffer->buffer[idx].x,transBuffer->buffer[idx].y)]];
}
}
}
// if path is too short for "estimated text length" then exit
if (pathLength(path)<text.length()*7) {
// Text is longer than path to draw on
return;
}
// NOW PAINT CHAR FOR CHAR USING CHARACTER WIDTHS TABLE
float lenUpToNow = 0;
CGAffineTransform transform=CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0);
NSUInteger charIndex = 0;
for(int i=0;i<text.length();i++) {
char *cString = (char*)malloc(2*sizeof(char));
cString[0] = text.at(i);
cString[1]=0;
float nww = arraychars[cString[0]]*8*1.4;
CGPoint charOrigin = originForPositionAlongPath(&lenUpToNow, 0, charIndex, path);
CGFloat slope = slopeForPositionAlongPath(&lenUpToNow, nww, charIndex, path);
std::cout << " CHARACTER " << cString << " placed at " << charOrigin.x << "," << charOrigin.y << std::endl;
// DO NOT FORGET TO DO THIS (TWO TRANSFORMATIONS) .. one for the rotation
// and one for mirroring, otherwise the text will be mirrored due to a
// crappy coordinate system in QuartzCore.
CGAffineTransform ct = CGAffineTransformConcat(transform,CGAffineTransformMakeRotation(slope));
CGContextSetTextMatrix(context, ct);
CGContextSetTextDrawingMode(context, kCGTextStroke);
CGContextShowTextAtPoint(context, charOrigin.x, charOrigin.y, cString, 1);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextShowTextAtPoint(context, charOrigin.x, charOrigin.y, cString, 1);
std::cout << " ... len (" << (int)cString[0] << ") = " << arraychars[cString[0]] << " up to now: " << lenUpToNow << std::endl;
lenUpToNow += nww;
charIndex++;
free(cString);
}
}
精彩评论