Текст кривой на существующем круге


для приложения, которое я строю, я нарисовал 2 круга. Один немного больше другого. Я хочу изогнуть текст между этими строками, для кругового меню, которое я строю.

Я читал большинство вещей о изгибе текста, который вы должны разделить свой текст на символы и нарисовать каждый символ самостоятельно с прямым углом в виду (вращая контекст, на котором вы рисуете).

Я просто не могу обернуть голову вокруг того, как получить правильные углы и положения для моего письмена.

Я включил скриншот на то, что меню, на данный момент, выглядит. Только тексты, которые я добавил, загружаются из изображения в UIImageView.

Я надеюсь, что кто-то может дать мне некоторые отправные точки о том, как я могу нарисовать текст в белом круге, в определенных точках.

изменить: Хорошо, я сейчас нахожусь в этой точке:

Я выполняю с помощью следующего кода:

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGRect imageSize = CGRectMake(0,0,300,300);
    float perSectionDegrees = 360 / [sections count];
    float totalRotation = 90;
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, imageSize.size.width, imageSize.size.height, 8, 4 * imageSize.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);

    CGContextSetRGBFillColor(context, 0, 0, 0, 1);

    CGPoint centerPoint = CGPointMake(imageSize.size.width / 2, imageSize.size.height / 2);
    double radius = (frame.size.width / 2);

    CGContextStrokeEllipseInRect(context, CGRectMake(centerPoint.x - (frame.size.width / 2), centerPoint.y - (frame.size.height / 2), frame.size.width, frame.size.height));

    for (int index = 0; index < [sections count]; index++)
    {
        NSString* menuItemText = [sections objectAtIndex:index];
        CGSize textSize = [menuItemText sizeWithFont:self.menuItemsFont];
        char* menuItemTextChar = (char*)[menuItemText cStringUsingEncoding:NSASCIIStringEncoding];

        float x = centerPoint.x + radius * cos(degreesToRadians(totalRotation));
        float y = centerPoint.y + radius * sin(degreesToRadians(totalRotation));

        CGContextSaveGState(context);

        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, degreesToRadians(totalRotation - 90));
        CGContextShowTextAtPoint(context, 0 - (textSize.width / 2), 0 - (textSize.height / 2), menuItemTextChar, strlen(menuItemTextChar));

        CGContextRestoreGState(context);

        totalRotation += perSectionDegrees;
    }

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    return [UIImage imageWithCGImage:contextImage];
}

это переменные, которые я использую там:

NSArray* sections = [[NSArray alloc] initWithObjects:@"settings", @"test", @"stats", @"nog iets", @"woei", @"woei2", nil];
self.menuItemsFont = [UIFont fontWithName:@"VAGRounded-Bold" size:18];

вращение слов кажется правильным, размещение также. Теперь мне нужно как-то выяснить, при каком вращении буквы (и их координаты) должны быть. Мне бы не помешала помощь с этим.

Edit: исправлено! Проверьте следующий код!

- (void) drawStringAtContext:(CGContextRef) context string:(NSString*) text atAngle:(float) angle withRadius:(float) radius
{
    CGSize textSize = [text sizeWithFont:self.menuItemsFont];

    float perimeter = 2 * M_PI * radius;
    float textAngle = textSize.width / perimeter * 2 * M_PI;

    angle += textAngle / 2;

    for (int index = 0; index < [text length]; index++)
    {
        NSRange range = {index, 1};
        NSString* letter = [text substringWithRange:range];     
        char* c = (char*)[letter cStringUsingEncoding:NSASCIIStringEncoding];
        CGSize charSize = [letter sizeWithFont:self.menuItemsFont];

        NSLog(@"Char %@ with size: %f x %f", letter, charSize.width, charSize.height);

        float x = radius * cos(angle);
        float y = radius * sin(angle);

        float letterAngle = (charSize.width / perimeter * -2 * M_PI);

        CGContextSaveGState(context);
        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, (angle - 0.5 * M_PI));
        CGContextShowTextAtPoint(context, 0, 0, c, strlen(c));
        CGContextRestoreGState(context);

        angle += letterAngle;
    }
}

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGPoint centerPoint = CGPointMake(frame.size.width / 2, frame.size.height / 2);
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGFloat* ringColorComponents = (float*)CGColorGetComponents(ringColor.CGColor);
    CGFloat* textColorComponents = (float*)CGColorGetComponents(textColor.CGColor);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, frame.size.width, frame.size.height, 8, 4 * frame.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);
    CGContextSetRGBStrokeColor(context, ringColorComponents[0], ringColorComponents[1], ringColorComponents[2], ringAlpha);
    CGContextSetLineWidth(context, ringWidth);  

    CGContextStrokeEllipseInRect(context, CGRectMake(ringWidth, ringWidth, frame.size.width - (ringWidth * 2), frame.size.height - (ringWidth * 2)));
    CGContextSetRGBFillColor(context, textColorComponents[0], textColorComponents[1], textColorComponents[2], textAlpha);

    CGContextSaveGState(context);
    CGContextTranslateCTM(context, centerPoint.x, centerPoint.y);

    float angleStep = 2 * M_PI / [sections count];
    float angle = degreesToRadians(90);

    textRadius = textRadius - 12;

    for (NSString* text in sections)
    {
        [self drawStringAtContext:context string:text atAngle:angle withRadius:textRadius];
        angle -= angleStep;
    }

    CGContextRestoreGState(context);

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    [self saveImage:[UIImage imageWithCGImage:contextImage] withName:@"test.png"];
    return [UIImage imageWithCGImage:contextImage];

}
12 53

12 ответов:

Я попытался быстро разобраться на бумаге, поэтому я могу ошибаться:)

преобразуйте длину строки в единицы на UnitCircle. Таким образом (строка.длина / периметр круга)*2Pi. Теперь у вас есть угол в радианах для всей строки. (Это угол между началом и концом строки)

для отдельных букв вы можете сделать то же самое, чтобы получить угол (в радианах) для отдельных букв (используя ширину букв)

Как только вы имеете угол в радианах вы можете определить положение X и y (и поворот) букв.

бонус: для равномерного расстояния вы можете даже разработать соотношение между общей длиной всех строк и всего периметра. И разделите оставшееся пространство поровну между строками.

обновление Я сделал доказательство концепции С помощью html5 / canvas, так что просматривать его с приличным браузером :) вы должны быть в состоянии перенести его. (имейте в виду, код не комментируется)
wtf: код отлично работает с открытой консолью отладки chrome и не работает при ее закрытии. (обходной путь: откройте консоль chrome: ctrl-shift-j и перезагрузите страницу: f5); FF3.6.8, кажется, все в порядке, но буквы "танцуют".

я адаптировал образец проекта Coretextarccocoa от Apple (упомянутый Томом х в ответ) и подумал, что я хотел бы поделиться им здесь.

Я также добавил несколько других функций, таких как возможность установить размер дуги на что-то меньшее, чем 180, а также цвет текста и смещение смещения в качестве свойств (так что вам не нужно иметь огромный кадр, чтобы показать весь текст).

 /*

 File: CoreTextArcView.m (iOS version)

 Abstract: Defines and implements the CoreTextArcView custom UIView subclass to
 draw text on a curve and illustrate best practices with CoreText.

 Based on CoreTextArcView provided by Apple for Mac OS X https://developer.apple.com/library/mac/#samplecode/CoreTextArcCocoa/Introduction/Intro.html

 Ported to iOS (& added color, arcsize features) August 2011 by Alec Vance, Juggleware LLC http://juggleware.com/

 */ 

#import <UIKit/UIKit.h>
#import <CoreText/CoreText.h>


@interface CoreTextArcView : UIView {
@private
    UIFont *            _font;
    NSString *          _string;
    CGFloat             _radius;
    UIColor *           _color;
    CGFloat             _arcSize;
    CGFloat             _shiftH, _shiftV; // horiz & vertical shift

    struct {
        unsigned int    showsGlyphBounds:1;
        unsigned int    showsLineMetrics:1;
        unsigned int    dimsSubstitutedGlyphs:1;
        unsigned int    reserved:29;
    }                   _flags;
}

@property(retain, nonatomic) UIFont *font;
@property(retain, nonatomic) NSString *text;
@property(readonly, nonatomic) NSAttributedString *attributedString;
@property(assign, nonatomic) CGFloat radius;
@property(nonatomic) BOOL showsGlyphBounds;
@property(nonatomic) BOOL showsLineMetrics;
@property(nonatomic) BOOL dimsSubstitutedGlyphs;
@property(retain, nonatomic) UIColor *color;
@property(nonatomic) CGFloat arcSize;
@property(nonatomic) CGFloat shiftH, shiftV;
@end


/*

 File: CoreTextArcView.m (iOS version)

 */ 

#import "CoreTextArcView.h"
#import <AssertMacros.h>
#import <QuartzCore/QuartzCore.h>

#define ARCVIEW_DEBUG_MODE          NO

#define ARCVIEW_DEFAULT_FONT_NAME   @"Helvetica"
#define ARCVIEW_DEFAULT_FONT_SIZE   64.0
#define ARCVIEW_DEFAULT_RADIUS      150.0
#define ARCVIEW_DEFAULT_ARC_SIZE    180.0



@implementation CoreTextArcView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.font = [UIFont fontWithName:ARCVIEW_DEFAULT_FONT_NAME size:ARCVIEW_DEFAULT_FONT_SIZE];
        self.text = @"Curvaceous Type";
        self.radius = ARCVIEW_DEFAULT_RADIUS;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = [UIColor whiteColor];
        self.arcSize = ARCVIEW_DEFAULT_ARC_SIZE;
        self.shiftH = self.shiftV = 0.0f;
    }
    return self;
}

typedef struct GlyphArcInfo {
    CGFloat         width;
    CGFloat         angle;  // in radians
} GlyphArcInfo;

static void PrepareGlyphArcInfo(CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

        // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);
        }

        glyphOffset += runGlyphCount;
    }

    double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    CGFloat prevHalfWidth = glyphArcInfo[0].width / 2.0;
    glyphArcInfo[0].angle = (prevHalfWidth / lineLength) * arcSizeRad;

    // Divide the arc into slices such that each one covers the distance from one glyph's center to the next.
    CFIndex lineGlyphIndex = 1;
    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {
        CGFloat halfWidth = glyphArcInfo[lineGlyphIndex].width / 2.0;
        CGFloat prevCenterToCenter = prevHalfWidth + halfWidth;

        glyphArcInfo[lineGlyphIndex].angle = (prevCenterToCenter / lineLength) * arcSizeRad;

        prevHalfWidth = halfWidth;
    }
}


// ensure that redraw occurs.
-(void)setText:(NSString *)text{
    [_string release];
    _string = [text retain];

    [self setNeedsDisplay];
}

//set arc size in degrees (180 = half circle)
-(void)setArcSize:(CGFloat)degrees{
    _arcSize = degrees * M_PI/180.0;
}

//get arc size in degrees
-(CGFloat)arcSize{
    return _arcSize * 180.0/M_PI;
}

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();


    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);


    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    if(ARCVIEW_DEBUG_MODE){
        // Draw a black background (debug)
        CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
        CGContextFillRect(context, self.layer.bounds);
    }

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, _arcSize/2.0);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            if (!drawSubstitutedGlyphsManually) {
                CTRunDraw(run, context, glyphRange);
            } 
            else {
                // We need to draw the glyphs manually in this case because we are effectively applying a graphics operation by setting the context fill color. Normally we would use kCTForegroundColorAttributeName, but this does not apply as we don't know the ranges for the colors in advance, and we wanted demonstrate how to manually draw.
                CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
                CGGlyph glyph;
                CGPoint position;

                CTRunGetGlyphs(run, glyphRange, &glyph);
                CTRunGetPositions(run, glyphRange, &position);

                CGContextSetFont(context, cgFont);
                CGContextSetFontSize(context, CTFontGetSize(runFont));
                CGContextSetRGBFillColor(context, 0.25, 0.25, 0.25, 0.5);
                CGContextShowGlyphsAtPositions(context, &glyph, &position, 1);

                CFRelease(cgFont);
            }

            // Draw the glyph bounds 
            if ((self.showsGlyphBounds) != 0) {
                CGRect glyphBounds = CTRunGetImageBounds(run, context, glyphRange);

                CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
                CGContextStrokeRect(context, glyphBounds);
            }
            // Draw the bounding boxes defined by the line metrics
            if ((self.showsLineMetrics) != 0) {
                CGRect lineMetrics;
                CGFloat ascent, descent;

                CTRunGetTypographicBounds(run, glyphRange, &ascent, &descent, NULL);

                // The glyph is centered around the y-axis
                lineMetrics.origin.x = -halfGlyphWidth;
                lineMetrics.origin.y = positionForThisGlyph.y - descent;
                lineMetrics.size.width = glyphWidth; 
                lineMetrics.size.height = ascent + descent;

                CGContextSetRGBStrokeColor(context, 0.0, 1.0, 0.0, 1.0);
                CGContextStrokeRect(context, lineMetrics);
            }
        }

        glyphOffset += runGlyphCount;
    }

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    



}

-(void)dealloc
{
    [_font release];
    [_string release];
    [_color release];
    [super dealloc]
}

@synthesize font = _font;
@synthesize text = _string;
@synthesize radius = _radius;
@synthesize color = _color;
@synthesize arcSize = _arcSize;
@synthesize shiftH = _shiftH;
@synthesize shiftV = _shiftV;

@dynamic attributedString;
- (NSAttributedString *)attributedString {
    // Create an attributed string with the current font and string.
    assert(self.font != nil);
    assert(self.text != nil);

    // Create our attributes...

    // font
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font.fontName, self.font.pointSize, NULL);

    // color
    CGColorRef colorRef = self.color.CGColor;

    // pack it into attributes dictionary

    NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                    (id)fontRef, (id)kCTFontAttributeName,
                                    colorRef, (id)kCTForegroundColorAttributeName,
                                    nil];
    assert(attributesDict != nil);


    // Create the attributed string
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.text attributes:attributesDict];

    CFRelease(fontRef);

    return [attrString autorelease];
}

@dynamic showsGlyphBounds;
- (BOOL)showsGlyphBounds {
    return _flags.showsGlyphBounds;
}

- (void)setShowsGlyphBounds:(BOOL)show {
    _flags.showsGlyphBounds = show ? 1 : 0;
}

@dynamic showsLineMetrics;
- (BOOL)showsLineMetrics {
    return _flags.showsLineMetrics;
}

- (void)setShowsLineMetrics:(BOOL)show {
    _flags.showsLineMetrics = show ? 1 : 0;
}

@dynamic dimsSubstitutedGlyphs;
- (BOOL)dimsSubstitutedGlyphs {
    return _flags.dimsSubstitutedGlyphs;
}

- (void)setDimsSubstitutedGlyphs:(BOOL)dim {
    _flags.dimsSubstitutedGlyphs = dim ? 1 : 0;
}

@end

чтобы сэкономить время, вот что я нашел для CoreTextArcView, который предоставляет

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text radius:(float)radius arcSize:(float)arcSize color:(UIColor *)color;
  (x,y)<---------------     w             --------------->
      +--------------------------------------------------+
     ^|                                                  |  <--
     ||                                                  |  frame
     ||                                                  |
     ||                 VED L A BEL                      |
     ||             CU R            HE                   |
     ||           xx                   RE  x             |
      |          xx                        xxx           |
      |        xxx xx                     x   xxx        |
    h |      xxx    xx                  xxx     xx       |
      |      x       xxx         <-----------------------------
      |     xx         xx   xxxxxxx   xx           x     |  arcSize :
     ||    xx            xxx       xxx             xx    |  opening angle
     ||    x              xxx      xx               x    |  in degrees
     ||   xx                xx  xxx                 x    |
     ||   x  <---- r  ----->   x                    x    |
     ||   x                      (xc,yc)            x    |
     ||   x                             <-----------------------
     ||   x                                        xx    |  xc = x + w /2
     v+---xx--------------------------------------xx-----+  yc = y + h /2 + r /2
           xx                                    xx
            x                                   xx
            xxx                                xx
              xxx                            xxx
                xxxx                      xxxx
                   xxxxx              xxxxx
                        xxxxxxxxxxxxxxx

это справедливо для r > 0 и arcsize > 0.

проверьте этот пример проекта Apple: CoreTextArcCocoa

демонстрирует использование основного текста для рисования текст вдоль дуги в какао приложение. Также, этот образец показано, как можно использовать какао панель шрифтов для получения настроек шрифта это может быть использовано основным текстом для выберите шрифт, используемый для рисования.

CoreText также доступен в iOS, так что вы должны быть в состоянии реализовать что-то подобный.

я попробовал проект git, упомянутый выше, и как ZpaceZombor сказал , есть неправильное смещение

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

Я просто изменился на

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

Я установил радиус в минимальное значение между шириной и высотой представления контейнера, поэтому я установил размер дуги в .

Я произвольно изменил строку

CGContextRotateCTM(context, _arcSize/2.0);

С

CGContextRotateCTM(context, M_PI_2);

Я изменил метод init к

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text color:(UIColor *)color{

    self = [super initWithFrame:frame];
    if (self) {
        self.font = font;
        self.text = text;
        self.radius = -1 * (frame.size.width > frame.size.height ? frame.size.height / 2 : frame.size.width / 2);
        _arcSize = 2* M_PI;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = color;
        self.shiftH = self.shiftV = 0.0f;

    }
    return self;
}

после многих попыток я произвел эту модификацию функции PrepareGlyphArcInfo

// this constants come from a single case ( fontSize = 22 | circle diameter = 250px | lower circle diameter 50px | 0.12f is a proportional acceptable value of 250px diameter | 0.18f is a proportional acceptable value of 50px | 0.035f is a proportional acceptable value of "big" chars
#define kReferredCharSpacing 0.12f
#define kReferredFontSize 22.f
#define kReferredMajorDiameter 250.f
#define kReferredMinorDiameter 50.f
#define kReferredMinorSpacingFix 0.18f
#define kReferredBigCharSpacingFix  0.035f

static void PrepareGlyphArcInfo(UIFont* font,CGFloat containerRadius,CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    CGFloat curMaxTypoWidth = 0.f;
    CGFloat curMinTypoWidth = 0.f;

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

            // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);

            if (curMaxTypoWidth < glyphArcInfo[runGlyphIndex + glyphOffset].width)
                curMaxTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

            if (curMinTypoWidth > glyphArcInfo[runGlyphIndex + glyphOffset].width || curMinTypoWidth == 0)
                curMinTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

        }

        glyphOffset += runGlyphCount;
    }

    //double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    glyphArcInfo[0].angle = M_PI_2; // start at the bottom circle

    CFIndex lineGlyphIndex = 1;

    // based on font size. (supposing that with fontSize = 22 we could use 0.12)
    CGFloat maxCharSpacing = font.pointSize * kReferredCharSpacing / kReferredFontSize;

    // for diameter minor than referred 250
    if ((fabsf(containerRadius)*2) < kReferredMajorDiameter)
        maxCharSpacing = maxCharSpacing + kReferredMinorSpacingFix * kReferredMinorDiameter / (fabsf(containerRadius)*2);

    CGFloat startAngle = fabsf(glyphArcInfo[0].angle);
    CGFloat endAngle = startAngle;

    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {

        CGFloat deltaWidth = curMaxTypoWidth - glyphArcInfo[lineGlyphIndex].width;

        // fix applied to large characters like uppercase letters or symbols
        CGFloat bigCharFix = (glyphArcInfo[lineGlyphIndex-1].width == curMaxTypoWidth || (glyphArcInfo[lineGlyphIndex-1].width+2) >= curMaxTypoWidth ? kReferredBigCharSpacingFix : 0 );

        glyphArcInfo[lineGlyphIndex].angle = - (maxCharSpacing * (glyphArcInfo[lineGlyphIndex].width + deltaWidth ) / curMaxTypoWidth) - bigCharFix;

        endAngle += fabsf(glyphArcInfo[lineGlyphIndex].angle);
    }

    // center text to bottom
    glyphArcInfo[0].angle = glyphArcInfo[0].angle + (endAngle - startAngle ) / 2;

}

и изменил drawRect: метод

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();

    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);

    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(self.font, self.radius, line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, M_PI_2);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            CTRunDraw(run, context, glyphRange);
        }

        glyphOffset += runGlyphCount;
    }

    CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
    CGContextSetAlpha(context,0.0);
    CGContextFillRect(context, rect);

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    

}

как вы можете видеть, я использую очень плохо метод расчета пространства между каждым символом ( в исходном примере пространство между символами также основано на размере дуги). В любом случае это, кажется, работает почти нормально.

самое лучшее решение смогло быть изогнуть прямоугольник (так называется линейный текст), с графическим усилием и менее странными вычислениями.

вот что я получил sample result

надеюсь, что это помогает

Это мой метод, чтобы нарисовать кривые приписанные строки на слоях, под заданным углом (в радианах):

[self drawCurvedStringOnLayer:self.layer withAttributedText:incident atAngle:angle withRadius:300];

строка также автоматически переворачивается в нижней части дуги.

enter image description here

- (void)drawCurvedStringOnLayer:(CALayer *)layer
             withAttributedText:(NSAttributedString *)text
                        atAngle:(float)angle
                     withRadius:(float)radius {

    // angle in radians

    CGSize textSize = CGRectIntegral([text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                        options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                        context:nil]).size;

    float perimeter = 2 * M_PI * radius;
    float textAngle = (textSize.width / perimeter * 2 * M_PI); 

    float textRotation;
    float textDirection;
    if (angle > degreesToRadians(10) && angle < degreesToRadians(170)) {
        //bottom string
        textRotation = 0.5 * M_PI ;
        textDirection = - 2 * M_PI;
        angle += textAngle / 2;
    } else {
        //top string
        textRotation = 1.5 * M_PI ;
        textDirection = 2 * M_PI;
        angle -= textAngle / 2;
    }

    for (int c = 0; c < text.length; c++) {
        NSRange range = {c, 1};
        NSAttributedString* letter = [text attributedSubstringFromRange:range];
        CGSize charSize = CGRectIntegral([letter boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                              options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                              context:nil]).size;

        float letterAngle = ( (charSize.width / perimeter) * textDirection );

        float x = radius * cos(angle + (letterAngle/2));
        float y = radius * sin(angle + (letterAngle/2));

        CATextLayer *singleChar = [self drawTextOnLayer:layer
                                           withText:letter
                                              frame:CGRectMake(layer.frame.size.width/2 - charSize.width/2 + x,
                                                               layer.frame.size.height/2 - charSize.height/2 + y,
                                                               charSize.width, charSize.height)
                                            bgColor:nil
                                            opacity:1];

        singleChar.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation(angle - textRotation) );

        angle += letterAngle;
    }
}


- (CATextLayer *)drawTextOnLayer:(CALayer *)layer
                        withText:(NSAttributedString *)text
                           frame:(CGRect)frame
                         bgColor:(UIColor *)bgColor
                         opacity:(float)opacity {

    CATextLayer *textLayer = [[CATextLayer alloc] init];
    [textLayer setFrame:frame];
    [textLayer setString:text];
    [textLayer setAlignmentMode:kCAAlignmentCenter];
    [textLayer setBackgroundColor:bgColor.CGColor];
    [textLayer setContentsScale:[UIScreen mainScreen].scale];
    [textLayer setOpacity:opacity];
    [layer addSublayer:textLayer];
    return textLayer;
}


/** Degrees to Radian **/
#define degreesToRadians(degrees) (( degrees ) / 180.0 * M_PI )

/** Radians to Degrees **/
#define radiansToDegrees(radians) (( radians ) * ( 180.0 / M_PI ) )

решение Juggleware отлично работает, я не могу найти способ изменить направление, хотя, т. е. как бы я пошел, чтобы переместить дугу по часовой стрелке против часовой стрелки?

обновление: после нескольких дней борьбы с чрезмерно сложным кодом в этом примере, я решил свернуть свой собственный. Я пошел на декларативный подход, используя CATextLayers, которые размещаются на круге и вращаются индивидуально. Таким образом, результаты были гораздо более простой задачей. Вот основные код:

-(void)layoutSublayersOfLayer:(CALayer*)layer
{
    if ( layer != self.layer )
    {
        return;
    }

    self.layer.sublayers = nil;

    LOG( @"Laying out sublayers..." );

    CGFloat xcenter = self.frame.size.width / 2;
    CGFloat ycenter = self.frame.size.height / 2;

    float angle = arcStart;
    float angleStep = arcSize / [self.text length];

    for ( NSUInteger i = 0; i < [self.text length]; ++i )
    {
        NSRange range = { .location = i, .length = 1 };
        NSString* c = [self.text substringWithRange:range];

        CGFloat yoffset = sin( DEGREES_TO_RADIANS(angle) ) * radius;
        CGFloat xoffset = cos( DEGREES_TO_RADIANS(angle) ) * radius;

        CGFloat rotAngle = 90 - angle;

        if ( clockwise )
        {
            yoffset = -yoffset;
            rotAngle = -90 + angle;
        }

        CATextLayer* tl = [[CATextLayer alloc] init];
        if ( debugMode )
        {
            tl.borderWidth = 1;
            tl.cornerRadius = 3;
            tl.borderColor = [UIColor whiteColor].CGColor;
        }
        tl.frame = CGRectMake( shiftH + xcenter - xoffset, shiftV + ycenter + yoffset, 20, 20 );
        tl.font = self.font.fontName;
        tl.fontSize = self.font.pointSize;
        tl.foregroundColor = self.color.CGColor;
        tl.string = c;
        tl.alignmentMode = @"center";

        tl.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(rotAngle) ) );

        if ( debugMode )
        {
            CATextLayer* debugLayer = [self debugLayerWithText:[NSString stringWithFormat:@"%u: %.0f°", i, angle]];
            debugLayer.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(-rotAngle) ) );
            [tl addSublayer:debugLayer];
        }        
        [self.layer addSublayer:tl];

        angle += angleStep;
    }
}

вы можете скачать пример проекта, который использует CoreTextArcView:https://github.com/javenisme/CurvaView

возьмите окружность внутреннего круга. Это круг, на который вы хотите отобразить базу символов. Мы будем называть эту окружность totalLength.

Я предполагаю, что у вас есть список строк для отображения по кругу в textItems.

принять ширину каждой строки в textWidths массив и распределить их равномерно по всей totalLength, возможно, как этот псевдо (pythonish) код:

block = max(textWidths)
assert(block * len(textWidths) <= totalLength)
offsets = [(block * i) + ((block-width) / 2) for i, width in enumerate(textWidths)]

хотя лучшие макеты, несомненно, можно сделать в тех случаях, когда утверждение срабатывает, все, что действительно имеет значение, - это то, что мы знаем, где отдельные слова начинаются и заканчиваются в известной области. Визуализировать на прямой линии длины totalLength мы просто начинаем рендеринг каждого блока текста в offsets[i].

чтобы получить его на круг, мы сопоставим эту прямую линию обратно на окружность. Для этого нам нужно сопоставить каждый пиксель вдоль этой линии с положением на окружности и углом. Эта функция преобразует смещение вдоль этой линии в угол (она принимает значения в диапазоне от 0 до totalLength)

def offsetToAngle(pixel):
    ratio = pixel / totalLength
    angle = math.pi * 2 * ratio # cool kids use radians.
    return angle

это ваш угол зрения. Чтобы получить должность:

def angleToPosition(angle, characterWidth):
    xNorm = math.sin(angle + circleRotation)
    yNorm = math.cos(angle + circleRotation)

    halfCWidth = characterWidth / 2
    x = xNorm * radius + yNorm * halfCWidth # +y = tangent
    y = yNorm * radius - xNorm * halfCWidth # -x = tangent again.

    # translate to the circle centre
    x += circleCentre.x
    y += circleCentre.y

    return x,y

это немного сложнее. Это в значительной степени суть ваших проблем, я бы подумал. Дело в том, что вам нужно смещаться назад по касательной к окружности, чтобы выработать точку для начала рендеринга так, чтобы середина символа попадала в радиус круга. То, что составляет "спину", зависит от вашей системы координат. если это 0,0 в левом нижнем углу, затем знаки касательных компонентов меняются местами. Я предположил, что сверху слева.

это важно: я также делаю большое предположение, что поворот текста происходит вокруг нижнего левого глифа. Если это не так, то все будет выглядеть немного странно. Это будет более заметно при больших размерах шрифта. Всегда есть способ компенсировать, где бы он ни вращался, и обычно есть способ сообщить системе, где вы хотите, чтобы начало вращения быть (что будет связано с CGContextTranslateCTM вызовите свой код, я бы предположил) вам нужно будет сделать небольшой эксперимент, чтобы символы рисовались в одной точке, вращаясь вокруг их нижнего левого края.

circleRotation это просто смещение, так что вы можете вращать весь круг, а не иметь вещи всегда быть в той же ориентации. Это тоже в радианах.

Итак, теперь для каждого символа в каждом блоке текста:

for text, offset in zip(textItems, offsets):
    pix = offset # start each block at the offset we calculated earlier.
    for c in text:
        cWidth = measureGlyph(c)
        # choose the circumference location of the middle of the character
        # this is to match with the tangent calculation of tangentToOffset
        angle = offsetToAngle(pix + cWidth / 2)
        x,y = angleToPosition(angle, cWidth)
        drawGlyph(c, x, y, angle)

        pix += cWidth # start of next character in circumference space

такова концепция, во всяком случае.

ссыль Али Seyman ' s ответ:

вы можете скачать пример проекта, который использует CoreTextArcView:https://github.com/javenisme/CurvaView

добавить этот метод, чтобы уменьшить размер кадра вида, так же, как UILabel.

- (void)sizeToFit{
[super sizeToFit];

CGFloat width = ceilf( fabsf((self.radius*2)) + self.font.lineHeight) + 3.0;
CGRect f = self.frame;
f.size = CGSizeMake(width,width);
self.frame = f;
[self setNeedsDisplay];
}

Если кто-то может улучшить уменьшение высоты, а также, Добро пожаловать, чтобы добавить.

enter image description here

#import <Cocoa/Cocoa.h>

@interface CircleTextCell : NSCell {

}

@end

#import "CircleTextCell.h"

#define PI (3.141592653589793)

@implementation CircleTextCell

- (void)drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
    NSAttributedString *str = [self attributedStringValue];
    NSSize stringSize = [str size];
    NSUInteger chars = [[str string] length];
    CGFloat radius = (stringSize.width + 5 * chars) / (2 * PI);
    CGFloat diameter = 2*radius;
    NSPoint scale = {1,1};
    if (diameter > cellFrame.size.width)
    {
        scale.x = cellFrame.size.width / diameter;
    }
    if (diameter > cellFrame.size.height)
    {
        scale.y = cellFrame.size.height / diameter;
    }
    NSAffineTransform *transform = [NSAffineTransform transform];
    NSAffineTransformStruct identity = [transform transformStruct];
    [transform scaleXBy: scale.x yBy: scale.y];
    [transform translateXBy: radius yBy: 0];
    [NSGraphicsContext saveGraphicsState];

    [transform concat];

    NSPoint origin = {0,0};
    CGFloat angleScale = 360 / (stringSize.width + (5 * chars));
    for (NSUInteger i=0 ; i<chars ; i++)
    {
        NSAttributedString *substr = 
            [str attributedSubstringFromRange: NSMakeRange(i, 1)];
        [substr drawAtPoint: origin];
        [transform setTransformStruct: identity];
        CGFloat displacement = [substr size].width + 5;
        [transform translateXBy: displacement yBy: 0];
        [transform rotateByDegrees: angleScale * displacement];
        [transform concat];
    }
    [NSGraphicsContext restoreGraphicsState];
}
@end

#import <Cocoa/Cocoa.h>

@class CircleTextCell;
@interface CircleTextView : NSView {
    CircleTextCell *cell;
}

@end

#import "CircleTextView.h"
#import "CircleTextCell.h"

@implementation CircleTextView
- (void)awakeFromNib
{
    NSDictionary *attributes = 
        [NSDictionary dictionaryWithObject: [NSFont fontWithName: @"Zapfino"
                                                            size:32]
                                    forKey: NSFontAttributeName];
    NSAttributedString *str =
        [[NSAttributedString alloc] initWithString: @"Hello World!  This is a very long text string that will be wrapped into a circle by a cell drawn in a custom view"
                                        attributes: attributes];
    cell = [[CircleTextCell alloc] init];
    [cell setAttributedStringValue: str];
}
- (void)drawRect:(NSRect)rect 
{
    [[NSColor whiteColor] setFill];
    [NSBezierPath fillRect: rect];
    [cell drawWithFrame: [self bounds] inView: self];
}

@end

enter image description hereЭто лучший url https://github.com/javenisme/CurvaView чтобы установить кривую ваш текст:

но в соответствии с кривой степени мудрый я просто обновить код немного, и мы можем установить кривую как степень мудрой . как 45,60,90 180, 360.

посмотрите на код:https://github.com/tikamsingh/CurveTextWithAngle

вы можете взять некоторые идеи.