C#
observer
フラクタル図形
再帰処理
NGraphics

NGraphicsを使ってドラゴン曲線を描く

はじめに

フラクタル図形の一種であるヘイウェイ・ドラゴン曲線と言われる図を描くプログラムをC#で書いてみました。

ヘイウェイ・ドラゴン曲線とは、例えば以下のような図です。

Wikipediaのドラゴン曲線を参考にしました。

コンソールアプリケーションとして作成し、作成した図形は、pngファイルとして出力しています。

ソースコードの説明

以下、ソースコードの簡単な説明です。

Program クラス

プログラムをつかさどるクラス。DragonCurveとDrawerを使って、図形を描画しています。
ここで、曲線の次数を与えています。

DragonCurve クラス

ドラゴン曲線を描くクラスです。実際の線は引いていません。DragonCurveクラスは、IObservable<T>を実装していて、線を引く代わりに、購読者クラス(Drawer)にその状態の変化を伝えています。
そのため実際の描画という行為からは完全に独立しています。

Drawメソッドは、再帰メソッドになっていて、 引数 generationは、再帰メソッドを呼び出す深さを表しています。 Drawメソッドは、2つの点 p1,p2から次の一点 c を求め線を引き、 (p1,c) (p2,c) それぞれについて、Drawメソッドを再帰的に呼び出すことで、図形を描いてい行きます。

NextPointが、次の点 c を求めるメソッドですが、思いのほか複雑になってしまいました。 もっと簡潔に書けるようなきもするのですが...。

Drawer クラス

描画を受け持つDrawerクラスは、IObserver<T>を実装している購読者クラスです。
DragonCurve からの通知を受け取り、線を描いて行きます。線を描くのに、以下のEasyCanvasクラスを利用しています。

EasyCanvas クラス

NGraphicパッケージを利用して、線を描いています。結果はpngファイルにしています。

ソースコード

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace DragonCurve
{
    class Program
    {
        static void Main(string[] args)
        {
            int generation = 10;
            int size = 500;
            var dragon = new DragonCurve();
            var drawer = new Drawer(size, size, generation);
            dragon.Subscribe(drawer);
            // 初期の線
            var line = new LineInfo
            {
                Start = new Point(size * 0.25f, size * 0.55f),
                End = new Point(size * 0.75f, size * 0.55f)
            };
            dragon.Start(line, generation);
        }
    }
}

DragonCurve.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DragonCurve
{
    public class Point
    {
        public float X { get; set; }
        public float Y { get; set; }
        public Point(float x, float y)
        {
            X = x;
            Y = y;
        }
    }

    public class LineInfo
    {
        public Point Start { get; set; }
        public Point End { get; set; }
    }

    public class DragonCurve : IObservable<LineInfo>
    {

        public void Start(LineInfo line, int genelation)
        {
            Draw(line.Start, line.End, genelation);
            foreach (var observer in _observers)
            {
                observer.OnCompleted();
            }
        }

        private void Draw(Point p1, Point p2, int generation)
        {
            if (generation == 0)
            {
                var info = new LineInfo
                {
                    Start = p1,
                    End = p2
                };
                Publish(info);
            }
            else
            {
                Point c = NextPoint(p1, p2);
                Draw(p1, c, generation - 1);
                Draw(p2, c, generation - 1);
            }
        }

        private Point NextPoint(Point p1, Point p2)
        {
            var direction = ToDegree(GetSlope(p1, p2));
            var p3 = new Point(p2.X - p1.X, p2.Y - p1.Y);
            var len = LineLength(p1, p2) / 2;
            var nlen = Math.Sqrt(len * len * 2);
            var nd = (direction + 315) % 360;
            var p4 = new Point(
                        (float)(Math.Cos(ToRadian(nd)) * nlen + p1.X),
                        (float)(Math.Sin(ToRadian(nd)) * nlen + p1.Y));
            return p4;
        }

        private double GetSlope(Point p1, Point p2)
        {
            var vx = p2.X - p1.X;
            var vy = p2.Y - p1.Y;
            var n = Math.Atan(vy / vx);
            if (vx < 0)
                return Math.PI + n;    // 180度を足す
            if (vy < 0)
                return (2 * Math.PI) + n;  // 360度を足す
            return n;
        }

        private double ToDegree(double rad)
        {
            return rad * 360 / (2 * Math.PI);
        }

        private double ToRadian(double deg)
        {
            return deg * (2 * Math.PI) / 360;
        }

        private double LineLength(Point p1, Point p2)
        {
            var w = (p2.X - p1.X);
            var h = (p2.Y - p1.Y);
            return Math.Sqrt(w * w + h * h);
        }

        // 状況変化を知らせるために購読者に通知する
        private void Publish(LineInfo state)
        {
            foreach (var observer in _observers)
            {
                observer.OnNext(state);
            }
        }

        private List<IObserver<LineInfo>> _observers = new List<IObserver<LineInfo>>();

        public IDisposable Subscribe(IObserver<LineInfo> observer)
        {
            _observers.Add(observer);
            return observer as IDisposable;
        }
    }
}

Drawer.cs

using NGraphics;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DragonCurve
{
    class Drawer : IObserver<LineInfo>
    {
        private EasyCanvas canvas;
        private int generation;

        public Drawer(int w, int h, int generation)
        {
            canvas = new EasyCanvas(w, h);
            this.generation = generation;
        }

        public void OnCompleted()
        {
            canvas.Write($"DragonCurve{generation}.png");
        }

        public void OnError(Exception error)
        {
            throw new NotImplementedException();
        }

        public void OnNext(LineInfo value)
        {
            canvas.DrawLine(value.Start.X, value.Start.Y, value.End.X, value.End.Y, color: Color.FromRGB(90,90,90));
        }
    }
}

EasyCanvas.cs

using NGraphics;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DragonCurve
{
    class EasyCanvas
    {
        private IImageCanvas canvas;

        static EasyCanvas()
        {
        }

        public EasyCanvas(int width, int height)
        {
            canvas = Platforms.Current.CreateImageCanvas(new Size(width, height), scale: 2);
        }

        public void SetPixel(int x, int y, NGraphics.SolidBrush brush)
        {
            canvas.FillRectangle(x, y, 1, 1, brush);
        }

        public void Write(string path)
        {
            canvas.GetImage().SaveAsPng(path);
        }

        internal void DrawLine(float x1, float y1, float x2, float y2, Color color)
        {
            canvas.DrawLine(x1, y1, x2, y2, color);
        }
    }
}

結果

generationが、5, 10, 15の時の結果を示します。

generation: 5

DragonCurve5.png

generation: 10

DragonCurve10.png

generation: 15

DragonCurve15.png


この記事は、Gushwell's C# Programming Pageで公開したものを大幅に変更・加筆したものです。