勝手に添削:数学的な曲線を描画する (2)

勝手に添削:数学的な曲線を描画する の完結篇。

今回は図形クラスに手を入れていく。

図形ごとに異なるクラスを定義しているんだけど、描画のルーチンは同じものだ。異なるのは座標計算の部分だけ。

ならば、座標データを外からパラメータとして渡すようにしてやろう。こんな具合に。

private var shapes:Array = [
    {
        text : "円",
        rad : 180,
        fx : function(r:Number, t:Number, x:Number):Number{
            return r * Math.cos(t) + x
        },
        fy : function(r:Number, t:Number, y:Number):Number{
            return r * Math.sin(t) + y
        }
    },
    {
        text : "三葉線", 
        rad : 180,
        fx : function(r:Number, t:Number, x:Number):Number{
            return r * Math.sin(3 * t) * Math.cos(t) + x
        },
        fy : function(r:Number, t:Number, y:Number):Number{
            return r * Math.sin(3 * t) * Math.sin(t) + y
        }
    },
    // (以下略)

あとは、このオブジェクトを食う描画クラスを作る。

Tweener みたいな使い方ができるように、static なメソッドに変えた。

import flash.display.Sprite;
import flash.events.Event;

class PolarDrawer{
  // 描画メソッド (Sprite, 円の半径, 中心x, 中心y, Object)
  static public function draw(m:Sprite, cx:Number, cy:Number, shape:Object):void {
    var rad:Number = shape.rad * Math.random();
    var angle:Number = 0;

    m.graphics.lineStyle(1, 0xff0000);
    m.graphics.moveTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy));

    m.addEventListener(Event.ENTER_FRAME, function(e:Event):void{
      angle += 0.05
      m.graphics.lineTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy))

      if(angle >= 2*Math.PI){
        m.removeEventListener("enterFrame", arguments.callee);
      }
    });
  }
}

shapes[i] を PolarDrawer.draw() を呼び出せば描画できる。クリック時イベントで次のように呼び出している。

  private function clickHandler(e:Event):void{
      var s:Sprite = new Sprite();
      addChild(s);
      PolarDrawer.draw(s, mouseX, mouseY, shapes[curveMode]);
  }

はい、だいぶシンプルになりました。行数はたった105行! (ソースは最後に掲載)

シンプルは重要

デザイン用のコードなんだから勢いでもいいじゃないか、と考える人もいるだろうけど、個人的にはデザイナの人にこそ綺麗なコードを書いてもらいたいと思ってる。

というのも、修正前のコードは7つのクラスに描画ルーチンが散らばっていた。例えば、次のような実験をしようにも、めんどくさくて青ざめてしまう。

  • 色を少しずつ変えて線を引きたい
  • 線の太さを変えたい
  • 線を引く開始角度をランダムにしたい

今回の改造で描画処理が1箇所に集約されたので、これらの実験が簡単に実現できるようになった。気軽に試して芸の肥やし(?)にできるというわけだ。

ただ、最初からきれいに書こうとしてたら、何もかけなくなってしまう。自分も最初は勢いで書き始める方だ。

コードを整形するのは、同じコードを3箇所以上に書きたくなったときにしている。同じものが散らばっているとメンテナンスが大変だし、後でソースを読むときに違う部分を探し出すのが面倒だからだ。

心構えをマスターしよう

プログラム言語が分かってきたら、OOP の概念とかデザインパターンに行ってしまうよりも前に、ソースをシンプルに書く方法を身につけていくのがよいと思う。

そんな「心構え」を教えてくれるのがこの本。

リファクタリング―プログラムの体質改善テクニック (Object Technology Series)

リファクタリング―プログラムの体質改善テクニック (Object Technology Series)

  • 作者: マーチン ファウラー
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000-05
  • メディア: 単行本
  • Amazon のレビューを見る

既存のコードをきれいに、シンプルにしていくためのレシピが多数紹介されている。今回や前回のエントリで行った改造も、全部この本に載ってるレシピで説明できる。

挙動を変えずに、ソースを変更していくこと(=リファクタリング)を学ぶにはうってつけ。去年読んだんだけど、もうちょっと早くこの本に出会ってればなぁ…と後悔した。自信をもってお薦めできる数少ない良本!

本で紹介されているソースコードは Java だけど、最初の導入でちょっと長めなソースがでてくるぐらいで、ほとんどが概念的なコード。ActionScript しか知らない人でも違和感なく読めると思う。ActionScript も Java みたいなもんだし。

ソースコード

きもい書き方で行数を短くしてたりするけど、そこはご愛嬌で。

package{
import flash.display.Sprite;
import flash.events.*;
import flash.text.TextField;

public class Test extends Sprite{
    private var dtext:TextField;
    private var curveMode:int = 0;
    private var vertex:int = 5; // for hypocycloid

    private var shapes:Array = [
        {
            text : "円",
            rad : 180,
            fx : function(r:Number, t:Number, x:Number):Number{return r * Math.cos(t) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(t) + y}
        },
        {
            text : "三葉線", 
            rad : 180,
            fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(3 * t) * Math.cos(t) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(3 * t) * Math.sin(t) + y}
        },
        {
            text : "螺旋", 
            rad : 15,
            fx : function(r:Number, t:Number, x:Number):Number{return r * t * Math.cos(t) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * t * Math.sin(t) + y}
        },
        {
            text : "アステロイド曲線", 
            rad : 150,
            fx : function(r:Number, t:Number, x:Number):Number{return r * Math.pow(Math.cos(t), 3) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * Math.pow(Math.sin(t), 3) + y}
        },
        {
            text : "内サイクロイド", 
            rad : 30,
            fx : function(r:Number, t:Number, x:Number):Number{return r * (vertex * Math.cos(t) + Math.cos(vertex * t)) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * (vertex * Math.sin(t) - Math.sin(vertex * t)) + y}
        },
        {
            text : "リサジュー曲線", 
            rad : 180,
            fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(2 * t) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(3 * t) + y}
        },
        {
            text : "四葉線",
            rad : 150,
            fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(2 * t) * Math.cos(t) + x},
            fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(2 * t) * Math.sin(t) + y}
        }
    ];

    public function Test(){
        addChild(dtext = new TextField());
        dtext.text = shapes[curveMode].text;

        stage.addEventListener(MouseEvent.CLICK,clickHandler);
        stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownHandler);
    }

    private function clickHandler(e:Event):void{
        PolarDrawer.draw(Sprite(addChild(new Sprite())), mouseX, mouseY, shapes[curveMode]);
    }

    private function erase():void{
        for(var i:int = numChildren - 1; i >= 0; i--)
            if(getChildAt(i) is Sprite)
                removeChildAt(i);
    }

    private function keyDownHandler(e:KeyboardEvent):void{
        if(49 <= e.keyCode && e.keyCode < 49 + shapes.length){
            curveMode = e.keyCode - 49;
            dtext.text = shapes[curveMode].text;
        }
        else if(e.keyCode == 49 + shapes.length)
            erase();
    }
}
}

import flash.display.Sprite;
import flash.events.Event;

class PolarDrawer{
    // 描画メソッド (Sprite, 円の半径, 中心x, 中心y, Object)
    static public function draw(m:Sprite, cx:Number, cy:Number, shape:Object):void {
        var rad:Number = shape.rad * Math.random();
        var angle:Number = 0;

        m.graphics.lineStyle(1, 0xff0000);
        m.graphics.moveTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy));

        m.addEventListener(Event.ENTER_FRAME, function(e:Event):void{
            angle += 0.05
            m.graphics.lineTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy))

            if(angle >= 2*Math.PI)
                m.removeEventListener("enterFrame", arguments.callee);
        });
    }
}