シェルピンスキーのクリスマスツリー(後編)

こんにちは、きうちです。

みなさま、新年あけましておめでとうございます!

本年も株式会社GEEXをよろしくお願いいたします!m(_ _)m

寒い日が続いていますが、風邪などひかないように気を付けつつ気分を新たに今年の目標に取り組んでいきたいですね。

さて今日は、前回のシェルピンスキーのクリスマスツリー(前編)でご紹介したプログラムについて、ご説明をしようと思います!

★ ★ ★

まず、同じような図形がいくつか並ぶことから、

それぞれの図形の座標や色を保持するクラスが欲しいな・・・

と思って追加しました。

以下の Part クラスです。

class Part // 絵のパーツを定義するためのデータクラス
{
    public Point[] PP { get; set; } // 3頂点

    public Pen Pen { get; set; } // ベースの色

    public bool WithSnow { get; set; } = true; // 雪をまとうか

    public Part(Point[] pp, Pen pen, bool withSnow) // コンストラクタ
    {
        PP = pp;
        Pen = pen;
        WithSnow = withSnow;
    }
}

コメントの通り頂点の座標と描画色を保持しています。

それ以外に「雪をまとうか」という bool の変数がありますが、これは後述します。

★ ★ ★

そして、フォームクラス。

Partのリスト parts を宣言し、初期化します。

再帰レベルを持つのは元のプログラムと同じです。

後は乱数を使うので乱数発生用の変数と、

それから文字も書きたいので文字フォント情報も持ちます。

private List parts = new List(); // パーツのリスト

private int maxLevel = 5; // 再帰レベル

private Random random = new Random(); // 乱数発生用

private System.Drawing.Font font = new System.Drawing.Font("Comic Sans MS", 64); // 文字描画フォント

コンストラクタにはデータ作成処理呼出しを書きます。

public Form1() // コンストラクタ
{
    InitializeComponent();

    MakeData(); // データ作成処理呼出し
}

ここで呼び出している MakeData がデータ作成処理で、次のようにします。

private void MakeData() // データ作成
{
    for (int i = 0; i <= 450; i += 150) // ▲を縦に4つ作る
    {
        parts.Add(new Part(MakeData1(i), new Pen(Brushes.Green, 3), true));
    }

    // 最後の1つはちょっと縦長にして、色を茶色にする。雪は無しにする。
    Part last = parts[parts.Count - 1];
    last.PP[1].X = 612;
    last.PP[2].X = 412;
    last.Pen = new Pen(Color.FromArgb(100, 50, 0), 3); // 色を焦げ茶色に変更…余談ですが、これを Pens.DarkGoldenrod にすると何か松っぽくなります^^
    last.WithSnow = false; // 幹には雪はまとわせないでおく

    // 雪原
    for (int y = Height - 32; y < Height; y += 16)
    {
        for (int x = 0; x < Width; x += 32)
        {
            parts.Add(new Part(MakeData2(x, y), new Pen(Color.White, 3), false));
        }
    }
}

最初の方のfor文で作っているのが木の部分です。後のfor文が雪原を作る部分です。

木の部分は4番目の要素について、幹のようになるように少し変えます。for文を抜けたところで処理している部分です。

ここで呼び出されている MakeData1 と MakeData2 で3頂点の座標を決めます。それぞれ次のような形です。

private Point[] MakeData1(int i) // データ作成(木)
{
    return [
        new Point(512, 30 + i),
        new Point(768, 230 + i),
        new Point(256, 230 + i),
];
}

private Point[] MakeData2(int x, int y) // データ作成(雪原)
{
    return [
        new Point(x, y - 64),
        new Point(x + 32, y - 1),
        new Point(x - 32, y - 1),
    ];
}

MakeData1は前出のプログラムで使用していた座標と少し似ていますが、縦幅を半分にしています。
そして、引数で受けとった i をY座標に足しこんでいます。

MakeData2 は幅64、高さ64の三頂点の座標を入れています。ただし、引数で受け取ったX座標、Y座標をベースにしています。

描画処理は以下のような感じ。

private void Form1_Paint(object sender, PaintEventArgs e) // 画面描画処理
{
    var g = e.Graphics;

    // パーツを描く
    foreach (Part p in parts)
    {
        DrawGasket(g, p.PP, p.Pen!, 0);
        DrawSGasket(g, p.PP, p.Pen!, p.WithSnow, 0);
    }

    // 文字を描く
    g.DrawString("Merry Christmas!", font, Brushes.Red, 148, Height / 2 - 112);
    g.DrawString("Merry Christmas!", font, Brushes.Yellow, 140, Height / 2 - 120);
}

partsの各要素についてシェルピンスキーのギャスケットを描く処理を呼び出しています。

また、文字を描く処理を行っています。

シェルピンスキーのギャスケットを描く処理はおおむね前出のプログラムと一緒ですが、
描画色の与え方を変更しています。

これは、0~100の乱数を発生させ、それが30未満だったら(=30%の確率で)描画色を白くし、それ以外はもともとPartで保持している描画色を指定するようになっています。
これは、ランダムに部分的に白く描くことで雪がつもっていることを表現するためにこうしています。

private void DrawSGasket(Graphics g, Point[] pp, Pen pen, bool withSnow, int level) // シェルピンスキーのギャスケットを描く
{
    if (level == maxLevel) return; // 指定した再帰レベルに到達したら何もせずreturn

    // 3辺のそれぞれの中点を求める
    Point[] mp = new Point[3];
    for (int i = 1; i <= pp.Length; i++)
    {
        int idx1 = i - 1;
        int idx2 = i == pp.Length ? 0 : i;
        mp[i - 1] = GetMiddlePoint(pp[idx1], pp[idx2]);
    }
    DrawGasket(g, mp, pen, level);

    // 3頂点と中点を基に新たな三角形を3つ定義する
    Point[] pp1 = { pp[0], mp[0], mp[2] };
    Point[] pp2 = { mp[0], pp[1], mp[1] };
    Point[] pp3 = { mp[2], mp[1], pp[2] };

    // 定義した新たな三角形でシェルピンスキーのギャスケットを描く
    DrawSGasket(g, pp1, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
    DrawSGasket(g, pp2, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
    DrawSGasket(g, pp3, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
}

DrawGasket, GetMiddlePoint, CalcAverage は前出のプログラムと同じです。

ただし、DrawGasket はちょっと引数の順番を変更しました(この方が読みやすそうだったので)。

★ ★ ★

最初の方で「雪をまとうか」という変数について後述しますとお伝えしました。
その値が、最終的に上記の DrawSGasket の第4引数に入ってきます。
白くするかしないか、の判定において乱数と共に参照されています。

また、「幹には雪はまとわせないでおく」というコメントが途中で出てきましたが、
これらはなぜそうしたかというと・・・

最初は、単純に「緑で葉の部分」「茶色で幹の部分」「白で雪原の部分」と作っていたんですが、
それをするとこんな感じになります。

これではちょっと物足りないな・・・と感じて、雪原以外はランダムに白くなるようにしたら
雪が積もっているように見えないかなー・・・とやろうと思いました。
件の変数はこの時追加しました。そうしたらこうなりました。

なんか幹に雪をまとっていると変だな。と感じたので、幹だけ白くしないようにしよう。
ということで、この「雪をまとうか」変数が追加されました。

その結果、こんな風になりました。

余談ですがそのあと、

・・・なんか赤味が足りないなー・・・

ということで、赤と黄色の文字を足してごまかすことにしました^^;
前回もご覧いただきました通り、以下のような形になりました。

★ ★ ★

いかがでしたでしょうか。

本当はもうすこしデコレーションして派手にしたかったんですが、まあ、シンプルなのもいいかなと。

なんか一昨年も似たようなことを言っていたような・・・まあ、いいか^^

実は私、昨年は、

「1年間、毎月ミニプログラムネタのブログを書いてみる!」

という目標をかかげていました。

それも、今回をもちまして無事終わりを迎えました!
これからは、もとに戻って「何かいいネタを思いついたら書く」としていきたいなと思います。

一年書いてみて、いろいろなことが分かりました。
この手のブログを書くときのやりやすい手順なんかもなんとなく定まりました。
何よりも一年間、なんとか目標通り続けられてよかった!と思っています^^

ご覧頂いている皆様、本当にありがとうございます。

それではまた!

追伸

プログラムを公開していますので、よかったらご覧ください!こちら