简体   繁体   中英

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 text 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);
}

这可能是无关紧要的,但您可以使用SVG( http://www.w3.org/TR/SVG/text.html#TextOnAPath )沿着路径发送文本,并且iPhoneOS支持它。

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);
}


}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM