Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drawing a path with subtracted text using Core Graphics

Creating filled paths in Core Graphics is straight-forward, as is creating filled text. But I am yet to find examples of paths filled EXCEPT for text in a sub-path. My experiments with text drawing modes, clipping etc have got me nowhere.

Here's an example (created in photoshop). How would you go about creating the foreground shape in Core Graphics?

Example of text subtracted from path (created in photoshop)

I would mention that this technique appears to be used heavily in an upcoming version of a major mobile OS, but I don't want to fall afoul of SO's NDA-police ;)

like image 752
Jaysen Marais Avatar asked Sep 10 '13 10:09

Jaysen Marais


2 Answers

Here's some code I ran and tested that will work for you. See the inline comments for details:

Update: I've removed the manualYOffset: parameter. It now does a calculation to center the text vertically in the circle. Enjoy!

- (void)drawRect:(CGRect)rect {
    // Make sure the UIView's background is set to clear either in code or in a storyboard/nib

    CGContextRef context = UIGraphicsGetCurrentContext();

    [[UIColor whiteColor] setFill];
    CGContextAddArc(context, CGRectGetMidX(rect), CGRectGetMidY(rect), CGRectGetWidth(rect)/2, 0, 2*M_PI, YES);
    CGContextFillPath(context);

    // Manual offset may need to be adjusted depending on the length of the text
    [self drawSubtractedText:@"Foo" inRect:rect inContext:context];
}

- (void)drawSubtractedText:(NSString *)text inRect:(CGRect)rect inContext:(CGContextRef)context {
    // Save context state to not affect other drawing operations
    CGContextSaveGState(context);

    // Magic blend mode
    CGContextSetBlendMode(context, kCGBlendModeDestinationOut);

    // This seemingly random value adjusts the text
    // vertically so that it is centered in the circle.
    CGFloat Y_OFFSET = -2 * (float)[text length] + 5;

    // Context translation for label
    CGFloat LABEL_SIDE = CGRectGetWidth(rect);
    CGContextTranslateCTM(context, 0, CGRectGetHeight(rect)/2-LABEL_SIDE/2+Y_OFFSET);

    // Label to center and adjust font automatically
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, LABEL_SIDE, LABEL_SIDE)];
    label.font = [UIFont boldSystemFontOfSize:120];
    label.adjustsFontSizeToFitWidth = YES;
    label.text = text;
    label.textAlignment = NSTextAlignmentCenter;
    label.backgroundColor = [UIColor clearColor];
    [label.layer drawInContext:context];

    // Restore the state of other drawing operations
    CGContextRestoreGState(context);
}

Here's the result (you can change the background to anything and you'll still be able to see through the text):

Result

like image 122
Christian Di Lorenzo Avatar answered Nov 05 '22 01:11

Christian Di Lorenzo


Below is a UIView subclass that will do what you want. It will correctly size and position 1 or more letters in the circle. Here's how it looks with 1-3 letters at various sizes (32, 64, 128, 256):

Screenshot

With the availability of user defined runtime attributes in Interface Builder, you can even configure the view from within IB. Just set the text property as a runtime attribute and the backgroundColor to the color you want for the circle.

User Defined Runtime Attributes

Here's the code:

@interface MELetterCircleView : UIView

/**
 * The text to display in the view. This should be limited to 
 * just a few characters.
 */
@property (nonatomic, strong) NSString *text;

@end



@interface MELetterCircleView ()

@property (nonatomic, strong) UIColor *circleColor;

@end

@implementation MELetterCircleView

- (instancetype)initWithFrame:(CGRect)frame text:(NSString *)text
{
    NSParameterAssert(text);
    self = [super initWithFrame:frame];
    if (self)
    {
        self.text = text;
    }

    return self;
}

// Override to set the circle's background color. 
// The view's background will always be clear.
-(void)setBackgroundColor:(UIColor *)backgroundColor
{
    self.circleColor = backgroundColor;
    [super setBackgroundColor:[UIColor clearColor]];
}


- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    [self.circleColor setFill];
    CGContextAddArc(context, CGRectGetMidX(rect), CGRectGetMidY(rect),
                             CGRectGetWidth(rect)/2, 0, 2*M_PI, YES);
    CGContextFillPath(context);

    [self drawSubtractedText:self.text inRect:rect inContext:context];

}

- (void)drawSubtractedText:(NSString *)text inRect:(CGRect)rect 
                 inContext:(CGContextRef)context
{
    CGContextSaveGState(context);

    // Magic blend mode
    CGContextSetBlendMode(context, kCGBlendModeDestinationOut);


    CGFloat pointSize = 
           [self optimumFontSizeForFont:[UIFont boldSystemFontOfSize:100.f]
                                 inRect:rect 
                               withText:text];

    UIFont *font = [UIFont boldSystemFontOfSize:pointSize];

    // Move drawing start point for centering label.
    CGContextTranslateCTM(context, 0, 
                           (CGRectGetMidY(rect) - (font.lineHeight/2)));

    CGRect frame = CGRectMake(0, 0, CGRectGetWidth(rect), font.lineHeight)];
    UILabel *label = [[UILabel alloc] initWithFrame:frame];
    label.font = font;
    label.text = text;
    label.textAlignment = NSTextAlignmentCenter;
    label.backgroundColor = [UIColor clearColor];
    [label.layer drawInContext:context];

    // Restore the state of other drawing operations
    CGContextRestoreGState(context);
}

-(CGFloat)optimumFontSizeForFont:(UIFont *)font inRect:(CGRect)rect 
                        withText:(NSString *)text
{
    // For current font point size, calculate points per pixel
    CGFloat pointsPerPixel = font.lineHeight / font.pointSize;

    // Scale up point size for the height of the label. 
    // This represents the optimum size of a single letter.
    CGFloat desiredPointSize = rect.size.height * pointsPerPixel;

    if ([text length] == 1)
    {
            // In the case of a single letter, we need to scale back a bit
            //  to take into account the circle curve.
            // We could calculate the inner square of the circle, 
            // but this is a good approximation.
        desiredPointSize = .80*desiredPointSize;
    }
    else
    {
        // More than a single letter. Let's make room for more.
        desiredPointSize = desiredPointSize / [text length];
    }

    return desiredPointSize;
}
@end
like image 43
memmons Avatar answered Nov 05 '22 01:11

memmons