Adding shadows and glows to text in Texture2D.m

Written by David Frampton @ 5:45 am, September 25, 2009

Text Examples

I like to mix things up a little by writing little tutorials or sharing tidbits of information in-between my complaints and rants. This is one of those side quests, explaining how to easily add drop shadows or glows to the text that the Texture2D class renders.

If you don’t know what the Texture2D class is, various incarnations of it have been included in many of the iPhone OpenGL sample code projects from Apple. It was a very very useful resource giving developers a black box with a simple interface for loading and displaying images and text in OpenGL ES on the iPhone.

Currently however, this class does not appear to be available in any of Apple’s example source. Cocos2d still uses it, so you can find one example here: Texture2D.m Texture2D.h. I will base the modifications on this version.

Texture2D has two initWithString methods, one that takes a font, the other that takes a name and size. I’m just going to add two more that also take a shadow offset, blur and color.

@interface Texture2D (Text)
- (id) initWithString:(NSString*)string dimensions:(CGSize)dimensions alignment:(UITextAlignment)alignment fontName:(NSString*)name fontSize:(CGFloat)size;
- (id) initWithString:(NSString*)string dimensions:(CGSize)dimensions alignment:(UITextAlignment)alignment fontName:(NSString*)name fontSize:(CGFloat)size
    shadowOffset:(CGSize)size
    shadowBlur:(float)blur
    shadowColor:(float[])shadowColor;
- (id) initWithString:(NSString*)string dimensions:(CGSize)dimensions alignment:(UITextAlignment)alignment font:(UIFont*)font;
- (id) initWithString:(NSString*)string dimensions:(CGSize)dimensions alignment:(UITextAlignment)alignment font:(UIFont*)font
    shadowOffset:(CGSize)shadowSize
    shadowBlur:(float)shadowBlur
    shadowColor:(float[])shadowColor;
@end

Of course you can make the API whatever you like, pass colors however you like or whatever. All of the methods will eventually end up calling the last method there, taking a UIFont and shadow parameters. The others are just for convenience.

Now for the guts of the thing.

The first thing you need to do is switch from using a grayscale A8 texture and context (which lets be honest was a not particularly useful pain in the ass anyway) to an RGBA8888 texture. This gives you full control of the shadow color, as well as making blending easier when you come to use it later.

Replace the 4 lines that create the context with:

    colorSpace = CGColorSpaceCreateDeviceRGB();
    data = calloc(1, width * height * 4);
    context = CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);

Also switch to using kTexture2DPixelFormat_RGBA8888 instead of kTexture2DPixelFormat_A8 in the call to initWithData

You should probably also change the line where it sets the fill color. Change the call

    CGContextSetGrayFillColor(context, 1.0, 1.0);

to:

    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0,1.0);

Note that this actually now allows you to set the color of the text. Where previously you would set the OpenGL color before rendering, you could now do it in this line, allowing a different shadow/glow color than the text color. It would be simple to pass the values in to this function and replace the 1.0s here if you ever wanted other than white text.

Moving right along, we get to the majic function call. CGContextSetShadowWithColor.

    if(shadowColor)
    {
        CGColorRef color = CGColorCreate(CGColorSpaceCreateDeviceRGB(), shadowColor);
        CGContextSetShadowWithColor (
           context,
           shadowSize,
           shadowBlur,
           color
        );
        CGColorRelease(color);
    }

I use shadowColor set to nil as the off switch for shadows, so it simply checks to see if we have a color, and if so sets the shadow properties on the CGContext.

And thats it! As I said before, make sure you set the pixelFormat in the call to initWithData to kTexture2DPixelFormat_RGBA8888 and change your blend modes when you render the text to something normal like glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

And create a texture with a shadow with something like:

    float shadowColor[4] = {0.0,0.0,0.0,1.0};
    text = [[Texture2D alloc] initWithString:@"Chopper 2 is gonna be freekin awesome!"
            dimensions:CGSizeMake(256, 128)
            alignment:UITextAlignmentLeft
            font:[UIFont fontWithName:@"Helvetica" size:18.0f]
            shadowOffset:CGSizeMake(2.0,-2.0)
            shadowBlur:2.0
            shadowColor:shadowColor];

Hope this is useful. I am not really handling the addition of extra padding to allow for large offsets or blurs, so watch out for clipping. It should be easy enough to deal with however. In the example below I actually offset the text on the left by the blur radius. This is a bit of a hack that doesn’t really solve the issue. An all encompassing solution is beyond what I wanted to do here, but in general, if you have clipping issues, play with that CGRect in the string’s drawInRect call.

Here is the new function in it’s entirety:

- (id) initWithString:(NSString*)string dimensions:(CGSize)dimensions alignment:(UITextAlignment)alignment font:(UIFont*)font
    shadowOffset:(CGSize)shadowSize
    shadowBlur:(float)shadowBlur
    shadowColor:(float[])shadowColor
{
    NSUInteger                width,
                            height,
                            i;
    CGContextRef            context;
    void*                    data;
    CGColorSpaceRef            colorSpace;

    if(font == nil) {
        NSLog(@"Invalid font");
        [self release];
        return nil;
    }

    width = dimensions.width;
    if((width != 1) && (width & (width - 1))) {
        i = 1;
        while(i < width)
        i *= 2;
        width = i;
    }
    height = dimensions.height;
    if((height != 1) && (height & (height - 1))) {
        i = 1;
        while(i < height)
        i *= 2;
        height = i;
    }

    colorSpace = CGColorSpaceCreateDeviceRGB();
    data = calloc(1, width * height * 4);
    context = CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    if(context == NULL) {
        NSLog(@"Failed creating CGBitmapContext", NULL);
        free(data);
        [self release];
        return nil;
    }

    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0,1.0);
    CGContextTranslateCTM(context, 0.0, height);
    CGContextScaleCTM(context, 1.0, -1.0);
    if(shadowColor)
    {
        CGColorRef color = CGColorCreate(CGColorSpaceCreateDeviceRGB(), shadowColor);
        CGContextSetShadowWithColor (
           context,
           shadowSize,
           shadowBlur,
           color
        );
        CGColorRelease(color);
    }
    UIGraphicsPushContext(context);
        [string drawInRect:CGRectMake(shadowBlur, 0, dimensions.width, dimensions.height) withFont:font lineBreakMode:UILineBreakModeWordWrap alignment:alignment];
    UIGraphicsPopContext();

    self = [self initWithData:data pixelFormat:kTexture2DPixelFormat_RGBA8888 pixelsWide:width pixelsHigh:height contentSize:dimensions];

    CGContextRelease(context);
    free(data);

    return self;
}








5 Comments

  1. Henry Maddocks

    Nice work Dave.

    Comment by Henry Maddocks — September 26, 2009 @ 12:23 am


  2. Aleksandar Vacić

    Oh, you beauty, you!
    Great stuff, something I wanted to do for Run Mate’s (my app) run view since the beginning but did not know was it even possible. And Apple dev forums were not helpful.

    Comment by Aleksandar Vacić — September 26, 2009 @ 10:38 am


  3. Eric Liang

    Awesome! I like it very much! This is exactly what i am looking for, appreciate your work.

    Comment by Eric Liang — October 13, 2009 @ 6:22 am


  4. Davey

    Thanks for this. It works well on iPhone 3G, but I couldn’t get it to work with iPhone 3GS, there’s no shadow on the 3GS. Any ideas?

    Comment by Davey — December 1, 2009 @ 9:56 pm


  5. Davey

    Oh, forget it. It does work on 3GS. Thanks a lot!

    Comment by Davey — December 1, 2009 @ 10:07 pm


RSS feed for comments on this post.

Sorry, the comment form is closed at this time.