16

I want to render some text on one of my screens that has a 3dish look to it. I am using UIKit and standard views controllers etc.

The effect will look something like this:

enter image description here

Can this be done somehow with UIKit & iOS? Ordinarily I would just use a static png however, the text is dynamic and updates based on user data

Imran
  • 1,488
  • 1
  • 15
  • 36
  • The shadow effect is pretty basic, but I'd also like to know how to transform the text like this.. Maybe a custom font could do it by itself ? – rdurand Dec 07 '12 at 15:43
  • Yes shadow would be straight forward but its not what I want in this case. I don't think custom fonts would be able to specify rendering details like this. – Imran Dec 07 '12 at 15:45
  • Have you tried with shadow property and shadow offset? – iDev Dec 09 '12 at 10:47
  • One way could be to create it in Photoshop or a similar program and insert it as a `UIImage`. – Bo A Dec 09 '12 at 10:52
  • tough one, but I doubt you can do it without rendering the number a few times.. Quartz isn't meant for 3D stuff. – Antwan van Houdt Dec 09 '12 at 11:16
  • Shadow offset looks completely different so not an option. I could use rendered pngs but that's a pain because you loose stuff like automatic label resizing. – Imran Dec 09 '12 at 12:52
  • Save number's (just 1234567890) 3d image with photoshop. And dynamically use them in your app. Like 312 means 3 UIImage's with 3.png, 1.png and 2.png. Or draw to context with Quartz. – Samet DEDE Dec 10 '12 at 13:23
  • 4
    Did you read the question epilepsy? I want to know if there is a way to do this without using static pngs. – Imran Dec 10 '12 at 13:34

2 Answers2

34

Well, here is a basic sample of this.

The idea is you draw layers of the same text over and over with an x/y 1 offset to create the "depth" look.

I have create an UIImage Category, called UIImage+3d, which you can test:

This is the header (.h) file

//
//  UIImage+3D.h
//
//  Created by Lefteris Haritou on 12/10/12.
//  Feel Free to use this code, but please keep the credits
//

#import <UIKit/UIKit.h>

@interface UIImage (Extensions)

+ (UIImage *)create3DImageWithText:(NSString *)_text Font:(UIFont*)_font ForegroundColor:(UIColor*)_foregroundColor ShadowColor:(UIColor*)_shadowColor outlineColor:(UIColor*)_outlineColor depth:(int)_depth useShine:(BOOL)_shine;

@end

Here is the implementation (.m) file

//
//  UIImage+3D.m
//
//  Created by Lefteris Haritou on 12/10/12.
//  Feel Free to use this code, but please keep the credits
//

#import "UIImage+3D.h"
#import <CoreText/CoreText.h>
#import <QuartzCore/QuartzCore.h>

@implementation UIImage (Extensions)

+ (UIImage *)create3DImageWithText:(NSString *)_text Font:(UIFont*)_font ForegroundColor:(UIColor*)_foregroundColor ShadowColor:(UIColor*)_shadowColor outlineColor:(UIColor*)_outlineColor depth:(int)_depth useShine:(BOOL)_shine {

    //calculate the size we will need for our text
    CGSize expectedSize = [_text sizeWithFont:_font constrainedToSize:CGSizeMake(MAXFLOAT, MAXFLOAT)];

    //increase our size, as we will draw in 3d, so we need extra space for 3d depth + shadow with blur
    expectedSize.height+=_depth+5;
    expectedSize.width+=_depth+5;

    UIColor *_newColor;

    UIGraphicsBeginImageContextWithOptions(expectedSize, NO, [[UIScreen mainScreen] scale]);
    CGContextRef context = UIGraphicsGetCurrentContext();

    //because we want to do a 3d depth effect, we are going to slightly decrease the color as we move back
    //so here we are going to create a color array that we will use with required depth levels
    NSMutableArray *_colorsArray = [[NSMutableArray alloc] initWithCapacity:_depth];

    CGFloat *components =  (CGFloat *)CGColorGetComponents(_foregroundColor.CGColor);

    //add as a first color in our array the original color
    [_colorsArray insertObject:_foregroundColor atIndex:0];

    //create a gradient of our color (darkening in the depth)
    int _colorStepSize = floor(100/_depth);

    for (int i=0; i<_depth; i++) {

        for (int k=0; k<3; k++) {
            if (components[k]>(_colorStepSize/255.f)) {
                components[k]-=(_colorStepSize/255.f);
            }
        }
        _newColor = [UIColor colorWithRed:components[0] green:components[1] blue:components[2] alpha:CGColorGetAlpha(_foregroundColor.CGColor)];

        //we are inserting always at first index as we want this array of colors to be reversed (darkest color being the last)
        [_colorsArray insertObject:_newColor atIndex:0];
    }

    //we will draw repeated copies of our text, with the outline color and foreground color, starting from the deepest
    for (int i=0; i<_depth; i++) {

        //change color
        _newColor = (UIColor*)[_colorsArray objectAtIndex:i];

        //draw the text
        CGContextSaveGState(context);

        CGContextSetShouldAntialias(context, YES);

        //draw outline if this is the last layer (front one)
        if (i+1==_depth) {
            CGContextSetLineWidth(context, 1);
            CGContextSetLineJoin(context, kCGLineJoinRound);

            CGContextSetTextDrawingMode(context, kCGTextStroke);
            [_outlineColor set];
            [_text drawAtPoint:CGPointMake(i, i) withFont:_font];
        }

        //draw filling
        [_newColor set];

        CGContextSetTextDrawingMode(context, kCGTextFill);

        //if this is the last layer (first one we draw), add the drop shadow too and the outline
        if (i==0) {
            CGContextSetShadowWithColor(context, CGSizeMake(-2, -2), 4.0f, _shadowColor.CGColor);
        }
        else if (i+1!=_depth){
            //add glow like blur
            CGContextSetShadowWithColor(context, CGSizeMake(-1, -1), 3.0f, _newColor.CGColor);
        }

        [_text drawAtPoint:CGPointMake(i, i) withFont:_font];        
        CGContextRestoreGState(context);
    }

    //if we need to apply the shine
    if (_shine) {
        //create an alpha mask from the top most layer of the image, so we can add a shine effect over it
        CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
        CGContextRef imageContext = CGBitmapContextCreate(NULL, (int)expectedSize.width, (int)expectedSize.height, 8, (int)expectedSize.width * 4, genericRGBColorspace,  kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
        UIGraphicsPushContext(imageContext);
        CGContextSetTextDrawingMode(imageContext, kCGTextFill);
        [_text drawAtPoint:CGPointMake(_depth-1, _depth-1) withFont:_font];
        CGImageRef alphaMask = CGBitmapContextCreateImage(imageContext);
        CGContextRelease(imageContext);
        UIGraphicsPopContext();

        //draw shine effect
        //clip context to the mask we created
        CGRect drawRect = CGRectZero;
        drawRect.size = expectedSize;
        CGContextSaveGState(context);
        CGContextClipToMask(context, drawRect, alphaMask);

        CGContextSetBlendMode(context, kCGBlendModeLuminosity);

        size_t num_locations = 4;
        CGFloat locations[4] = { 0.0, 0.4, 0.6, 1};
        CGFloat gradientComponents[16] = {
            0.0, 0.0, 0.0, 1.0,
            0.6, 0.6, 0.6, 1.0,
            0.8, 0.8, 0.8, 1.0,
            0.0, 0.0, 0.0, 1.0
        };

        CGGradientRef glossGradient = CGGradientCreateWithColorComponents(genericRGBColorspace, gradientComponents, locations, num_locations);
        CGPoint start = CGPointMake(0, 0);
        CGPoint end = CGPointMake(0, expectedSize.height);
        CGContextDrawLinearGradient(context, glossGradient, start, end, 0);

        CGColorSpaceRelease(genericRGBColorspace);
        CGGradientRelease(glossGradient);
        CGImageRelease(alphaMask);
        CGContextRestoreGState(context);
    }

    UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return finalImage;
}


@end

And to use this , simply import the category extension then use it as follows:

UIImage *my3dImage = [UIImage create3DImageWithText:@"3" Font:[UIFont systemFontOfSize:250] ForegroundColor:[UIColor colorWithRed:(200/255.f) green:(200/255.f) blue:(200/255.f) alpha:1.0] ShadowColor:[UIColor blackColor] outlineColor:[UIColor colorWithRed:(225/255.f) green:(225/255.f) blue:(225/255.f) alpha:1.0] depth:8 useShine:NO];
UIImageView *imgView = [[UIImageView alloc] initWithImage:my3dImage];
[self.view addSubview: imgView];

enter image description here

Another example is this:

UIImage *my3dImage = [UIImage create3DImageWithText:@"3D" Font:[UIFont fontWithName:@"MarkerFelt-Wide" size:180] ForegroundColor:[UIColor colorWithRed:(222/255.f) green:(100/255.f) blue:(100/255.f) alpha:1.0] ShadowColor:[UIColor blackColor] outlineColor:[UIColor colorWithRed:(216/255.f) green:(120/255.f) blue:(120/255.f) alpha:1.0] depth:6 useShine:NO];
UIImageView *imgView = [[UIImageView alloc] initWithImage:my3dImage];
imgView.center = self.view.center;
[self.view addSubview: imgView];

And the result looks like this:

enter image description here

I edited the code to add a shine effect over the image, which I believe makes it look cooler

UIImage *my3dImage = [UIImage create3DImageWithText:@"3D" Font:[UIFont fontWithName:@"MarkerFelt-Wide" size:180] ForegroundColor:[UIColor colorWithRed:(222/255.f) green:(100/255.f) blue:(100/255.f) alpha:1.0] ShadowColor:[UIColor blackColor] outlineColor:[UIColor colorWithRed:(216/255.f) green:(120/255.f) blue:(120/255.f) alpha:1.0] depth:6 useShine:YES];
UIImageView *imgView = [[UIImageView alloc] initWithImage:my3dImage];
imgView.center = self.view.center;
[self.view addSubview: imgView];

enter image description here

Lefteris
  • 14,550
  • 2
  • 56
  • 95
  • 2
    This looks nice, great job ! – rdurand Dec 11 '12 at 08:38
  • @Lefteris: just got this idea : maybe you should give the ability to choose the "direction" of the 3D. For example, only "go" down, or left and up, etc. See what I mean ? For a better example, in the question, the 3D effect only goes down, whereas in your answer it goes down and right. I don't think it's that hard to include in your code (let the user pass it as a parameter in the method call with an enum like when you use *UIViewAnimationCurveEaseInOut*) and it could be pretty nice ! Also I think you should definitely send this to cocoacontrols.com ! – rdurand Dec 26 '12 at 16:46
  • @rdurand If I have the time, I will create a git project from this, with more options, like some that you suggested and of course better results. The above was a 30 min thing I did quickly to demonstrate this. As for CocoaControls, this does not qualify as a custom control actually :-) – Lefteris Dec 26 '12 at 21:17
  • Great ! As for CocoaControls, I would personally like finding something like this on CC.. It's not "really" a control, but the *About* section of the site says : *"Cocoa Controls lists custom controls **and views** for iOS and Mac OS X, **helping you improve the quality of your Cocoa application with the least work possible**."* I'd say you're right on it, or I am wrong ? ;) – rdurand Dec 27 '12 at 08:37
  • @Lefteris - sure, it qualifies :) – Aaron Brethorst Jul 10 '13 at 19:19
  • Luminosity blend mode does not work on device, you have to use kCGBlendModeScreen – jjxtra Apr 10 '14 at 02:10
10

The following code might not be perfect, but it should be a good starting point.

Basically you draw the font twice, slightly changing the size and the offset. Depending on the font and size you're dealing with you're probably have to play a bit with fontSize, fontSizeDelta and fontOffset.

The result looks somewhat like this:

enter image description here

- (UIImage *)imageWith3dString:(NSString *)text
{
    CGFloat fontSize = 150.0;
    CGFloat fontSizeDelta = 3.0;
    CGFloat fontOffset = 5.0;

    NSString *fontName = @"Bebas";
    UIFont *font = [UIFont fontWithName:fontName size:fontSize];
    CGSize textSize = [text sizeWithFont:font
                       constrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];

    CGSize size = CGSizeMake(textSize.width, fontSize);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef ctx = CGBitmapContextCreate(NULL,
                                             (int)size.width,
                                             (int)size.height,
                                             8,
                                             (int)(4 * size.width),
                                             colorSpace,
                                             kCGImageAlphaPremultipliedLast);

    // Draw with shadow
    CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 10.0, [UIColor colorWithWhite:0.0 alpha:0.6].CGColor);

    CGContextSetRGBStrokeColor(ctx, 1.0, 1.0, 1.0, 0.6);
    CGContextSetAllowsAntialiasing(ctx, YES);  
    CGContextSetLineWidth(ctx, 2.0);
    CGContextSetTextDrawingMode(ctx, kCGTextFillStroke);

    CGContextSetRGBFillColor(ctx, 222 / 255.0, 222 / 255.0, 222 / 255.0, 1.0);
    CGContextSetCharacterSpacing(ctx, 2.6);
    CGContextSelectFont(ctx, [fontName UTF8String], fontSize - fontSizeDelta, kCGEncodingMacRoman);
    CGContextShowTextAtPoint(ctx, 0.0, 3.0 + fontOffset, [text UTF8String], text.length);

    CGContextSetShadowWithColor(ctx, CGSizeZero, 0.0, NULL); // disable shadow
    CGContextSetCharacterSpacing(ctx, 1.0);
    CGContextSelectFont(ctx, [fontName UTF8String], fontSize, kCGEncodingMacRoman);
    CGContextShowTextAtPoint(ctx, 0.0, 3.0, [text UTF8String], text.length);


    CGImageRef imageRef = CGBitmapContextCreateImage(ctx);
    UIImage *image = [UIImage imageWithCGImage:imageRef];

    CGColorSpaceRelease(colorSpace);
    CGImageRelease(imageRef);
    CGContextRelease(ctx);

    return image;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[self imageWith3dString:@"3"]];

    [self.view addSubview:imageView];
}

YMMV

Tomas Camin
  • 9,996
  • 2
  • 43
  • 62
  • Hi Tomas, thats pretty close, I think thats probably going to be the closest thing possible. – Imran Dec 11 '12 at 10:15
  • My logs go cray when I try this: I get errors like `Dec 12 17:28:01 imac-1 3D Text[1380] : CGContextSetStyle: invalid context 0x0` and `Dec 12 17:28:01 imac-1 3D Text[1380] : CGContextSetRGBStrokeColor: invalid context 0x0` Any Help? – Undo Dec 13 '12 at 00:28
  • 1
    I guess you don't have the Bebas font installed. Try changing the fontName to something different, like "Helvetica" – Tomas Camin Dec 13 '12 at 07:45