C#
.NET
WindowsForm
グラフィック

【息抜き】C#でGitHub風の自動生成アイコンを作る

はじめに

こんにちは。前回の投稿から余裕で1年以上経過していました。
今回はリハビリがてら息抜き的な記事を投稿します。

ある日Qiitaを読んでいて、他の方のアイコンが示し合わせたかのように
スペースイベーダー的デザインになっていることに気づきました。(下図参照、画像は自作)

636593973876907708.png 636593970107053403.png 636593973975615084.png

これは、調べたらGitHubの自動生成アイコンで、こういうユーザーごとに異なるアイコンを
アイデンティコン(Identicon)というそうです。
GitHubアカウントアイコン自動生成→Qiitaにログインしている方はこういう表示になるってことですね。
(Minecraftが流行っているからドット絵風アイコンにしている人が多いと思ったのは内緒)

今回は、このアイデンティコンをランダムに自動生成するプログラムを作りたいと思います。

1. ドットを打つ

1.1. 考え方

アイコンは左右対称なので、以下のような手順でn×nの二次元配列に
ドットを打つロジックを考えますた。

1)二次元配列の左半分にランダムにドットを打つ。
 (ドットを打つマス=true、打たないマス=falseを代入)
2)二次元配列の右半分にコピーする。
3)最終行中央列まで1)→2)を繰り返す。

図で表すとこんな感じ↓
1) → 2) 1回目
01.png 02.png

1) → 2) 2回目
03.png 04.png

1) → 2) 3回目
05.png 06.png

・・・

最後はこんな感じのドットパターンが出来上がっているであろう!
07.png

1.2. 実装

ほい。

ドット打つ.cs
/// <summary>
/// 左右対称なドットパターンをランダム生成する
/// </summary>
/// <param name="dotCount"></param>
/// <param name="random"></param>
/// <returns></returns>
private bool[,] CreateSymmetricalDotPatternRandomly( int dotCount, Random random )
{
    var matrix = new bool[dotCount, dotCount];
    int dotCountHalf = ( ( dotCount % 2 ) != 0 ) // 行列の中央列
        ? dotCount / 2 + 1                       // 一辺の要素数が奇数の時
        : dotCount / 2;                          // 一辺の要素数が偶数の時

    for ( int row = 0; row < dotCount; row++ )
    {
        for ( int col = 0; col < dotCountHalf; col++ )
        {
            // 行列の左半分にランダムにドットを打つ
            matrix[row, col] = random.Next( 0, 2 ) == 1 ? true : false;

            // 行列の右半分にコピー
            matrix[row, ( dotCount - 1 ) - col] = matrix[row, col];
        }
    }

    return matrix;
}

「ほい。」じゃねえよ:anger:という声が聞こえてきそうなので一応説明致します・・・。

・Randomオブジェクトはメソッド内で用意してもいいと思ったが、
 このメソッドを例えばfor文内で呼ばれると 非常に不愉快なことになりそうなので
 引数でクレクレにした。

・一辺のドットの数が偶数の時、左半分までドットを打つには単純にドットの数 ÷ 2の位置まで
 打ってやればよいが、これでは一辺のドットの数が奇数の時、中央の列が全くドットが打たれない
 「センター歯抜け」状態になってしまうので奇数の時はドットの数 ÷ 2 + 1の位置まで
 打つようにしている。(ソースコード中のdotCountHalfを参照)
 この区別はうまくやればfor文内に組み込めそうだったが、自分の技術力ではループ内にゴミクズを散布し
 プログラム異常終了:scream:人生終了:hotsprings:になりそうだったので事前に算出する形で逃げた。

・ドットを打つ/打たないという表現は、数値の0 or 1をランダム生成し、
 0ならばfalse(打たない)1ならばtrue(打つ)
 とした。本来であればEnumで表現したほうが良いのだろうが、
 二日酔いのため調子が出ずEnumを定義出来なかったためこのような形とさせて頂いた。

1.3. 動作確認

そろそろ、「貴様、御託はいいからモノを見せろ:grey_question::grey_exclamation:」という怒号(feat.灰皿)が
飛んできそうなので急いで結果を提示。

一辺のドット数=6(偶数)の時
08.PNG

一辺のドット数=7(奇数)の時
09.PNG

何とかなってそうです。

2. ドットの色を作る

2.1. 前景色を作る

まずドットを打つとしたマスに塗る色(前景色)を考えます。最初はRGB値をランダム設定して
生成してもいいかなと思ったのですが、汚い色になることが多かったのでColor構造体に
定義済みの色をぶっこ抜いて使うことにしました。

拝借.cs
/// <summary>
/// 色の一覧
/// </summary>
private static readonly List<Color> LIST_COLOR = new List<Color>();

...

// Color構造体から定義済みの色をぶっこ抜く
foreach ( var info in typeof( Color ).GetProperties( BindingFlags.Public | BindingFlags.Static ) )
{
    LIST_COLOR.Add( (Color)info.GetValue( null, null ) );
}

で、パクった色をランダムに返す関数を作ります。

ほげ.cs
/// <summary>
/// ランダムなColorオブジェクトを取得する
/// </summary>
/// <returns></returns>
private Color GetColorRandomly( Random random ) => LIST_COLOR[random.Next( 0, LIST_COLOR.Count )];

2.2. 背景色を作る

次にドットを打たないとしたマスに塗る色(背景色)を考えます。
前景色との対比をはっきりさせたいので、ここでは背景色=前景色の反転色とします。
反転色のRGB値の求め方は255 - 元の色のRGB値なので以下の関数で反転色を生成できます。

ほげら.cs
/// <summary>
/// 指定したColorオブジェクトの反転色を生成する
/// </summary>
/// <param name="color"></param>
/// <returns></returns>
private Color CreateInvertedColor( Color color ) => Color.FromArgb(
    byte.MaxValue - color.R,
    byte.MaxValue - color.G,
    byte.MaxValue - color.B );

3. 描く!

「1. ドットを打つ」で下絵となるドットパターンが、
「2. ドットの色を作る」で絵具となる前景色/背景色が出来たので、
最後にこれらを使ってBitmapにアイデンティコンをグリグリ描く関数を作ります。

ほげほげ.cs
/// <summary>
/// ドット一つ分のサイズ(ピクセル)
/// </summary>
private static readonly int SIZE_DOT = 20;

/// <summary>
/// 一辺のドットの数
/// </summary>
private static readonly int COUNT_DOT = 5;

...

/// <summary>
/// アイデンティコンをランダム生成する
/// </summary>
/// <param name="random"></param>
/// <returns></returns>
private Bitmap CreateIdenticonRandomly( Random random )
{
    // ドットパターンをランダムに生成
    var dotPattern = CreateSymmetricalDotPatternRandomly( COUNT_DOT, random );

    // 前景色・背景色を生成
    var foreColor = GetColorRandomly( random );
    var backGroundColor = CreateInvertedColor( foreColor );

    // アイデンティコンを描画
    var image = new Bitmap( pictureBox.ClientSize.Width, pictureBox.ClientSize.Height );
    var brush = new SolidBrush( Color.Black );
    using ( var g = Graphics.FromImage( image ) )
    {
        int posY = pictureBox.ClientRectangle.Y;
        for ( int row = 0; row < COUNT_DOT; row++, posY += SIZE_DOT )
        {
            int posX = pictureBox.ClientRectangle.X;
            for ( int col = 0; col < COUNT_DOT; col++, posX += SIZE_DOT )
            {
                brush.Color = ( dotPattern[row, col] ) ? foreColor : backGroundColor;
                g.FillRectangle( brush, posX, posY, SIZE_DOT, SIZE_DOT );
            }
        }
    }

    return image;
}

まぁ、特筆すべきことも無い処理ですが、一応ざっくり説明。

・まずドットを打つ開始座標に、生成したimageを設定する相手のpictureBox
 クライアント領域のX,Y座標を指定。

・ドット一つ分のサイズずつ右に移動しながら塗りつぶし矩形を描く。この時ドットパターンの
 true or falseを見ながらブラシに前景色/背景色設定する。
 一行描き切ったらドット一つ分のサイズ下に移動。

・↑という処理を繰り返すと、ドット一つ分のサイズ=20×5マスなので
 100×100のアイデンティコンが描きあがる。後は戻り値で返しているimage
 pictureBox.Imageに設定すればフォームに表示される。

WindowsFormアプリケーションとして作成、動作させてみた!
Anime.gif

それっぽいのが出来てますね。

4. おわりに

タイトルに【息抜き】などと冠していますが、実は記事を書いている途中に諸般の事情で
PCを再起動したところ、書いていた記事の半分以上がぶっ飛び魂が抜けかけました。

今回はドットを打つパターンをランダムとしましたが、ドットの数を多くして
何らかの規則性を持たしても面白いかもしれません。

また2色だけでなく、3色以上に塗り分けられるように改修しても
意外と綺麗なパターンが出来上がるかもしれません。
チラッ) チラッ) チラッ)

では、アディオス!