TypeScriptで梅・桃・桜

パッケージ製品開発担当の大です。こんにちは。
ここ数年、基本的にJavaメインで製品開発をやってきましたが、今年はちょっと方向を変えていきます。TypeScriptメインでいこうと思っています。

ところで世の中すっかり春ですね。今年はPM2.5だの杉花粉の量も黄砂の量もすごいだの連日ニュースで報道されていますが、暖かくなって草木が一斉に芽吹いているのを見るのはやっぱり楽しいものです。

さっき、会社のすぐそばにある熱田神宮に行ってみたら、駐車場の脇の桜が咲いていました。

熱田神宮のシキザクラ

熱田神宮のシキザクラ(クリックで拡大)

八重桜っぽい花びらで綺麗ですね。「シキザクラ」って書いてありました。
ほかの普通の(?)桜の見頃は、来週ぐらいになりそうです。

というわけで今回は、TypeScriptの勉強がてら、春の花をHTML5 Canvasに描いてみたいと思います。

まず、htmlを用意します。こんな感じでcanvasをひとつ置いただけのものです。

<!DOCTYPE html>

<html lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>梅・桃・桜</title>
    <script src="app.js"></script>
</head>
<body>
    <h1>梅・桃・桜</h1>

    <canvas id="content" style="background: #ccffff" width="600" height="200"></canvas>
</body>
</html>

scriptタグで参照されているJavaScriptファイル(app.js)が、TypeScriptファイル(app.ts)をコンパイルしたときにできるものです。あとは、コードをapp.tsに書いていきます。

梅を描く

梅は、画像検索で家紋なんかを調べてみると、花びらは円で描いちゃえば良さげですね。真ん中のおしべ(?)の部分はちょっと複雑そうですが、今回は楽してここも円で描いちゃいます。

梅は花びらを円で描く

梅は花びらを円で描く

interface Point {
    x: number;
    y: number;
}

module 花見 {
    function degreeToRadian(degree: number): number {
        return degree * Math.PI / 180;
    }

    export class 梅 {
        constructor(public center: Point, public radius: number) {
        }

        draw(context: CanvasRenderingContext2D) {
            context.save();
            try {
                context.translate(this.center.x, this.center.y);
                context.fillStyle = "#ffeeee";

                context.beginPath();
                // 72度づつ回転しながら5枚の花びらを描きます
                for (var i = 0; i < 5; i++) {
                    context.arc(0, this.radius/2, this.radius/2, 0, degreeToRadian(360));
                    context.rotate(degreeToRadian(72));
                }
                context.fill();

                // 真ん中の黄色いとこ
                context.beginPath();
                context.fillStyle = "#ffffe0";
                context.arc(0, 0, this.radius/3, 0, degreeToRadian(360));
                context.fill();

            } finally {
                context.restore();
            }
        }
    }
}

window.onload = () => {
    var canvas = <HTMLCanvasElement>document.getElementById('content');
    var context = canvas.getContext("2d");
    new 花見.梅({x:100, y:100}, 50).draw(context);
};

1~4行目は、座標を表すPointインタフェースを定義しています。このインタフェースは、12行目の梅クラスのコンストラクタの引数で、中心の座標を渡すのに使用されています。

TypeScriptでは、この例のようにコンストラクタの引数にpublic(またはprivate)を指定すると、プロパティを指定したのと同じことになります。つまり、この記述は以下と同等です。

export class 梅 {
    public center: Point;
    public radius: number;

    constructor(center: Point, radius: number) {
        this.center = center;
        this.radius = radius;
    }
    ....

6行目のmodule~は、名前空間のようなものです。module内で定義したクラスや関数をmoduleの外から使えるようにするには、定義にexportを付加します(11行目)。

42行目~46行目は、ページのロード時の処理です。ここで、canvasを取得して梅を描画しています。梅コンストラクタを

new 花見.梅({x:100, y:100}, 50)

のように呼び出していますね。Pointインタフェースが期待される場所に、オブジェクトをリテラルで記述しています。これは Structual Subtypingと呼ばれる機能で、わざわざインタフェースを実装したクラスを用意しなくても、必要なプロパティが定義されているオブジェクトならインタフェースと互換とみなされるんだそうです。
ダックタイピングよりもうちょっと厳密にチェックしてくれる感じで良いですね。

43行目の<HTMLCanvasElement>は、Type Assertionと呼ばれる機能で、キャストみたいなものです。
document.getElementByIdが返すのがHTMLElementなので、そのままでは次の行でcanvas.getContextしたらコンパイルエラーになってしまうのですね。
Type Assertionはコンパイルすると消えてしまうので実行時には効きません。

HTMLCanvasElementとかCanvasRenderingContext2DとかのJavaScript組込みのインタフェースは、TypeScriptがデフォルトで読み込むlib.d.tsというファイルで定義されています。

梅クラスのdrawの中で、canvasのAPIを使用して梅を描画しています。
実行するとこんな感じで描画されました。

梅の描画

梅の描画

花麩みたいですね(;´∀`)

クラスを分割する

ところで、私はこれまで知らなかったんですが、梅・桃・桜はどれも、「バラ目・バラ科・サクラ属」の植物なんだそうです。どうりで良く似ていると思いました。

このサイトによると、見分け方は枝への花の付き方と花びらの形状に注目すればいいみたいですね。今回は枝がなくて花の付き方は表現できないので、花びらの形状のみで区別するようにします。

桃は花びらがとがってる

桃は花びらがとがってる

桜は花びらの先が割れている

桜は花びらの先が割れている

というわけで、桃と桜を描画する前に、先ほど定義した桃クラスから共通部分を抜き出した抽象的なクラス「サクラ属」を作ってみます。

    // abstract
    export class サクラ属 {
        constructor(public center: Point, public radius: number) {
        }

        draw(context: CanvasRenderingContext2D) {
            context.save();
            try {
                context.translate(this.center.x, this.center.y);
                context.fillStyle = "#ffeeee";

                context.beginPath();
                // 72度づつ回転しながら5枚の花びらを描きます
                for (var i = 0; i < 5; i++) {
                    this.drawPetal(context);
                    context.rotate(degreeToRadian(72));
                }
                context.fill();

                // 真ん中の黄色いとこ
                context.beginPath();
                context.fillStyle = "#ffffe0";
                context.arc(0, 0, this.radius/3, 0, degreeToRadian(360));
                context.fill();

            } finally {
                context.restore();
            }
        }

        drawPetal(context: CanvasRenderingContext2D) {
            // abstract
        }
    }

    export class 梅 extends サクラ属 {
        drawPetal(context: CanvasRenderingContext2D) {
            context.arc(0, this.radius/2, this.radius/2, 0, degreeToRadian(360));
        }
    }

残念ながらTypeScriptの2013年3月現在のバージョン(0.8.3)には抽象クラス的なものがないので、とりあえずこんな感じになりました。
花びらの描画部分のみを梅クラスで実装しています。

桃クラス・桜クラスの追加

では、サクラ属を継承して桃クラスと桜クラスを追加してみます。

    export class 桃 extends サクラ属 {
        drawPetal(context: CanvasRenderingContext2D) {
            var ctrl = this.radius * 2 / 3;
            context.moveTo(0, 0);
            context.quadraticCurveTo(ctrl, ctrl, 0, this.radius);
            context.quadraticCurveTo(-ctrl, ctrl, 0, 0);
        }
    }

    export class 桜 extends サクラ属 {
        drawPetal(context: CanvasRenderingContext2D) {
            var ctrl1 = this.radius / 2;
            var ctrl2 = this.radius / 10;
            context.moveTo(0, 0);
            context.quadraticCurveTo(ctrl1, ctrl1, ctrl2, this.radius - ctrl2);
            context.lineTo(0, this.radius - (ctrl2 * 2));
            context.lineTo(-ctrl2, this.radius - ctrl2);
            context.quadraticCurveTo(-ctrl1, ctrl1, 0, 0);
        }
    }

梅よりもちょっとだけ描画が複雑になっていますが、まぁ一筆書きで花びら描いてるだけです。

実行結果

実行結果

実行結果(クリックで拡大)

最終的なコードは以下のようになりました。

それでは、良い春を!