ノリノリでアプリを作っていてちょっとしたアイコン画像が必要になった時って結構面倒ですよねー。Xcode使ってる途中でPhotoShopなど他の重いツールを立ち上げるのは結構億劫です。ワンポイントで使用したいだけなのに、@1x, @2x, @3x...今後はAppleWatch42mm/38mm用など、多くのファイルが必要になり調子よく書いているコードの手を止めてしまうことになりがちです。
そこで今回はコードだけで画像が生成できるライブラリ ASCIImage のご紹介です。(←深夜の通販風)
■ ASCIImage
https://github.com/cparnot/ASCIImage
ASCIImageとは
ASCIImage は NSString で定義した絵の表現を UIImage に変換してくれます。どうせバイナリを base64 して NSString で定義するだけでしょ?と思った方は次を見て下さい。
NSArray *representation =
@[
@"· · · · 1 1 1 · · · ·",
@"· · 1 · · · · · 1 · ·",
@"· 1 · · · · · · · 1 ·",
@"1 · · 2 · · · 3 · · 1",
@"1 · · · # · # · · · 1",
@"1 · · · · # · · · · 1",
@"1 · · · # · # · · · 1",
@"1 · · 3 · · · 2 · · 1",
@"· 1 · · · · · · · 1 ·",
@"· · 1 · · · · · 1 · ·",
@"· · · 1 1 1 1 1 · · ·",
];
この文字列配列を ASCIIImage に渡すと、次のような画像が生成されます。
はい。とても直感的ですよね。このように ASCIIImage は、アスキーアート(のような絵)の表現を画像に変換してくれます。
何が便利か?
これだけだとツールで画像作ったほうが早いという意見も出てきますよね。でも、ここからがポイント。ASCIIImage は書き出す画像をデバイスに適したサイズで出力してくれます。これはつまり @1x, @2x, @3xなどデバイスごとの画像を用意しなくても良いという意味になります。
さらには、好きなサイズでのスケーリング出力も可能で、どれだけ大きく指定しても滑らかに出力されます。10倍,100倍と拡大してもドットが荒くなることはありません。ほらこの通り。
書き方
前述のとおり ASCIIImage は文字列配列を使用してドットを定義します。横は1文字が1ピクセル、縦は1段が1ピクセルとなります。サイズは自由に決めて構いません。正方形でなくとも縦長、横長の長方形となってもOK。ただし当然ながら全ての段が同じ文字数(=同じピクセル数)でないと画像は出力されません。
例えば、10×20pxの画像を定義する場合は、10文字のNSStringを20個持った配列を定義すれば良いわけです。
配置する文字は1〜9,A〜Z,a〜zの文字でポイントを配置し、一段ごとに文字列を配列として定義します。それ以外の文字は無視されるようなのでビジュアル的なガイドとして配置するとよいでしょう。公式のサンプルでは半角の中黒「·」を1マスずつ置いて見やすくしているようです。
ASCIIImage は内部的に NSBezierPath の stroke / fill を繰り返しています。その際の描画線の始点と終点を定義していくような感じです。なのでタイトルの「ドット絵」という表現は不適切ですね。実際はアウトラインデータなのです。
直線
例えば直線だけのハンバーガーメニューだと、次のように定義します。1から1へ、2から2へ、3から3へ、という具合に線が描画されます。定義した配列は、PARImageクラスのimageWithASCIIRepresentation〜で始まるいくつかのメソッドに渡します。ここでは単純に色とアンチエイリアスの指定のみ渡すメソッドを使用しています。
NSArray *representation = @[
@"· · · · · · · · · · ·",
@"· · · · · · · · · · ·",
@"· 1 · · · · · · · 1 ·",
@"· · · · · · · · · · ·",
@"· · · · · · · · · · ·",
@"· 2 · · · · · · · 2 ·",
@"· · · · · · · · · · ·",
@"· · · · · · · · · · ·",
@"· 3 · · · · · · · 3 ·",
@"· · · · · · · · · · ·",
@"· · · · · · · · · · ·",
];
PARImage *image = [PARImage imageWithASCIIRepresentation:representation color:[UIColor blackColor] shouldAntialias:YES];
四角
次は四角。と言ってもただの線の繰り返しです。わかりやすく赤い色を指定。
NSArray *representation = @[
@"· · · · · · · · · · ",
@"· 1 · · · · · · 1 · ",
@"· 4 · · · · · · 2 · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· 4 · · · · · · 2 · ",
@"· 3 · · · · · · 3 · ",
@"· · · · · · · · · · ",
];
PARImage *image = [PARImage imageWithASCIIRepresentation:representation color:[UIColor redColor] shouldAntialias:YES];
円・楕円
円は4点を定義します。ベジェ曲線の集合なのでこれで円が描けます。
NSArray *representation = @[
@"· · · · · · · · · · ",
@"· 1 · · · · · · 1 · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· · · · · · · · · · ",
@"· 1 · · · · · · 1 · ",
@"· · · · · · · · · · ",
];
PARImage *image = [PARImage imageWithASCIIRepresentation:representation scaleFactor:5.f color:[UIColor redColor] shouldAntialias:YES];
この指定だと円は描画できますが、中は塗りつぶしてしまいます。四角のように線で円を描く場合は、次のメソッドを使用し線の色を指定します。
PARImage *image = [PARImage imageWithASCIIRepresentation:representation contextHandler:^(NSMutableDictionary *context) {
// 塗りつぶし色
context[ASCIIContextFillColor] = [UIColor clearColor];
// 線の太さ
context[ASCIIContextLineWidth] = @1.f;
// 線色を指定
context[ASCIIContextStrokeColor] = [UIColor redColor];
// アンチエイリアス
context[ASCIIContextShouldAntialias] = @(YES);
}];
まとめ
ちゃんとした画像を使用したければPhotoShopなどでちゃんとPNGファイルを生成したほうが効率は良いですが、最近のデザインの傾向として、それ画像じゃなくてもよくね?的なのがあったりするので、こういった実装は重宝すると思います。
あと、ipaファイルは展開すれば(開発者じゃなくても)誰でも画像ファイルを流用出来てしまうので、それを防止する目的でも使用できますね。
MSXのスプライト定義のように(←古っ)ちょっとレトロな開発を体験できるので個人的には気に入っています。
おまけ
調子に乗ってこんなの作っちゃいました。アンチエイリアスを使用せず、あえてドットを際立たせています。
NSArray *invader1 = @[
@" 1 2 ",
@" H ",
@" ",
@" F G 3 4 ",
@" ",
@" ",
@" D E 5 6 ",
@" ",
@" ",
@"B C 7 8",
@" h i a b ",
@" l e ",
@" k j d c ",
@" ",
@" ",
@"A 9",
@" KKK LLL ",
@" KKK LLL ",
@" KKK LLL ",
@" MMM Q Q NNN ",
@" MMM Q Q NNN ",
@" MMM Q Q NNN ",
@" OOO RRR SSS PPP ",
@" OOO RRR SSS PPP ",
@" OOO RRR SSS PPP ",
];
PARImage *image1 = [PARImage imageWithASCIIRepresentation:invader1 contextHandler:^(NSMutableDictionary *context) {
if ([context[ASCIIContextShapeIndex] integerValue] < 10) {
context[ASCIIContextFillColor] = [UIColor colorWithRed:0.23 green:0.98 blue:0.25 alpha:1];
} else {
context[ASCIIContextFillColor] = [UIColor blackColor];
}
context[ASCIIContextShouldAntialias] = @(NO);
context[ASCIIContextShouldClose] = @(YES);
}];
NSArray *invader2 = @[
@"· · · 11· · · · · 44· · · ",
@"· · · 11· · · · · 44· · · ",
@"· · · · 22· · · 33· · · · ",
@"· · · · 22· · · 33· · · · ",
@"· · · 5 · · · · · ·5· · · ",
@"· · · 6 · · · · · ·6· · · ",
@"· · 7 · aa· · · bb· ·7· · ",
@"· · 8 · aa· · · bb· ·8· · ",
@"· 9 · · · · · · · · · ·9· ",
@"· A · · · · · · · · · ·A· ",
@"· BB· D · · · · · ·D· CC· ",
@"· · · E · · · · · ·E· · · ",
@"· · · FF· · · · · GG· · · ",
@"· BB· FF· · · · · GG· CC· ",
@"· · · · H ·H· J ·J· · · · ",
@"· · · · H ·H· J ·J· · · · ",
];
PARImage *image2 = [PARImage imageWithASCIIRepresentation: invader2 contextHandler:^(NSMutableDictionary *context) {
if ([context[ASCIIContextShapeIndex] integerValue] < 18) {
context[ASCIIContextFillColor] = [UIColor colorWithRed:0.98 green:0.23 blue:0.98 alpha:1];
} else {
context[ASCIIContextFillColor] = [UIColor blackColor];
}
context[ASCIIContextShouldAntialias] = @(NO);
}];