プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

ImageData によるマンデルブロ集合の描画

typescriptlogo.png

『[TypeScript] ImageData によるピクセル単位の描画』の続き。

前回は、HTML5Canvas に ImageData を作成してランダムなピクセルを描画した。

今回も同様にピクセル単位の描画を行ってみよう。少しだけ応用してマンデルブロ集合を描画してみる。

マンデルブロ集合

マンデルブロ集合は、フラクタルとして有名な図形だ。

詳しくは、次の場所が参考になる:

HTML5Canvasマンデルブロ集合を描画する

HTML5Canvas にピクセル単位の描画を行う方法については、前回を参考にしてほしい。

今回は、TypeScript の特長である「クラス設計のやり易さ」を活かすこととし、前回よりもきちんとクラス分けを行ってみたい。

先ず、前回同様の mandelbrot.html という HTML ファイルを用意する。 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Mandelbrot Sample</title>
    <script src="graph.js"></script>
    <script src="mandelbrot.js"></script>
</head>
<body onload="MandelbrotSample.MainProgram.run()">
    <canvas width="800" height="800"></canvas>
</body>
</html>
<!--
Usage:
    mandelbrot.html		                  : position = (-2.8, 2.0), scale = 4.0
    mandelbrot.html?posy=1&scale=2        : position = (-2.8, 1.0), scale = 2.0
    mandelbrot.html?posx=0&posy=0&scale=1 : position = ( 0.0, 0.0), scale = 1.0

    - This HTML can work with mandelbrot.js and graph.js online or offline.
-->

この HTML ファイルでは、graph.js と mandelbrot.js という2つの JavaScript ファイルを組み込んでいる。 それぞれ graph.ts と mandelbrot.ts いう TypeScript ファイルから生成されることになる。

onload="MandelbrotSample.MainProgram.run()" という記述があるが、このメソッドは mandelbrot.ts 内に用意する。

graph.ts

graph.ts は、Canvas にピクセル単位の描画を行うモジュールだ。

今回は、Canvas もクラス化してみよう。ピクセル毎に描画を行う部分もクラス化する。

また、座標、大きさ、矩形、色などの基本的なクラスもここに置くことにする。

module Graph {
    // 色
    export class Color {
        constructor(public red: number = 0x00, public green: number = 0x00, public blue: number = 0x00, public alpha: number = 0xff) { }
    }

    // 二次元ベクトル
    export class Vector {
        constructor(public x: number = 0.0, public y: number = 0.0) {}

        add(vector: Vector): Vector {
            return new Vector(this.x + vector.x, this.y + vector.y);
        }

        multiply(vector: Vector): Vector {
            return new Vector(this.x * vector.x, this.y * vector.y);
        }

        multiplyBy(value: number): Vector {
            return new Vector(this.x * value, this.y * value);
        }
    }

    // 二次元の大きさ
    export class Size {
        constructor(public width: number, public height: number) {}
    }

    // 矩形
    export class Rectangle {
        constructor(public position: Vector, public size: Size) {}
    }

    // ImageData への描画用
    class ImageDataHelper {
        // ImageData の指定した座標の 1 ピクセルを指定した色にする
        static setPixel(imageData: ImageData, position: Vector, color: Color) {
            // ImageData のサイズ
            var imageDataSize         = new Size(imageData.width, imageData.height);
            // 指定したピクセルの座標が有効でなかったら
            if (!ImageDataHelper.isValid(imageDataSize, position))
                return;

            // 指定した座標のピクセルが ImageData の data のどの位置にあるかを計算
            var index                 = ImageDataHelper.toIndex(imageDataSize, position);
            // その位置から、赤、緑、青、アルファ値の順で1バイトずつ書き込むことで、ピクセルがその色になる
            imageData.data[index + 0] = color.red  ;
            imageData.data[index + 1] = color.green;
            imageData.data[index + 2] = color.blue ;
            imageData.data[index + 3] = color.alpha;
        }

        // 指定したピクセルの座標が有効かどうか
        private static isValid(imageDataSize: Size, position: Vector): boolean {
            return position.x >= 0.0 && position.x <= imageDataSize.width  &&
                   position.y >= 0.0 && position.y <= imageDataSize.height;
        }

        // 指定した座標のピクセルが ImageData の data のどの位置にあるかを計算
        private static toIndex(imageDataSize: Size, position: Vector): number {
            return (position.x + position.y * imageDataSize.width) * 4;
        }
    }

    // Canvas に ImageData を置きピクセル毎に描画を行う
    export class Sheet {
        private context_  : CanvasRenderingContext2D;
        private imageData_: ImageData;

        get context() {
            return this.context_;
        }

        constructor(context: CanvasRenderingContext2D, size: Size) {
            this.context_   = context;
            this.imageData_ = context.createImageData(size.width, size.height);
        }

        // ImageData の指定した座標の 1 ピクセルを指定した色にする
        setPixel(position: Vector, color: Color) {
            ImageDataHelper.setPixel(this.imageData_, position, color);
        }

        // 指定した位置に ImageData を描画
        draw(position: Vector = new Vector()) {
            this.context.putImageData(this.imageData_, position.x, position.y);
        }
    }

    // キャンバス
    export class Canvas {
        private canvas_ : HTMLCanvasElement;
        private context_: CanvasRenderingContext2D;

        constructor() {
            this.canvas_  = <HTMLCanvasElement>document.querySelector("canvas");
            this.context_ = this.canvas_.getContext("2d");
        }

        get size(): Graph.Size {
            return Canvas.getCanvasSize(this.canvas_);
        }

        get position(): Graph.Rectangle {
            return new Graph.Rectangle(new Graph.Vector(), this.size);
        }

        get context(): CanvasRenderingContext2D {
            return this.context_;
        }

        private static getCanvasSize(canvas: HTMLCanvasElement): Graph.Size {
            return new Graph.Size(canvas.width, canvas.height);
        }
    }
}

mandelbrot.ts

mandelbrot.ts は、Canvasマンデルブロ集合を描画する行うモジュールだ。

メイン プログラム、マンデルブロ集合、マンデルブロ集合を描画するときのパラメーター、そしてパラメーターをクエリ文字列から取得するためのユーティリティの各クラスからなる。

/// <reference path="graph.ts"/>

module MandelbrotSample {
    class Utility {
        // クエリ文字列から数を取得
        static getNumberFromQueryString(key: string): number {
            var queryString = Utility.getQueryString(key);
            if (queryString != "") {
                try {
                    return parseInt(queryString);
                } catch (ex) {}
            }
            return null;
        }
        
        // クエリ文字列の取得
        static getQueryString(key: string, default_: string = null): string {
            if (default_ == null)
                default_ = "";
            key             = key.replace(/[\/, "\\\[").replace(/[\]]/, "\\\]");
            var regex       = new RegExp("[\\?&]" + key + "=([^&#]*)");
            var queryString = regex.exec(window.location.href);
            return queryString == null ? default_ : queryString[1];
        }
    }

    // パラメーター
    class Parameter {
        position      : Graph.Vector = new Graph.Vector(-2.8, 2.0);
        maximum       : number       = 32;
        private scale_: number       = 4.0;

        get ratio(): number {
            return this.scale_ / this.size.width;
        }

        constructor(public size: Graph.Size) {
            this.setPositionX();
            this.setPositionY();
            this.setScale    ();
            this.setMaximum  ();
        }

        private setPositionX() {
            var positionX = Utility.getNumberFromQueryString("posx");
            if (positionX != null)
                this.position.x = positionX;
        }

        private setPositionY() {
            var positionY = Utility.getNumberFromQueryString("posy");
            if (positionY != null)
                this.position.y = positionY;
        }

        private setScale() {
            var scale = Utility.getNumberFromQueryString("scale");
            if (scale != null)
                this.scale_ = scale;
        }

        private setMaximum() {
            var maximum = Utility.getNumberFromQueryString("max");
            if (maximum != null)
                this.maximum = maximum;
        }
    }

    class Mandelbrot {
        private position_ : Graph.Vector;
        private sheet_    : Graph.Sheet;
        private parameter_: Parameter;

        constructor(context: CanvasRenderingContext2D, position: Graph.Rectangle, size: Graph.Size) {
            this.position_  = position.position;
            this.sheet_     = new Graph.Sheet(context, size);
            this.parameter_ = new Parameter(size);
        }

        draw(palette: Graph.Color) {
            var point = new Graph.Vector();
            for (point.y = 0; point.y < this.parameter_.size.height; point.y++) {
                for (point.x = 0; point.x < this.parameter_.size.width; point.x++) {
                    var a = this.parameter_.position.add(new Graph.Vector(point.x, -point.y).multiplyBy(this.parameter_.ratio));
                    this.setPixel(point, this.getCount(a), palette);
                }
            }
            this.sheet_.draw(this.position_);
        }

        private getCount(a: Graph.Vector): number {
            var squareBorder = 25.0;
            var square       = new Graph.Vector();
            var point        = new Graph.Vector();
            var count        = 0;
            do {
                point  = new Graph.Vector(square.x - square.y + a.x, 2.0 * point.x * point.y + a.y);
                square = point.multiply(point);
                count++;
            } while (square.x + square.y < squareBorder && count <= this.parameter_.maximum);
            return count < this.parameter_.maximum ? count : 0;
        }

        private setPixel(point: Graph.Vector, count: number, palette: Graph.Color) {
            this.sheet_.setPixel(point, palette[Mandelbrot.toColorIndex(count, palette.length)]);
        }

        private static toColorIndex(colorNumber: number, paletteSize: number): number {
            var colorIndexNumber = paletteSize * 2 - 2;
            var colorIndex       = colorNumber % colorIndexNumber;
            if (colorIndex >= paletteSize)
                colorIndex = colorIndexNumber - colorIndex;
            return colorIndex;
        }
    }

    export class MainProgram {
        static run() {
            var canvas     = new Graph.Canvas();
            var mandelbrot = new Mandelbrot(canvas.context, canvas.position, canvas.size);
            mandelbrot.draw(MainProgram.getPalette());
        }

        // パレット (予め色を格納しておき、パレット番号で色を参照)
        private static getPalette(): Graph.Color {
            return [
                new Graph.Color(0x02, 0x08, 0x80),
                new Graph.Color(0x10, 0x10, 0x70),
                new Graph.Color(0x20, 0x18, 0x60),
                new Graph.Color(0x30, 0x20, 0x50),
                new Graph.Color(0x40, 0x28, 0x40),
                new Graph.Color(0x50, 0x30, 0x30),
                new Graph.Color(0x60, 0x38, 0x20),
                new Graph.Color(0x70, 0x40, 0x10),
                new Graph.Color(0x80, 0x48, 0x0e),
                new Graph.Color(0x90, 0x50, 0x0c),
                new Graph.Color(0xa0, 0x58, 0x0a),
                new Graph.Color(0xb0, 0x60, 0x08),
                new Graph.Color(0xc0, 0x68, 0x06),
                new Graph.Color(0xd0, 0x70, 0x04),
                new Graph.Color(0xe8, 0x78, 0x02),
                new Graph.Color(0xff, 0x80, 0x01)
            ];
        }
    }
}

クラス図

以上のクラス構成を、クラス図で見てみよう。次のようになる。

クラス図
クラス図

TypeScript を使っている為に、このようなクラス設計は比較的容易だ。

実行例

では、実行してみよう。

コンパイルすると、graph.js と mandelbrot.js が生成される。

mandelbrot.html を Internet Explorer 9 以降等の HTML5 対応の Web ブラウザーで表示してみると、次のようにマンデルブロ集合が描画される。

mandelbrot.html
mandelbrot.html

実際のサンプルを次の場所に用意した。

マンデルブロ集合のどこを描画するかを指定するパラメーターがクエリ文字列で渡せるようになっているので、次のようにして描画位置を変えることができる。

C# で作成した例

比較の為に、同様のものを C# (Windows ストア アプリ) で作成してみよう。

Visual Studio でメニュー「新規作成」 - 「プロジェクト」から「Visual C#」 - 「Windows ストア」 - 「新しいアプリケーション (XAML)」で Windows ストア アプリを新規作成する。

App.xaml と App.xaml.cs には特に変更を加えない。

MainPage.xaml と MainPage.xaml.cs

謂わば mandelbrot.html にあたる部分。 TypeScript の場合同様、こちらも Canvas にイメージのオブジェクトを置き、描画する。

<Page
    x:Class="MandelbrotSample.WindowsStore.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MandelbrotSample.WindowsStore"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Loaded="Page_Loaded">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Canvas x:Name="canvas">
            <Image x:Name="image" />
        </Canvas>
        <ProgressRing x:Name="progress" Width="50" Height="50" Foreground="Blue" />
    </Grid>
</Page>
using MandelbrotSample.WindowsStore.Graph;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace MandelbrotSample.WindowsStore
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            InitializeComponent();
        }

        async void Page_Loaded(object sender, RoutedEventArgs e)
        {
            await Run();
        }

        async Task Run()
        {
            var canvasPosition = new IntRectangle { Position = new IntVector(),
                                                    Size     = new IntSize { Width  = (int)canvas.ActualWidth ,
                                                                             Height = (int)canvas.ActualHeight } };

            progress.IsActive  = true ;
            await MainProgram.RunAsync(canvasPosition, image);
            progress.IsActive  = false;
        }
    }
}
Graph.cs

graph.ts にあたる部分は次のような感じ。

こちらでは、Palette はクラスにした。

また、TypeScript では number という型だったものは、int と double に分けた。

using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.UI;
using Windows.UI.Xaml.Media.Imaging;

namespace MandelbrotSample.WindowsStore.Graph
{
    public class Palette
    {
        readonly Color colors;

        public int Size
        {
            get { return colors.Length; }
        }

        public Palette(int size)
        { colors = new Color[size]; }

        public Color this[int index]
        {
            get
            {
                return index >= 0 && index < Size ? colors[index] : new Color();
            }
            set
            {
                if (index >= 0 && index < Size)
                    colors[index] = value;
            }
        }
    }

    public class Vector
    {
        public double X { get; set; }
        public double Y { get; set; }

        public static Vector operator +(Vector vector1, Vector vector2)
        {
            return new Vector { X = vector1.X + vector2.X, Y = vector1.Y + vector2.Y };
        }

        public static Vector operator -(Vector vector1, Vector vector2)
        {
            return new Vector { X = vector1.X - vector2.X, Y = vector1.Y - vector2.Y };
        }

        public static Vector operator *(Vector vector1, Vector vector2)
        {
            return new Vector { X = vector1.X * vector2.X, Y = vector1.Y * vector2.Y };
        }

        public static Vector operator *(Vector vector, double value)
        {
            return new Vector { X = vector.X * value, Y = vector.Y * value };
        }

        public static Vector operator /(Vector vector, double value)
        {
            return new Vector { X = vector.X / value, Y = vector.Y / value };
        }
    }

    public class IntVector
    {
        public int X { get; set; }
        public int Y { get; set; }

        public static IntVector operator -(IntVector vector, IntSize size)
        {
            return new IntVector { X = vector.X - size.Width, Y = vector.Y - size.Height };
        }
    }
    
    public class IntSize
    {
        public int Width  { get; set; }
        public int Height { get; set; }

        public static IntSize operator +(IntSize size1, IntSize size2)
        {
            return new IntSize { Width = size1.Width + size2.Width, Height = size1.Height + size2.Height };
        }

        public static IntSize operator *(IntSize size, IntVector vector)
        {
            return new IntSize { Width = size.Width * vector.X, Height = size.Height * vector.Y };
        }
    }

    public class IntRectangle
    {
        public IntVector Position { get; set; }
        public IntSize   Size     { get; set; }
    }

    public static class WriteableBitmapExtension
    {
        public static void Clear(this WriteableBitmap bitmap, Color color)
        {
            var arraySize = bitmap.PixelBuffer.Capacity;
            var array     = new byte[arraySize];
            for (var index = 0; index < arraySize; index += 4) {
                array[index    ] = color.B;
                array[index + 1] = color.G;
                array[index + 2] = color.R;
                array[index + 3] = color.A;
            }
            using (var pixelStream = bitmap.PixelBuffer.AsStream()) {
                pixelStream.Seek(0, SeekOrigin.Begin);
                pixelStream.Write(array, 0, array.Length);
            }
        }

        public static void SetPixel(this WriteableBitmap bitmap, int x, int y, Color color)
        {
            var bitmapWidth  = bitmap.PixelWidth;
            var bitmapHeight = bitmap.PixelHeight;
            if (!IsValid(bitmapWidth, bitmapHeight, x, y))
                return;
            var index = ToIndex(bitmapWidth, x, y);
            Debug.Assert(index >= 0 && index < bitmap.PixelBuffer.Capacity);

            using (var pixelStream = bitmap.PixelBuffer.AsStream()) {
                var array = new byte { color.B, color.G, color.R, color.A };
                pixelStream.Seek(index, SeekOrigin.Begin);
                pixelStream.Write(array, 0, array.Length);
            }
        }

        static int ToIndex(int bitmapWidth, int x, int y)
        {
            return (x + y * bitmapWidth) * 4;
        }

        static bool IsValid(int width, int height, int x, int y)
        {
            return x >= 0 && x < width  &&
                   y >= 0 && y < height;
        }
    }

    public class Sheet
    {
        public WriteableBitmap Bitmap { get; set; }

        public Sheet(IntSize size, Color backgroundColor = new Color())
        {
            Bitmap = new WriteableBitmap(size.Width, size.Height);
            Bitmap.Clear(backgroundColor);
        }

        public void SetPixel(IntVector position, Color color)
        { Bitmap.SetPixel(position.X, position.Y, color); }
    }
}
Mandelbrot.cs

mandelbrot.ts にあたる部分は次のような感じ。

TypeScript の run メソッドは、非同期の RunAsync とした。

using MandelbrotSample.WindowsStore.Graph;
using System;
using System.Threading.Tasks;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;

namespace MandelbrotSample.WindowsStore
{
    public class Mandelbrot
    {
        public class Parameter {
            public Vector    Position { get; private set; }
            public int       Maximum  { get; private set; }
            public IntSize   Size     { get; private set; }
            public double    Ratio    { get; private set; }

            public Parameter(IntSize size) : this(position: new Vector { X = -2.8, Y = 1.5 }, scale: 5.0, maximum: 32, size: size)
            {}

            public Parameter(Vector position, double scale, int maximum, IntSize size)
            {
                Position = position;
                Maximum  = maximum;
                Size     = size;
                Ratio    = scale / size.Width;
            }
        }

        const double       squareBorder = 25.0;

        readonly Sheet     sheet;
        readonly Parameter parameter;

        public WriteableBitmap Bitmap
        {
            get { return sheet.Bitmap; }
        }

        public Mandelbrot(IntRectangle position, Color backgroundColor = new Color())
        {
            sheet          = new Sheet(position.Size, backgroundColor);
            this.parameter = new Parameter(size: position.Size);
        }

        public Mandelbrot(IntRectangle position, Parameter parameter, Color backgroundColor = new Color())
        {
            sheet          = new Sheet(position.Size, backgroundColor);
            this.parameter = parameter;
        }

        public void Draw(Palette palette)
        {
            var point = new Graph.IntVector();
            for (point.Y = 0; point.Y < parameter.Size.Height; point.Y++) {
                for (point.X = 0; point.X < this.parameter.Size.Width; point.X++) {
                    var a = parameter.Position + new Vector { X = point.X, Y = -point.Y } * parameter.Ratio;
                    SetPixel(point, GetCount(a), palette);
                }
            }
        }

        int GetCount(Vector a)
        {
            var square = new Vector();
            var point  = new Vector();
            var count  = 0;
            do {
                point  = new Vector { X = square.X - square.Y + a.X, Y = 2.0 * point.X * point.Y + a.Y };
                square = point * point;
                count++;
            } while (square.X + square.Y < squareBorder && count <= parameter.Maximum);
            return count < parameter.Maximum ? count : 0;
        }

        void SetPixel(IntVector point, int count, Palette palette)
        {
            sheet.SetPixel(point, palette[ToColorIndex(count, palette.Size)]);
        }

        static int ToColorIndex(int colorNumber,  int paletteSize)
        {
            var colorIndexNumber = paletteSize * 2 - 2;
            var colorIndex       = colorNumber % colorIndexNumber;
            if (colorIndex >= paletteSize)
                colorIndex = colorIndexNumber - colorIndex;
            return colorIndex;
        }
    }

    class MainProgram
    {
        public static async Task RunAsync(IntRectangle canvasPosition, Image image)
        {
            await Window.Current.Dispatcher.RunIdleAsync(e => image.Source = DrawMandelbrot(canvasPosition));
        }

        static Palette GetPalette()
        {
            var palette = new Graph.Palette(16);
            palette[ 0] = new Color { R = 0x02, G = 0x08, B = 0x80 };
            palette[ 1] = new Color { R = 0x10, G = 0x10, B = 0x70 };
            palette[ 2] = new Color { R = 0x20, G = 0x18, B = 0x60 };
            palette[ 3] = new Color { R = 0x30, G = 0x20, B = 0x50 };
            palette[ 4] = new Color { R = 0x40, G = 0x28, B = 0x40 };
            palette[ 5] = new Color { R = 0x50, G = 0x30, B = 0x30 };
            palette[ 6] = new Color { R = 0x60, G = 0x38, B = 0x20 };
            palette[ 7] = new Color { R = 0x70, G = 0x40, B = 0x10 };
            palette[ 8] = new Color { R = 0x80, G = 0x48, B = 0x0e };
            palette[ 9] = new Color { R = 0x90, G = 0x50, B = 0x0c };
            palette[10] = new Color { R = 0xa0, G = 0x58, B = 0x0a };
            palette[11] = new Color { R = 0xb0, G = 0x60, B = 0x08 };
            palette[12] = new Color { R = 0xc0, G = 0x68, B = 0x06 };
            palette[13] = new Color { R = 0xd0, G = 0x70, B = 0x04 };
            palette[14] = new Color { R = 0xe8, G = 0x78, B = 0x02 };
            palette[15] = new Color { R = 0xff, G = 0x80, B = 0x01 };
            return palette;
        }

        static WriteableBitmap DrawMandelbrot(IntRectangle canvasPosition)
        {
            var mandelbrot = new Mandelbrot(position: canvasPosition, backgroundColor: Colors.MidnightBlue);
            mandelbrot.Draw(GetPalette());
            return mandelbrot.Bitmap;
        }
    }
}

実行結果もほぼ変わらない。

MandelbrotSample.WindowsStore
MandelbrotSample.WindowsStore