在 QuartzCore 中沿着路径绘制文本

发布于 2024-08-18 02:14:06 字数 269 浏览 7 评论 0原文

假设我有一个点数组,它们形成一条线和一个文本。我怎样才能在

 - (void)drawRect:(CGRect)rect 

UIView 中沿着这条线绘制文本?

我能够毫无问题地绘制路径。是否有一个我忽略的标准方法或一个框架可以让我沿着该路径绘制文本?理想情况下,我想仅使用 QuartzCore/CoreGraphics 来完成此操作。

我尝试计算每个字符的宽度并旋转每个字符。这种方法可行,但我想知道是否有更优雅的方法。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

未央 2024-08-25 02:14:06

我相信您可以在 Mac OS X 中执行此操作,但在 iPhone 上最接近的是 CGContextShowGlyphsWithAdvances,而且它甚至不会旋转。

使用循环并使用如下所示的内容绘制每个字符应该不会太难。这是根据 Apple 文档改编的,未经测试,因此请注意:

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

编辑:这是 PositionAlongPath 函数的一个想法。同样,它们没有经过测试,但应该很接近。 originAlong... 如果超出路径,则返回 (-1, -1)。

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

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);
}
喜你已久 2024-08-25 02:14:06

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

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.

謌踐踏愛綪 2024-08-25 02:14:06

不幸的是,上面的例子没有按预期工作。
我现在终于找到了沿路径绘制文本的正确方法。

我们开始吧:

您不能将这段代码 1:1 当作它只是从我的应用程序中摘录的,但我会通过一些注释来澄清这一点。

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


}

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


}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文