はじめに
フラクタル図形の一種であるヘイウェイ・ドラゴン曲線と言われる図を描くプログラムを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);
}
}
}
※ ソースコードは、GitHubで公開しています。
結果
generationが、5, 10, 15の時の結果を示します。
generation: 5
generation: 10
generation: 15
この記事は、Gushwell's C# Programming Pageで公開したものを大幅に変更・加筆したものです。