1枚のマスク画像から色とりどりのインベーダを生成する方法


目的

このマスク画像から色を指定して様々なインベーダを生成します。

※ CGContextDrawImageをするので上下逆様でマスク画像を作成してください。(20100820 追記あり)

コード

window-basedアプリを作成してapplicationDelegateに以下のとおり記述してください。単にInvaderViewをタイル状addSubViewしているだけですがインベーダの描画はInvaderViewのdrawRect:で行うのでこれで十分です。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    
    // Override point for customization after application launch.
  
    [window makeKeyAndVisible];
  
  CGFloat windowWidth = window.bounds.size.width;
  CGFloat windowHeight = window.bounds.size.height;
  
  UIImage *image = [UIImage imageNamed:@"invader.gif"];
  
  CGFloat invaderWidth = image.size.width;
  CGFloat invaderHeight = image.size.height;
  
  int m = windowWidth / invaderWidth;
  int n = windowHeight / invaderHeight;
  
  window.backgroundColor = [UIColor whiteColor];
  int i, j;
  InvaderView *view;
  CGRect frame = CGRectMake(0, 0, invaderWidth, invaderHeight);
  for (j = 0; j <= n; j++) {
    for (i = 0; i <= m; i++) {
      view = [[[InvaderView alloc] initWithFrame:frame] autorelease];
      view.maskImage = image;
      view.foregroundColor = [UIColor colorWithHue:1.0f / m * i saturation:1.0f brightness:1.0f alpha:1.0f];
      view.center = CGPointMake(i * invaderWidth + invaderWidth / 2, j * invaderHeight + invaderHeight / 2);
      NSLog(@"color: %@",view.foregroundColor);
      [window addSubview:view];
    }    
  }
  return YES;
}

次は、InvaderViewのヘッダファイル。
プロパティのforegroundColorがインベーダの色になります。

@interface InvaderView : UIView {
  UIImage *maskImage_;
  UIColor *foregroundColor_;
}
@property (nonatomic, retain) UIImage *maskImage;
@property (nonatomic, retain) UIColor *foregroundColor;

@end

InvaderViewの実装

#import "InvaderView.h"


@implementation InvaderView
@synthesize maskImage = maskImage_;
@synthesize foregroundColor = foregroundColor_;

- (id)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])) {
        // Initialization code
    self.maskImage = nil;
    self.foregroundColor = [UIColor clearColor];
    // 背景色に他の色を指定するとインベーダの背景が黒になってしまい、
    // 透過してくれなくなります
    self.backgroundColor = [UIColor clearColor]; 
  }
  return self;
}

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
  
  CGContextRef ctx = UIGraphicsGetCurrentContext();
  CGContextSaveGState(ctx);

  // 指定色を一面に塗ってそのコンテキストからCGImageRefを生成します
  // これとインベーダのマスク画像を合成することで完成です。
  CGImageRef colorImage;
  CGContextSetFillColorWithColor(ctx, self.foregroundColor.CGColor);
  CGContextFillRect(ctx, self.bounds);
  colorImage = CGBitmapContextCreateImage(ctx);
  
  // 一旦コンテキストをクリア
  CGContextClearRect(ctx, self.bounds);
  
  // マスクの作成をします。CGImageMaskCreateは引数に指定されている
  // とおりにマスク元画像から値を取りだして渡してあげます。
  CGImageRef maskImage = self.maskImage.CGImage;
  CGImageRef mask = CGImageMaskCreate(
                    CGImageGetWidth(maskImage), 
                    CGImageGetHeight(maskImage), 
                    CGImageGetBitsPerComponent(maskImage), 
                    CGImageGetBitsPerPixel(maskImage), 
                    CGImageGetBytesPerRow(maskImage), 
                    CGImageGetDataProvider(maskImage),
                    NULL, false);
  // マスク合成して、コンテキストに描画します
  CGImageRef maskedImage = CGImageCreateWithMask(colorImage, mask);
  CGContextDrawImage(ctx, self.bounds, maskedImage);
  
  // 後処理
  CGImageRelease(mask);
  CGImageRelease(colorImage);
  CGImageRelease(maskedImage);

  CGContextRestoreGState(ctx);
}

- (void)dealloc {
  self.maskImage = nil;
  self.foregroundColor = nil;
  [super dealloc];
}

@end

CALayerのデリゲートメソッドにあるdrawLayer:inContextメソッドの実装でも同様にして実装が可能。ただし、デリゲートにそのCALayerが描画されるUIViewのサブクラスを指定してしまうと正常に実行できなくなるのでデリゲートクラスはNSObjectのサブクラスを新たに作成するのが良いでしょう。
公式ドキュメントにも

Warning: Since the view is the layer’s delegate, you should 
never set the view as a delegate of another CALayer object.
Additionally, you should never change the delegate of this layer.

と記載されています。@ClimbAppDevさんに教えていただきました。ありがとうございました。
[参考]


[追記](20100820)
CGContextDrawImageを使わずに以下の方法を使うとマスク画像は逆様でなくても良いようだ。

[[UIImage imageWithCGImage:maskedImage] drawInRect:self.bounds];