Posted at

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で公開したものを大幅に変更・加筆したものです。