2011年5月3日火曜日

イメージを描画する(3)

ふと公園の芝生で寝っころがりながら、ぼんやりと夢想に耽る。写真家にもそんな気分のときってあると思います。とゆうわけで、描いた夢を具体的にするために Cocoa にはこんなクラスがあります。

The NSGraphicsContext class is the programmatic interface to objects that represent graphics contexts. A context can be thought of as a destination to which drawing and graphics state operations are sent for execution. Each graphics context contains its own graphics environment and state.
私家版 描画のおさらい
Cocoa の描画はビューを基本(view-based)に行われます。なのでビューへ描画する場合は通常 NSView のサブクラスを作成して drawRect: をオーバーライドします。そして再描画が必要な場合は(ウィンドウサイズが変更されたりとか)メインスレッドのイベントループのなかで自動に drawRect: を呼出します。drawRect: が呼出されているときは Cocoa がすでにそのビューへの描画環境を整えていてくれているので、

The receiver can assume the focus has been locked and the coordinate transformations of its frame and bounds rectangles have been applied; all it needs to do is invoke rendering client functions.

ビューへフォーカスがロックされていることや、座標系がそのビューであることを仮定できます。そしてこの再描画が終わると描画環境を再びもとに戻して(ありがとう)くれています。

この drawRect: を直接自分で呼出すことはあまりありません。プログラムのタイミングで再描画が必要になった場合には setNeedsDisplay: に YES を渡して再描画が必要なことを知らせてイベントループの中で再描画を要求するか、displayIfNeeded ですぐに再描画をおこないます。その他の要求するタイミングによっていくつかある display〜 メソッドを使い分けます。

イメージを描画する(1)では再描画がオープンパネルから新しいイメージを指定したときにも行われて欲しいので drawImage を drawRect: の外(setBitmapImageRepWithData:)からも呼んでます。なので lockFocus, unlockFocus を呼出して、描画環境を整えてやる必要がありました。対してイメージを描画する(2)では drawRect: 内なのでロックする必要がありません。

詳しい内容につきましては ”Cocoa Drawing Guide”, ”View Programming Guide” をご参照ください。

どうする?
いままでは bitmapData でポインタを取得して、あれやこれやと行っていました。その他に setColor:atX:y: setPixel:atX:y で各ピクセルにアクセスできますが、これも手間はさほどかわりません。いまは描いた夢をいっきに具体的にしたいのです。

それじゃ NSBitmapImageRep をレシーバに lockFocus, unlockFocus を使えばいいんじゃない?そうだよ、写真家さん、そうしちゃいなよ!...残念なことに NSImage では使えますが、NSBitmapImageRep にはそれがありません。でも写真家はそうゆう星のもとに生まれた人間なので(じゃNSImage 使えよ)こんなことではめげません。そこで NSGraphicsContext を使うのです。

冒頭のリファレンスの通り NSGraphicsContext は描画環境(以下グラフィクス・コンテキスト)を表すオブジェクトへのインターフェイスです。ここを通してどうこうしてやれば NSBitmapImageRep に描画できると期待します。

こうする
Graphics contexts are maintained on a stack. You push a graphics context onto the stack by sending it a saveGraphicsState message, and pop it off the stack by sending it a restoreGraphicsState message. By sending restoreGraphicsState to an NSGraphicsContext object you remove it from the stack, and the next graphics context on the stack becomes the current graphics context.

グラフィクス・コンテキストはスタック上に保持されていて、saveGraphicsState でスタックにプッシュされ restoreGraphicsState でポップされる。restoreGraphicsState を NSGraphicsContext に送信してスタックから削除しときなさいよと。そうしとけばスタック上の次のグラフィクス・コンテキストがカレントになりますよ。ということでしょうか。

手順は
現在のグラフィクス・コンテキストをスタック上にポップしておいて、その間に NSBitmapImageRep に描画できるようなコンテキストをカレントに設定する。描画が終わったら restoreGraphicsState を呼んで削除する。削除されたら次のグラフィクス・コンテキストが戻ってくる。

じゃあ NSBitmapImageRep に描画できるようなコンテキストはどうする?

+ (NSGraphicsContext *)graphicsContextWithBitmapImageRep:(NSBitmapImageRep *)bitmapRep

ありました。これでちょちょいのちょいです。
では手順を。
  1. ビューから NSBitmapImageRep のインスタンスを取得する。
  2. 生成したビットマップ・イメージに描画するためのパスを生成する。
  3. ビットマップ・イメージのグラフィクス・コンテキストを取得する。
  4. グラフィクス・コンテキストをスタック上にポップする。
  5. グラフィクス・コンテキストを変更する。
  6. ビットマップ・イメージにパスを描画する。
  7.  グラフィクス・コンテキストを元に戻す。
  8. ビットマップ・イメージをビューに描画する。
例によってウインドウにカスタム・ビューを貼付けてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

NSBitmapImageRep *bitmapImageRep;

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
 if (self) {
        bitmapImageRep = nil;
    }
    return self;
}

- (void)drawRect:(NSRect)dirtyRect {
 
 if (bitmapImageRep) {
  [bitmapImageRep release];
  bitmapImageRep = nil;
 }
 
 // CustomView からbitmapImageRep を取得する。
 NSRect rect = [self bounds];
 bitmapImageRep = [[self bitmapImageRepForCachingDisplayInRect:rect] retain];
 
 // bitmapImageRep へ描画するためのパスを作成する。
 NSBezierPath *bezierPath = [NSBezierPath bezierPath];
 [bezierPath moveToPoint:NSMakePoint(0.0, cos(2.0 * M_PI) * rect.size.height)];

 NSPoint amplitude;
 double radians = 2.0 * M_PI / rect.size.width;
 double ampAdjuster = rect.size.height / 2.0;
 NSInteger phase;
 
 for (phase = 0; phase < rect.size.width; phase++) {
  amplitude = NSMakePoint(phase, (cos(radians * phase) * ampAdjuster ) + ampAdjuster);
  [bezierPath lineToPoint:amplitude];
 }
 
 // bitmapImageRep のグラフィクス・コンテキスト取得
 NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapImageRep];
 
 // 取得したコンテキストが nil でなければ描画する。
 if (context) {
  
  // 現在のグラフィクス・コンテキストを保存
  [NSGraphicsContext saveGraphicsState];
  
  // 現在のグラフィクス・コンテキストを bitmapImageRep へのコンテキストへとセットする。
  [NSGraphicsContext setCurrentContext:context];
  
  // 下地を塗る。
  [[NSColor whiteColor] set];
  NSRectFill(rect);
  
  // パスを描く。
  [[NSColor redColor] set];
  [bezierPath stroke];
  
  // 現在のグラフィクス・コンテキストを削除し、元に戻す。
  [NSGraphicsContext restoreGraphicsState];
 }
 
 // ビューに描画する。
 [bitmapImageRep drawInRect:rect];
}

- (void) dealloc
{
 [bitmapImageRep release];
 [super dealloc];
}

@end


Cosine Wave !
今回はビューからビットマップを取得してそれをまたビューに描画しています。写真家はいつも遠回りな人生なのでこうゆうのは慣れっこです。そうです僕が芝生の上で寝っころがりながらみたものは Cosine Wave でした。きれいなコサイン波が描けました。

手順1はメソッド名の通り本来キャッシュのために使用するもののようです。autorelease された NSBitmapImageRep か作成できなければ nil が返ってきます。なので nil をチェックするべきでしょうがしていません。

手順2でコサイン波を NSBezierPath で作成してます。moveToPoint でスタートの位置を決めています。それからループの中で次に線を引くポイントを決め、そこに向けて線を引いて、を繰り返してます。Cocoa の描画モデルは Cocoa Drawing Guide ”The Painter Model” で説明されている通り。いまはまだ図形を作成しただけの状態です。

手順3〜5は前述の通り。”NSGraphicsContext”, ”Cocoa Drawing Gude” にはよく saveGraphicsState と restoreGraphicsState をのバランスを必ずとってね!と書いてあります。

手順6で手順2で作成したパスを実際にグラフィックとして描画しています。パスは適当なグラフィクス・コンテキストのなかで、fill や stroke メッセージを送信してやるとそこに描画されます。ここではグラフィクス・コンテキストが変更されていますので、ビットマップに描画しています。

手順7〜8はグラフィクス・コンテキストを元に戻し、ビューに描画してます。

まだまだ NSGraphicsContext
グラフィクス・コンテキストがもっているグラフィクスの状態(state)をみてみます。

Current
transformation
matrix (CTM)
特定のビューの座標系から描画先のデバイスへの座標系へ変換のために指定する。Cocoa はビューの drawRect: を呼出す前に CTM を変更します。CTM の変更は NSAffineTranceform オブジェクトを使います。原点、尺度、回転の座標系を変更できます。
Clipping area描画されるときに塗りつぶされる描画指定領域を指定する。Cocoa はビューの drawRect: を呼出す前にクリッピング・エリアを可視の領域(visible area)に変更します。NSBezierPath オブジェクトを使って変更できます。
Line widthパスの幅を指定する。デフォルトの幅は1.0です。NSBezierPath オブジェクトを使って変更できます。
Line join style2つの線の結合スタイルを指定する。デフォルトのスタイルは NSMiterLineJoinStyle です。NSBezierPath オブジェクトを使って変更できます。
Line cap Styleパスの線端のスタイルを指定する。デフォルトのスタイルは NSButtLineCapStyle です。NSBezierPath オブジェクトを使って変更できます。
Line dash style破線のパターンを定義する。デフォルトはなく、実線になります。NSBezierPath オブジェクトを使って変更できます。
Line miter limit線の結合スタイルを NSMiterLineJoinStyle に設定したときのみ適用され、その限度を決定する。結合部分の角は線の幅で決まります。限度を超える場合に角が切り取られます。デフォルトの限度は10.0です。NSBezierPath を使って変更できます。
Flatness value曲線が描かれるときの精度を指定する。ピクセル単位で計られる最大許容差範囲です。値が小さくなれば曲線が滑らかに描かれますが、計算コストが高くつきます。同じ値でもデバイスによっては解釈がわずかに異なるかも知れません。デフォルトの値は0.6です。NSBezierPath を使って変更できます。
Stroke color線が描かれるときのカラーを指定する。システムがサポートするカラースペースのカラーが使えます。この値はアルファ値の情報を含んでいます。カラーの情報は NSColor で管理されます。
Fill color特定の範囲を塗りつぶすカラーを指定する。システムがサポートするカラースペースのカラーが使えます。この値はアルファ値の情報を含んでいます。カラーの情報は NSColor で管理されます。
Shadow描かれる内容に適用するシャドウの属性を指定する。設定は NSShadow クラスを使っておこないます。
Rendering intent設定されているカラー・スペースをを現在のカラー・スペースにマッピングする方法を指定する。Cocoa では直接この属性を設定できません。Quartz を使ってください。
Font nameテキストを描画するとき使用するフォントを指定する。フォント情報は NSFont クラスを使って変更できます。
Font sizeテキストを描画するとき使用するフォント・サイズを指定する。フォント情報は NSFont クラスを使って変更できます。
Font character spacingテキストを描画するとき使用する字間(character spacing)を指定する。Cocoa でこの属性は間接的にしかサポートされていません。
Text drawing modeテキストをどのように描画するかを指定する。Cocoa でこの属性は間接的にしかサポートされていません。
Image interpolation quality画像のサイズを変更した場合の補完の処理の仕方を指定する。NSGraphicsContext クラスでこの設定を変更できます。
Compositing operationソースと描画先のイメージのブレンド処理の仕方を指定する。Cocoa でサポートされるブレンド・モードは Quartz と関連がありますが、使用方法と動作が異なります。NSGraphicsContext クラスでデフォルト値を指定できます。
Global alpha透明度を指定する。Cocoa は直接的にこの属性をサポートしてません。値の変更は Quartz の CGContextSetAlpha を使用しなければなりません。
Anti-aliasing settingアンチエイリアシングを指定する。NSGraphicsContext クラスでこの設定を変更できます。

ウィンディングの規則(winding rule:開いたパス線が交わるときに囲む領域の処理の仕方)は現在のグラフィクス・コンテキストには保存されません。NSBezierPath オブジェクトでデフォルト値を設定してください、とのこと。今回使った NSColor の set はStroke Color と Fill Color の両方を設定します。それぞれ別に設定する場合は、setStroke, setFill です。

0 件のコメント:

コメントを投稿